642 lines
19 KiB
TypeScript
642 lines
19 KiB
TypeScript
import { observable } from "@trpc/server/observable";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
import { app } from "electron";
|
|
import path from "node:path";
|
|
import { createRouter, procedure } from "../trpc";
|
|
import { dbPath, closeDatabase } from "../../db";
|
|
import * as fs from "fs/promises";
|
|
|
|
// FormatterConfig schema
|
|
const FormatterConfigSchema = z.object({
|
|
enabled: z.boolean(),
|
|
});
|
|
|
|
// Shortcut schema (array of key names)
|
|
const SetShortcutSchema = z.object({
|
|
type: z.enum(["pushToTalk", "toggleRecording"]),
|
|
shortcut: z.array(z.string()),
|
|
});
|
|
|
|
// Model providers schemas
|
|
const OpenRouterConfigSchema = z.object({
|
|
apiKey: z.string(),
|
|
});
|
|
|
|
const OllamaConfigSchema = z.object({
|
|
url: z.string().url().or(z.literal("")),
|
|
});
|
|
|
|
const ModelProvidersConfigSchema = z.object({
|
|
openRouter: OpenRouterConfigSchema.optional(),
|
|
ollama: OllamaConfigSchema.optional(),
|
|
});
|
|
|
|
const DictationSettingsSchema = z.object({
|
|
autoDetectEnabled: z.boolean(),
|
|
selectedLanguage: z.string().min(1), // Must be valid when autoDetectEnabled is false
|
|
});
|
|
|
|
const AppPreferencesSchema = z.object({
|
|
launchAtLogin: z.boolean().optional(),
|
|
minimizeToTray: z.boolean().optional(),
|
|
showWidgetWhileInactive: z.boolean().optional(),
|
|
showInDock: z.boolean().optional(),
|
|
});
|
|
|
|
const UIThemeSchema = z.object({
|
|
theme: z.enum(["light", "dark", "system"]),
|
|
});
|
|
|
|
export const settingsRouter = createRouter({
|
|
// Get all settings
|
|
getSettings: procedure.query(async ({ ctx }) => {
|
|
try {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getAllSettings();
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error getting settings:", error);
|
|
}
|
|
return {};
|
|
}
|
|
}),
|
|
|
|
// Update transcription settings
|
|
updateTranscriptionSettings: procedure
|
|
.input(
|
|
z.object({
|
|
language: z.string().optional(),
|
|
autoTranscribe: z.boolean().optional(),
|
|
confidenceThreshold: z.number().optional(),
|
|
enablePunctuation: z.boolean().optional(),
|
|
enableTimestamps: z.boolean().optional(),
|
|
preloadWhisperModel: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
// Check if preloadWhisperModel setting is changing
|
|
const currentSettings =
|
|
await settingsService.getTranscriptionSettings();
|
|
const preloadChanged =
|
|
input.preloadWhisperModel !== undefined &&
|
|
currentSettings &&
|
|
input.preloadWhisperModel !== currentSettings.preloadWhisperModel;
|
|
|
|
// Merge with existing settings to provide all required fields
|
|
const mergedSettings = {
|
|
language: "en",
|
|
autoTranscribe: true,
|
|
confidenceThreshold: 0.5,
|
|
enablePunctuation: true,
|
|
enableTimestamps: false,
|
|
...currentSettings,
|
|
...input,
|
|
};
|
|
|
|
await settingsService.setTranscriptionSettings(mergedSettings);
|
|
|
|
// Handle model preloading change
|
|
if (preloadChanged) {
|
|
const transcriptionService = ctx.serviceManager.getService(
|
|
"transcriptionService",
|
|
);
|
|
if (transcriptionService) {
|
|
await transcriptionService.handleModelChange();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error updating transcription settings:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Get formatter configuration
|
|
getFormatterConfig: procedure.query(async ({ ctx }) => {
|
|
try {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getFormatterConfig();
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.transcription.error("Error getting formatter config:", error);
|
|
}
|
|
return null;
|
|
}
|
|
}),
|
|
|
|
// Set formatter configuration
|
|
setFormatterConfig: procedure
|
|
.input(FormatterConfigSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
await settingsService.setFormatterConfig(input);
|
|
return true;
|
|
}),
|
|
// Get shortcuts configuration
|
|
getShortcuts: procedure.query(async ({ ctx }) => {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getShortcuts();
|
|
}),
|
|
// Set individual shortcut
|
|
setShortcut: procedure
|
|
.input(SetShortcutSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const shortcutManager = ctx.serviceManager.getService("shortcutManager");
|
|
if (!shortcutManager) {
|
|
throw new TRPCError({
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
message: "ShortcutManager not available",
|
|
});
|
|
}
|
|
|
|
const result = await shortcutManager.setShortcut(
|
|
input.type,
|
|
input.shortcut,
|
|
);
|
|
|
|
if (!result.valid) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: result.error || "Invalid shortcut",
|
|
});
|
|
}
|
|
|
|
return { success: true, warning: result.warning };
|
|
}),
|
|
|
|
// Set shortcut recording state
|
|
setShortcutRecordingState: procedure
|
|
.input(z.boolean())
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const shortcutManager =
|
|
ctx.serviceManager.getService("shortcutManager");
|
|
if (!shortcutManager) {
|
|
throw new Error("ShortcutManager not available");
|
|
}
|
|
|
|
shortcutManager.setIsRecordingShortcut(input);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Shortcut recording state updated", {
|
|
isRecording: input,
|
|
});
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting shortcut recording state:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Active keys subscription for shortcut recording
|
|
activeKeysUpdates: procedure.subscription(({ ctx }) => {
|
|
return observable<string[]>((emit) => {
|
|
const shortcutManager = ctx.serviceManager.getService("shortcutManager");
|
|
const logger = ctx.serviceManager.getLogger();
|
|
|
|
if (!shortcutManager) {
|
|
logger?.main.warn(
|
|
"ShortcutManager not available for activeKeys subscription",
|
|
);
|
|
emit.next([]);
|
|
return () => {};
|
|
}
|
|
|
|
// Emit initial state
|
|
emit.next(shortcutManager.getActiveKeys());
|
|
|
|
// Set up listener for changes
|
|
const handleActiveKeysChanged = (keys: string[]) => {
|
|
emit.next(keys);
|
|
};
|
|
|
|
shortcutManager.on("activeKeysChanged", handleActiveKeysChanged);
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
shortcutManager.off("activeKeysChanged", handleActiveKeysChanged);
|
|
};
|
|
});
|
|
}),
|
|
|
|
// Set preferred microphone
|
|
setPreferredMicrophone: procedure
|
|
.input(
|
|
z.object({
|
|
deviceName: z.string().nullable(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
// Get current recording settings
|
|
const currentSettings = await settingsService.getRecordingSettings();
|
|
|
|
// Merge with new microphone preference
|
|
const updatedSettings = {
|
|
defaultFormat: "wav" as const,
|
|
sampleRate: 16000 as const,
|
|
autoStopSilence: false,
|
|
silenceThreshold: 0.1,
|
|
maxRecordingDuration: 300,
|
|
...currentSettings,
|
|
preferredMicrophoneName: input.deviceName || undefined,
|
|
};
|
|
|
|
await settingsService.setRecordingSettings(updatedSettings);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Preferred microphone updated:", input.deviceName);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting preferred microphone:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Get app version
|
|
getAppVersion: procedure.query(() => {
|
|
return app.getVersion();
|
|
}),
|
|
|
|
// Get dictation settings
|
|
getDictationSettings: procedure.query(async ({ ctx }) => {
|
|
try {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
const allSettings = await settingsService.getAllSettings();
|
|
return (
|
|
allSettings.dictation || {
|
|
autoDetectEnabled: true,
|
|
selectedLanguage: "en",
|
|
}
|
|
);
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error getting dictation settings:", error);
|
|
}
|
|
return {
|
|
autoDetectEnabled: true,
|
|
selectedLanguage: "en",
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Set dictation settings
|
|
setDictationSettings: procedure
|
|
.input(DictationSettingsSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
// Validation: if autoDetectEnabled is false, ensure selectedLanguage is valid
|
|
if (
|
|
!input.autoDetectEnabled &&
|
|
(!input.selectedLanguage || input.selectedLanguage === "auto")
|
|
) {
|
|
throw new Error(
|
|
"Selected language must be specified when auto-detect is disabled",
|
|
);
|
|
}
|
|
|
|
// Set default to "en" if switching from auto-detect enabled to disabled with invalid language
|
|
let selectedLanguage = input.selectedLanguage;
|
|
if (
|
|
!input.autoDetectEnabled &&
|
|
(!selectedLanguage || selectedLanguage === "auto")
|
|
) {
|
|
selectedLanguage = "en";
|
|
}
|
|
|
|
const dictationSettings = {
|
|
autoDetectEnabled: input.autoDetectEnabled,
|
|
selectedLanguage,
|
|
};
|
|
|
|
await settingsService.setDictationSettings(dictationSettings);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Dictation settings updated:", dictationSettings);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting dictation settings:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Get model providers configuration
|
|
getModelProvidersConfig: procedure.query(async ({ ctx }) => {
|
|
try {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getModelProvidersConfig();
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error getting model providers config:", error);
|
|
}
|
|
return null;
|
|
}
|
|
}),
|
|
|
|
// Set model providers configuration
|
|
setModelProvidersConfig: procedure
|
|
.input(ModelProvidersConfigSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
await settingsService.setModelProvidersConfig(input);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Model providers configuration updated");
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting model providers config:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Set OpenRouter configuration
|
|
setOpenRouterConfig: procedure
|
|
.input(OpenRouterConfigSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
await settingsService.setOpenRouterConfig(input);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("OpenRouter configuration updated");
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting OpenRouter config:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Set Ollama configuration
|
|
setOllamaConfig: procedure
|
|
.input(OllamaConfigSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const settingsService =
|
|
ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
await settingsService.setOllamaConfig(input);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Ollama configuration updated");
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error setting Ollama config:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Get data path
|
|
getDataPath: procedure.query(() => {
|
|
return app.getPath("userData");
|
|
}),
|
|
|
|
// Get app preferences (launch at login, minimize to tray, etc.)
|
|
getPreferences: procedure.query(async ({ ctx }) => {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getPreferences();
|
|
}),
|
|
|
|
// Update app preferences
|
|
updatePreferences: procedure
|
|
.input(AppPreferencesSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
await settingsService.setPreferences(input);
|
|
// Window updates are handled via settings events in AppManager
|
|
|
|
return true;
|
|
}),
|
|
|
|
// Update UI theme
|
|
updateUITheme: procedure
|
|
.input(UIThemeSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
|
|
// Get current UI settings
|
|
const currentUISettings = await settingsService.getUISettings();
|
|
|
|
// Update with new theme
|
|
await settingsService.setUISettings({
|
|
...currentUISettings,
|
|
theme: input.theme,
|
|
});
|
|
// Window updates are handled via settings events in AppManager
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("UI theme updated", { theme: input.theme });
|
|
}
|
|
|
|
return true;
|
|
}),
|
|
|
|
// Get telemetry settings
|
|
getTelemetrySettings: procedure.query(async ({ ctx }) => {
|
|
try {
|
|
const settingsService = ctx.serviceManager.getService("settingsService");
|
|
if (!settingsService) {
|
|
throw new Error("SettingsService not available");
|
|
}
|
|
return await settingsService.getTelemetrySettings();
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error getting telemetry settings:", error);
|
|
}
|
|
return { enabled: true };
|
|
}
|
|
}),
|
|
|
|
// Update telemetry settings
|
|
updateTelemetrySettings: procedure
|
|
.input(
|
|
z.object({
|
|
enabled: z.boolean(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const telemetryService =
|
|
ctx.serviceManager.getService("telemetryService");
|
|
if (!telemetryService) {
|
|
throw new Error("TelemetryService not available");
|
|
}
|
|
|
|
// Update the telemetry service state
|
|
await telemetryService.setEnabled(input.enabled);
|
|
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Telemetry settings updated", {
|
|
enabled: input.enabled,
|
|
});
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error updating telemetry settings:", error);
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
// Reset app - deletes database and models, then restarts
|
|
resetApp: procedure.mutation(async ({ ctx }) => {
|
|
try {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.info("Resetting app - deleting database and models");
|
|
}
|
|
|
|
// Close database connection before deleting
|
|
await closeDatabase();
|
|
|
|
// Add a small delay to ensure the connection is fully closed on Windows
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const userDataPath = app.getPath("userData");
|
|
|
|
// Delete database files (main db + WAL/SHM files)
|
|
const dbFile = path.join(userDataPath, "amical.db");
|
|
await fs.rm(dbFile, { force: true }).catch(() => {});
|
|
await fs.rm(`${dbFile}-wal`, { force: true }).catch(() => {});
|
|
await fs.rm(`${dbFile}-shm`, { force: true }).catch(() => {});
|
|
|
|
// Delete models directory
|
|
const modelsDir = path.join(userDataPath, "models");
|
|
await fs.rm(modelsDir, { recursive: true, force: true }).catch(() => {});
|
|
|
|
// In development, also delete the local db file if it exists
|
|
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
|
try {
|
|
await fs.unlink(dbPath);
|
|
} catch {
|
|
// Ignore if file doesn't exist
|
|
}
|
|
}
|
|
|
|
// Handle restart differently in development vs production
|
|
if (process.env.NODE_ENV === "development" || !app.isPackaged) {
|
|
//! restarting will not work properly in dev mode
|
|
app.quit();
|
|
} else {
|
|
// Production mode: relaunch the app
|
|
app.relaunch();
|
|
app.quit();
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
const logger = ctx.serviceManager.getLogger();
|
|
if (logger) {
|
|
logger.main.error("Error resetting app:", error);
|
|
}
|
|
throw new Error("Failed to reset app");
|
|
}
|
|
}),
|
|
});
|
|
// This comment prevents prettier from removing the trailing newline
|