chore: formatting fixes
This commit is contained in:
parent
dd6af5e879
commit
119a46c339
167 changed files with 4507 additions and 3248 deletions
|
|
@ -1,38 +1,46 @@
|
|||
import dotenv from 'dotenv';
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import log from 'electron-log';
|
||||
import { app } from 'electron';
|
||||
import path from 'node:path';
|
||||
import log from "electron-log";
|
||||
import { app } from "electron";
|
||||
import path from "node:path";
|
||||
|
||||
// Configure electron-log immediately when module is imported
|
||||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
|
||||
|
||||
// Configure main logger - check for LOG_LEVEL override
|
||||
const envLogLevel = process.env.LOG_LEVEL as 'error' | 'warn' | 'info' | 'debug' | undefined;
|
||||
const defaultFileLevel: 'debug' | 'info' = isDev ? 'debug' : 'info';
|
||||
const defaultConsoleLevel: 'debug' | 'warn' = isDev ? 'debug' : 'warn';
|
||||
const envLogLevel = process.env.LOG_LEVEL as
|
||||
| "error"
|
||||
| "warn"
|
||||
| "info"
|
||||
| "debug"
|
||||
| undefined;
|
||||
const defaultFileLevel: "debug" | "info" = isDev ? "debug" : "info";
|
||||
const defaultConsoleLevel: "debug" | "warn" = isDev ? "debug" : "warn";
|
||||
|
||||
log.transports.file.level = envLogLevel || defaultFileLevel;
|
||||
log.transports.console.level = envLogLevel || defaultConsoleLevel;
|
||||
|
||||
// Configure file transport
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB
|
||||
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}';
|
||||
log.transports.file.format =
|
||||
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}";
|
||||
|
||||
// Set custom log file path
|
||||
const logPath = isDev
|
||||
? path.join(app.getPath('userData'), 'logs', 'amical-dev.log')
|
||||
: path.join(app.getPath('logs'), 'amical.log');
|
||||
? path.join(app.getPath("userData"), "logs", "amical-dev.log")
|
||||
: path.join(app.getPath("logs"), "amical.log");
|
||||
|
||||
log.transports.file.resolvePathFn = () => logPath;
|
||||
|
||||
// Configure console transport for better development experience
|
||||
if (isDev) {
|
||||
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}';
|
||||
log.transports.console.format =
|
||||
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}";
|
||||
log.transports.console.useStyles = true;
|
||||
} else {
|
||||
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}';
|
||||
log.transports.console.format =
|
||||
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{scope}] {text}";
|
||||
log.transports.console.useStyles = false;
|
||||
}
|
||||
|
||||
|
|
@ -48,28 +56,28 @@ if (!isDev) {
|
|||
// -----------------------------------------------
|
||||
// `LOG_DEBUG_SCOPES` can be a comma-separated list of scope names (main,ai,swift)
|
||||
// or regex patterns wrapped in slashes (e.g. /ai.*/, /.*/)
|
||||
const rawDebugScopes = (process.env.LOG_DEBUG_SCOPES ?? '')
|
||||
.split(',')
|
||||
const rawDebugScopes = (process.env.LOG_DEBUG_SCOPES ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Utility: escape regex special chars for exact-match tokens
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const debugScopePatterns: RegExp[] = rawDebugScopes.map((token) => {
|
||||
if (token.startsWith('/') && token.endsWith('/') && token.length > 1) {
|
||||
if (token.startsWith("/") && token.endsWith("/") && token.length > 1) {
|
||||
// Regex pattern (strip the leading & trailing slashes)
|
||||
const pattern = token.slice(1, -1);
|
||||
try {
|
||||
return new RegExp(pattern, 'i');
|
||||
return new RegExp(pattern, "i");
|
||||
} catch {
|
||||
// Fall through to exact match if regex is invalid
|
||||
}
|
||||
}
|
||||
// Treat as exact scope name
|
||||
return new RegExp(`^${escapeRegExp(token)}$`, 'i');
|
||||
return new RegExp(`^${escapeRegExp(token)}$`, "i");
|
||||
});
|
||||
|
||||
export function isScopeDebug(scope: string): boolean {
|
||||
|
|
@ -80,7 +88,7 @@ export function isScopeDebug(scope: string): boolean {
|
|||
if (debugScopePatterns.length > 0) {
|
||||
log.hooks.push((message) => {
|
||||
// Only filter debug messages
|
||||
if (message.level !== 'debug') return message;
|
||||
if (message.level !== "debug") return message;
|
||||
|
||||
// Check if this scope should have debug enabled
|
||||
const scope = message.scope;
|
||||
|
|
@ -102,24 +110,24 @@ function createLoggerForScope(scope: string) {
|
|||
|
||||
// Create scoped loggers for different modules
|
||||
export const logger = {
|
||||
main: createLoggerForScope('main'),
|
||||
ipc: createLoggerForScope('ipc'),
|
||||
renderer: createLoggerForScope('renderer'),
|
||||
network: createLoggerForScope('network'),
|
||||
audio: createLoggerForScope('audio'),
|
||||
ai: createLoggerForScope('ai'),
|
||||
swift: createLoggerForScope('swift'),
|
||||
ui: createLoggerForScope('ui'),
|
||||
db: createLoggerForScope('db'),
|
||||
updater: createLoggerForScope('updater'),
|
||||
main: createLoggerForScope("main"),
|
||||
ipc: createLoggerForScope("ipc"),
|
||||
renderer: createLoggerForScope("renderer"),
|
||||
network: createLoggerForScope("network"),
|
||||
audio: createLoggerForScope("audio"),
|
||||
ai: createLoggerForScope("ai"),
|
||||
swift: createLoggerForScope("swift"),
|
||||
ui: createLoggerForScope("ui"),
|
||||
db: createLoggerForScope("db"),
|
||||
updater: createLoggerForScope("updater"),
|
||||
};
|
||||
|
||||
// Log startup information
|
||||
logger.main.info('Logger initialized', {
|
||||
logger.main.info("Logger initialized", {
|
||||
isDev,
|
||||
fileLogLevel: log.transports.file.level,
|
||||
consoleLogLevel: log.transports.console.level,
|
||||
envLogLevel: envLogLevel || 'not set',
|
||||
envLogLevel: envLogLevel || "not set",
|
||||
logPath,
|
||||
version: app.getVersion(),
|
||||
platform: process.platform,
|
||||
|
|
@ -135,7 +143,11 @@ export function createScopedLogger(scope: string) {
|
|||
}
|
||||
|
||||
// Error handling utilities
|
||||
export function logError(error: Error, context?: string, metadata?: Record<string, any>) {
|
||||
export function logError(
|
||||
error: Error,
|
||||
context?: string,
|
||||
metadata?: Record<string, any>,
|
||||
) {
|
||||
const errorInfo = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
|
|
@ -144,13 +156,13 @@ export function logError(error: Error, context?: string, metadata?: Record<strin
|
|||
...metadata,
|
||||
};
|
||||
|
||||
logger?.main.error('Error occurred:', errorInfo);
|
||||
logger?.main.error("Error occurred:", errorInfo);
|
||||
}
|
||||
|
||||
export function logPerformance(
|
||||
operation: string,
|
||||
startTime: number,
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, any>,
|
||||
) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger?.main.info(`Performance: ${operation}`, {
|
||||
|
|
@ -161,7 +173,7 @@ export function logPerformance(
|
|||
|
||||
// Development helpers
|
||||
export function logDebugInfo(component: string, data: any) {
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
||||
logger?.main.debug(`[${component}]`, data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Load .env file FIRST before any other imports
|
||||
import dotenv from 'dotenv';
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import {
|
||||
|
|
@ -10,26 +10,29 @@ import {
|
|||
ipcMain,
|
||||
screen,
|
||||
clipboard,
|
||||
} from 'electron';
|
||||
import path from 'node:path';
|
||||
import fsPromises from 'node:fs/promises'; // For reading the audio file (async)
|
||||
import started from 'electron-squirrel-startup';
|
||||
import { initializeDatabase } from '../db/config';
|
||||
import { HelperEvent, KeyEventPayload } from '@amical/types';
|
||||
import { logger, logError, logPerformance } from './logger';
|
||||
import { AudioCapture } from '../modules/audio/audio-capture';
|
||||
import { setupApplicationMenu } from './menu';
|
||||
import { AiService } from '../modules/ai/ai-service';
|
||||
import { SwiftIOBridge } from './swift-io-bridge'; // Added import
|
||||
import { DownloadedModel } from '../constants/models';
|
||||
import { ModelManagerService } from '../modules/models/model-manager';
|
||||
import { LocalWhisperClient } from '../modules/ai/local-whisper-client';
|
||||
import { TranscriptionSession, ChunkData } from '../modules/transcription/transcription-session';
|
||||
import { ContextualTranscriptionManager } from '../modules/transcription/contextual-transcription-manager';
|
||||
import { SettingsService } from '../modules/settings';
|
||||
import { createIPCHandler } from 'electron-trpc-experimental/main';
|
||||
import { router } from '../trpc/router';
|
||||
import { AutoUpdaterService } from './services/auto-updater';
|
||||
} from "electron";
|
||||
import path from "node:path";
|
||||
import fsPromises from "node:fs/promises"; // For reading the audio file (async)
|
||||
import started from "electron-squirrel-startup";
|
||||
import { initializeDatabase } from "../db/config";
|
||||
import { HelperEvent, KeyEventPayload } from "@amical/types";
|
||||
import { logger, logError, logPerformance } from "./logger";
|
||||
import { AudioCapture } from "../modules/audio/audio-capture";
|
||||
import { setupApplicationMenu } from "./menu";
|
||||
import { AiService } from "../modules/ai/ai-service";
|
||||
import { SwiftIOBridge } from "./swift-io-bridge"; // Added import
|
||||
import { DownloadedModel } from "../constants/models";
|
||||
import { ModelManagerService } from "../modules/models/model-manager";
|
||||
import { LocalWhisperClient } from "../modules/ai/local-whisper-client";
|
||||
import {
|
||||
TranscriptionSession,
|
||||
ChunkData,
|
||||
} from "../modules/transcription/transcription-session";
|
||||
import { ContextualTranscriptionManager } from "../modules/transcription/contextual-transcription-manager";
|
||||
import { SettingsService } from "../modules/settings";
|
||||
import { createIPCHandler } from "electron-trpc-experimental/main";
|
||||
import { router } from "../trpc/router";
|
||||
import { AutoUpdaterService } from "./services/auto-updater";
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) {
|
||||
|
|
@ -51,17 +54,19 @@ let currentWindowDisplayId: number | null = null; // For tracking current displa
|
|||
let activeSpaceChangeSubscriptionId: number | null = null; // For display change notifications
|
||||
|
||||
// New chunk-based transcription variables
|
||||
let contextualTranscriptionManager: ContextualTranscriptionManager | null = null;
|
||||
const activeTranscriptionSessions: Map<string, TranscriptionSession> = new Map();
|
||||
let contextualTranscriptionManager: ContextualTranscriptionManager | null =
|
||||
null;
|
||||
const activeTranscriptionSessions: Map<string, TranscriptionSession> =
|
||||
new Map();
|
||||
let autoUpdaterService: AutoUpdaterService | null = null;
|
||||
|
||||
// Store is imported from '../lib/store' and is database-backed
|
||||
|
||||
// Function to create the local transcription client
|
||||
const createTranscriptionClient = () => {
|
||||
logger.ai.info('Using local Whisper inference');
|
||||
logger.ai.info("Using local Whisper inference");
|
||||
if (!localWhisperClient) {
|
||||
throw new Error('Local Whisper client not initialized');
|
||||
throw new Error("Local Whisper client not initialized");
|
||||
}
|
||||
return localWhisperClient;
|
||||
};
|
||||
|
|
@ -71,25 +76,32 @@ const createTranscriptionClient = () => {
|
|||
const requestPermissions = async () => {
|
||||
try {
|
||||
// Request accessibility permissions
|
||||
if (process.platform === 'darwin') {
|
||||
const accessibilityEnabled = systemPreferences.isTrustedAccessibilityClient(false);
|
||||
if (process.platform === "darwin") {
|
||||
const accessibilityEnabled =
|
||||
systemPreferences.isTrustedAccessibilityClient(false);
|
||||
if (!accessibilityEnabled) {
|
||||
// On macOS, we need to use a different approach for accessibility permissions
|
||||
// The user will need to grant accessibility permissions through System Preferences
|
||||
console.log(
|
||||
'Please enable accessibility permissions in System Preferences > Security & Privacy > Privacy > Accessibility'
|
||||
"Please enable accessibility permissions in System Preferences > Security & Privacy > Privacy > Accessibility",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Request microphone permissions
|
||||
const microphoneEnabled = systemPreferences.getMediaAccessStatus('microphone');
|
||||
logger.main.info('Microphone access status:', { status: microphoneEnabled });
|
||||
if (microphoneEnabled !== 'granted') {
|
||||
await systemPreferences.askForMediaAccess('microphone');
|
||||
const microphoneEnabled =
|
||||
systemPreferences.getMediaAccessStatus("microphone");
|
||||
logger.main.info("Microphone access status:", {
|
||||
status: microphoneEnabled,
|
||||
});
|
||||
if (microphoneEnabled !== "granted") {
|
||||
await systemPreferences.askForMediaAccess("microphone");
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error instanceof Error ? error : new Error(String(error)), 'requesting permissions');
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"requesting permissions",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -103,11 +115,11 @@ const createOrShowMainWindow = () => {
|
|||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarStyle: "hidden",
|
||||
trafficLightPosition: { x: 20, y: 16 },
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
|
|
@ -115,9 +127,11 @@ const createOrShowMainWindow = () => {
|
|||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
|
||||
);
|
||||
}
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
if (autoUpdaterService) {
|
||||
autoUpdaterService.setMainWindow(null);
|
||||
|
|
@ -127,7 +141,9 @@ const createOrShowMainWindow = () => {
|
|||
// Update tRPC handler to include the main window
|
||||
createIPCHandler({
|
||||
router,
|
||||
windows: [mainWindow, floatingButtonWindow].filter(Boolean) as BrowserWindow[],
|
||||
windows: [mainWindow, floatingButtonWindow].filter(
|
||||
Boolean,
|
||||
) as BrowserWindow[],
|
||||
});
|
||||
|
||||
// Set main window reference for auto-updater
|
||||
|
|
@ -152,7 +168,7 @@ const createFloatingButtonWindow = () => {
|
|||
focusable: false,
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
|
|
@ -163,18 +179,20 @@ const createFloatingButtonWindow = () => {
|
|||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
const devUrl = new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
devUrl.pathname = 'fab.html';
|
||||
devUrl.pathname = "fab.html";
|
||||
floatingButtonWindow.loadURL(devUrl.toString());
|
||||
} else {
|
||||
floatingButtonWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${WIDGET_WINDOW_VITE_NAME}/fab.html`)
|
||||
path.join(__dirname, `../renderer/${WIDGET_WINDOW_VITE_NAME}/fab.html`),
|
||||
);
|
||||
}
|
||||
|
||||
// Set a higher level for macOS to stay on top of fullscreen apps
|
||||
if (process.platform === 'darwin') {
|
||||
floatingButtonWindow.setAlwaysOnTop(true, 'floating', 1);
|
||||
floatingButtonWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
if (process.platform === "darwin") {
|
||||
floatingButtonWindow.setAlwaysOnTop(true, "floating", 1);
|
||||
floatingButtonWindow.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
floatingButtonWindow.setHiddenInMissionControl(true);
|
||||
}
|
||||
|
||||
|
|
@ -184,13 +202,18 @@ const createFloatingButtonWindow = () => {
|
|||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
app.on("ready", async () => {
|
||||
// Initialize database and run migrations first
|
||||
try {
|
||||
await initializeDatabase();
|
||||
logger.db.info('Database initialized and migrations completed successfully');
|
||||
logger.db.info(
|
||||
"Database initialized and migrations completed successfully",
|
||||
);
|
||||
} catch (error) {
|
||||
logError(error instanceof Error ? error : new Error(String(error)), 'initializing database');
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"initializing database",
|
||||
);
|
||||
// You might want to handle this error differently, perhaps showing a dialog to the user
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +226,7 @@ app.on('ready', async () => {
|
|||
windows: [floatingButtonWindow!],
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
if (process.platform === "darwin" && app.dock) {
|
||||
app.dock.show();
|
||||
}
|
||||
|
||||
|
|
@ -223,11 +246,13 @@ app.on('ready', async () => {
|
|||
(globalThis as any).logger = logger;
|
||||
|
||||
// Initialize Contextual Transcription Manager
|
||||
contextualTranscriptionManager = new ContextualTranscriptionManager(modelManagerService);
|
||||
contextualTranscriptionManager = new ContextualTranscriptionManager(
|
||||
modelManagerService,
|
||||
);
|
||||
|
||||
// Initialize Auto-Updater Service
|
||||
autoUpdaterService = new AutoUpdaterService();
|
||||
|
||||
|
||||
// Make auto-updater service available globally for tRPC
|
||||
(globalThis as any).autoUpdaterService = autoUpdaterService;
|
||||
|
||||
|
|
@ -249,25 +274,28 @@ app.on('ready', async () => {
|
|||
const formatterConfig = await settingsService.getFormatterConfig();
|
||||
if (formatterConfig) {
|
||||
aiService.configureFormatter(formatterConfig);
|
||||
logger.ai.info('Formatter configured', {
|
||||
logger.ai.info("Formatter configured", {
|
||||
provider: formatterConfig.provider,
|
||||
enabled: formatterConfig.enabled,
|
||||
});
|
||||
}
|
||||
} catch (formatterError) {
|
||||
logger.ai.warn('Failed to load formatter configuration:', formatterError);
|
||||
logger.ai.warn("Failed to load formatter configuration:", formatterError);
|
||||
}
|
||||
|
||||
logger.ai.info('AI Service initialized', {
|
||||
client: 'Local Whisper',
|
||||
logger.ai.info("AI Service initialized", {
|
||||
client: "Local Whisper",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error instanceof Error ? error : new Error(String(error)), 'initializing AI Service');
|
||||
logger.ai.warn('Transcription will not work until configuration is fixed');
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"initializing AI Service",
|
||||
);
|
||||
logger.ai.warn("Transcription will not work until configuration is fixed");
|
||||
aiService = null;
|
||||
}
|
||||
|
||||
audioCapture.on('recording-finished', async (filePath: string) => {
|
||||
audioCapture.on("recording-finished", async (filePath: string) => {
|
||||
// Ensure AI service is available and up-to-date
|
||||
if (!aiService) {
|
||||
try {
|
||||
|
|
@ -280,53 +308,60 @@ app.on('ready', async () => {
|
|||
const formatterConfig = await settingsService.getFormatterConfig();
|
||||
if (formatterConfig) {
|
||||
aiService.configureFormatter(formatterConfig);
|
||||
logger.ai.info('Formatter reconfigured', {
|
||||
logger.ai.info("Formatter reconfigured", {
|
||||
provider: formatterConfig.provider,
|
||||
enabled: formatterConfig.enabled,
|
||||
});
|
||||
}
|
||||
} catch (formatterError) {
|
||||
logger.ai.warn('Failed to reload formatter configuration:', formatterError);
|
||||
logger.ai.warn(
|
||||
"Failed to reload formatter configuration:",
|
||||
formatterError,
|
||||
);
|
||||
}
|
||||
|
||||
logger.ai.info('AI Service reinitialized', {
|
||||
client: 'Local Whisper',
|
||||
logger.ai.info("AI Service reinitialized", {
|
||||
client: "Local Whisper",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
'reinitializing AI Service'
|
||||
"reinitializing AI Service",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.audio.info('Recording finished', { filePath });
|
||||
logger.audio.info("Recording finished", { filePath });
|
||||
if (aiService) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const audioBuffer = await fsPromises.readFile(filePath);
|
||||
logger.audio.info('Audio file read', {
|
||||
logger.audio.info("Audio file read", {
|
||||
size: audioBuffer.length,
|
||||
sizeKB: Math.round(audioBuffer.length / 1024),
|
||||
});
|
||||
|
||||
const transcription = await aiService.transcribeAudio(audioBuffer);
|
||||
logPerformance('audio transcription', startTime, {
|
||||
logPerformance("audio transcription", startTime, {
|
||||
audioSizeKB: Math.round(audioBuffer.length / 1024),
|
||||
transcriptionLength: transcription?.length || 0,
|
||||
});
|
||||
logger.ai.info('Transcription completed', {
|
||||
logger.ai.info("Transcription completed", {
|
||||
resultLength: transcription?.length || 0,
|
||||
hasResult: !!transcription,
|
||||
});
|
||||
|
||||
// Copy transcription to clipboard
|
||||
if (transcription && typeof transcription === 'string') {
|
||||
logger.main.info('Transcription pasted to active application');
|
||||
if (transcription && typeof transcription === "string") {
|
||||
logger.main.info("Transcription pasted to active application");
|
||||
// Attempt to paste into the active application
|
||||
swiftIOBridgeClientInstance!.call('pasteText', { transcript: transcription });
|
||||
swiftIOBridgeClientInstance!.call("pasteText", {
|
||||
transcript: transcription,
|
||||
});
|
||||
} else {
|
||||
logger.main.warn('Transcription result was empty or not a string, not copying');
|
||||
logger.main.warn(
|
||||
"Transcription result was empty or not a string, not copying",
|
||||
);
|
||||
}
|
||||
|
||||
// Optionally, delete the audio file after processing
|
||||
|
|
@ -335,21 +370,21 @@ app.on('ready', async () => {
|
|||
} catch (error) {
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
'transcription or file handling'
|
||||
"transcription or file handling",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.ai.warn('AI Service not available, cannot transcribe audio');
|
||||
logger.ai.warn("AI Service not available, cannot transcribe audio");
|
||||
}
|
||||
});
|
||||
|
||||
audioCapture.on('recording-error', (error: Error) => {
|
||||
console.error('Main: Received recording error from AudioCapture:', error);
|
||||
audioCapture.on("recording-error", (error: Error) => {
|
||||
console.error("Main: Received recording error from AudioCapture:", error);
|
||||
});
|
||||
|
||||
// Handle individual audio chunks for real-time transcription
|
||||
audioCapture.on('chunk-ready', async (chunkData: ChunkData) => {
|
||||
logger.audio.info('Received chunk for transcription', {
|
||||
audioCapture.on("chunk-ready", async (chunkData: ChunkData) => {
|
||||
logger.audio.info("Received chunk for transcription", {
|
||||
sessionId: chunkData.sessionId,
|
||||
chunkId: chunkData.chunkId,
|
||||
audioDataSize: chunkData.audioData.length,
|
||||
|
|
@ -358,18 +393,27 @@ app.on('ready', async () => {
|
|||
|
||||
try {
|
||||
// Get or create transcription session for this recording session
|
||||
let transcriptionSession = activeTranscriptionSessions.get(chunkData.sessionId);
|
||||
let transcriptionSession = activeTranscriptionSessions.get(
|
||||
chunkData.sessionId,
|
||||
);
|
||||
|
||||
if (!transcriptionSession) {
|
||||
// Create new transcription session
|
||||
const transcriptionClient = contextualTranscriptionManager!.createDefaultClient();
|
||||
const transcriptionClient =
|
||||
contextualTranscriptionManager!.createDefaultClient();
|
||||
|
||||
transcriptionSession = new TranscriptionSession(chunkData.sessionId, transcriptionClient);
|
||||
activeTranscriptionSessions.set(chunkData.sessionId, transcriptionSession);
|
||||
transcriptionSession = new TranscriptionSession(
|
||||
chunkData.sessionId,
|
||||
transcriptionClient,
|
||||
);
|
||||
activeTranscriptionSessions.set(
|
||||
chunkData.sessionId,
|
||||
transcriptionSession,
|
||||
);
|
||||
|
||||
// Set up session event handlers
|
||||
transcriptionSession.on('chunk-completed', (result) => {
|
||||
logger.ai.info('Chunk transcription completed', {
|
||||
transcriptionSession.on("chunk-completed", (result) => {
|
||||
logger.ai.info("Chunk transcription completed", {
|
||||
sessionId: chunkData.sessionId,
|
||||
chunkId: result.chunkId,
|
||||
textLength: result.text.length,
|
||||
|
|
@ -377,8 +421,8 @@ app.on('ready', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
transcriptionSession.on('session-completed', (sessionResult) => {
|
||||
logger.ai.info('Transcription session completed', {
|
||||
transcriptionSession.on("session-completed", (sessionResult) => {
|
||||
logger.ai.info("Transcription session completed", {
|
||||
sessionId: sessionResult.sessionId,
|
||||
finalTextLength: sessionResult.finalText.length,
|
||||
totalChunks: sessionResult.chunkResults.length,
|
||||
|
|
@ -386,21 +430,29 @@ app.on('ready', async () => {
|
|||
});
|
||||
|
||||
// Paste the final result to active application
|
||||
if (sessionResult.finalText && sessionResult.finalText.trim().length > 0) {
|
||||
logger.main.info('Final transcription pasted to active application', {
|
||||
textLength: sessionResult.finalText.length,
|
||||
if (
|
||||
sessionResult.finalText &&
|
||||
sessionResult.finalText.trim().length > 0
|
||||
) {
|
||||
logger.main.info(
|
||||
"Final transcription pasted to active application",
|
||||
{
|
||||
textLength: sessionResult.finalText.length,
|
||||
},
|
||||
);
|
||||
swiftIOBridgeClientInstance!.call("pasteText", {
|
||||
transcript: sessionResult.finalText,
|
||||
});
|
||||
swiftIOBridgeClientInstance!.call('pasteText', { transcript: sessionResult.finalText });
|
||||
} else {
|
||||
logger.main.warn('Final transcription was empty, not pasting');
|
||||
logger.main.warn("Final transcription was empty, not pasting");
|
||||
}
|
||||
|
||||
// Clean up completed session
|
||||
activeTranscriptionSessions.delete(chunkData.sessionId);
|
||||
});
|
||||
|
||||
transcriptionSession.on('chunk-error', (errorInfo) => {
|
||||
logger.ai.error('Chunk transcription error', {
|
||||
transcriptionSession.on("chunk-error", (errorInfo) => {
|
||||
logger.ai.error("Chunk transcription error", {
|
||||
sessionId: chunkData.sessionId,
|
||||
chunkId: errorInfo.chunkId,
|
||||
error: errorInfo.error,
|
||||
|
|
@ -408,13 +460,15 @@ app.on('ready', async () => {
|
|||
// Continue processing other chunks even if one fails
|
||||
});
|
||||
|
||||
logger.ai.info('Created new transcription session', { sessionId: chunkData.sessionId });
|
||||
logger.ai.info("Created new transcription session", {
|
||||
sessionId: chunkData.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
// Add chunk to session for processing
|
||||
transcriptionSession.addChunk(chunkData);
|
||||
} catch (error) {
|
||||
logger.ai.error('Error handling chunk-ready event', {
|
||||
logger.ai.error("Error handling chunk-ready event", {
|
||||
sessionId: chunkData.sessionId,
|
||||
chunkId: chunkData.chunkId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
|
@ -423,42 +477,47 @@ app.on('ready', async () => {
|
|||
});
|
||||
|
||||
// Handle audio data chunks from renderer
|
||||
ipcMain.handle('audio-data-chunk', (event, chunk: ArrayBuffer, isFinalChunk: boolean) => {
|
||||
if (chunk instanceof ArrayBuffer) {
|
||||
console.log(
|
||||
`Main: IPC received audio-data-chunk (ArrayBuffer) of size: ${chunk.byteLength} bytes. isFinalChunk: ${isFinalChunk}`
|
||||
);
|
||||
const buffer = Buffer.from(chunk);
|
||||
if (buffer.length === 0) {
|
||||
console.warn('Main: Received an empty audio chunk after conversion.');
|
||||
ipcMain.handle(
|
||||
"audio-data-chunk",
|
||||
(event, chunk: ArrayBuffer, isFinalChunk: boolean) => {
|
||||
if (chunk instanceof ArrayBuffer) {
|
||||
console.log(
|
||||
`Main: IPC received audio-data-chunk (ArrayBuffer) of size: ${chunk.byteLength} bytes. isFinalChunk: ${isFinalChunk}`,
|
||||
);
|
||||
const buffer = Buffer.from(chunk);
|
||||
if (buffer.length === 0) {
|
||||
console.warn("Main: Received an empty audio chunk after conversion.");
|
||||
}
|
||||
// The AudioCapture class will now need to handle buffering and the isFinalChunk flag
|
||||
audioCapture?.handleAudioChunk(buffer, isFinalChunk);
|
||||
} else {
|
||||
console.error(
|
||||
"Main: Received audio chunk, but it is not an ArrayBuffer. Type:",
|
||||
typeof chunk,
|
||||
);
|
||||
throw new Error("Invalid audio chunk type received.");
|
||||
}
|
||||
// The AudioCapture class will now need to handle buffering and the isFinalChunk flag
|
||||
audioCapture?.handleAudioChunk(buffer, isFinalChunk);
|
||||
} else {
|
||||
console.error(
|
||||
'Main: Received audio chunk, but it is not an ArrayBuffer. Type:',
|
||||
typeof chunk
|
||||
);
|
||||
throw new Error('Invalid audio chunk type received.');
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle('recording-starting', async () => {
|
||||
console.log('Main: Received recording-starting event.');
|
||||
ipcMain.handle("recording-starting", async () => {
|
||||
console.log("Main: Received recording-starting event.");
|
||||
|
||||
// Preload the transcription model for fast processing
|
||||
try {
|
||||
if (contextualTranscriptionManager) {
|
||||
if (!contextualTranscriptionManager.isModelLoaded()) {
|
||||
logger.ai.info('Preloading transcription model for recording session');
|
||||
logger.ai.info(
|
||||
"Preloading transcription model for recording session",
|
||||
);
|
||||
await contextualTranscriptionManager.preloadModel();
|
||||
logger.ai.info('Transcription model preloaded successfully');
|
||||
logger.ai.info("Transcription model preloaded successfully");
|
||||
} else {
|
||||
logger.ai.info('Transcription model already loaded');
|
||||
logger.ai.info("Transcription model already loaded");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.ai.error('Error preloading transcription model', {
|
||||
logger.ai.error("Error preloading transcription model", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
|
@ -468,52 +527,56 @@ app.on('ready', async () => {
|
|||
//const accessibilityContext = await swiftIOBridgeClientInstance!.call('getAccessibilityContext', { editableOnly: true });
|
||||
//console.log('Main: Accessibility context captured:', JSON.stringify(accessibilityContext, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Main: Error getting accessibility context:', error);
|
||||
console.error("Main: Error getting accessibility context:", error);
|
||||
}
|
||||
|
||||
await swiftIOBridgeClientInstance!.call('muteSystemAudio', {});
|
||||
await swiftIOBridgeClientInstance!.call("muteSystemAudio", {});
|
||||
});
|
||||
|
||||
ipcMain.handle('recording-stopping', async () => {
|
||||
console.log('Main: Received recording-stopping event.');
|
||||
await swiftIOBridgeClientInstance!.call('restoreSystemAudio', {});
|
||||
ipcMain.handle("recording-stopping", async () => {
|
||||
console.log("Main: Received recording-stopping event.");
|
||||
await swiftIOBridgeClientInstance!.call("restoreSystemAudio", {});
|
||||
});
|
||||
|
||||
|
||||
// Initialize the SwiftIOBridgeClient
|
||||
swiftIOBridgeClientInstance = new SwiftIOBridge();
|
||||
|
||||
swiftIOBridgeClientInstance.on('helperEvent', (event: HelperEvent) => {
|
||||
logger.swift.debug('Received helperEvent from SwiftIOBridge', { event });
|
||||
swiftIOBridgeClientInstance.on("helperEvent", (event: HelperEvent) => {
|
||||
logger.swift.debug("Received helperEvent from SwiftIOBridge", { event });
|
||||
|
||||
switch (event.type) {
|
||||
case 'flagsChanged': {
|
||||
case "flagsChanged": {
|
||||
const payload = event.payload;
|
||||
logger.swift.debug('Received flagsChanged event', {
|
||||
logger.swift.debug("Received flagsChanged event", {
|
||||
fnKeyPressed: payload?.fnKeyPressed,
|
||||
});
|
||||
// Use flagsChanged for more reliable Fn key state tracking
|
||||
if (payload?.fnKeyPressed !== undefined) {
|
||||
logger.swift.info('Setting recording state', { state: payload.fnKeyPressed });
|
||||
floatingButtonWindow!.webContents.send('recording-state-changed', payload.fnKeyPressed);
|
||||
logger.swift.info("Setting recording state", {
|
||||
state: payload.fnKeyPressed,
|
||||
});
|
||||
floatingButtonWindow!.webContents.send(
|
||||
"recording-state-changed",
|
||||
payload.fnKeyPressed,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'keyDown': {
|
||||
case "keyDown": {
|
||||
const payload = event.payload;
|
||||
// console.log(`Main: Received keyDown for key: ${payload?.key}.`);
|
||||
// Keep keyDown handling as fallback, but flagsChanged should be primary
|
||||
if (payload?.key?.toLowerCase() === 'fn') {
|
||||
if (payload?.key?.toLowerCase() === "fn") {
|
||||
// console.log('Main: Fn keyDown detected (fallback)');
|
||||
// Don't send recording-state-changed here as flagsChanged should handle it
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'keyUp': {
|
||||
case "keyUp": {
|
||||
const payload = event.payload;
|
||||
// console.log(`Main: Received keyUp for key: ${payload?.key}.`);
|
||||
// Keep keyUp handling as fallback, but flagsChanged should be primary
|
||||
if (payload?.key?.toLowerCase() === 'fn') {
|
||||
if (payload?.key?.toLowerCase() === "fn") {
|
||||
// console.log('Main: Fn keyUp detected (fallback)');
|
||||
// Don't send recording-state-changed here as flagsChanged should handle it
|
||||
}
|
||||
|
|
@ -526,13 +589,16 @@ app.on('ready', async () => {
|
|||
}
|
||||
});
|
||||
|
||||
swiftIOBridgeClientInstance.on('error', (error) => {
|
||||
logError(error instanceof Error ? error : new Error(String(error)), 'SwiftIOBridge error');
|
||||
swiftIOBridgeClientInstance.on("error", (error) => {
|
||||
logError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"SwiftIOBridge error",
|
||||
);
|
||||
// Potentially notify the user or attempt to restart
|
||||
});
|
||||
|
||||
swiftIOBridgeClientInstance.on('close', (code) => {
|
||||
logger.swift.warn('Swift helper process closed', { code });
|
||||
swiftIOBridgeClientInstance.on("close", (code) => {
|
||||
logger.swift.warn("Swift helper process closed", { code });
|
||||
// Handle unexpected close, maybe attempt restart
|
||||
});
|
||||
|
||||
|
|
@ -542,64 +608,84 @@ app.on('ready', async () => {
|
|||
}
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (process.platform === "darwin") {
|
||||
try {
|
||||
console.log('Main: Setting up display change notifications');
|
||||
console.log("Main: Setting up display change notifications");
|
||||
|
||||
activeSpaceChangeSubscriptionId = systemPreferences.subscribeWorkspaceNotification(
|
||||
'NSWorkspaceActiveDisplayDidChangeNotification',
|
||||
() => {
|
||||
if (floatingButtonWindow && !floatingButtonWindow.isDestroyed()) {
|
||||
try {
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
const displayForCursor = screen.getDisplayNearestPoint(cursorPoint);
|
||||
if (currentWindowDisplayId !== displayForCursor.id) {
|
||||
console.log(
|
||||
`[Main Process] Moving floating window to display ID: ${displayForCursor.id}`
|
||||
activeSpaceChangeSubscriptionId =
|
||||
systemPreferences.subscribeWorkspaceNotification(
|
||||
"NSWorkspaceActiveDisplayDidChangeNotification",
|
||||
() => {
|
||||
if (floatingButtonWindow && !floatingButtonWindow.isDestroyed()) {
|
||||
try {
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
const displayForCursor =
|
||||
screen.getDisplayNearestPoint(cursorPoint);
|
||||
if (currentWindowDisplayId !== displayForCursor.id) {
|
||||
console.log(
|
||||
`[Main Process] Moving floating window to display ID: ${displayForCursor.id}`,
|
||||
);
|
||||
floatingButtonWindow.setBounds(displayForCursor.workArea);
|
||||
currentWindowDisplayId = displayForCursor.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[Main Process] Error handling display change:",
|
||||
error,
|
||||
);
|
||||
floatingButtonWindow.setBounds(displayForCursor.workArea);
|
||||
currentWindowDisplayId = displayForCursor.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Main Process] Error handling display change:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (activeSpaceChangeSubscriptionId !== undefined && activeSpaceChangeSubscriptionId >= 0) {
|
||||
console.log(`Main: Successfully subscribed to display change notifications`);
|
||||
if (
|
||||
activeSpaceChangeSubscriptionId !== undefined &&
|
||||
activeSpaceChangeSubscriptionId >= 0
|
||||
) {
|
||||
console.log(
|
||||
`Main: Successfully subscribed to display change notifications`,
|
||||
);
|
||||
} else {
|
||||
console.error('Main: Failed to subscribe to display change notifications');
|
||||
console.error(
|
||||
"Main: Failed to subscribe to display change notifications",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Main: Error during subscription to display notifications:', e);
|
||||
console.error(
|
||||
"Main: Error during subscription to display notifications:",
|
||||
e,
|
||||
);
|
||||
activeSpaceChangeSubscriptionId = null;
|
||||
}
|
||||
} else {
|
||||
console.log('Main: Display change tracking is a macOS-only feature');
|
||||
console.log("Main: Display change tracking is a macOS-only feature");
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up intervals and subscriptions
|
||||
app.on('will-quit', () => {
|
||||
app.on("will-quit", () => {
|
||||
// globalShortcut.unregisterAll();
|
||||
globalShortcut.unregisterAll();
|
||||
if (swiftIOBridgeClientInstance) {
|
||||
console.log('Main: Stopping Swift helper...');
|
||||
console.log("Main: Stopping Swift helper...");
|
||||
swiftIOBridgeClientInstance.stopHelper();
|
||||
}
|
||||
if (modelManagerService) {
|
||||
console.log('Main: Cleaning up model downloads...');
|
||||
console.log("Main: Cleaning up model downloads...");
|
||||
modelManagerService.cleanup();
|
||||
}
|
||||
if (contextualTranscriptionManager) {
|
||||
console.log('Main: Cleaning up transcription models...');
|
||||
console.log("Main: Cleaning up transcription models...");
|
||||
contextualTranscriptionManager.dispose();
|
||||
}
|
||||
if (process.platform === 'darwin' && activeSpaceChangeSubscriptionId !== null) {
|
||||
systemPreferences.unsubscribeWorkspaceNotification(activeSpaceChangeSubscriptionId);
|
||||
console.log('Main: Unsubscribed from display change notifications');
|
||||
if (
|
||||
process.platform === "darwin" &&
|
||||
activeSpaceChangeSubscriptionId !== null
|
||||
) {
|
||||
systemPreferences.unsubscribeWorkspaceNotification(
|
||||
activeSpaceChangeSubscriptionId,
|
||||
);
|
||||
console.log("Main: Unsubscribed from display change notifications");
|
||||
activeSpaceChangeSubscriptionId = null;
|
||||
}
|
||||
});
|
||||
|
|
@ -607,13 +693,13 @@ app.on('will-quit', () => {
|
|||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
|
|
@ -637,19 +723,25 @@ app.on('activate', () => {
|
|||
|
||||
// Function to log the accessibility tree (added)
|
||||
async function logAccessibilityTree() {
|
||||
if (swiftIOBridgeClientInstance && swiftIOBridgeClientInstance.isHelperRunning()) {
|
||||
if (
|
||||
swiftIOBridgeClientInstance &&
|
||||
swiftIOBridgeClientInstance.isHelperRunning()
|
||||
) {
|
||||
try {
|
||||
// console.log('Main: Requesting full accessibility tree...');
|
||||
// Call with empty params for the whole tree, as per schema for GetAccessibilityTreeDetailsParams
|
||||
const result = await swiftIOBridgeClientInstance.call('getAccessibilityTreeDetails', {});
|
||||
const result = await swiftIOBridgeClientInstance.call(
|
||||
"getAccessibilityTreeDetails",
|
||||
{},
|
||||
);
|
||||
// Using JSON.stringify to see the whole structure since it's 'any' for now
|
||||
// console.log('Main: Accessibility tree received:', JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Main: Error calling getAccessibilityTreeDetails:', error);
|
||||
console.error("Main: Error calling getAccessibilityTreeDetails:", error);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
'Main: SwiftIOBridge not ready or helper not running, cannot log accessibility tree.'
|
||||
"Main: SwiftIOBridge not ready or helper not running, cannot log accessibility tree.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,143 @@
|
|||
import { app, Menu, MenuItemConstructorOptions, BrowserWindow } from 'electron';
|
||||
import { app, Menu, MenuItemConstructorOptions, BrowserWindow } from "electron";
|
||||
|
||||
// Forward declaration or import of the function type if it's complex
|
||||
// For simplicity, we assume createOrShowSettingsWindow is a () => void function
|
||||
|
||||
export const setupApplicationMenu = (
|
||||
createOrShowSettingsWindow: () => void,
|
||||
checkForUpdates?: () => void
|
||||
checkForUpdates?: () => void,
|
||||
) => {
|
||||
const menuTemplate: MenuItemConstructorOptions[] = [
|
||||
// { role: 'appMenu' } for macOS
|
||||
...(process.platform === 'darwin'
|
||||
...(process.platform === "darwin"
|
||||
? ([
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: 'about' as const },
|
||||
{ type: 'separator' as const },
|
||||
...(checkForUpdates ? [{
|
||||
label: 'Check for Updates...',
|
||||
click: () => checkForUpdates(),
|
||||
} as MenuItemConstructorOptions, { type: 'separator' as const }] : []),
|
||||
{ role: "about" as const },
|
||||
{ type: "separator" as const },
|
||||
...(checkForUpdates
|
||||
? [
|
||||
{
|
||||
label: "Check for Updates...",
|
||||
click: () => checkForUpdates(),
|
||||
} as MenuItemConstructorOptions,
|
||||
{ type: "separator" as const },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Settings',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
label: "Settings",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
click: () => createOrShowSettingsWindow(),
|
||||
},
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'services' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'hide' as const },
|
||||
{ role: 'hideOthers' as const },
|
||||
{ role: 'unhide' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'quit' as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "services" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "hide" as const },
|
||||
{ role: "hideOthers" as const },
|
||||
{ role: "unhide" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "quit" as const },
|
||||
],
|
||||
},
|
||||
] as MenuItemConstructorOptions[])
|
||||
: []),
|
||||
// { role: 'fileMenu' } for Windows/Linux
|
||||
...(process.platform !== 'darwin'
|
||||
...(process.platform !== "darwin"
|
||||
? ([
|
||||
{
|
||||
label: 'File',
|
||||
label: "File",
|
||||
submenu: [
|
||||
{
|
||||
label: 'Settings',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
label: "Settings",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
click: () => createOrShowSettingsWindow(),
|
||||
},
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'quit' as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "quit" as const },
|
||||
],
|
||||
},
|
||||
] as MenuItemConstructorOptions[])
|
||||
: []),
|
||||
// { role: 'editMenu' }
|
||||
{
|
||||
label: 'Edit',
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: 'undo' as const },
|
||||
{ role: 'redo' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'cut' as const },
|
||||
{ role: 'copy' as const },
|
||||
{ role: 'paste' as const },
|
||||
...(process.platform === 'darwin'
|
||||
{ role: "undo" as const },
|
||||
{ role: "redo" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "cut" as const },
|
||||
{ role: "copy" as const },
|
||||
{ role: "paste" as const },
|
||||
...(process.platform === "darwin"
|
||||
? [
|
||||
{ role: 'pasteAndMatchStyle' as const },
|
||||
{ role: 'delete' as const },
|
||||
{ role: 'selectAll' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: "pasteAndMatchStyle" as const },
|
||||
{ role: "delete" as const },
|
||||
{ role: "selectAll" as const },
|
||||
{ type: "separator" as const },
|
||||
{
|
||||
label: 'Speech',
|
||||
submenu: [{ role: 'startSpeaking' as const }, { role: 'stopSpeaking' as const }],
|
||||
label: "Speech",
|
||||
submenu: [
|
||||
{ role: "startSpeaking" as const },
|
||||
{ role: "stopSpeaking" as const },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ role: 'delete' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'selectAll' as const },
|
||||
{ role: "delete" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "selectAll" as const },
|
||||
]),
|
||||
],
|
||||
},
|
||||
// { role: 'viewMenu' }
|
||||
{
|
||||
label: 'View',
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: 'reload' as const },
|
||||
{ role: 'forceReload' as const },
|
||||
{ role: 'toggleDevTools' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'resetZoom' as const },
|
||||
{ role: 'zoomIn' as const },
|
||||
{ role: 'zoomOut' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'togglefullscreen' as const },
|
||||
{ role: "reload" as const },
|
||||
{ role: "forceReload" as const },
|
||||
{ role: "toggleDevTools" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "resetZoom" as const },
|
||||
{ role: "zoomIn" as const },
|
||||
{ role: "zoomOut" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "togglefullscreen" as const },
|
||||
],
|
||||
},
|
||||
// { role: 'windowMenu' }
|
||||
{
|
||||
label: 'Window',
|
||||
label: "Window",
|
||||
submenu: [
|
||||
{ role: 'minimize' as const },
|
||||
{ role: 'zoom' as const },
|
||||
...(process.platform === 'darwin'
|
||||
{ role: "minimize" as const },
|
||||
{ role: "zoom" as const },
|
||||
...(process.platform === "darwin"
|
||||
? [
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'front' as const },
|
||||
{ type: 'separator' as const },
|
||||
{ role: 'window' as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "front" as const },
|
||||
{ type: "separator" as const },
|
||||
{ role: "window" as const },
|
||||
]
|
||||
: [{ role: 'close' as const }]),
|
||||
: [{ role: "close" as const }]),
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'help' as const,
|
||||
role: "help" as const,
|
||||
submenu: [
|
||||
...(checkForUpdates ? [{
|
||||
label: 'Check for Updates...',
|
||||
click: () => checkForUpdates(),
|
||||
} as MenuItemConstructorOptions, { type: 'separator' as const }] : []),
|
||||
...(checkForUpdates
|
||||
? [
|
||||
{
|
||||
label: "Check for Updates...",
|
||||
click: () => checkForUpdates(),
|
||||
} as MenuItemConstructorOptions,
|
||||
{ type: "separator" as const },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Learn More',
|
||||
label: "Learn More",
|
||||
click: async () => {
|
||||
const { shell } = await import('electron');
|
||||
shell.openExternal('https://electronjs.org');
|
||||
const { shell } = await import("electron");
|
||||
shell.openExternal("https://electronjs.org");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// See the Electron documentation for details on how to use preload scripts:
|
||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import log from 'electron-log/renderer';
|
||||
import { exposeElectronTRPC } from 'electron-trpc-experimental/preload';
|
||||
import type { ElectronAPI } from '../types/electron-api';
|
||||
import type { FormatterConfig } from '../modules/formatter';
|
||||
import type { Transcription, NewTranscription } from '../db/schema';
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
|
||||
import log from "electron-log/renderer";
|
||||
import { exposeElectronTRPC } from "electron-trpc-experimental/preload";
|
||||
import type { ElectronAPI } from "../types/electron-api";
|
||||
import type { FormatterConfig } from "../modules/formatter";
|
||||
import type { Transcription, NewTranscription } from "../db/schema";
|
||||
|
||||
interface ShortcutData {
|
||||
shortcut: string;
|
||||
|
|
@ -14,39 +14,47 @@ interface ShortcutData {
|
|||
}
|
||||
|
||||
const api: ElectronAPI = {
|
||||
onRecordingStarting: async () => await ipcRenderer.invoke('recording-starting'),
|
||||
onRecordingStopping: async () => await ipcRenderer.invoke('recording-stopping'),
|
||||
sendAudioChunk: (chunk: ArrayBuffer, isFinalChunk: boolean = false): Promise<void> =>
|
||||
ipcRenderer.invoke('audio-data-chunk', chunk, isFinalChunk),
|
||||
onRecordingStarting: async () =>
|
||||
await ipcRenderer.invoke("recording-starting"),
|
||||
onRecordingStopping: async () =>
|
||||
await ipcRenderer.invoke("recording-stopping"),
|
||||
sendAudioChunk: (
|
||||
chunk: ArrayBuffer,
|
||||
isFinalChunk: boolean = false,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("audio-data-chunk", chunk, isFinalChunk),
|
||||
|
||||
onRecordingStateChanged: (callback: (newState: boolean) => void) => {
|
||||
const handler = (_event: IpcRendererEvent, newState: boolean) => callback(newState);
|
||||
ipcRenderer.on('recording-state-changed', handler);
|
||||
const handler = (_event: IpcRendererEvent, newState: boolean) =>
|
||||
callback(newState);
|
||||
ipcRenderer.on("recording-state-changed", handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('recording-state-changed', handler);
|
||||
ipcRenderer.removeListener("recording-state-changed", handler);
|
||||
};
|
||||
},
|
||||
// Switched to invoke/handle for request-response
|
||||
onGlobalShortcut: (callback: (data: ShortcutData) => void) => {
|
||||
const handler = (_event: IpcRendererEvent, data: ShortcutData) => callback(data);
|
||||
ipcRenderer.on('global-shortcut-event', handler);
|
||||
const handler = (_event: IpcRendererEvent, data: ShortcutData) =>
|
||||
callback(data);
|
||||
ipcRenderer.on("global-shortcut-event", handler);
|
||||
// Optional: Return a cleanup function to remove the listener
|
||||
return () => {
|
||||
ipcRenderer.removeListener('global-shortcut-event', handler);
|
||||
ipcRenderer.removeListener("global-shortcut-event", handler);
|
||||
};
|
||||
},
|
||||
onKeyEvent: (callback: (keyEvent: unknown) => void) => {
|
||||
const handler = (_event: IpcRendererEvent, keyEvent: unknown) => callback(keyEvent);
|
||||
ipcRenderer.on('key-event', handler);
|
||||
const handler = (_event: IpcRendererEvent, keyEvent: unknown) =>
|
||||
callback(keyEvent);
|
||||
ipcRenderer.on("key-event", handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('key-event', handler);
|
||||
ipcRenderer.removeListener("key-event", handler);
|
||||
};
|
||||
},
|
||||
onForceStopMediaRecorder: (callback: () => void) => {
|
||||
const handler = () => callback();
|
||||
ipcRenderer.on('force-stop-mediarecorder', handler);
|
||||
ipcRenderer.on("force-stop-mediarecorder", handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('force-stop-mediarecorder', handler);
|
||||
ipcRenderer.removeListener("force-stop-mediarecorder", handler);
|
||||
};
|
||||
},
|
||||
// If you want a way to remove all listeners for this event from renderer:
|
||||
|
|
@ -55,52 +63,63 @@ const api: ElectronAPI = {
|
|||
// }
|
||||
|
||||
// Model Management API
|
||||
getAvailableModels: () => ipcRenderer.invoke('get-available-models'),
|
||||
getDownloadedModels: () => ipcRenderer.invoke('get-downloaded-models'),
|
||||
isModelDownloaded: (modelId: string) => ipcRenderer.invoke('is-model-downloaded', modelId),
|
||||
getDownloadProgress: (modelId: string) => ipcRenderer.invoke('get-download-progress', modelId),
|
||||
getActiveDownloads: () => ipcRenderer.invoke('get-active-downloads'),
|
||||
downloadModel: (modelId: string) => ipcRenderer.invoke('download-model', modelId),
|
||||
cancelDownload: (modelId: string) => ipcRenderer.invoke('cancel-download', modelId),
|
||||
deleteModel: (modelId: string) => ipcRenderer.invoke('delete-model', modelId),
|
||||
getModelsDirectory: () => ipcRenderer.invoke('get-models-directory'),
|
||||
getAvailableModels: () => ipcRenderer.invoke("get-available-models"),
|
||||
getDownloadedModels: () => ipcRenderer.invoke("get-downloaded-models"),
|
||||
isModelDownloaded: (modelId: string) =>
|
||||
ipcRenderer.invoke("is-model-downloaded", modelId),
|
||||
getDownloadProgress: (modelId: string) =>
|
||||
ipcRenderer.invoke("get-download-progress", modelId),
|
||||
getActiveDownloads: () => ipcRenderer.invoke("get-active-downloads"),
|
||||
downloadModel: (modelId: string) =>
|
||||
ipcRenderer.invoke("download-model", modelId),
|
||||
cancelDownload: (modelId: string) =>
|
||||
ipcRenderer.invoke("cancel-download", modelId),
|
||||
deleteModel: (modelId: string) => ipcRenderer.invoke("delete-model", modelId),
|
||||
getModelsDirectory: () => ipcRenderer.invoke("get-models-directory"),
|
||||
|
||||
// Local Whisper API
|
||||
isLocalWhisperAvailable: () => ipcRenderer.invoke('is-local-whisper-available'),
|
||||
getLocalWhisperModels: () => ipcRenderer.invoke('get-local-whisper-models'),
|
||||
getSelectedModel: () => ipcRenderer.invoke('get-selected-model'),
|
||||
setSelectedModel: (modelId: string) => ipcRenderer.invoke('set-selected-model', modelId),
|
||||
isLocalWhisperAvailable: () =>
|
||||
ipcRenderer.invoke("is-local-whisper-available"),
|
||||
getLocalWhisperModels: () => ipcRenderer.invoke("get-local-whisper-models"),
|
||||
getSelectedModel: () => ipcRenderer.invoke("get-selected-model"),
|
||||
setSelectedModel: (modelId: string) =>
|
||||
ipcRenderer.invoke("set-selected-model", modelId),
|
||||
setWhisperExecutablePath: (path: string) =>
|
||||
ipcRenderer.invoke('set-whisper-executable-path', path),
|
||||
ipcRenderer.invoke("set-whisper-executable-path", path),
|
||||
|
||||
// Formatter Configuration API
|
||||
getFormatterConfig: () => ipcRenderer.invoke('get-formatter-config'),
|
||||
getFormatterConfig: () => ipcRenderer.invoke("get-formatter-config"),
|
||||
setFormatterConfig: (config: FormatterConfig) =>
|
||||
ipcRenderer.invoke('set-formatter-config', config),
|
||||
ipcRenderer.invoke("set-formatter-config", config),
|
||||
|
||||
// Transcription Database API
|
||||
getTranscriptions: (options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: 'timestamp' | 'createdAt';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
sortBy?: "timestamp" | "createdAt";
|
||||
sortOrder?: "asc" | "desc";
|
||||
search?: string;
|
||||
}) => ipcRenderer.invoke('get-transcriptions', options),
|
||||
getTranscriptionById: (id: number) => ipcRenderer.invoke('get-transcription-by-id', id),
|
||||
createTranscription: (data: Omit<NewTranscription, 'id' | 'createdAt' | 'updatedAt'>) =>
|
||||
ipcRenderer.invoke('create-transcription', data),
|
||||
updateTranscription: (id: number, data: Partial<Omit<Transcription, 'id' | 'createdAt'>>) =>
|
||||
ipcRenderer.invoke('update-transcription', id, data),
|
||||
deleteTranscription: (id: number) => ipcRenderer.invoke('delete-transcription', id),
|
||||
}) => ipcRenderer.invoke("get-transcriptions", options),
|
||||
getTranscriptionById: (id: number) =>
|
||||
ipcRenderer.invoke("get-transcription-by-id", id),
|
||||
createTranscription: (
|
||||
data: Omit<NewTranscription, "id" | "createdAt" | "updatedAt">,
|
||||
) => ipcRenderer.invoke("create-transcription", data),
|
||||
updateTranscription: (
|
||||
id: number,
|
||||
data: Partial<Omit<Transcription, "id" | "createdAt">>,
|
||||
) => ipcRenderer.invoke("update-transcription", id, data),
|
||||
deleteTranscription: (id: number) =>
|
||||
ipcRenderer.invoke("delete-transcription", id),
|
||||
getTranscriptionsCount: (search?: string) =>
|
||||
ipcRenderer.invoke('get-transcriptions-count', search),
|
||||
ipcRenderer.invoke("get-transcriptions-count", search),
|
||||
searchTranscriptions: (searchTerm: string, limit?: number) =>
|
||||
ipcRenderer.invoke('search-transcriptions', searchTerm, limit),
|
||||
|
||||
ipcRenderer.invoke("search-transcriptions", searchTerm, limit),
|
||||
|
||||
// Vocabulary Database API
|
||||
on: (channel: string, callback: (...args: any[]) => void) => {
|
||||
const handler = (_event: IpcRendererEvent, ...args: any[]) => callback(...args);
|
||||
const handler = (_event: IpcRendererEvent, ...args: any[]) =>
|
||||
callback(...args);
|
||||
ipcRenderer.on(channel, handler);
|
||||
// Store the handler mapping for proper cleanup
|
||||
if (!(window as any).__electronEventHandlers) {
|
||||
|
|
@ -109,7 +128,9 @@ const api: ElectronAPI = {
|
|||
if (!(window as any).__electronEventHandlers.has(channel)) {
|
||||
(window as any).__electronEventHandlers.set(channel, []);
|
||||
}
|
||||
(window as any).__electronEventHandlers.get(channel).push({ original: callback, handler });
|
||||
(window as any).__electronEventHandlers
|
||||
.get(channel)
|
||||
.push({ original: callback, handler });
|
||||
},
|
||||
off: (channel: string, callback: (...args: any[]) => void) => {
|
||||
if (
|
||||
|
|
@ -136,9 +157,9 @@ const api: ElectronAPI = {
|
|||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', api);
|
||||
contextBridge.exposeInMainWorld("electronAPI", api);
|
||||
|
||||
// Expose tRPC for electron-trpc-experimental
|
||||
process.once('loaded', async () => {
|
||||
process.once("loaded", async () => {
|
||||
exposeElectronTRPC();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { autoUpdater } from 'electron-updater';
|
||||
import { app, dialog, BrowserWindow } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { app, dialog, BrowserWindow } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class AutoUpdaterService extends EventEmitter {
|
||||
private checkingForUpdate = false;
|
||||
|
|
@ -10,12 +10,12 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
||||
// Only set up auto-updater in production
|
||||
if (process.env.NODE_ENV !== 'development' && app.isPackaged) {
|
||||
if (process.env.NODE_ENV !== "development" && app.isPackaged) {
|
||||
this.setupAutoUpdater();
|
||||
} else {
|
||||
logger.updater.info('Auto-updater disabled in development mode');
|
||||
logger.updater.info("Auto-updater disabled in development mode");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,20 +29,20 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
// Development settings
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// In development, you can test with a local update server
|
||||
// autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml');
|
||||
autoUpdater.forceDevUpdateConfig = true;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.updater.info('Checking for update...');
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
logger.updater.info("Checking for update...");
|
||||
this.checkingForUpdate = true;
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
logger.updater.info('Update available', {
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
logger.updater.info("Update available", {
|
||||
version: info.version,
|
||||
releaseDate: info.releaseDate,
|
||||
});
|
||||
|
|
@ -51,36 +51,39 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
this.showUpdateDialog(info);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
logger.updater.info('Update not available', { version: info.version });
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
logger.updater.info("Update not available", { version: info.version });
|
||||
this.checkingForUpdate = false;
|
||||
this.updateAvailable = false;
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
logger.updater.error('Error in auto-updater', { error: err.message });
|
||||
autoUpdater.on("error", (err) => {
|
||||
logger.updater.error("Error in auto-updater", { error: err.message });
|
||||
this.checkingForUpdate = false;
|
||||
|
||||
|
||||
// Show error dialog only if user manually checked for updates
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
dialog.showErrorBox('Update Error', `Error checking for updates: ${err.message}`);
|
||||
dialog.showErrorBox(
|
||||
"Update Error",
|
||||
`Error checking for updates: ${err.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progressObj) => {
|
||||
logger.updater.info('Download progress', {
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
logger.updater.info("Download progress", {
|
||||
bytesPerSecond: progressObj.bytesPerSecond,
|
||||
percent: progressObj.percent,
|
||||
transferred: progressObj.transferred,
|
||||
total: progressObj.total,
|
||||
});
|
||||
|
||||
|
||||
// Emit event for tRPC subscription
|
||||
this.emit('download-progress', progressObj);
|
||||
this.emit("download-progress", progressObj);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
logger.updater.info('Update downloaded', { version: info.version });
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
logger.updater.info("Update downloaded", { version: info.version });
|
||||
this.showInstallDialog(info);
|
||||
});
|
||||
}
|
||||
|
|
@ -91,20 +94,21 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
}
|
||||
|
||||
const result = await dialog.showMessageBox(this.mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Update Available',
|
||||
type: "info",
|
||||
title: "Update Available",
|
||||
message: `A new version (${info.version}) is available.`,
|
||||
detail: 'Would you like to download it now? The update will be installed when you restart the app.',
|
||||
buttons: ['Download Now', 'Later'],
|
||||
detail:
|
||||
"Would you like to download it now? The update will be installed when you restart the app.",
|
||||
buttons: ["Download Now", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
logger.updater.info('User chose to download update');
|
||||
logger.updater.info("User chose to download update");
|
||||
autoUpdater.downloadUpdate();
|
||||
} else {
|
||||
logger.updater.info('User chose to skip update');
|
||||
logger.updater.info("User chose to skip update");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,69 +118,75 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
}
|
||||
|
||||
const result = await dialog.showMessageBox(this.mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Update Ready',
|
||||
type: "info",
|
||||
title: "Update Ready",
|
||||
message: `Update ${info.version} has been downloaded.`,
|
||||
detail: 'The update will be installed when you restart the app. Would you like to restart now?',
|
||||
buttons: ['Restart Now', 'Later'],
|
||||
detail:
|
||||
"The update will be installed when you restart the app. Would you like to restart now?",
|
||||
buttons: ["Restart Now", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
logger.updater.info('User chose to restart and install update');
|
||||
logger.updater.info("User chose to restart and install update");
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
logger.updater.info('User chose to install update later');
|
||||
logger.updater.info("User chose to install update later");
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdates(userInitiated = false): Promise<void> {
|
||||
// Skip in development
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
logger.updater.info('Skipping update check in development mode');
|
||||
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
||||
logger.updater.info("Skipping update check in development mode");
|
||||
if (userInitiated && this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
dialog.showMessageBox(this.mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Development Mode',
|
||||
message: 'Update checking is disabled in development mode.',
|
||||
buttons: ['OK']
|
||||
type: "info",
|
||||
title: "Development Mode",
|
||||
message: "Update checking is disabled in development mode.",
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checkingForUpdate) {
|
||||
logger.updater.info('Already checking for updates');
|
||||
logger.updater.info("Already checking for updates");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.updater.info('Starting update check', { userInitiated });
|
||||
logger.updater.info("Starting update check", { userInitiated });
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (error) {
|
||||
logger.updater.error('Failed to check for updates', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
logger.updater.error("Failed to check for updates", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
|
||||
if (userInitiated && this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
dialog.showErrorBox('Update Check Failed', 'Failed to check for updates. Please try again later.');
|
||||
dialog.showErrorBox(
|
||||
"Update Check Failed",
|
||||
"Failed to check for updates. Please try again later.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdatesAndNotify(): Promise<void> {
|
||||
// Skip in development
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
logger.updater.info('Skipping background update check in development mode');
|
||||
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
||||
logger.updater.info(
|
||||
"Skipping background update check in development mode",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await autoUpdater.checkForUpdatesAndNotify();
|
||||
} catch (error) {
|
||||
logger.updater.error('Failed to check for updates and notify', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
logger.updater.error("Failed to check for updates and notify", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -191,20 +201,20 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
|
||||
async downloadUpdate(): Promise<void> {
|
||||
// Skip in development
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
logger.updater.info('Skipping update download in development mode');
|
||||
throw new Error('Update downloads are disabled in development mode');
|
||||
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
||||
logger.updater.info("Skipping update download in development mode");
|
||||
throw new Error("Update downloads are disabled in development mode");
|
||||
}
|
||||
|
||||
if (!this.updateAvailable) {
|
||||
throw new Error('No update available to download');
|
||||
throw new Error("No update available to download");
|
||||
}
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
} catch (error) {
|
||||
logger.updater.error('Failed to download update', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
logger.updater.error("Failed to download update", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -212,11 +222,11 @@ export class AutoUpdaterService extends EventEmitter {
|
|||
|
||||
quitAndInstall(): void {
|
||||
// Skip in development
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
logger.updater.info('Skipping quit and install in development mode');
|
||||
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
||||
logger.updater.info("Skipping quit and install in development mode");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process'; // Added import for process
|
||||
import { app, app as electronApp } from 'electron'; // electronApp for app.getAppPath() consistency
|
||||
import split2 from 'split2';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import process from "node:process"; // Added import for process
|
||||
import { app, app as electronApp } from "electron"; // electronApp for app.getAppPath() consistency
|
||||
import split2 from "split2";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { createScopedLogger } from './logger';
|
||||
import { EventEmitter } from "events";
|
||||
import { createScopedLogger } from "./logger";
|
||||
import {
|
||||
RpcRequestSchema,
|
||||
RpcRequest,
|
||||
|
|
@ -25,7 +25,7 @@ import {
|
|||
MuteSystemAudioResult,
|
||||
RestoreSystemAudioParams,
|
||||
RestoreSystemAudioResult,
|
||||
} from '@amical/types';
|
||||
} from "@amical/types";
|
||||
|
||||
// Define the interface for RPC methods
|
||||
interface RPCMethods {
|
||||
|
|
@ -63,9 +63,12 @@ interface SwiftIOBridgeEvents {
|
|||
|
||||
export class SwiftIOBridge extends EventEmitter {
|
||||
private proc: ChildProcessWithoutNullStreams | null = null;
|
||||
private pending = new Map<string, { callback: (resp: RpcResponse) => void; startTime: number }>();
|
||||
private pending = new Map<
|
||||
string,
|
||||
{ callback: (resp: RpcResponse) => void; startTime: number }
|
||||
>();
|
||||
private helperPath: string;
|
||||
private logger = createScopedLogger('swift-bridge');
|
||||
private logger = createScopedLogger("swift-bridge");
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -74,18 +77,18 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
}
|
||||
|
||||
private determineHelperPath(): string {
|
||||
const helperName = 'SwiftHelper'; // Swift native helper executable
|
||||
const helperName = "SwiftHelper"; // Swift native helper executable
|
||||
return electronApp.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin', helperName)
|
||||
? path.join(process.resourcesPath, "bin", helperName)
|
||||
: path.join(
|
||||
electronApp.getAppPath(),
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
'native-helpers',
|
||||
'swift-helper',
|
||||
'bin',
|
||||
helperName
|
||||
"..",
|
||||
"..",
|
||||
"packages",
|
||||
"native-helpers",
|
||||
"swift-helper",
|
||||
"bin",
|
||||
helperName,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -93,27 +96,27 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
try {
|
||||
fs.accessSync(this.helperPath, fs.constants.X_OK);
|
||||
} catch (err) {
|
||||
this.logger.error('SwiftHelper executable not found or not executable', {
|
||||
this.logger.error("SwiftHelper executable not found or not executable", {
|
||||
helperPath: this.helperPath,
|
||||
});
|
||||
this.emit(
|
||||
'error',
|
||||
"error",
|
||||
new Error(
|
||||
`Helper executable not found at ${this.helperPath}. Attempt to build it if in dev mode.`
|
||||
)
|
||||
`Helper executable not found at ${this.helperPath}. Attempt to build it if in dev mode.`,
|
||||
),
|
||||
);
|
||||
// In a real app, you might try to build it here or provide more robust error handling.
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Spawning SwiftHelper', { helperPath: this.helperPath });
|
||||
this.proc = spawn(this.helperPath, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
this.logger.info("Spawning SwiftHelper", { helperPath: this.helperPath });
|
||||
this.proc = spawn(this.helperPath, [], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
|
||||
this.proc.stdout.pipe(split2()).on('data', (line: string) => {
|
||||
this.proc.stdout.pipe(split2()).on("data", (line: string) => {
|
||||
if (!line.trim()) return; // Ignore empty lines
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
this.logger.debug('Received message from helper', { message });
|
||||
this.logger.debug("Received message from helper", { message });
|
||||
|
||||
// Try to parse as RpcResponse first
|
||||
const responseValidation = RpcResponseSchema.safeParse(message);
|
||||
|
|
@ -130,52 +133,57 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
const eventValidation = HelperEventSchema.safeParse(message);
|
||||
if (eventValidation.success) {
|
||||
const helperEvent = eventValidation.data;
|
||||
this.emit('helperEvent', helperEvent);
|
||||
this.emit("helperEvent", helperEvent);
|
||||
return; // Handled as a helper event
|
||||
}
|
||||
|
||||
// If it's neither a recognized RPC response nor a helper event
|
||||
this.logger.warn('Received unknown message from helper', { message });
|
||||
this.logger.warn("Received unknown message from helper", { message });
|
||||
} catch (e) {
|
||||
this.logger.error('Error parsing JSON from helper', { error: e, line });
|
||||
this.emit('error', new Error(`Error parsing JSON from helper: ${line}`));
|
||||
this.logger.error("Error parsing JSON from helper", { error: e, line });
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(`Error parsing JSON from helper: ${line}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.proc.stderr.on('data', (data: Buffer) => {
|
||||
this.proc.stderr.on("data", (data: Buffer) => {
|
||||
const errorMsg = data.toString();
|
||||
this.logger.warn('SwiftHelper stderr output', { message: errorMsg });
|
||||
this.logger.warn("SwiftHelper stderr output", { message: errorMsg });
|
||||
// Don't emit as error since stderr is often just debug info
|
||||
// this.emit('error', new Error(`Helper stderr: ${errorMsg}`));
|
||||
});
|
||||
|
||||
this.proc.on('error', (err) => {
|
||||
this.logger.error('Failed to start SwiftHelper process', { error: err });
|
||||
this.emit('error', err);
|
||||
this.proc.on("error", (err) => {
|
||||
this.logger.error("Failed to start SwiftHelper process", { error: err });
|
||||
this.emit("error", err);
|
||||
this.proc = null;
|
||||
});
|
||||
|
||||
this.proc.on('close', (code, signal) => {
|
||||
this.logger.info('SwiftHelper process exited', { code, signal });
|
||||
this.emit('close', code, signal);
|
||||
this.proc.on("close", (code, signal) => {
|
||||
this.logger.info("SwiftHelper process exited", { code, signal });
|
||||
this.emit("close", code, signal);
|
||||
this.proc = null;
|
||||
// Optionally, implement retry logic or notify further
|
||||
});
|
||||
|
||||
process.nextTick(() => {
|
||||
this.emit('ready'); // Emit ready on next tick
|
||||
this.emit("ready"); // Emit ready on next tick
|
||||
});
|
||||
this.logger.info('Helper process started and listeners attached');
|
||||
this.logger.info("Helper process started and listeners attached");
|
||||
}
|
||||
|
||||
public call<M extends keyof RPCMethods>(
|
||||
method: M,
|
||||
params: RPCMethods[M]['params'],
|
||||
timeoutMs = 5000
|
||||
): Promise<RPCMethods[M]['result']> {
|
||||
params: RPCMethods[M]["params"],
|
||||
timeoutMs = 5000,
|
||||
): Promise<RPCMethods[M]["result"]> {
|
||||
if (!this.proc || !this.proc.stdin || !this.proc.stdin.writable) {
|
||||
return Promise.reject(
|
||||
new Error('Swift helper process is not running or stdin is not writable.')
|
||||
new Error(
|
||||
"Swift helper process is not running or stdin is not writable.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -186,61 +194,69 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
// Validate request payload before sending
|
||||
const validationResult = RpcRequestSchema.safeParse(requestPayload);
|
||||
if (!validationResult.success) {
|
||||
this.logger.error('Invalid RPC request payload', {
|
||||
this.logger.error("Invalid RPC request payload", {
|
||||
method,
|
||||
error: validationResult.error.flatten(),
|
||||
});
|
||||
return Promise.reject(
|
||||
new Error(`Invalid RPC request payload: ${validationResult.error.message}`)
|
||||
new Error(
|
||||
`Invalid RPC request payload: ${validationResult.error.message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug('Sending RPC request', {
|
||||
this.logger.debug("Sending RPC request", {
|
||||
method,
|
||||
id,
|
||||
startedAt: new Date(startTime).toISOString(),
|
||||
});
|
||||
this.proc.stdin.write(JSON.stringify(requestPayload) + '\n', (err) => {
|
||||
this.proc.stdin.write(JSON.stringify(requestPayload) + "\n", (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error writing to helper stdin', { method, id, error: err });
|
||||
this.logger.error("Error writing to helper stdin", {
|
||||
method,
|
||||
id,
|
||||
error: err,
|
||||
});
|
||||
// Note: The promise might have already been set up, consider how to reject it.
|
||||
// For now, this error will be logged. The timeout will eventually reject.
|
||||
} else {
|
||||
this.logger.debug('Successfully sent RPC request', { method, id });
|
||||
this.logger.debug("Successfully sent RPC request", { method, id });
|
||||
}
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<RPCMethods[M]['result']>((resolve, reject) => {
|
||||
this.pending.set(id, {
|
||||
callback: (resp: RpcResponse) => {
|
||||
this.pending.delete(id); // Clean up immediately
|
||||
const completedAt = Date.now();
|
||||
const duration = completedAt - startTime;
|
||||
const responsePromise = new Promise<RPCMethods[M]["result"]>(
|
||||
(resolve, reject) => {
|
||||
this.pending.set(id, {
|
||||
callback: (resp: RpcResponse) => {
|
||||
this.pending.delete(id); // Clean up immediately
|
||||
const completedAt = Date.now();
|
||||
const duration = completedAt - startTime;
|
||||
|
||||
if (resp.error) {
|
||||
const error = new Error(resp.error.message);
|
||||
(error as any).code = resp.error.code;
|
||||
(error as any).data = resp.error.data;
|
||||
reject(error);
|
||||
} else {
|
||||
// Log the raw resp.result with timing information
|
||||
this.logger.debug('Raw RPC response result received', {
|
||||
method,
|
||||
id,
|
||||
result: resp.result,
|
||||
startedAt: new Date(startTime).toISOString(),
|
||||
completedAt: new Date(completedAt).toISOString(),
|
||||
durationMs: duration,
|
||||
});
|
||||
// Here, we might need to validate resp.result against the specific method's result schema
|
||||
// For now, casting as any, but for type safety, validation is better.
|
||||
// Example: const resultValidation = RPCMethods[method].resultSchema.safeParse(resp.result);
|
||||
resolve(resp.result as any);
|
||||
}
|
||||
},
|
||||
startTime,
|
||||
});
|
||||
});
|
||||
if (resp.error) {
|
||||
const error = new Error(resp.error.message);
|
||||
(error as any).code = resp.error.code;
|
||||
(error as any).data = resp.error.data;
|
||||
reject(error);
|
||||
} else {
|
||||
// Log the raw resp.result with timing information
|
||||
this.logger.debug("Raw RPC response result received", {
|
||||
method,
|
||||
id,
|
||||
result: resp.result,
|
||||
startedAt: new Date(startTime).toISOString(),
|
||||
completedAt: new Date(completedAt).toISOString(),
|
||||
durationMs: duration,
|
||||
});
|
||||
// Here, we might need to validate resp.result against the specific method's result schema
|
||||
// For now, casting as any, but for type safety, validation is better.
|
||||
// Example: const resultValidation = RPCMethods[method].resultSchema.safeParse(resp.result);
|
||||
resolve(resp.result as any);
|
||||
}
|
||||
},
|
||||
startTime,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -251,8 +267,8 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
const duration = timedOutAt - startTime;
|
||||
reject(
|
||||
new Error(
|
||||
`SwiftIOBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`
|
||||
)
|
||||
`SwiftIOBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
|
@ -267,14 +283,17 @@ export class SwiftIOBridge extends EventEmitter {
|
|||
|
||||
public stopHelper(): void {
|
||||
if (this.proc) {
|
||||
this.logger.info('Stopping SwiftHelper process');
|
||||
this.logger.info("Stopping SwiftHelper process");
|
||||
this.proc.kill();
|
||||
this.proc = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Typed event emitter methods
|
||||
on<E extends keyof SwiftIOBridgeEvents>(event: E, listener: SwiftIOBridgeEvents[E]): this {
|
||||
on<E extends keyof SwiftIOBridgeEvents>(
|
||||
event: E,
|
||||
listener: SwiftIOBridgeEvents[E],
|
||||
): this {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue