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.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate Windows NSIS Installer Images
4
+ *
5
+ * Creates PNG versions that electron-builder will handle:
6
+ * - installer-header.png (150x57) - Top banner in installer
7
+ * - installer-sidebar.png (164x314) - Left sidebar wizard image
8
+ *
9
+ * Note: electron-builder accepts PNG and converts to BMP automatically for NSIS
10
+ */
11
+
12
+ const sharp = require('sharp');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ const BUILD_DIR = path.join(__dirname, '../build');
17
+
18
+ // markupr brand colors
19
+ const COLORS = {
20
+ primary: '#6366f1', // Indigo
21
+ secondary: '#8b5cf6', // Purple
22
+ background: '#1e1e2e', // Dark slate
23
+ text: '#ffffff', // White
24
+ accent: '#22d3ee' // Cyan accent
25
+ };
26
+
27
+ /**
28
+ * Create installer header image (150x57)
29
+ * This appears at the top of the installer wizard
30
+ */
31
+ async function createInstallerHeader() {
32
+ const width = 150;
33
+ const height = 57;
34
+
35
+ // Create a gradient header with markupr branding
36
+ const svg = `
37
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
38
+ <defs>
39
+ <linearGradient id="headerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
40
+ <stop offset="0%" style="stop-color:${COLORS.primary};stop-opacity:1" />
41
+ <stop offset="100%" style="stop-color:${COLORS.secondary};stop-opacity:1" />
42
+ </linearGradient>
43
+ </defs>
44
+ <rect width="${width}" height="${height}" fill="url(#headerGrad)"/>
45
+ <text x="10" y="35" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600" fill="${COLORS.text}">
46
+ markupr
47
+ </text>
48
+ </svg>
49
+ `;
50
+
51
+ // Create PNG version (electron-builder handles conversion)
52
+ const pngPath = path.join(BUILD_DIR, 'installer-header.png');
53
+ await sharp(Buffer.from(svg))
54
+ .resize(width, height)
55
+ .png()
56
+ .toFile(pngPath);
57
+ console.log(`Created: ${pngPath}`);
58
+
59
+ // Also create BMP using raw pixel conversion
60
+ const bmpPath = path.join(BUILD_DIR, 'installer-header.bmp');
61
+ await createBmpFromSvg(svg, width, height, bmpPath);
62
+ console.log(`Created: ${bmpPath}`);
63
+ }
64
+
65
+ /**
66
+ * Create installer sidebar image (164x314)
67
+ * This appears on the left side of the wizard-style installer
68
+ */
69
+ async function createInstallerSidebar() {
70
+ const width = 164;
71
+ const height = 314;
72
+
73
+ // Create a branded sidebar with gradient and logo area
74
+ const svg = `
75
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
76
+ <defs>
77
+ <linearGradient id="sidebarGrad" x1="0%" y1="0%" x2="0%" y2="100%">
78
+ <stop offset="0%" style="stop-color:${COLORS.primary};stop-opacity:1" />
79
+ <stop offset="50%" style="stop-color:${COLORS.secondary};stop-opacity:1" />
80
+ <stop offset="100%" style="stop-color:${COLORS.background};stop-opacity:1" />
81
+ </linearGradient>
82
+ </defs>
83
+ <rect width="${width}" height="${height}" fill="url(#sidebarGrad)"/>
84
+
85
+ <!-- Logo circle -->
86
+ <circle cx="82" cy="80" r="40" fill="${COLORS.text}" fill-opacity="0.15"/>
87
+ <circle cx="82" cy="80" r="35" fill="${COLORS.text}" fill-opacity="0.1"/>
88
+
89
+ <!-- F letter for markupr -->
90
+ <text x="82" y="95" font-family="Segoe UI, Arial, sans-serif" font-size="40" font-weight="700" fill="${COLORS.text}" text-anchor="middle">
91
+ F
92
+ </text>
93
+
94
+ <!-- App name -->
95
+ <text x="82" y="150" font-family="Segoe UI, Arial, sans-serif" font-size="14" font-weight="600" fill="${COLORS.text}" text-anchor="middle">
96
+ markupr
97
+ </text>
98
+
99
+ <!-- Tagline -->
100
+ <text x="82" y="170" font-family="Segoe UI, Arial, sans-serif" font-size="9" fill="${COLORS.text}" fill-opacity="0.8" text-anchor="middle">
101
+ AI-Ready Feedback
102
+ </text>
103
+ <text x="82" y="185" font-family="Segoe UI, Arial, sans-serif" font-size="9" fill="${COLORS.text}" fill-opacity="0.8" text-anchor="middle">
104
+ Capture Tool
105
+ </text>
106
+
107
+ <!-- Decorative elements -->
108
+ <circle cx="30" cy="250" r="20" fill="${COLORS.accent}" fill-opacity="0.2"/>
109
+ <circle cx="130" cy="280" r="15" fill="${COLORS.primary}" fill-opacity="0.3"/>
110
+ <circle cx="50" cy="290" r="10" fill="${COLORS.secondary}" fill-opacity="0.2"/>
111
+
112
+ <!-- Version hint at bottom -->
113
+ <text x="82" y="300" font-family="Segoe UI, Arial, sans-serif" font-size="8" fill="${COLORS.text}" fill-opacity="0.5" text-anchor="middle">
114
+ Setup Wizard
115
+ </text>
116
+ </svg>
117
+ `;
118
+
119
+ // Create PNG version
120
+ const pngPath = path.join(BUILD_DIR, 'installer-sidebar.png');
121
+ await sharp(Buffer.from(svg))
122
+ .resize(width, height)
123
+ .png()
124
+ .toFile(pngPath);
125
+ console.log(`Created: ${pngPath}`);
126
+
127
+ // Also create BMP
128
+ const bmpPath = path.join(BUILD_DIR, 'installer-sidebar.bmp');
129
+ await createBmpFromSvg(svg, width, height, bmpPath);
130
+ console.log(`Created: ${bmpPath}`);
131
+ }
132
+
133
+ /**
134
+ * Create a BMP file from SVG using raw pixel data
135
+ * BMP format: 24-bit uncompressed
136
+ */
137
+ async function createBmpFromSvg(svg, width, height, outputPath) {
138
+ // Get raw RGBA pixel data
139
+ const { data, info } = await sharp(Buffer.from(svg))
140
+ .resize(width, height)
141
+ .raw()
142
+ .toBuffer({ resolveWithObject: true });
143
+
144
+ // BMP is stored bottom-to-top, BGR format
145
+ const rowSize = Math.ceil((width * 3) / 4) * 4; // Rows padded to 4-byte boundary
146
+ const pixelDataSize = rowSize * height;
147
+ const fileSize = 54 + pixelDataSize; // 54 byte header + pixel data
148
+
149
+ const bmp = Buffer.alloc(fileSize);
150
+
151
+ // BMP File Header (14 bytes)
152
+ bmp.write('BM', 0); // Signature
153
+ bmp.writeUInt32LE(fileSize, 2); // File size
154
+ bmp.writeUInt32LE(0, 6); // Reserved
155
+ bmp.writeUInt32LE(54, 10); // Pixel data offset
156
+
157
+ // DIB Header (40 bytes - BITMAPINFOHEADER)
158
+ bmp.writeUInt32LE(40, 14); // DIB header size
159
+ bmp.writeInt32LE(width, 18); // Width
160
+ bmp.writeInt32LE(height, 22); // Height (positive = bottom-up)
161
+ bmp.writeUInt16LE(1, 26); // Color planes
162
+ bmp.writeUInt16LE(24, 28); // Bits per pixel
163
+ bmp.writeUInt32LE(0, 30); // Compression (0 = none)
164
+ bmp.writeUInt32LE(pixelDataSize, 34); // Image size
165
+ bmp.writeInt32LE(2835, 38); // X pixels per meter (~72 DPI)
166
+ bmp.writeInt32LE(2835, 42); // Y pixels per meter
167
+ bmp.writeUInt32LE(0, 46); // Colors in color table
168
+ bmp.writeUInt32LE(0, 50); // Important colors
169
+
170
+ // Pixel data (bottom-to-top, BGR)
171
+ const channels = info.channels; // Should be 3 (RGB) or 4 (RGBA)
172
+
173
+ for (let y = height - 1; y >= 0; y--) {
174
+ const bmpRow = (height - 1 - y) * rowSize + 54;
175
+ for (let x = 0; x < width; x++) {
176
+ const srcOffset = (y * width + x) * channels;
177
+ const dstOffset = bmpRow + x * 3;
178
+
179
+ // Convert RGB(A) to BGR
180
+ bmp[dstOffset] = data[srcOffset + 2]; // Blue
181
+ bmp[dstOffset + 1] = data[srcOffset + 1]; // Green
182
+ bmp[dstOffset + 2] = data[srcOffset]; // Red
183
+ }
184
+ }
185
+
186
+ fs.writeFileSync(outputPath, bmp);
187
+ }
188
+
189
+ /**
190
+ * Create Windows icon (ICO format placeholder)
191
+ */
192
+ async function createWindowsIcon() {
193
+ const outputPath = path.join(BUILD_DIR, 'icon.ico');
194
+
195
+ // Check if icon already exists
196
+ if (fs.existsSync(outputPath)) {
197
+ console.log(`Icon already exists: ${outputPath}`);
198
+ return;
199
+ }
200
+
201
+ // Create a placeholder icon PNG
202
+ const svg = `
203
+ <svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
204
+ <defs>
205
+ <linearGradient id="iconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
206
+ <stop offset="0%" style="stop-color:${COLORS.primary};stop-opacity:1" />
207
+ <stop offset="100%" style="stop-color:${COLORS.secondary};stop-opacity:1" />
208
+ </linearGradient>
209
+ </defs>
210
+ <rect width="256" height="256" rx="40" fill="url(#iconGrad)"/>
211
+ <text x="128" y="165" font-family="Arial, sans-serif" font-size="120" font-weight="700" fill="${COLORS.text}" text-anchor="middle">
212
+ F
213
+ </text>
214
+ </svg>
215
+ `;
216
+
217
+ // Create PNG at multiple sizes for ICO conversion
218
+ const sizes = [16, 32, 48, 64, 128, 256];
219
+ for (const size of sizes) {
220
+ const pngPath = path.join(BUILD_DIR, `icon-${size}.png`);
221
+ await sharp(Buffer.from(svg))
222
+ .resize(size, size)
223
+ .png()
224
+ .toFile(pngPath);
225
+ console.log(`Created: ${pngPath}`);
226
+ }
227
+
228
+ console.log('\nNote: To create icon.ico, run:');
229
+ console.log(' npx electron-icon-builder --input=build/icon-256.png --output=build');
230
+ console.log(' or use an online converter with the PNG files');
231
+ }
232
+
233
+ async function main() {
234
+ console.log('Generating Windows installer images...\n');
235
+
236
+ // Ensure build directory exists
237
+ if (!fs.existsSync(BUILD_DIR)) {
238
+ fs.mkdirSync(BUILD_DIR, { recursive: true });
239
+ }
240
+
241
+ try {
242
+ await createInstallerHeader();
243
+ await createInstallerSidebar();
244
+ await createWindowsIcon();
245
+
246
+ console.log('\nDone! Windows installer images created in build/');
247
+ } catch (error) {
248
+ console.error('Error generating images:', error);
249
+ process.exit(1);
250
+ }
251
+ }
252
+
253
+ main();
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Generate tray icons for all states and sizes
3
+ *
4
+ * Generates 40 PNGs: 5 states x 2 sizes (16x16, 32x32) x 2 variants (normal, Template)
5
+ * Plus 4 animation frames for processing state (16 additional PNGs)
6
+ *
7
+ * States:
8
+ * - idle: gray outline circle (microphone shape)
9
+ * - recording: red filled circle with pulse animation support
10
+ * - processing: dashed circle with rotation frames
11
+ * - complete: green circle with checkmark
12
+ * - error: orange warning triangle
13
+ *
14
+ * Requires: sharp (already in dependencies)
15
+ * Usage: node scripts/generate-tray-icons.mjs
16
+ */
17
+
18
+ import sharp from 'sharp';
19
+ import { mkdir, writeFile } from 'fs/promises';
20
+ import { join, dirname } from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import { existsSync } from 'fs';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const ASSETS_DIR = join(__dirname, '../assets');
26
+
27
+ // Icon dimensions
28
+ const SIZES = [
29
+ { suffix: '', size: 16 },
30
+ { suffix: '@2x', size: 32 },
31
+ ];
32
+
33
+ // Colors from design spec
34
+ const COLORS = {
35
+ gray: '#6B7280',
36
+ red: '#EF4444',
37
+ green: '#10B981',
38
+ orange: '#F59E0B',
39
+ white: '#FFFFFF',
40
+ black: '#000000',
41
+ };
42
+
43
+ /**
44
+ * Generate SVG for idle state (microphone/circle outline)
45
+ */
46
+ function generateIdleSvg(size, isTemplate = false) {
47
+ const color = isTemplate ? COLORS.black : COLORS.gray;
48
+ return `
49
+ <svg width="${size}" height="${size}" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
50
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${color}" stroke-width="1.5"/>
51
+ </svg>
52
+ `.trim();
53
+ }
54
+
55
+ /**
56
+ * Generate SVG for recording state (filled red circle)
57
+ */
58
+ function generateRecordingSvg(size, isTemplate = false) {
59
+ const color = isTemplate ? COLORS.black : COLORS.red;
60
+ return `
61
+ <svg width="${size}" height="${size}" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
62
+ <circle cx="8" cy="8" r="5" fill="${color}"/>
63
+ </svg>
64
+ `.trim();
65
+ }
66
+
67
+ /**
68
+ * Generate SVG for processing state with rotation
69
+ * @param {number} size - Icon size
70
+ * @param {number} rotation - Rotation angle in degrees (0, 90, 180, 270)
71
+ * @param {boolean} isTemplate - Whether this is a template image for macOS
72
+ */
73
+ function generateProcessingSvg(size, rotation = 0, isTemplate = false) {
74
+ const color = isTemplate ? COLORS.black : COLORS.gray;
75
+ return `
76
+ <svg width="${size}" height="${size}" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
77
+ <g transform="rotate(${rotation} 8 8)">
78
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${color}" stroke-width="1.5"
79
+ stroke-dasharray="4 3" stroke-linecap="round"/>
80
+ </g>
81
+ </svg>
82
+ `.trim();
83
+ }
84
+
85
+ /**
86
+ * Generate SVG for complete state (green checkmark in circle)
87
+ */
88
+ function generateCompleteSvg(size, isTemplate = false) {
89
+ const bgColor = isTemplate ? COLORS.black : COLORS.green;
90
+ return `
91
+ <svg width="${size}" height="${size}" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
92
+ <circle cx="8" cy="8" r="6" fill="${bgColor}"/>
93
+ <path d="M5 8l2 2 4-4" stroke="${COLORS.white}" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
94
+ </svg>
95
+ `.trim();
96
+ }
97
+
98
+ /**
99
+ * Generate SVG for error state (warning triangle)
100
+ */
101
+ function generateErrorSvg(size, isTemplate = false) {
102
+ const color = isTemplate ? COLORS.black : COLORS.orange;
103
+ return `
104
+ <svg width="${size}" height="${size}" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
105
+ <path d="M8 2L14 13H2L8 2Z" fill="${color}"/>
106
+ <text x="8" y="11" text-anchor="middle" fill="${COLORS.white}" font-size="8" font-weight="bold" font-family="system-ui, sans-serif">!</text>
107
+ </svg>
108
+ `.trim();
109
+ }
110
+
111
+ /**
112
+ * Convert SVG to PNG using sharp
113
+ */
114
+ async function svgToPng(svg, outputPath, targetSize) {
115
+ // Parse the SVG to get its viewBox dimensions
116
+ const viewBoxMatch = svg.match(/viewBox="0 0 (\d+) (\d+)"/);
117
+ const svgSize = viewBoxMatch ? parseInt(viewBoxMatch[1]) : 16;
118
+
119
+ // Create the PNG at the target size
120
+ await sharp(Buffer.from(svg))
121
+ .resize(targetSize, targetSize)
122
+ .png()
123
+ .toFile(outputPath);
124
+ }
125
+
126
+ /**
127
+ * Generate all icon files
128
+ */
129
+ async function generateIcons() {
130
+ // Ensure assets directory exists
131
+ if (!existsSync(ASSETS_DIR)) {
132
+ await mkdir(ASSETS_DIR, { recursive: true });
133
+ console.log(`Created assets directory: ${ASSETS_DIR}`);
134
+ }
135
+
136
+ const generated = [];
137
+ const errors = [];
138
+
139
+ // State definitions
140
+ const states = [
141
+ { name: 'idle', generator: generateIdleSvg },
142
+ { name: 'recording', generator: generateRecordingSvg },
143
+ { name: 'complete', generator: generateCompleteSvg },
144
+ { name: 'error', generator: generateErrorSvg },
145
+ ];
146
+
147
+ // Generate static state icons
148
+ for (const state of states) {
149
+ for (const { suffix, size } of SIZES) {
150
+ // Normal colored version
151
+ const normalFilename = `tray-${state.name}${suffix}.png`;
152
+ const normalPath = join(ASSETS_DIR, normalFilename);
153
+ try {
154
+ const svg = state.generator(size, false);
155
+ await svgToPng(svg, normalPath, size);
156
+ generated.push(normalFilename);
157
+ } catch (err) {
158
+ errors.push({ file: normalFilename, error: err.message });
159
+ }
160
+
161
+ // Template version for macOS (auto dark/light mode)
162
+ const templateFilename = `tray-${state.name}Template${suffix}.png`;
163
+ const templatePath = join(ASSETS_DIR, templateFilename);
164
+ try {
165
+ const svg = state.generator(size, true);
166
+ await svgToPng(svg, templatePath, size);
167
+ generated.push(templateFilename);
168
+ } catch (err) {
169
+ errors.push({ file: templateFilename, error: err.message });
170
+ }
171
+ }
172
+ }
173
+
174
+ // Generate processing animation frames (4 frames at 0, 90, 180, 270 degrees)
175
+ const processingFrames = [0, 1, 2, 3];
176
+ const rotations = [0, 90, 180, 270];
177
+
178
+ for (let i = 0; i < processingFrames.length; i++) {
179
+ const frame = processingFrames[i];
180
+ const rotation = rotations[i];
181
+
182
+ for (const { suffix, size } of SIZES) {
183
+ // Normal colored version
184
+ const normalFilename = `tray-processing-${frame}${suffix}.png`;
185
+ const normalPath = join(ASSETS_DIR, normalFilename);
186
+ try {
187
+ const svg = generateProcessingSvg(size, rotation, false);
188
+ await svgToPng(svg, normalPath, size);
189
+ generated.push(normalFilename);
190
+ } catch (err) {
191
+ errors.push({ file: normalFilename, error: err.message });
192
+ }
193
+
194
+ // Template version for macOS
195
+ const templateFilename = `tray-processing-${frame}Template${suffix}.png`;
196
+ const templatePath = join(ASSETS_DIR, templateFilename);
197
+ try {
198
+ const svg = generateProcessingSvg(size, rotation, true);
199
+ await svgToPng(svg, templatePath, size);
200
+ generated.push(templateFilename);
201
+ } catch (err) {
202
+ errors.push({ file: templateFilename, error: err.message });
203
+ }
204
+ }
205
+ }
206
+
207
+ // Also generate a static processing icon (for fallback)
208
+ for (const { suffix, size } of SIZES) {
209
+ const normalFilename = `tray-processing${suffix}.png`;
210
+ const normalPath = join(ASSETS_DIR, normalFilename);
211
+ try {
212
+ const svg = generateProcessingSvg(size, 0, false);
213
+ await svgToPng(svg, normalPath, size);
214
+ generated.push(normalFilename);
215
+ } catch (err) {
216
+ errors.push({ file: normalFilename, error: err.message });
217
+ }
218
+
219
+ const templateFilename = `tray-processingTemplate${suffix}.png`;
220
+ const templatePath = join(ASSETS_DIR, templateFilename);
221
+ try {
222
+ const svg = generateProcessingSvg(size, 0, true);
223
+ await svgToPng(svg, templatePath, size);
224
+ generated.push(templateFilename);
225
+ } catch (err) {
226
+ errors.push({ file: templateFilename, error: err.message });
227
+ }
228
+ }
229
+
230
+ // Summary
231
+ console.log('\n=== Tray Icon Generation Complete ===\n');
232
+ console.log(`Generated: ${generated.length} icons`);
233
+
234
+ if (generated.length > 0) {
235
+ console.log('\nGenerated files:');
236
+ generated.forEach((f) => console.log(` - ${f}`));
237
+ }
238
+
239
+ if (errors.length > 0) {
240
+ console.log('\nErrors:');
241
+ errors.forEach(({ file, error }) => console.log(` - ${file}: ${error}`));
242
+ }
243
+
244
+ console.log('\nIcon states:');
245
+ console.log(' - idle: Gray circle outline (ready to record)');
246
+ console.log(' - recording: Red filled circle');
247
+ console.log(' - processing: Gray dashed circle (4 rotation frames)');
248
+ console.log(' - complete: Green circle with checkmark');
249
+ console.log(' - error: Orange warning triangle');
250
+
251
+ return { generated, errors };
252
+ }
253
+
254
+ // Run the generator
255
+ generateIcons().catch((err) => {
256
+ console.error('Failed to generate icons:', err);
257
+ process.exit(1);
258
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * notarize.js
3
+ *
4
+ * Apple Notarization script for markupr
5
+ * This script runs automatically after code signing via electron-builder
6
+ *
7
+ * Handles notarization for:
8
+ * - .app bundles (main application)
9
+ * - .dmg files (disk image installers)
10
+ * - .zip files (compressed archives)
11
+ *
12
+ * Required Environment Variables:
13
+ * APPLE_ID - Your Apple ID email
14
+ * APPLE_APP_SPECIFIC_PASSWORD - App-specific password from appleid.apple.com
15
+ * APPLE_TEAM_ID - Your Apple Developer Team ID
16
+ *
17
+ * @see https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution
18
+ */
19
+
20
+ const { notarize } = require('@electron/notarize');
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+
24
+ // Logging utilities
25
+ const log = {
26
+ info: (msg) => console.log(`[notarize] INFO: ${msg}`),
27
+ progress: (msg) => console.log(`[notarize] >>> ${msg}`),
28
+ success: (msg) => console.log(`[notarize] SUCCESS: ${msg}`),
29
+ warn: (msg) => console.warn(`[notarize] WARN: ${msg}`),
30
+ error: (msg) => console.error(`[notarize] ERROR: ${msg}`),
31
+ divider: () => console.log('='.repeat(60)),
32
+ };
33
+
34
+ /**
35
+ * Format duration in human-readable form
36
+ */
37
+ function formatDuration(ms) {
38
+ const seconds = Math.floor(ms / 1000);
39
+ const minutes = Math.floor(seconds / 60);
40
+ const remainingSeconds = seconds % 60;
41
+
42
+ if (minutes > 0) {
43
+ return `${minutes}m ${remainingSeconds}s`;
44
+ }
45
+ return `${seconds}s`;
46
+ }
47
+
48
+ /**
49
+ * Check if required environment variables are set
50
+ */
51
+ function checkCredentials() {
52
+ const required = ['APPLE_ID', 'APPLE_APP_SPECIFIC_PASSWORD', 'APPLE_TEAM_ID'];
53
+ const missing = required.filter(key => !process.env[key]);
54
+
55
+ if (missing.length > 0) {
56
+ return {
57
+ valid: false,
58
+ missing,
59
+ };
60
+ }
61
+
62
+ return { valid: true, missing: [] };
63
+ }
64
+
65
+ /**
66
+ * Notarize a single artifact
67
+ */
68
+ async function notarizeArtifact(artifactPath, appBundleId) {
69
+ log.progress(`Notarizing: ${path.basename(artifactPath)}`);
70
+ log.info(`Full path: ${artifactPath}`);
71
+
72
+ const startTime = Date.now();
73
+
74
+ try {
75
+ await notarize({
76
+ appBundleId,
77
+ appPath: artifactPath,
78
+ appleId: process.env.APPLE_ID,
79
+ appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
80
+ teamId: process.env.APPLE_TEAM_ID,
81
+ });
82
+
83
+ const duration = formatDuration(Date.now() - startTime);
84
+ log.success(`Notarized ${path.basename(artifactPath)} in ${duration}`);
85
+ return true;
86
+ } catch (error) {
87
+ log.error(`Failed to notarize ${path.basename(artifactPath)}`);
88
+ log.error(error.message);
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Main notarization function called by electron-builder afterSign hook
95
+ */
96
+ exports.default = async function notarizing(context) {
97
+ const { electronPlatformName, appOutDir } = context;
98
+ const appBundleId = 'com.eddiesanjuan.markupr';
99
+
100
+ log.divider();
101
+ log.info('markupr Notarization');
102
+ log.divider();
103
+
104
+ // Only notarize on macOS
105
+ if (electronPlatformName !== 'darwin') {
106
+ log.info('Skipping: not macOS platform');
107
+ return;
108
+ }
109
+
110
+ // Check credentials
111
+ const credentials = checkCredentials();
112
+ if (!credentials.valid) {
113
+ log.warn('Skipping: missing credentials');
114
+ log.info(`Missing: ${credentials.missing.join(', ')}`);
115
+ log.info('');
116
+ log.info('To enable notarization, set these environment variables:');
117
+ log.info(' export APPLE_ID="your@email.com"');
118
+ log.info(' export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"');
119
+ log.info(' export APPLE_TEAM_ID="XXXXXXXXXX"');
120
+ log.info('');
121
+ log.info('Get an app-specific password at: https://appleid.apple.com/account/manage');
122
+ return;
123
+ }
124
+
125
+ log.info(`Team ID: ${process.env.APPLE_TEAM_ID}`);
126
+ log.info(`Apple ID: ${process.env.APPLE_ID.replace(/(.{3}).*(@.*)/, '$1***$2')}`);
127
+ log.info('');
128
+
129
+ const appName = context.packager.appInfo.productFilename;
130
+ const appPath = path.join(appOutDir, `${appName}.app`);
131
+
132
+ // Verify app exists
133
+ if (!fs.existsSync(appPath)) {
134
+ log.error(`App bundle not found: ${appPath}`);
135
+ throw new Error(`App bundle not found: ${appPath}`);
136
+ }
137
+
138
+ const totalStartTime = Date.now();
139
+ const results = [];
140
+
141
+ // Step 1: Notarize the .app bundle
142
+ log.divider();
143
+ log.progress('Step 1/1: Notarizing app bundle');
144
+ log.divider();
145
+
146
+ const appResult = await notarizeArtifact(appPath, appBundleId);
147
+ results.push({ artifact: '.app bundle', success: appResult });
148
+
149
+ // Summary
150
+ log.divider();
151
+ log.info('Notarization Summary');
152
+ log.divider();
153
+
154
+ const successful = results.filter(r => r.success).length;
155
+ const failed = results.filter(r => !r.success).length;
156
+
157
+ results.forEach(({ artifact, success }) => {
158
+ const status = success ? 'OK' : 'FAILED';
159
+ console.log(` [${status}] ${artifact}`);
160
+ });
161
+
162
+ log.info('');
163
+ log.info(`Total: ${successful} succeeded, ${failed} failed`);
164
+ log.info(`Duration: ${formatDuration(Date.now() - totalStartTime)}`);
165
+ log.divider();
166
+
167
+ if (failed > 0) {
168
+ log.error('Some artifacts failed notarization');
169
+ log.info('');
170
+ log.info('Troubleshooting tips:');
171
+ log.info(' 1. Verify your Apple ID is enrolled in the Developer Program');
172
+ log.info(' 2. Generate a new app-specific password at https://appleid.apple.com');
173
+ log.info(' 3. Ensure your Team ID matches your Developer account');
174
+ log.info(' 4. Check that code signing succeeded before notarization');
175
+ log.info(' 5. Run `xcrun notarytool history` to see submission status');
176
+ throw new Error('Notarization failed');
177
+ }
178
+
179
+ log.success('All notarization complete!');
180
+ };