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,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
|
+
};
|