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,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrayManager - System tray icon management for markupr
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - 5 visual states: idle, recording, processing, complete, error
|
|
6
|
+
* - Context menu with actions
|
|
7
|
+
* - Click behavior (toggle recording on macOS)
|
|
8
|
+
* - Tooltips showing current state
|
|
9
|
+
* - Processing animation (rotating dashed circle)
|
|
10
|
+
* - Recording pulse animation support (opacity-based)
|
|
11
|
+
* - Complete state with green checkmark
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Tray, Menu, nativeImage, app, NativeImage, shell } from 'electron';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import type { TrayState } from '../shared/types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for TrayManager operations
|
|
20
|
+
*/
|
|
21
|
+
export interface ITrayManager {
|
|
22
|
+
initialize(): void;
|
|
23
|
+
setState(state: TrayState): void;
|
|
24
|
+
setTooltip(text: string): void;
|
|
25
|
+
destroy(): void;
|
|
26
|
+
onClick(callback: () => void): void;
|
|
27
|
+
onSettingsClick(callback: () => void): void;
|
|
28
|
+
getTray(): Tray | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tooltip messages for each tray state
|
|
33
|
+
*/
|
|
34
|
+
const STATE_TOOLTIPS: Record<TrayState, string> = {
|
|
35
|
+
idle: 'markupr - Ready (Cmd+Shift+F)',
|
|
36
|
+
recording: 'markupr - Recording... (Cmd+Shift+F to stop)',
|
|
37
|
+
processing: 'markupr - Processing...',
|
|
38
|
+
complete: 'markupr - Feedback captured!',
|
|
39
|
+
error: 'markupr - Error (click for details)',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const DONATE_URL = 'https://ko-fi.com/eddiesanjuan';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* TrayManager implementation
|
|
46
|
+
*/
|
|
47
|
+
class TrayManagerImpl implements ITrayManager {
|
|
48
|
+
private tray: Tray | null = null;
|
|
49
|
+
private contextMenu: Menu | null = null;
|
|
50
|
+
private currentState: TrayState = 'idle';
|
|
51
|
+
private clickCallbacks: Array<() => void> = [];
|
|
52
|
+
private settingsCallbacks: Array<() => void> = [];
|
|
53
|
+
private animationInterval: NodeJS.Timeout | null = null;
|
|
54
|
+
private animationFrame: number = 0;
|
|
55
|
+
private iconCache: Map<string, NativeImage> = new Map();
|
|
56
|
+
private completeTimeout: NodeJS.Timeout | null = null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the path to an icon asset
|
|
60
|
+
* On macOS, uses Template images for automatic dark/light mode handling
|
|
61
|
+
*/
|
|
62
|
+
private getIconPath(state: TrayState, frame?: number): string {
|
|
63
|
+
const isMac = process.platform === 'darwin';
|
|
64
|
+
const suffix = isMac ? 'Template' : '';
|
|
65
|
+
const frameSuffix = frame !== undefined ? `-${frame}` : '';
|
|
66
|
+
|
|
67
|
+
// Path relative to the built main process directory
|
|
68
|
+
// In production: dist/main/index.js -> assets/
|
|
69
|
+
// In development: similar structure
|
|
70
|
+
const assetsPath = app.isPackaged
|
|
71
|
+
? join(process.resourcesPath, 'assets')
|
|
72
|
+
: join(__dirname, '../../assets');
|
|
73
|
+
|
|
74
|
+
return join(assetsPath, `tray-${state}${frameSuffix}${suffix}.png`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load and cache an icon, creating a placeholder if the file doesn't exist
|
|
79
|
+
*/
|
|
80
|
+
private loadIcon(state: TrayState, frame?: number): NativeImage {
|
|
81
|
+
const cacheKey = `${state}-${frame ?? 'default'}`;
|
|
82
|
+
|
|
83
|
+
if (this.iconCache.has(cacheKey)) {
|
|
84
|
+
return this.iconCache.get(cacheKey)!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const iconPath = this.getIconPath(state, frame);
|
|
88
|
+
let icon: NativeImage;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
icon = nativeImage.createFromPath(iconPath);
|
|
92
|
+
|
|
93
|
+
// If the icon is empty, create a placeholder
|
|
94
|
+
if (icon.isEmpty()) {
|
|
95
|
+
icon = this.createPlaceholderIcon(state);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Create placeholder if file doesn't exist
|
|
99
|
+
icon = this.createPlaceholderIcon(state);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Resize for menu bar (16x16 on macOS, 16x16 or 32x32 on others)
|
|
103
|
+
const size = process.platform === 'darwin' ? 16 : 16;
|
|
104
|
+
icon = icon.resize({ width: size, height: size });
|
|
105
|
+
if (process.platform === 'darwin') {
|
|
106
|
+
icon.setTemplateImage(true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.iconCache.set(cacheKey, icon);
|
|
110
|
+
return icon;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a placeholder icon when the actual icon file doesn't exist
|
|
115
|
+
* Uses simple shapes to represent each state
|
|
116
|
+
*/
|
|
117
|
+
private createPlaceholderIcon(state: TrayState): NativeImage {
|
|
118
|
+
// Create a 32x32 data URL icon (will be resized later)
|
|
119
|
+
// These are simple SVG-based placeholders
|
|
120
|
+
const size = 32;
|
|
121
|
+
const canvas = this.createIconCanvas(state, size);
|
|
122
|
+
|
|
123
|
+
return nativeImage.createFromDataURL(canvas);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate a data URL for a simple icon based on state
|
|
128
|
+
*/
|
|
129
|
+
private createIconCanvas(state: TrayState, size: number): string {
|
|
130
|
+
const center = size / 2;
|
|
131
|
+
const radius = size / 2 - 2;
|
|
132
|
+
|
|
133
|
+
let svg: string;
|
|
134
|
+
|
|
135
|
+
switch (state) {
|
|
136
|
+
case 'idle':
|
|
137
|
+
// Circle outline (ready to record)
|
|
138
|
+
svg = `
|
|
139
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
140
|
+
<circle cx="${center}" cy="${center}" r="${radius}"
|
|
141
|
+
fill="none" stroke="#666666" stroke-width="2"/>
|
|
142
|
+
</svg>
|
|
143
|
+
`;
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'recording':
|
|
147
|
+
// Filled red circle (recording active)
|
|
148
|
+
svg = `
|
|
149
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
150
|
+
<circle cx="${center}" cy="${center}" r="${radius}" fill="#FF3B30"/>
|
|
151
|
+
</svg>
|
|
152
|
+
`;
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'processing':
|
|
156
|
+
// Spinning indicator (processing)
|
|
157
|
+
svg = `
|
|
158
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
159
|
+
<circle cx="${center}" cy="${center}" r="${radius}"
|
|
160
|
+
fill="none" stroke="#666666" stroke-width="2" stroke-dasharray="8 4"/>
|
|
161
|
+
</svg>
|
|
162
|
+
`;
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'complete':
|
|
166
|
+
// Green circle with checkmark (success state)
|
|
167
|
+
svg = `
|
|
168
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
169
|
+
<circle cx="${center}" cy="${center}" r="${radius}" fill="#10B981"/>
|
|
170
|
+
<path d="M${center - 4} ${center}l3 3 5-5" stroke="white" stroke-width="2"
|
|
171
|
+
fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
172
|
+
</svg>
|
|
173
|
+
`;
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'error':
|
|
177
|
+
// Warning triangle (error state)
|
|
178
|
+
svg = `
|
|
179
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
180
|
+
<polygon points="${center},4 ${size - 4},${size - 4} 4,${size - 4}"
|
|
181
|
+
fill="#FF9500" stroke="#FF9500" stroke-width="1"/>
|
|
182
|
+
<text x="${center}" y="${size - 8}" text-anchor="middle"
|
|
183
|
+
fill="white" font-size="16" font-weight="bold">!</text>
|
|
184
|
+
</svg>
|
|
185
|
+
`;
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
// Fallback circle
|
|
190
|
+
svg = `
|
|
191
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
192
|
+
<circle cx="${center}" cy="${center}" r="${radius}" fill="#999999"/>
|
|
193
|
+
</svg>
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Convert SVG to data URL
|
|
198
|
+
const base64 = Buffer.from(svg.trim()).toString('base64');
|
|
199
|
+
return `data:image/svg+xml;base64,${base64}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Initialize the system tray
|
|
204
|
+
*/
|
|
205
|
+
initialize(): void {
|
|
206
|
+
if (this.tray) {
|
|
207
|
+
console.warn('[TrayManager] Already initialized');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const icon = this.loadIcon('idle');
|
|
212
|
+
this.tray = new Tray(icon);
|
|
213
|
+
|
|
214
|
+
this.tray.setToolTip(STATE_TOOLTIPS.idle);
|
|
215
|
+
this.updateContextMenu();
|
|
216
|
+
|
|
217
|
+
if (process.platform === 'darwin') {
|
|
218
|
+
// Use mouse-up so we can strictly separate left click (popover) and right click (context menu).
|
|
219
|
+
// This prevents the "menu + popover both open" behavior seen when macOS emits both click variants.
|
|
220
|
+
this.tray.on('mouse-up', (event) => {
|
|
221
|
+
const button = (event as { button?: number }).button;
|
|
222
|
+
if (button === 2) {
|
|
223
|
+
this.tray?.popUpContextMenu(this.contextMenu ?? undefined);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.clickCallbacks.forEach((cb) => cb());
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
// On Windows/Linux, regular click opens/toggles app; right-click menu is handled by setContextMenu.
|
|
230
|
+
this.tray.on('click', () => {
|
|
231
|
+
this.clickCallbacks.forEach((cb) => cb());
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log('[TrayManager] Initialized');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Update the context menu based on current state
|
|
240
|
+
*/
|
|
241
|
+
private updateContextMenu(): void {
|
|
242
|
+
if (!this.tray) return;
|
|
243
|
+
|
|
244
|
+
const isRecording = this.currentState === 'recording';
|
|
245
|
+
const isProcessing = this.currentState === 'processing';
|
|
246
|
+
|
|
247
|
+
const menu = Menu.buildFromTemplate([
|
|
248
|
+
{
|
|
249
|
+
label: 'Buy Developer a Coffee',
|
|
250
|
+
click: () => {
|
|
251
|
+
void shell.openExternal(DONATE_URL);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{ type: 'separator' },
|
|
255
|
+
{
|
|
256
|
+
label: isRecording ? 'Stop Recording' : 'Start Recording',
|
|
257
|
+
enabled: !isProcessing,
|
|
258
|
+
click: () => {
|
|
259
|
+
this.clickCallbacks.forEach((cb) => cb());
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{ type: 'separator' },
|
|
263
|
+
{
|
|
264
|
+
label: 'Settings...',
|
|
265
|
+
accelerator: 'CmdOrCtrl+,',
|
|
266
|
+
click: () => {
|
|
267
|
+
this.settingsCallbacks.forEach((cb) => cb());
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{ type: 'separator' },
|
|
271
|
+
{
|
|
272
|
+
label: 'About markupr',
|
|
273
|
+
role: 'about',
|
|
274
|
+
},
|
|
275
|
+
{ type: 'separator' },
|
|
276
|
+
{
|
|
277
|
+
label: 'Quit markupr',
|
|
278
|
+
accelerator: 'CmdOrCtrl+Q',
|
|
279
|
+
click: () => {
|
|
280
|
+
app.quit();
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
this.contextMenu = menu;
|
|
286
|
+
|
|
287
|
+
if (process.platform === 'darwin') {
|
|
288
|
+
// Keep left-click dedicated to opening the popover.
|
|
289
|
+
// Showing the menu is explicit via right-click.
|
|
290
|
+
this.tray.setContextMenu(null);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.tray.setContextMenu(menu);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Set the tray state and update icon/tooltip
|
|
299
|
+
*/
|
|
300
|
+
setState(state: TrayState): void {
|
|
301
|
+
if (!this.tray) {
|
|
302
|
+
console.warn('[TrayManager] Not initialized');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.currentState = state;
|
|
307
|
+
|
|
308
|
+
// Stop any existing animation and clear complete timeout
|
|
309
|
+
this.stopAnimation();
|
|
310
|
+
if (this.completeTimeout) {
|
|
311
|
+
clearTimeout(this.completeTimeout);
|
|
312
|
+
this.completeTimeout = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Set the appropriate icon
|
|
316
|
+
const icon = this.loadIcon(state);
|
|
317
|
+
this.tray.setImage(icon);
|
|
318
|
+
|
|
319
|
+
// Set tooltip
|
|
320
|
+
this.tray.setToolTip(STATE_TOOLTIPS[state]);
|
|
321
|
+
|
|
322
|
+
// Start animation based on state
|
|
323
|
+
if (state === 'processing') {
|
|
324
|
+
this.startProcessingAnimation();
|
|
325
|
+
} else if (state === 'recording' && process.platform !== 'darwin') {
|
|
326
|
+
this.startRecordingAnimation();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Update context menu (Start/Stop label changes)
|
|
330
|
+
this.updateContextMenu();
|
|
331
|
+
|
|
332
|
+
console.log(`[TrayManager] State changed to: ${state}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Start the processing animation (rotating icon)
|
|
337
|
+
*/
|
|
338
|
+
private startProcessingAnimation(): void {
|
|
339
|
+
if (!this.tray) return;
|
|
340
|
+
|
|
341
|
+
// Animation: cycle through 4 rotation frames
|
|
342
|
+
const frames = 4;
|
|
343
|
+
this.animationFrame = 0;
|
|
344
|
+
|
|
345
|
+
this.animationInterval = setInterval(() => {
|
|
346
|
+
this.animationFrame = (this.animationFrame + 1) % frames;
|
|
347
|
+
|
|
348
|
+
// Try to load frame-specific icon, fall back to generating one
|
|
349
|
+
const icon = this.loadProcessingFrameIcon(this.animationFrame, frames);
|
|
350
|
+
this.tray?.setImage(icon);
|
|
351
|
+
}, 200); // 200ms per frame = 5 FPS
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Start the recording animation (pulse effect via opacity)
|
|
356
|
+
* Creates a subtle pulse by alternating between full and slightly dim icons
|
|
357
|
+
*/
|
|
358
|
+
private startRecordingAnimation(): void {
|
|
359
|
+
if (!this.tray) return;
|
|
360
|
+
|
|
361
|
+
if (process.platform === 'darwin') {
|
|
362
|
+
// Template icons can disappear/flicker when rapidly swapped on macOS menu bar.
|
|
363
|
+
// Keep a stable recording icon instead of pulsing.
|
|
364
|
+
this.tray.setImage(this.loadIcon('recording'));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Animation: pulse between opacity levels (simulated via icon switching)
|
|
369
|
+
// We use 2 frames: normal (100%) and dimmed (60%)
|
|
370
|
+
const frames = 4; // Full cycle: bright -> dim -> bright -> dim
|
|
371
|
+
this.animationFrame = 0;
|
|
372
|
+
|
|
373
|
+
this.animationInterval = setInterval(() => {
|
|
374
|
+
this.animationFrame = (this.animationFrame + 1) % frames;
|
|
375
|
+
|
|
376
|
+
// Load appropriate icon - frame 0,1 = full brightness, frame 2,3 = dimmed
|
|
377
|
+
const isDim = this.animationFrame >= 2;
|
|
378
|
+
const icon = this.loadRecordingFrameIcon(isDim);
|
|
379
|
+
this.tray?.setImage(icon);
|
|
380
|
+
}, 375); // 375ms per frame = 1.5s full cycle (matches CSS pulse animation)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Load or generate a recording animation frame
|
|
385
|
+
* @param isDim Whether to show the dimmed (60% opacity) version
|
|
386
|
+
*/
|
|
387
|
+
private loadRecordingFrameIcon(isDim: boolean): NativeImage {
|
|
388
|
+
const cacheKey = `recording-frame-${isDim ? 'dim' : 'bright'}`;
|
|
389
|
+
|
|
390
|
+
if (this.iconCache.has(cacheKey)) {
|
|
391
|
+
return this.iconCache.get(cacheKey)!;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Generate recording icon with appropriate opacity
|
|
395
|
+
const size = 32;
|
|
396
|
+
const center = size / 2;
|
|
397
|
+
const radius = size / 2 - 2;
|
|
398
|
+
const opacity = isDim ? 0.6 : 1.0;
|
|
399
|
+
|
|
400
|
+
const svg = `
|
|
401
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
402
|
+
<circle cx="${center}" cy="${center}" r="${radius}" fill="#EF4444" opacity="${opacity}"/>
|
|
403
|
+
</svg>
|
|
404
|
+
`;
|
|
405
|
+
|
|
406
|
+
const base64 = Buffer.from(svg.trim()).toString('base64');
|
|
407
|
+
const dataUrl = `data:image/svg+xml;base64,${base64}`;
|
|
408
|
+
const icon = nativeImage.createFromDataURL(dataUrl).resize({ width: 16, height: 16 });
|
|
409
|
+
if (process.platform === 'darwin') {
|
|
410
|
+
icon.setTemplateImage(true);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.iconCache.set(cacheKey, icon);
|
|
414
|
+
return icon;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Load or generate a processing animation frame
|
|
419
|
+
*/
|
|
420
|
+
private loadProcessingFrameIcon(frame: number, totalFrames: number): NativeImage {
|
|
421
|
+
const cacheKey = `processing-frame-${frame}`;
|
|
422
|
+
|
|
423
|
+
if (this.iconCache.has(cacheKey)) {
|
|
424
|
+
return this.iconCache.get(cacheKey)!;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// First try to load from file
|
|
428
|
+
const iconPath = this.getIconPath('processing', frame);
|
|
429
|
+
try {
|
|
430
|
+
const icon = nativeImage.createFromPath(iconPath);
|
|
431
|
+
if (!icon.isEmpty()) {
|
|
432
|
+
const resized = icon.resize({ width: 16, height: 16 });
|
|
433
|
+
this.iconCache.set(cacheKey, resized);
|
|
434
|
+
return resized;
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
// Fall through to generate
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Generate a rotating dashed circle
|
|
441
|
+
const size = 32;
|
|
442
|
+
const center = size / 2;
|
|
443
|
+
const radius = size / 2 - 2;
|
|
444
|
+
const rotation = (frame / totalFrames) * 360;
|
|
445
|
+
|
|
446
|
+
const svg = `
|
|
447
|
+
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
448
|
+
<g transform="rotate(${rotation} ${center} ${center})">
|
|
449
|
+
<circle cx="${center}" cy="${center}" r="${radius}"
|
|
450
|
+
fill="none" stroke="#666666" stroke-width="2"
|
|
451
|
+
stroke-dasharray="6 4" stroke-linecap="round"/>
|
|
452
|
+
</g>
|
|
453
|
+
</svg>
|
|
454
|
+
`;
|
|
455
|
+
|
|
456
|
+
const base64 = Buffer.from(svg.trim()).toString('base64');
|
|
457
|
+
const dataUrl = `data:image/svg+xml;base64,${base64}`;
|
|
458
|
+
const icon = nativeImage.createFromDataURL(dataUrl).resize({ width: 16, height: 16 });
|
|
459
|
+
if (process.platform === 'darwin') {
|
|
460
|
+
icon.setTemplateImage(true);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.iconCache.set(cacheKey, icon);
|
|
464
|
+
return icon;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Stop the processing animation
|
|
469
|
+
*/
|
|
470
|
+
private stopAnimation(): void {
|
|
471
|
+
if (this.animationInterval) {
|
|
472
|
+
clearInterval(this.animationInterval);
|
|
473
|
+
this.animationInterval = null;
|
|
474
|
+
}
|
|
475
|
+
this.animationFrame = 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Set a custom tooltip
|
|
480
|
+
*/
|
|
481
|
+
setTooltip(text: string): void {
|
|
482
|
+
if (!this.tray) {
|
|
483
|
+
console.warn('[TrayManager] Not initialized');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.tray.setToolTip(text);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Register a click callback
|
|
491
|
+
*/
|
|
492
|
+
onClick(callback: () => void): void {
|
|
493
|
+
this.clickCallbacks.push(callback);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Register a settings click callback
|
|
498
|
+
*/
|
|
499
|
+
onSettingsClick(callback: () => void): void {
|
|
500
|
+
this.settingsCallbacks.push(callback);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get the Tray instance (for positioning popover)
|
|
505
|
+
*/
|
|
506
|
+
getTray(): Tray | null {
|
|
507
|
+
return this.tray;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Destroy the tray icon and clean up
|
|
512
|
+
*/
|
|
513
|
+
destroy(): void {
|
|
514
|
+
this.stopAnimation();
|
|
515
|
+
this.iconCache.clear();
|
|
516
|
+
|
|
517
|
+
if (this.completeTimeout) {
|
|
518
|
+
clearTimeout(this.completeTimeout);
|
|
519
|
+
this.completeTimeout = null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (this.tray) {
|
|
523
|
+
this.tray.destroy();
|
|
524
|
+
this.tray = null;
|
|
525
|
+
}
|
|
526
|
+
this.contextMenu = null;
|
|
527
|
+
|
|
528
|
+
this.clickCallbacks = [];
|
|
529
|
+
this.settingsCallbacks = [];
|
|
530
|
+
|
|
531
|
+
console.log('[TrayManager] Destroyed');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Export singleton instance
|
|
536
|
+
export const trayManager = new TrayManagerImpl();
|
|
537
|
+
|
|
538
|
+
// Export types and interface
|
|
539
|
+
export type { TrayState };
|
|
540
|
+
export type { ITrayManager as TrayManager };
|