markupr 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingsManager - Secure Settings Storage for markupr
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Persistent settings storage with electron-store (schema validated)
|
|
6
|
+
* - Secure API key storage with keytar (macOS Keychain, Windows Credential Manager)
|
|
7
|
+
* - Encrypted fallback key storage via safeStorage when keytar is unavailable
|
|
8
|
+
* - Settings migration between versions
|
|
9
|
+
* - Change event emission for reactive updates
|
|
10
|
+
* - IPC handlers for renderer access
|
|
11
|
+
*
|
|
12
|
+
* Security:
|
|
13
|
+
* - keytar uses OS-level secure storage (Keychain, Credential Manager)
|
|
14
|
+
* - fallback secrets are encrypted with safeStorage before disk persistence
|
|
15
|
+
* - plaintext fallback is only used as a last resort when secure storage is unavailable
|
|
16
|
+
* - Settings are validated against schema before saving
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import Store from 'electron-store';
|
|
20
|
+
import * as keytar from 'keytar';
|
|
21
|
+
import { app, ipcMain, safeStorage } from 'electron';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { IPC_CHANNELS, type AppSettings, type HotkeyConfig } from '../../shared/types';
|
|
24
|
+
|
|
25
|
+
// AppSettings is imported from '../../shared/types' (single source of truth)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Settings change callback type
|
|
29
|
+
*/
|
|
30
|
+
type SettingsChangeCallback = (key: string, newValue: unknown, oldValue: unknown) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* SettingsManager interface
|
|
34
|
+
*/
|
|
35
|
+
export interface ISettingsManager {
|
|
36
|
+
// Core
|
|
37
|
+
get<K extends keyof AppSettings>(key: K): AppSettings[K];
|
|
38
|
+
set<K extends keyof AppSettings>(key: K, value: AppSettings[K]): void;
|
|
39
|
+
getAll(): AppSettings;
|
|
40
|
+
reset(): void;
|
|
41
|
+
|
|
42
|
+
// Secure storage (API keys)
|
|
43
|
+
getApiKey(service: string): Promise<string | null>;
|
|
44
|
+
setApiKey(service: string, key: string): Promise<void>;
|
|
45
|
+
deleteApiKey(service: string): Promise<void>;
|
|
46
|
+
hasApiKey(service: string): Promise<boolean>;
|
|
47
|
+
|
|
48
|
+
// Events
|
|
49
|
+
onChange(callback: SettingsChangeCallback): () => void;
|
|
50
|
+
|
|
51
|
+
// Migration
|
|
52
|
+
migrate(): void;
|
|
53
|
+
|
|
54
|
+
// IPC
|
|
55
|
+
registerIpcHandlers(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Constants
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
const KEYTAR_SERVICE = 'com.markupr.app';
|
|
63
|
+
const LEGACY_KEYTAR_SERVICES = ['com.feedbackflow.app', 'feedbackflow'] as const;
|
|
64
|
+
const FALLBACK_SECRET_STORE_NAME = 'secure-keys';
|
|
65
|
+
const LEGACY_INSECURE_SECRET_STORE_KEY = '__plaintext_fallback__';
|
|
66
|
+
const INSECURE_SECRET_PREFIX = 'plaintext:';
|
|
67
|
+
const SETTINGS_VERSION = 2;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default hotkey configuration
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_HOTKEY_CONFIG: HotkeyConfig = {
|
|
73
|
+
toggleRecording: 'CommandOrControl+Shift+F',
|
|
74
|
+
manualScreenshot: 'CommandOrControl+Shift+S',
|
|
75
|
+
pauseResume: 'CommandOrControl+Shift+P',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default settings values
|
|
80
|
+
*/
|
|
81
|
+
const DEFAULT_SETTINGS: AppSettings = {
|
|
82
|
+
// General
|
|
83
|
+
outputDirectory: '', // Set dynamically in constructor
|
|
84
|
+
launchAtLogin: false,
|
|
85
|
+
checkForUpdates: true,
|
|
86
|
+
|
|
87
|
+
// Recording
|
|
88
|
+
defaultCountdown: 0,
|
|
89
|
+
showTranscriptionPreview: true,
|
|
90
|
+
showAudioWaveform: true,
|
|
91
|
+
|
|
92
|
+
// Capture
|
|
93
|
+
pauseThreshold: 1500,
|
|
94
|
+
minTimeBetweenCaptures: 500,
|
|
95
|
+
imageFormat: 'png',
|
|
96
|
+
imageQuality: 85,
|
|
97
|
+
maxImageWidth: 1920,
|
|
98
|
+
|
|
99
|
+
// Transcription
|
|
100
|
+
transcriptionService: 'openai',
|
|
101
|
+
language: 'en',
|
|
102
|
+
enableKeywordTriggers: false,
|
|
103
|
+
|
|
104
|
+
// Hotkeys
|
|
105
|
+
hotkeys: { ...DEFAULT_HOTKEY_CONFIG },
|
|
106
|
+
|
|
107
|
+
// Appearance
|
|
108
|
+
theme: 'system',
|
|
109
|
+
accentColor: '#3B82F6', // Blue-500
|
|
110
|
+
|
|
111
|
+
// Audio
|
|
112
|
+
audioDeviceId: null,
|
|
113
|
+
|
|
114
|
+
// Advanced
|
|
115
|
+
debugMode: false,
|
|
116
|
+
keepAudioBackups: false,
|
|
117
|
+
|
|
118
|
+
// Onboarding
|
|
119
|
+
hasCompletedOnboarding: false,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Schema for electron-store validation
|
|
124
|
+
*/
|
|
125
|
+
const SETTINGS_SCHEMA = {
|
|
126
|
+
outputDirectory: { type: 'string' },
|
|
127
|
+
launchAtLogin: { type: 'boolean' },
|
|
128
|
+
checkForUpdates: { type: 'boolean' },
|
|
129
|
+
defaultCountdown: { type: 'number', enum: [0, 3, 5] },
|
|
130
|
+
showTranscriptionPreview: { type: 'boolean' },
|
|
131
|
+
showAudioWaveform: { type: 'boolean' },
|
|
132
|
+
pauseThreshold: { type: 'number', minimum: 500, maximum: 3000 },
|
|
133
|
+
minTimeBetweenCaptures: { type: 'number', minimum: 300, maximum: 2000 },
|
|
134
|
+
imageFormat: { type: 'string', enum: ['png', 'jpeg'] },
|
|
135
|
+
imageQuality: { type: 'number', minimum: 1, maximum: 100 },
|
|
136
|
+
maxImageWidth: { type: 'number', minimum: 800, maximum: 2400 },
|
|
137
|
+
transcriptionService: { type: 'string', enum: ['openai'] },
|
|
138
|
+
language: { type: 'string' },
|
|
139
|
+
enableKeywordTriggers: { type: 'boolean' },
|
|
140
|
+
hotkeys: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
toggleRecording: { type: 'string' },
|
|
144
|
+
manualScreenshot: { type: 'string' },
|
|
145
|
+
pauseResume: { type: 'string' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
theme: { type: 'string', enum: ['dark', 'light', 'system'] },
|
|
149
|
+
accentColor: { type: 'string' },
|
|
150
|
+
audioDeviceId: { type: ['string', 'null'] },
|
|
151
|
+
debugMode: { type: 'boolean' },
|
|
152
|
+
keepAudioBackups: { type: 'boolean' },
|
|
153
|
+
hasCompletedOnboarding: { type: 'boolean' },
|
|
154
|
+
} as const;
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Implementation
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
export class SettingsManager implements ISettingsManager {
|
|
161
|
+
private store: Store<AppSettings>;
|
|
162
|
+
private secureStore: Store<Record<string, string>>;
|
|
163
|
+
private changeCallbacks: Set<SettingsChangeCallback> = new Set();
|
|
164
|
+
private ipcRegistered = false;
|
|
165
|
+
|
|
166
|
+
constructor() {
|
|
167
|
+
// Initialize electron-store with schema
|
|
168
|
+
// We use type assertion here because electron-store's Schema type is overly strict
|
|
169
|
+
// and doesn't match JSON Schema 7 format we're using
|
|
170
|
+
this.store = new Store<AppSettings>({
|
|
171
|
+
name: 'settings',
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
schema: SETTINGS_SCHEMA as any,
|
|
174
|
+
defaults: this.getDefaultsWithPaths(),
|
|
175
|
+
clearInvalidConfig: false, // Don't clear on schema violation, migrate instead
|
|
176
|
+
});
|
|
177
|
+
this.secureStore = new Store<Record<string, string>>({
|
|
178
|
+
name: FALLBACK_SECRET_STORE_NAME,
|
|
179
|
+
clearInvalidConfig: false,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Run migrations
|
|
183
|
+
this.migrate();
|
|
184
|
+
this.store.set('hotkeys', {
|
|
185
|
+
...DEFAULT_HOTKEY_CONFIG,
|
|
186
|
+
...(this.store.get('hotkeys') || {}),
|
|
187
|
+
});
|
|
188
|
+
this.normalizeTranscriptionService();
|
|
189
|
+
|
|
190
|
+
console.log('[SettingsManager] Initialized with settings version:', SETTINGS_VERSION);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get defaults with dynamic paths resolved
|
|
195
|
+
*/
|
|
196
|
+
private getDefaultsWithPaths(): AppSettings {
|
|
197
|
+
const documentsPath = app.isReady()
|
|
198
|
+
? app.getPath('documents')
|
|
199
|
+
: join(process.env.HOME || process.env.USERPROFILE || '', 'Documents');
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...DEFAULT_SETTINGS,
|
|
203
|
+
outputDirectory: join(documentsPath, 'markupr'),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --------------------------------------------------------------------------
|
|
208
|
+
// Core Methods
|
|
209
|
+
// --------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get a single setting value
|
|
213
|
+
*/
|
|
214
|
+
get<K extends keyof AppSettings>(key: K): AppSettings[K] {
|
|
215
|
+
return this.store.get(key);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set a single setting value
|
|
220
|
+
*/
|
|
221
|
+
set<K extends keyof AppSettings>(key: K, value: AppSettings[K]): void {
|
|
222
|
+
const oldValue = this.store.get(key);
|
|
223
|
+
|
|
224
|
+
// Validate before setting
|
|
225
|
+
if (!this.validateSetting(key, value)) {
|
|
226
|
+
console.warn(`[SettingsManager] Invalid value for ${key}:`, value);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.store.set(key, value);
|
|
231
|
+
this.emitChange(key, value, oldValue);
|
|
232
|
+
|
|
233
|
+
console.log(`[SettingsManager] Set ${key}:`, value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get all settings
|
|
238
|
+
*/
|
|
239
|
+
getAll(): AppSettings {
|
|
240
|
+
return this.store.store;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reset all settings to defaults
|
|
245
|
+
*/
|
|
246
|
+
reset(): void {
|
|
247
|
+
const oldSettings = this.getAll();
|
|
248
|
+
const defaults = this.getDefaultsWithPaths();
|
|
249
|
+
|
|
250
|
+
this.store.clear();
|
|
251
|
+
this.store.set(defaults);
|
|
252
|
+
|
|
253
|
+
// Emit changes for all settings
|
|
254
|
+
for (const key of Object.keys(defaults) as Array<keyof AppSettings>) {
|
|
255
|
+
if (oldSettings[key] !== defaults[key]) {
|
|
256
|
+
this.emitChange(key, defaults[key], oldSettings[key]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log('[SettingsManager] Reset to defaults');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update multiple settings at once (legacy compatibility method)
|
|
265
|
+
* Note: For new code, prefer using set() for individual settings
|
|
266
|
+
*/
|
|
267
|
+
update(updates: Partial<AppSettings>): AppSettings {
|
|
268
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
269
|
+
if (value !== undefined) {
|
|
270
|
+
this.set(key as keyof AppSettings, value as AppSettings[keyof AppSettings]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return this.getAll();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --------------------------------------------------------------------------
|
|
277
|
+
// Validation
|
|
278
|
+
// --------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate a single setting value
|
|
282
|
+
*/
|
|
283
|
+
private validateSetting<K extends keyof AppSettings>(key: K, value: AppSettings[K]): boolean {
|
|
284
|
+
switch (key) {
|
|
285
|
+
case 'pauseThreshold':
|
|
286
|
+
return typeof value === 'number' && value >= 500 && value <= 3000;
|
|
287
|
+
|
|
288
|
+
case 'minTimeBetweenCaptures':
|
|
289
|
+
return typeof value === 'number' && value >= 300 && value <= 2000;
|
|
290
|
+
|
|
291
|
+
case 'imageQuality':
|
|
292
|
+
return typeof value === 'number' && value >= 1 && value <= 100;
|
|
293
|
+
|
|
294
|
+
case 'maxImageWidth':
|
|
295
|
+
return typeof value === 'number' && value >= 800 && value <= 2400;
|
|
296
|
+
|
|
297
|
+
case 'defaultCountdown':
|
|
298
|
+
return value === 0 || value === 3 || value === 5;
|
|
299
|
+
|
|
300
|
+
case 'imageFormat':
|
|
301
|
+
return value === 'png' || value === 'jpeg';
|
|
302
|
+
|
|
303
|
+
case 'theme':
|
|
304
|
+
return value === 'dark' || value === 'light' || value === 'system';
|
|
305
|
+
|
|
306
|
+
case 'transcriptionService':
|
|
307
|
+
return (value as unknown as string) === 'openai';
|
|
308
|
+
|
|
309
|
+
case 'accentColor':
|
|
310
|
+
return typeof value === 'string' && /^#[0-9A-Fa-f]{6}$/.test(value as string);
|
|
311
|
+
|
|
312
|
+
default:
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --------------------------------------------------------------------------
|
|
318
|
+
// Secure Storage (API Keys)
|
|
319
|
+
// --------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
private canUseEncryptedFallback(): boolean {
|
|
322
|
+
try {
|
|
323
|
+
return safeStorage.isEncryptionAvailable();
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private getFallbackApiKey(service: string): string | null {
|
|
330
|
+
try {
|
|
331
|
+
const encrypted = this.secureStore.get(service);
|
|
332
|
+
if (!encrypted) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!this.canUseEncryptedFallback()) {
|
|
337
|
+
console.warn(
|
|
338
|
+
`[SettingsManager] Encrypted fallback exists for ${service}, but safeStorage is unavailable`
|
|
339
|
+
);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return safeStorage.decryptString(Buffer.from(encrypted, 'base64'));
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.warn(`[SettingsManager] Failed to read fallback API key for ${service}:`, error);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private setFallbackApiKey(service: string, key: string): void {
|
|
351
|
+
if (!this.canUseEncryptedFallback()) {
|
|
352
|
+
throw new Error('safeStorage encryption is unavailable');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const encrypted = safeStorage.encryptString(key).toString('base64');
|
|
356
|
+
this.secureStore.set(service, encrypted);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private clearFallbackApiKey(service: string): void {
|
|
360
|
+
try {
|
|
361
|
+
this.secureStore.delete(service);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.warn(`[SettingsManager] Failed to clear fallback API key for ${service}:`, error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private getInsecureStoreKey(service: string): string {
|
|
368
|
+
return `${INSECURE_SECRET_PREFIX}${service}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private getInsecureApiKey(service: string): string | null {
|
|
372
|
+
const storeKey = this.getInsecureStoreKey(service);
|
|
373
|
+
const directValue = this.secureStore.get(storeKey);
|
|
374
|
+
if (typeof directValue === 'string' && directValue.length > 0) {
|
|
375
|
+
return directValue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Legacy fallback: previous builds stored a map under settings.json.
|
|
379
|
+
try {
|
|
380
|
+
const insecureMap = this.store.get(
|
|
381
|
+
LEGACY_INSECURE_SECRET_STORE_KEY as keyof AppSettings
|
|
382
|
+
) as unknown as Record<string, string> | undefined;
|
|
383
|
+
const value = insecureMap?.[service];
|
|
384
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.warn(`[SettingsManager] Failed to read plaintext fallback API key for ${service}:`, error);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private setInsecureApiKey(service: string, key: string): void {
|
|
392
|
+
const storeKey = this.getInsecureStoreKey(service);
|
|
393
|
+
this.secureStore.set(storeKey, key);
|
|
394
|
+
|
|
395
|
+
// Best-effort cleanup of legacy fallback map entry.
|
|
396
|
+
const legacyMap = (this.store.get(
|
|
397
|
+
LEGACY_INSECURE_SECRET_STORE_KEY as keyof AppSettings
|
|
398
|
+
) as unknown as Record<string, string> | undefined) || {};
|
|
399
|
+
if (legacyMap[service]) {
|
|
400
|
+
delete legacyMap[service];
|
|
401
|
+
this.store.set(
|
|
402
|
+
LEGACY_INSECURE_SECRET_STORE_KEY as keyof AppSettings,
|
|
403
|
+
legacyMap as unknown as AppSettings[keyof AppSettings]
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private clearInsecureApiKey(service: string): void {
|
|
409
|
+
try {
|
|
410
|
+
this.secureStore.delete(this.getInsecureStoreKey(service));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.warn(`[SettingsManager] Failed to clear plaintext fallback API key for ${service}:`, error);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const legacyMap = (this.store.get(
|
|
416
|
+
LEGACY_INSECURE_SECRET_STORE_KEY as keyof AppSettings
|
|
417
|
+
) as unknown as Record<string, string> | undefined) || {};
|
|
418
|
+
if (!legacyMap[service]) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
delete legacyMap[service];
|
|
422
|
+
this.store.set(
|
|
423
|
+
LEGACY_INSECURE_SECRET_STORE_KEY as keyof AppSettings,
|
|
424
|
+
legacyMap as unknown as AppSettings[keyof AppSettings]
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get an API key from secure storage
|
|
430
|
+
*/
|
|
431
|
+
async getApiKey(service: string): Promise<string | null> {
|
|
432
|
+
try {
|
|
433
|
+
const key = await keytar.getPassword(KEYTAR_SERVICE, service);
|
|
434
|
+
if (key) {
|
|
435
|
+
return key;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Migration path: older builds stored keys under a different keychain service name.
|
|
439
|
+
for (const legacyService of LEGACY_KEYTAR_SERVICES) {
|
|
440
|
+
const legacyKey = await keytar.getPassword(legacyService, service);
|
|
441
|
+
if (!legacyKey) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
await keytar.setPassword(KEYTAR_SERVICE, service, legacyKey);
|
|
447
|
+
console.log(
|
|
448
|
+
`[SettingsManager] Migrated API key for ${service} from "${legacyService}" to "${KEYTAR_SERVICE}"`
|
|
449
|
+
);
|
|
450
|
+
} catch (migrationError) {
|
|
451
|
+
console.warn(
|
|
452
|
+
`[SettingsManager] Failed to migrate API key for ${service} from "${legacyService}":`,
|
|
453
|
+
migrationError
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return legacyKey;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return this.getFallbackApiKey(service) || this.getInsecureApiKey(service);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(`[SettingsManager] Failed to get API key for ${service}:`, error);
|
|
463
|
+
return this.getFallbackApiKey(service) || this.getInsecureApiKey(service);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Store an API key in secure storage
|
|
469
|
+
*/
|
|
470
|
+
async setApiKey(service: string, key: string): Promise<void> {
|
|
471
|
+
try {
|
|
472
|
+
await keytar.setPassword(KEYTAR_SERVICE, service, key);
|
|
473
|
+
this.clearFallbackApiKey(service);
|
|
474
|
+
this.clearInsecureApiKey(service);
|
|
475
|
+
console.log(`[SettingsManager] Stored API key for ${service}`);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.warn(
|
|
478
|
+
`[SettingsManager] Keytar store failed for ${service}; attempting encrypted fallback:`,
|
|
479
|
+
error
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
this.setFallbackApiKey(service, key);
|
|
484
|
+
this.clearInsecureApiKey(service);
|
|
485
|
+
console.log(`[SettingsManager] Stored API key for ${service} via encrypted fallback`);
|
|
486
|
+
} catch (fallbackError) {
|
|
487
|
+
console.warn(
|
|
488
|
+
`[SettingsManager] Encrypted fallback failed for ${service}; storing plaintext fallback:`,
|
|
489
|
+
fallbackError
|
|
490
|
+
);
|
|
491
|
+
try {
|
|
492
|
+
this.setInsecureApiKey(service, key);
|
|
493
|
+
console.log(`[SettingsManager] Stored API key for ${service} via plaintext fallback`);
|
|
494
|
+
} catch (insecureError) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Unable to store API key for ${service}: ${
|
|
497
|
+
insecureError instanceof Error ? insecureError.message : String(insecureError)
|
|
498
|
+
}`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Delete an API key from secure storage
|
|
507
|
+
*/
|
|
508
|
+
async deleteApiKey(service: string): Promise<void> {
|
|
509
|
+
let keytarError: unknown = null;
|
|
510
|
+
try {
|
|
511
|
+
await keytar.deletePassword(KEYTAR_SERVICE, service);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
keytarError = error;
|
|
514
|
+
console.warn(`[SettingsManager] Failed to delete keytar API key for ${service}:`, error);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.clearFallbackApiKey(service);
|
|
518
|
+
this.clearInsecureApiKey(service);
|
|
519
|
+
|
|
520
|
+
if (keytarError && !this.canUseEncryptedFallback()) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(`[SettingsManager] Deleted API key for ${service}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Check if an API key exists in secure storage
|
|
529
|
+
*/
|
|
530
|
+
async hasApiKey(service: string): Promise<boolean> {
|
|
531
|
+
const key = await this.getApiKey(service);
|
|
532
|
+
return key !== null && key.length > 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// --------------------------------------------------------------------------
|
|
536
|
+
// Change Events
|
|
537
|
+
// --------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Subscribe to settings changes
|
|
541
|
+
* @returns Unsubscribe function
|
|
542
|
+
*/
|
|
543
|
+
onChange(callback: SettingsChangeCallback): () => void {
|
|
544
|
+
this.changeCallbacks.add(callback);
|
|
545
|
+
return () => {
|
|
546
|
+
this.changeCallbacks.delete(callback);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Emit a change event to all subscribers
|
|
552
|
+
*/
|
|
553
|
+
private emitChange(key: string, newValue: unknown, oldValue: unknown): void {
|
|
554
|
+
for (const callback of this.changeCallbacks) {
|
|
555
|
+
try {
|
|
556
|
+
callback(key, newValue, oldValue);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error('[SettingsManager] Error in change callback:', error);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// --------------------------------------------------------------------------
|
|
564
|
+
// Migration
|
|
565
|
+
// --------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Run settings migrations
|
|
569
|
+
*/
|
|
570
|
+
migrate(): void {
|
|
571
|
+
const currentVersion = this.store.get('_version' as keyof AppSettings) as number | undefined;
|
|
572
|
+
|
|
573
|
+
if (currentVersion === SETTINGS_VERSION) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
console.log(`[SettingsManager] Migrating from version ${currentVersion || 1} to ${SETTINGS_VERSION}`);
|
|
578
|
+
|
|
579
|
+
// Migration from v1 (legacy settings)
|
|
580
|
+
if (!currentVersion || currentVersion < 2) {
|
|
581
|
+
this.migrateV1ToV2();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Set current version
|
|
585
|
+
this.store.set('_version' as keyof AppSettings, SETTINGS_VERSION as unknown as AppSettings[keyof AppSettings]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Normalize deprecated transcription service values to the current default.
|
|
590
|
+
*/
|
|
591
|
+
private normalizeTranscriptionService(): void {
|
|
592
|
+
const current = this.store.get('transcriptionService') as unknown;
|
|
593
|
+
if (current === 'deepgram') {
|
|
594
|
+
this.store.set('transcriptionService', 'openai');
|
|
595
|
+
console.log('[SettingsManager] Normalized legacy transcriptionService "deepgram" -> "openai"');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Migrate from v1 (legacy JSON settings) to v2 (electron-store with new schema)
|
|
601
|
+
*/
|
|
602
|
+
private migrateV1ToV2(): void {
|
|
603
|
+
console.log('[SettingsManager] Running v1 -> v2 migration');
|
|
604
|
+
|
|
605
|
+
// Map old settings to new settings
|
|
606
|
+
const legacyMappings: Record<string, keyof AppSettings> = {
|
|
607
|
+
screenshotQuality: 'imageQuality',
|
|
608
|
+
pauseThresholdMs: 'pauseThreshold',
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
for (const [oldKey, newKey] of Object.entries(legacyMappings)) {
|
|
612
|
+
const oldValue = this.store.get(oldKey as keyof AppSettings);
|
|
613
|
+
if (oldValue !== undefined) {
|
|
614
|
+
this.store.set(newKey, oldValue);
|
|
615
|
+
this.store.delete(oldKey as keyof AppSettings);
|
|
616
|
+
console.log(`[SettingsManager] Migrated ${oldKey} -> ${newKey}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Remove deprecated settings
|
|
621
|
+
const deprecatedKeys = ['autoClipboard', 'outputFormat', 'deepgramApiKey'];
|
|
622
|
+
for (const key of deprecatedKeys) {
|
|
623
|
+
if (this.store.has(key as keyof AppSettings)) {
|
|
624
|
+
this.store.delete(key as keyof AppSettings);
|
|
625
|
+
console.log(`[SettingsManager] Removed deprecated setting: ${key}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Ensure all new settings have defaults
|
|
630
|
+
const defaults = this.getDefaultsWithPaths();
|
|
631
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
632
|
+
if (!this.store.has(key as keyof AppSettings)) {
|
|
633
|
+
this.store.set(key as keyof AppSettings, value as AppSettings[keyof AppSettings]);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --------------------------------------------------------------------------
|
|
639
|
+
// IPC Handlers
|
|
640
|
+
// --------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Register IPC handlers for renderer communication
|
|
644
|
+
*/
|
|
645
|
+
registerIpcHandlers(): void {
|
|
646
|
+
if (this.ipcRegistered) {
|
|
647
|
+
console.warn('[SettingsManager] IPC handlers already registered');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Get single setting
|
|
652
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET, (_, key: keyof AppSettings) => {
|
|
653
|
+
return this.get(key);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Get all settings
|
|
657
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_ALL, () => {
|
|
658
|
+
return this.getAll();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Set single setting
|
|
662
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_SET, (_, key: keyof AppSettings, value: AppSettings[keyof AppSettings]) => {
|
|
663
|
+
this.set(key, value);
|
|
664
|
+
return this.get(key);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Get API key (secure)
|
|
668
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_API_KEY, async (_, service: string) => {
|
|
669
|
+
return this.getApiKey(service);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Set API key (secure)
|
|
673
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_SET_API_KEY, async (_, service: string, key: string) => {
|
|
674
|
+
await this.setApiKey(service, key);
|
|
675
|
+
return true;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Delete API key (secure)
|
|
679
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_DELETE_API_KEY, async (_, service: string) => {
|
|
680
|
+
await this.deleteApiKey(service);
|
|
681
|
+
return true;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Check if API key exists
|
|
685
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_HAS_API_KEY, async (_, service: string) => {
|
|
686
|
+
return this.hasApiKey(service);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
this.ipcRegistered = true;
|
|
690
|
+
console.log('[SettingsManager] IPC handlers registered');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get the storage path for debugging
|
|
695
|
+
*/
|
|
696
|
+
getStorePath(): string {
|
|
697
|
+
return this.store.path;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// Singleton Export
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
let instance: SettingsManager | null = null;
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Get or create the SettingsManager singleton
|
|
709
|
+
*/
|
|
710
|
+
export function getSettingsManager(): SettingsManager {
|
|
711
|
+
if (!instance) {
|
|
712
|
+
instance = new SettingsManager();
|
|
713
|
+
}
|
|
714
|
+
return instance;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Create a new SettingsManager instance (for testing)
|
|
719
|
+
*/
|
|
720
|
+
export function createSettingsManager(): SettingsManager {
|
|
721
|
+
return new SettingsManager();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Singleton instance
|
|
725
|
+
export const settingsManager = getSettingsManager();
|
|
726
|
+
|
|
727
|
+
export { DEFAULT_SETTINGS, SETTINGS_VERSION };
|
|
728
|
+
// Re-export AppSettings from shared/types for downstream consumers
|
|
729
|
+
export type { AppSettings } from '../../shared/types';
|
|
730
|
+
export default settingsManager;
|