@bytechain.cn/colamd 1.5.0 → 1.5.1-beta.2

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 (193) hide show
  1. package/.trae/specs/optimize-theme-loading/checklist.md +20 -0
  2. package/.trae/specs/optimize-theme-loading/spec.md +103 -0
  3. package/.trae/specs/optimize-theme-loading/tasks.md +40 -0
  4. package/CHANGELOG.md +323 -0
  5. package/CLAUDE.md +56 -0
  6. package/README.md +422 -26
  7. package/README_CN.md +480 -28
  8. package/android/app/build/.npmkeep +0 -0
  9. package/android/app/build.gradle +76 -0
  10. package/android/app/capacitor.build.gradle +24 -0
  11. package/android/app/proguard-rules.pro +21 -0
  12. package/android/app/release.keystore +0 -0
  13. package/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
  14. package/android/app/src/main/AndroidManifest.xml +64 -0
  15. package/android/app/src/main/java/cn/bytechain/colamd/MainActivity.java +180 -0
  16. package/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  17. package/android/app/src/main/res/drawable/splash.png +0 -0
  18. package/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
  19. package/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
  20. package/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
  21. package/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
  22. package/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
  23. package/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
  24. package/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
  25. package/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
  26. package/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
  27. package/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
  28. package/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
  29. package/android/app/src/main/res/layout/activity_main.xml +12 -0
  30. package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  31. package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  32. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  33. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  34. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  35. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  36. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  37. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  38. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  39. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  40. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  41. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  42. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  43. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  44. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  45. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  46. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  47. package/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
  48. package/android/app/src/main/res/values/strings.xml +7 -0
  49. package/android/app/src/main/res/values/styles.xml +22 -0
  50. package/android/app/src/main/res/xml/file_paths.xml +5 -0
  51. package/android/app/src/main/res/xml/network_security_config.xml +8 -0
  52. package/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
  53. package/android/build.gradle +29 -0
  54. package/android/capacitor.settings.gradle +21 -0
  55. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  56. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  57. package/android/gradle.properties +22 -0
  58. package/android/gradlew +248 -0
  59. package/android/gradlew.bat +92 -0
  60. package/android/settings.gradle +5 -0
  61. package/android/variables.gradle +16 -0
  62. package/bytechain.cn-colamd-1.5.1-beta.2.tgz +0 -0
  63. package/capacitor.config.js +29 -0
  64. package/capacitor.config.ts +30 -0
  65. package/demo.md +191 -484
  66. package/dist/main/index.js +77 -46
  67. package/dist/renderer/assets/{arc-tTbbM8LO.js → arc-CPdeInCG.js} +1 -1
  68. package/dist/renderer/assets/{architectureDiagram-3BPJPVTR-CEgYow6c.js → architectureDiagram-3BPJPVTR-BAbnaR9G.js} +4 -3
  69. package/dist/renderer/assets/{blockDiagram-GPEHLZMM-LHyVtPwW.js → blockDiagram-GPEHLZMM-CYSWjnJg.js} +5 -4
  70. package/dist/renderer/assets/{c4Diagram-AAUBKEIU-C1P1eJrf.js → c4Diagram-AAUBKEIU-Rb1tstnr.js} +3 -2
  71. package/dist/renderer/assets/{channel-upve91Tq.js → channel-DpG2A6fE.js} +1 -1
  72. package/dist/renderer/assets/{chunk-2J33WTMH-lag2vhq9.js → chunk-2J33WTMH-DFc0Jxy_.js} +1 -1
  73. package/dist/renderer/assets/{chunk-4BX2VUAB-BXJ8Ggh-.js → chunk-4BX2VUAB-BhRxDTNn.js} +1 -1
  74. package/dist/renderer/assets/{chunk-55IACEB6-CiBpxRa1.js → chunk-55IACEB6-DEgMVBk8.js} +1 -1
  75. package/dist/renderer/assets/{chunk-727SXJPM-ODeKQFXC.js → chunk-727SXJPM-bjBIfiz8.js} +5 -5
  76. package/dist/renderer/assets/{chunk-AQP2D5EJ-BK7xJolB.js → chunk-AQP2D5EJ-DwQMzTzD.js} +3 -3
  77. package/dist/renderer/assets/{chunk-FMBD7UC4-BxpCZPtz.js → chunk-FMBD7UC4-CkphwJzs.js} +1 -1
  78. package/dist/renderer/assets/{chunk-ND2GUHAM-CqqaU9Ue.js → chunk-ND2GUHAM-oU09z4y4.js} +1 -1
  79. package/dist/renderer/assets/{chunk-QZHKN3VN-Biq_K124.js → chunk-QZHKN3VN-rCbVuPBn.js} +1 -1
  80. package/dist/renderer/assets/{classDiagram-v2-Q7XG4LA2-Cq95X99o.js → classDiagram-4FO5ZUOK-DGS2faoM.js} +7 -6
  81. package/dist/renderer/assets/{classDiagram-4FO5ZUOK-Cq95X99o.js → classDiagram-v2-Q7XG4LA2-DGS2faoM.js} +7 -6
  82. package/dist/renderer/assets/{cose-bilkent-S5V4N54A-XasiD0bu.js → cose-bilkent-S5V4N54A-iqY6-EwA.js} +2 -1
  83. package/dist/renderer/assets/{dagre-BM42HDAG-Nq84Gfx4.js → dagre-BM42HDAG-5t3X5sDa.js} +4 -3
  84. package/dist/renderer/assets/{diagram-2AECGRRQ-DwuB1GWt.js → diagram-2AECGRRQ-DzHiYDPh.js} +4 -3
  85. package/dist/renderer/assets/{diagram-5GNKFQAL-C2tgeI1h.js → diagram-5GNKFQAL-BiNv6Keq.js} +5 -4
  86. package/dist/renderer/assets/{diagram-KO2AKTUF-D5KzjNBc.js → diagram-KO2AKTUF-ClzeDG6f.js} +4 -3
  87. package/dist/renderer/assets/{diagram-LMA3HP47-C12xHS1c.js → diagram-LMA3HP47-CGkw7wII.js} +4 -3
  88. package/dist/renderer/assets/{diagram-OG6HWLK6-CnxI9oEa.js → diagram-OG6HWLK6-Dl-Hyk1_.js} +5 -4
  89. package/dist/renderer/assets/{erDiagram-TEJ5UH35-D_uPaKwn.js → erDiagram-TEJ5UH35-BxUN79Qb.js} +5 -4
  90. package/dist/renderer/assets/{flowDiagram-I6XJVG4X-B6q_1-tE.js → flowDiagram-I6XJVG4X-CzFk-KNI.js} +7 -6
  91. package/dist/renderer/assets/{ganttDiagram-6RSMTGT7-CFo7ifF9.js → ganttDiagram-6RSMTGT7-C2xl6Igx.js} +3 -2
  92. package/dist/renderer/assets/{gitGraphDiagram-PVQCEYII-WSexHTnq.js → gitGraphDiagram-PVQCEYII-_fn7XCa7.js} +5 -4
  93. package/dist/renderer/assets/{graph-DyX_9f6d.js → graph-CDoHYrHm.js} +1 -1
  94. package/dist/renderer/assets/index-B4uDgADr.js +530 -0
  95. package/dist/renderer/assets/index-CBcVpA3d.js +30 -0
  96. package/dist/renderer/assets/index-CGj1spkU.js +27 -0
  97. package/dist/renderer/assets/{index-dyHEFYvY.css → index-CeFpoCKV.css} +443 -400
  98. package/dist/renderer/assets/index-D4CPFkph.js +9 -0
  99. package/dist/renderer/assets/{index-DW7LS8C1.js → index-DAlXyxzt.js} +1183 -346
  100. package/dist/renderer/assets/index-DxOzbfR-.js +110 -0
  101. package/dist/renderer/assets/index-Y89U1ptl.js +9 -0
  102. package/dist/renderer/assets/{infoDiagram-5YYISTIA-DaeJdLRq.js → infoDiagram-5YYISTIA-DL6XIxLz.js} +3 -2
  103. package/dist/renderer/assets/{ishikawaDiagram-YF4QCWOH-DDCZc35f.js → ishikawaDiagram-YF4QCWOH-BUZLjRo-.js} +2 -1
  104. package/dist/renderer/assets/{journeyDiagram-JHISSGLW-BEdmpAgl.js → journeyDiagram-JHISSGLW-C4rH_mQM.js} +5 -4
  105. package/dist/renderer/assets/{kanban-definition-UN3LZRKU-BEFtQcFb.js → kanban-definition-UN3LZRKU-DRbrBcWV.js} +3 -2
  106. package/dist/renderer/assets/{layout-CAJgQHdw.js → layout-DZl4n4qu.js} +2 -2
  107. package/dist/renderer/assets/{linear-B2ggJ8Am.js → linear-B0Krxg21.js} +1 -1
  108. package/dist/renderer/assets/{mindmap-definition-RKZ34NQL-DSxVgHB5.js → mindmap-definition-RKZ34NQL-DdmPsWrn.js} +4 -3
  109. package/dist/renderer/assets/{pieDiagram-4H26LBE5-CwYoJBuL.js → pieDiagram-4H26LBE5-BPZLqwG0.js} +5 -4
  110. package/dist/renderer/assets/preload-helper-tXtZnHb0.js +88 -0
  111. package/dist/renderer/assets/{quadrantDiagram-W4KKPZXB-CST9Fvg9.js → quadrantDiagram-W4KKPZXB-Dr-oWRk9.js} +3 -2
  112. package/dist/renderer/assets/{requirementDiagram-4Y6WPE33-DtrH52jS.js → requirementDiagram-4Y6WPE33-B6QZd0lo.js} +4 -3
  113. package/dist/renderer/assets/{sankeyDiagram-5OEKKPKP-ca1tPzJ_.js → sankeyDiagram-5OEKKPKP-Cyl9ojEt.js} +2 -1
  114. package/dist/renderer/assets/{sequenceDiagram-3UESZ5HK-Dfp1EJZ7.js → sequenceDiagram-3UESZ5HK-D48Yr9T6.js} +4 -3
  115. package/dist/renderer/assets/{stateDiagram-AJRCARHV-Bha2QoNB.js → stateDiagram-AJRCARHV-qyb8ETsa.js} +7 -6
  116. package/dist/renderer/assets/{stateDiagram-v2-BHNVJYJU-DWgFUYu1.js → stateDiagram-v2-BHNVJYJU-DmDOyyrJ.js} +5 -4
  117. package/dist/renderer/assets/{timeline-definition-PNZ67QCA-C3h_-OTj.js → timeline-definition-PNZ67QCA-C-KQxTi1.js} +3 -2
  118. package/dist/renderer/assets/{vennDiagram-CIIHVFJN-DFzjSrZi.js → vennDiagram-CIIHVFJN-BdaZlnH-.js} +2 -1
  119. package/dist/renderer/assets/{wardley-L42UT6IY-Cx-VbqoS.js → wardley-L42UT6IY-b-_GPpqL.js} +1 -1
  120. package/dist/renderer/assets/{wardleyDiagram-YWT4CUSO-S2D9XqX6.js → wardleyDiagram-YWT4CUSO-B2hBE-EE.js} +4 -3
  121. package/dist/renderer/assets/web-BKE0SH0E.js +36 -0
  122. package/dist/renderer/assets/web-CBsFp24u.js +564 -0
  123. package/dist/renderer/assets/web-Dc8YgoHP.js +24 -0
  124. package/dist/renderer/assets/web-TfDzToU7.js +58 -0
  125. package/dist/renderer/assets/{xychartDiagram-2RQKCTM6-Cfxigbts.js → xychartDiagram-2RQKCTM6-CSvswDTY.js} +3 -2
  126. package/dist/renderer/index.html +62 -3
  127. package/docs/academic-demo.md +566 -0
  128. package/docs/demo.html +748 -0
  129. package/docs/demo.md +546 -0
  130. package/docs/demo.pdf +0 -0
  131. package/docs/theme-paradigm.md +658 -0
  132. package/electron-builder.yml +7 -0
  133. package/electron.vite.config.js +31 -0
  134. package/electron.vite.config.ts +1 -1
  135. package/ios/App/App/AppDelegate.swift +49 -0
  136. package/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
  137. package/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  138. package/ios/App/App/Assets.xcassets/Contents.json +6 -0
  139. package/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +23 -0
  140. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
  141. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
  142. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
  143. package/ios/App/App/Base.lproj/LaunchScreen.storyboard +32 -0
  144. package/ios/App/App/Base.lproj/Main.storyboard +19 -0
  145. package/ios/App/App/Info.plist +49 -0
  146. package/ios/App/App.xcodeproj/project.pbxproj +408 -0
  147. package/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  148. package/ios/App/Podfile +28 -0
  149. package/package.json +23 -3
  150. package/resources/templates/slides/template-forest-ink.html +540 -0
  151. package/scripts/generate-icons.js +102 -0
  152. package/src/main/index.ts +87 -63
  153. package/src/preload/index.d.ts +51 -0
  154. package/src/preload/index.js +70 -0
  155. package/src/renderer/capacitor-api.ts +713 -0
  156. package/src/renderer/editor/editor.ts +87 -4
  157. package/src/renderer/editor/plugins/index.ts +24 -32
  158. package/src/renderer/editor/plugins/math-plugin.ts +1 -1
  159. package/src/renderer/editor/plugins/mermaid-plugin-custom.css +13 -398
  160. package/src/renderer/editor/plugins/mermaid-plugin.ts +62 -71
  161. package/src/renderer/editor/plugins/themes/base/dark.css +23 -0
  162. package/src/renderer/editor/plugins/themes/base/elegant.css +32 -0
  163. package/src/renderer/editor/plugins/themes/base/light.css +20 -0
  164. package/src/renderer/editor/plugins/themes/base/newsprint.css +27 -0
  165. package/src/renderer/editor/plugins/themes/components/mermaid/academic.css +43 -0
  166. package/src/renderer/editor/plugins/themes/components/mermaid/dark.css +20 -0
  167. package/src/renderer/editor/plugins/themes/components/mermaid/elegant.css +24 -0
  168. package/src/renderer/editor/plugins/themes/components/mermaid/light.css +21 -0
  169. package/src/renderer/editor/plugins/themes/components/mermaid/newsprint.css +26 -0
  170. package/src/renderer/editor/plugins/themes/components/mermaid/variables.css +592 -0
  171. package/src/renderer/editor/plugins/themes/foundation.css +143 -0
  172. package/src/renderer/editor/plugins/themes/theme-manager.ts +92 -0
  173. package/src/renderer/env.d.ts +4 -1
  174. package/src/renderer/index.html +59 -1
  175. package/src/renderer/main.ts +432 -57
  176. package/src/renderer/mobile.css +429 -0
  177. package/themes/README.md +3 -0
  178. package/themes/academic-paper.css +1321 -0
  179. package/themes/elegant.css +14 -7
  180. package/themes/forest-ink.css +664 -0
  181. package/themes/pixso-design.css +1261 -0
  182. package/themes/swiss-design.css +596 -0
  183. package/themes/template.css +498 -0
  184. package/tsconfig.main.json +1 -0
  185. package/tsconfig.main.tsbuildinfo +1 -0
  186. package/tsconfig.preload.json +1 -0
  187. package/tsconfig.preload.tsbuildinfo +1 -0
  188. package/tsconfig.renderer.json +1 -0
  189. package/tsconfig.renderer.tsbuildinfo +1 -0
  190. package/tsconfig.tsbuildinfo +1 -0
  191. package/.trae/documents/fix-mermaid-colors-and-sankey.md +0 -50
  192. package/src/renderer/themes/theme-manager.ts +0 -40
  193. /package/src/renderer/{themes → editor/plugins/themes}/base.css +0 -0
@@ -0,0 +1,102 @@
1
+ const sharp = require('sharp');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const rootDir = path.join(__dirname, '..');
6
+ const svgPath = path.join(rootDir, 'resources', 'icon.svg');
7
+ const androidResPath = path.join(rootDir, 'android', 'app', 'src', 'main', 'res');
8
+ const iosAssetsPath = path.join(rootDir, 'ios', 'App', 'App', 'Assets.xcassets', 'AppIcon.appiconset');
9
+
10
+ const androidIcons = [
11
+ { name: 'mipmap-mdpi/ic_launcher.png', size: 48 },
12
+ { name: 'mipmap-mdpi/ic_launcher_round.png', size: 48 },
13
+ { name: 'mipmap-hdpi/ic_launcher.png', size: 72 },
14
+ { name: 'mipmap-hdpi/ic_launcher_round.png', size: 72 },
15
+ { name: 'mipmap-xhdpi/ic_launcher.png', size: 96 },
16
+ { name: 'mipmap-xhdpi/ic_launcher_round.png', size: 96 },
17
+ { name: 'mipmap-xxhdpi/ic_launcher.png', size: 144 },
18
+ { name: 'mipmap-xxhdpi/ic_launcher_round.png', size: 144 },
19
+ { name: 'mipmap-xxxhdpi/ic_launcher.png', size: 192 },
20
+ { name: 'mipmap-xxxhdpi/ic_launcher_round.png', size: 192 },
21
+ ];
22
+
23
+ const androidForegroundIcons = [
24
+ { name: 'mipmap-mdpi/ic_launcher_foreground.png', size: 108 },
25
+ { name: 'mipmap-hdpi/ic_launcher_foreground.png', size: 162 },
26
+ { name: 'mipmap-xhdpi/ic_launcher_foreground.png', size: 216 },
27
+ { name: 'mipmap-xxhdpi/ic_launcher_foreground.png', size: 324 },
28
+ { name: 'mipmap-xxxhdpi/ic_launcher_foreground.png', size: 432 },
29
+ ];
30
+
31
+ const iosIcons = [
32
+ { name: 'AppIcon-512@2x.png', size: 1024 },
33
+ ];
34
+
35
+ async function generateIcons() {
36
+ console.log('Generating icons from SVG...');
37
+
38
+ for (const icon of androidIcons) {
39
+ const outputPath = path.join(androidResPath, icon.name);
40
+ const dir = path.dirname(outputPath);
41
+
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+
46
+ await sharp(svgPath)
47
+ .resize(icon.size, icon.size)
48
+ .png()
49
+ .toFile(outputPath);
50
+
51
+ console.log(`Generated: ${icon.name} (${icon.size}x${icon.size})`);
52
+ }
53
+
54
+ console.log('\nGenerating adaptive icon foreground layers...');
55
+
56
+ for (const icon of androidForegroundIcons) {
57
+ const outputPath = path.join(androidResPath, icon.name);
58
+ const dir = path.dirname(outputPath);
59
+
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+
64
+ const iconSize = Math.round(icon.size * 0.67);
65
+ const padding = Math.round((icon.size - iconSize) / 2);
66
+
67
+ await sharp(svgPath)
68
+ .resize(iconSize, iconSize)
69
+ .extend({
70
+ top: padding,
71
+ bottom: padding,
72
+ left: padding,
73
+ right: padding,
74
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
75
+ })
76
+ .png()
77
+ .toFile(outputPath);
78
+
79
+ console.log(`Generated: ${icon.name} (${icon.size}x${icon.size}, icon: ${iconSize}x${iconSize})`);
80
+ }
81
+
82
+ console.log('\nGenerating iOS icons...');
83
+
84
+ for (const icon of iosIcons) {
85
+ const outputPath = path.join(iosAssetsPath, icon.name);
86
+
87
+ if (!fs.existsSync(iosAssetsPath)) {
88
+ fs.mkdirSync(iosAssetsPath, { recursive: true });
89
+ }
90
+
91
+ await sharp(svgPath)
92
+ .resize(icon.size, icon.size)
93
+ .png()
94
+ .toFile(outputPath);
95
+
96
+ console.log(`Generated: ${icon.name} (${icon.size}x${icon.size})`);
97
+ }
98
+
99
+ console.log('\nDone!');
100
+ }
101
+
102
+ generateIcons().catch(console.error);
package/src/main/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { app, BrowserWindow, ipcMain, dialog, Menu, shell } from 'electron'
2
2
  import { join, basename, dirname, extname } from 'path'
3
- import { readFile, writeFile, readdir, copyFile, mkdir } from 'fs/promises'
4
- import { watch, FSWatcher, existsSync, readdirSync, readFileSync, createServer } from 'fs'
3
+ import { readFile, writeFile, readdir, copyFile, mkdir, stat } from 'fs/promises'
4
+ import { existsSync, readFileSync } from 'fs'
5
5
  import { IncomingMessage, ServerResponse } from 'http'
6
6
  import { createServer as createHttpServer } from 'http'
7
7
 
@@ -14,19 +14,10 @@ function ensureThemesDir(): void {
14
14
  }
15
15
  }
16
16
 
17
- async function scanCustomThemes(): Promise<string[]> {
18
- try {
19
- const files = await readdir(themesDir)
20
- return files.filter(f => f.endsWith('.css')).sort()
21
- } catch {
22
- return []
23
- }
24
- }
25
-
26
17
  // Per-window state
27
18
  interface WindowState {
28
19
  filePath: string | null
29
- watcher: FSWatcher | null
20
+ watcher: ReturnType<typeof setInterval> | null
30
21
  isInternalSave: boolean
31
22
  debounceTimer: ReturnType<typeof setTimeout> | null
32
23
  agentState: 'idle' | 'active' | 'cooldown'
@@ -110,7 +101,7 @@ function suggestFileName(win: BrowserWindow, content?: string): string | undefin
110
101
 
111
102
  function stopWatching(state: WindowState): void {
112
103
  if (state.watcher) {
113
- state.watcher.close()
104
+ clearInterval(state.watcher)
114
105
  state.watcher = null
115
106
  }
116
107
  if (state.agentCooldownTimer) {
@@ -152,28 +143,36 @@ function watchFile(win: BrowserWindow, state: WindowState): void {
152
143
  if (!state.filePath) return
153
144
  stopWatching(state)
154
145
  const filePath = state.filePath
155
- state.watcher = watch(filePath, (eventType) => {
156
- if (eventType !== 'change' || state.isInternalSave) return
157
-
158
- // Agent activity detection
159
- const now = Date.now()
160
- const gap = now - state.lastExternalChange
161
- state.lastExternalChange = now
162
- if (gap > 0 && gap < 2000) {
163
- transitionAgentState(win, state, 'active')
164
- } else if (state.agentState === 'active') {
165
- transitionAgentState(win, state, 'active') // reset cooldown timer
166
- }
146
+ let lastMtime = 0
147
+
148
+ stat(filePath).then(s => { lastMtime = s.mtimeMs }).catch(() => {})
149
+
150
+ state.watcher = setInterval(() => {
151
+ if (state.isInternalSave) return
152
+ stat(filePath).then(s => {
153
+ if (s.mtimeMs === lastMtime) return
154
+ lastMtime = s.mtimeMs
155
+
156
+ // Agent activity detection
157
+ const now = Date.now()
158
+ const gap = now - state.lastExternalChange
159
+ state.lastExternalChange = now
160
+ if (gap > 0 && gap < 2000) {
161
+ transitionAgentState(win, state, 'active')
162
+ } else if (state.agentState === 'active') {
163
+ transitionAgentState(win, state, 'active') // reset cooldown timer
164
+ }
167
165
 
168
- if (state.debounceTimer) clearTimeout(state.debounceTimer)
169
- state.debounceTimer = setTimeout(() => {
170
- readFile(filePath, 'utf-8')
171
- .then((data) => {
172
- if (!win.isDestroyed()) win.webContents.send('file-changed', resolveImagePaths(data, filePath))
173
- })
174
- .catch(() => {})
175
- }, 100)
176
- })
166
+ if (state.debounceTimer) clearTimeout(state.debounceTimer)
167
+ state.debounceTimer = setTimeout(() => {
168
+ readFile(filePath, 'utf-8')
169
+ .then((data) => {
170
+ if (!win.isDestroyed()) win.webContents.send('file-changed', resolveImagePaths(data, filePath))
171
+ })
172
+ .catch(() => {})
173
+ }, 100)
174
+ }).catch(() => {})
175
+ }, 500)
177
176
  }
178
177
 
179
178
  // Rewrite relative image paths in markdown to absolute file:// URLs
@@ -382,8 +381,9 @@ ipcMain.handle('save-export-file', async (event, dataUrl: string, defaultName: s
382
381
  ipcMain.handle('export-pdf', async (event) => {
383
382
  const win = getWinFromEvent(event)
384
383
  if (!win) return false
384
+ const defaultName = (suggestFileName(win) || 'colamd-print') + '.pdf'
385
385
  const result = await dialog.showSaveDialog(win, {
386
- defaultPath: suggestFileName(win),
386
+ defaultPath: defaultName,
387
387
  filters: [{ name: 'PDF', extensions: ['pdf'] }]
388
388
  })
389
389
  if (result.canceled || !result.filePath) return false
@@ -392,11 +392,13 @@ ipcMain.handle('export-pdf', async (event) => {
392
392
  const cssKey = await win.webContents.insertCSS(
393
393
  'html, body { height: auto !important; overflow: visible !important; } #titlebar { display: none !important; } #editor { height: auto !important; overflow: visible !important; } #editor .ProseMirror { min-height: auto !important; } .mermaid-error { display: none !important; } .mermaid-loading { display: none !important; } svg .error-icon, svg .error-text { display: none !important; } .math-inline-raw, .math-block-raw, textarea.mermaid-source { display: none !important; }'
394
394
  )
395
- const pdfData = await win.webContents.printToPDF({
396
- marginType: 0,
397
- printBackground: true,
398
- pageSize: 'A4'
399
- })
395
+ const pdfData = await Promise.race([
396
+ win.webContents.printToPDF({
397
+ printBackground: true,
398
+ pageSize: 'A4'
399
+ }),
400
+ new Promise<Buffer>((_, reject) => setTimeout(() => reject(new Error('PDF timeout')), 30000))
401
+ ])
400
402
  await win.webContents.removeInsertedCSS(cssKey)
401
403
  await writeFile(result.filePath, pdfData)
402
404
  return true
@@ -408,8 +410,9 @@ ipcMain.handle('export-pdf', async (event) => {
408
410
  ipcMain.handle('export-html', async (event, htmlContent: string) => {
409
411
  const win = getWinFromEvent(event)
410
412
  if (!win) return false
413
+ const defaultName = (suggestFileName(win) || 'colamd-export') + '.html'
411
414
  const result = await dialog.showSaveDialog(win, {
412
- defaultPath: suggestFileName(win),
415
+ defaultPath: defaultName,
413
416
  filters: [{ name: 'HTML', extensions: ['html'] }]
414
417
  })
415
418
  if (result.canceled || !result.filePath) return false
@@ -652,7 +655,8 @@ ipcMain.handle('load-custom-theme', async (event) => {
652
655
  const destPath = join(themesDir, fileName)
653
656
  await copyFile(srcPath, destPath)
654
657
  const css = await readFile(destPath, 'utf-8')
655
- buildMenu() // rebuild menu to include new theme
658
+ invalidateThemeCache()
659
+ await buildMenu() // rebuild menu to include new theme
656
660
  return { name: fileName, css }
657
661
  } catch {
658
662
  return null
@@ -677,18 +681,40 @@ ipcMain.handle('register-plugins', (_event, plugins: Array<{ id: string; name: s
677
681
  checked: p.enabled,
678
682
  click: () => sendToFocused('menu-toggle-plugin', p.id)
679
683
  }))
680
- buildMenu()
684
+ void buildMenu()
681
685
  return true
682
686
  })
683
687
 
684
688
  ipcMain.handle('sync-plugin-state', (_event, id: string, enabled: boolean) => {
685
689
  const item = pluginMenuItems.find((p: any) => p.id === id)
686
690
  if (item) item.checked = enabled
687
- buildMenu()
691
+ void buildMenu()
688
692
  })
689
693
 
690
694
  // Menu — targets the focused window
691
695
 
696
+ let cachedThemeFiles: string[] = []
697
+ let themeFilesValid = false
698
+
699
+ function invalidateThemeCache(): void {
700
+ themeFilesValid = false
701
+ }
702
+
703
+ async function scanThemeFiles(): Promise<void> {
704
+ try {
705
+ const files = await readdir(themesDir)
706
+ cachedThemeFiles = files.filter(f => f.endsWith('.css')).sort()
707
+ } catch {
708
+ cachedThemeFiles = []
709
+ }
710
+ themeFilesValid = true
711
+ }
712
+
713
+ async function getCachedThemeFiles(): Promise<string[]> {
714
+ if (!themeFilesValid) await scanThemeFiles()
715
+ return cachedThemeFiles
716
+ }
717
+
692
718
  function getFocusedWindow(): BrowserWindow | null {
693
719
  return BrowserWindow.getFocusedWindow()
694
720
  }
@@ -698,26 +724,23 @@ function sendToFocused(channel: string, ...args: unknown[]): void {
698
724
  if (win) win.webContents.send(channel, ...args)
699
725
  }
700
726
 
701
- function buildMenu(): void {
727
+ async function buildMenu(): Promise<void> {
702
728
  const isMac = process.platform === 'darwin'
703
729
 
704
- // Scan custom themes synchronously for menu building
705
730
  const customThemeItems: Electron.MenuItemConstructorOptions[] = []
706
- try {
707
- const files = readdirSync(themesDir).filter((f: string) => f.endsWith('.css')).sort()
708
- for (const file of files) {
709
- customThemeItems.push({
710
- label: file.replace(/\.css$/, ''),
711
- click: async () => {
712
- try {
713
- const css = await readFile(join(themesDir, file), 'utf-8')
714
- sendToFocused('set-theme', `custom:${file}`)
715
- sendToFocused('set-custom-css', css)
716
- } catch { /* ignore */ }
717
- }
718
- })
719
- }
720
- } catch { /* themes dir may not exist yet */ }
731
+ const files = await getCachedThemeFiles()
732
+ for (const file of files) {
733
+ customThemeItems.push({
734
+ label: file.replace(/\.css$/, ''),
735
+ click: async () => {
736
+ try {
737
+ const css = await readFile(join(themesDir, file), 'utf-8')
738
+ sendToFocused('set-theme', `custom:${file}`)
739
+ sendToFocused('set-custom-css', css)
740
+ } catch { /* ignore */ }
741
+ }
742
+ })
743
+ }
721
744
 
722
745
  const themeSubmenu: Electron.MenuItemConstructorOptions[] = [
723
746
  { label: 'Light', click: () => sendToFocused('set-theme', 'light') },
@@ -842,9 +865,10 @@ function buildMenu(): void {
842
865
 
843
866
  // App lifecycle
844
867
 
845
- app.whenReady().then(() => {
868
+ app.whenReady().then(async () => {
846
869
  ensureThemesDir()
847
- buildMenu()
870
+ await scanThemeFiles()
871
+ await buildMenu()
848
872
 
849
873
  // Check command line args for file paths
850
874
  const args = process.argv.slice(app.isPackaged ? 1 : 2)
@@ -0,0 +1,51 @@
1
+ export interface ElectronAPI {
2
+ openFile: () => Promise<{
3
+ path: string;
4
+ content: string;
5
+ } | null>;
6
+ openFilePath: (path: string) => Promise<{
7
+ path: string;
8
+ content: string;
9
+ } | null>;
10
+ saveFile: (content: string) => Promise<boolean>;
11
+ saveFileAs: (content: string) => Promise<boolean>;
12
+ exportPDF: () => Promise<boolean>;
13
+ exportHTML: (html: string) => Promise<boolean>;
14
+ newSlides: () => Promise<string | null>;
15
+ openAsSlides: (content: string) => Promise<boolean>;
16
+ loadCustomTheme: () => Promise<{
17
+ name: string;
18
+ css: string;
19
+ } | null>;
20
+ loadThemeCSS: (fileName: string) => Promise<string | null>;
21
+ getPathForFile: (file: File) => string;
22
+ openExternal: (url: string) => void;
23
+ onFileChanged: (callback: (content: string) => void) => void;
24
+ onNewFile: (callback: () => void) => void;
25
+ onFileOpened: (callback: (data: {
26
+ path: string;
27
+ content: string;
28
+ }) => void) => void;
29
+ onMenuOpen: (callback: () => void) => void;
30
+ onMenuSave: (callback: () => void) => void;
31
+ onMenuSaveAs: (callback: () => void) => void;
32
+ onMenuExportPDF: (callback: () => void) => void;
33
+ onMenuExportHTML: (callback: () => void) => void;
34
+ onMenuNewSlides: (callback: () => void) => void;
35
+ onMenuOpenAsSlides: (callback: () => void) => void;
36
+ onNewSlidesContent: (callback: (content: string) => void) => void;
37
+ onSetTheme: (callback: (theme: string) => void) => void;
38
+ onSetCustomCSS: (callback: (css: string) => void) => void;
39
+ exportSlides: (content: string) => Promise<boolean>;
40
+ onMenuExportSlides: (callback: () => void) => void;
41
+ onAgentActivity: (callback: (state: string) => void) => void;
42
+ registerPlugins: (plugins: Array<{
43
+ id: string;
44
+ name: string;
45
+ enabled: boolean;
46
+ }>) => Promise<boolean>;
47
+ syncPluginState: (id: string, enabled: boolean) => Promise<void>;
48
+ onMenuTogglePlugin: (callback: (id: string) => void) => void;
49
+ onMenuImportTheme: (callback: () => void) => void;
50
+ exportFile: (dataUrl: string, defaultName: string) => Promise<boolean>;
51
+ }
@@ -0,0 +1,70 @@
1
+ import { contextBridge, ipcRenderer, webUtils } from 'electron';
2
+ contextBridge.exposeInMainWorld('electronAPI', {
3
+ openFile: () => ipcRenderer.invoke('open-file'),
4
+ openFilePath: (path) => ipcRenderer.invoke('open-file-path', path),
5
+ saveFile: (content) => ipcRenderer.invoke('save-file', content),
6
+ saveFileAs: (content) => ipcRenderer.invoke('save-file-as', content),
7
+ exportPDF: () => ipcRenderer.invoke('export-pdf'),
8
+ exportHTML: (html) => ipcRenderer.invoke('export-html', html),
9
+ exportSlides: (content) => ipcRenderer.invoke('export-slides', content),
10
+ newSlides: () => ipcRenderer.invoke('new-slides'),
11
+ openAsSlides: (content) => ipcRenderer.invoke('open-as-slides', content),
12
+ loadCustomTheme: () => ipcRenderer.invoke('load-custom-theme'),
13
+ loadThemeCSS: (fileName) => ipcRenderer.invoke('load-theme-css', fileName),
14
+ getPathForFile: (file) => webUtils.getPathForFile(file),
15
+ openExternal: (url) => ipcRenderer.send('open-external', url),
16
+ onFileChanged: (callback) => {
17
+ ipcRenderer.on('file-changed', (_event, content) => callback(content));
18
+ },
19
+ onNewFile: (callback) => {
20
+ ipcRenderer.on('new-file', () => callback());
21
+ },
22
+ onFileOpened: (callback) => {
23
+ ipcRenderer.on('file-opened', (_event, data) => callback(data));
24
+ },
25
+ onMenuOpen: (callback) => {
26
+ ipcRenderer.on('menu-open', () => callback());
27
+ },
28
+ onMenuSave: (callback) => {
29
+ ipcRenderer.on('menu-save', () => callback());
30
+ },
31
+ onMenuSaveAs: (callback) => {
32
+ ipcRenderer.on('menu-save-as', () => callback());
33
+ },
34
+ onMenuExportPDF: (callback) => {
35
+ ipcRenderer.on('menu-export-pdf', () => callback());
36
+ },
37
+ onMenuExportHTML: (callback) => {
38
+ ipcRenderer.on('menu-export-html', () => callback());
39
+ },
40
+ onMenuNewSlides: (callback) => {
41
+ ipcRenderer.on('menu-new-slides', () => callback());
42
+ },
43
+ onMenuOpenAsSlides: (callback) => {
44
+ ipcRenderer.on('menu-open-as-slides', () => callback());
45
+ },
46
+ onNewSlidesContent: (callback) => {
47
+ ipcRenderer.on('new-slides-content', (_event, content) => callback(content));
48
+ },
49
+ onSetTheme: (callback) => {
50
+ ipcRenderer.on('set-theme', (_event, theme) => callback(theme));
51
+ },
52
+ onSetCustomCSS: (callback) => {
53
+ ipcRenderer.on('set-custom-css', (_event, css) => callback(css));
54
+ },
55
+ onMenuImportTheme: (callback) => {
56
+ ipcRenderer.on('menu-import-theme', () => callback());
57
+ },
58
+ onMenuExportSlides: (callback) => {
59
+ ipcRenderer.on('menu-export-slides', () => callback());
60
+ },
61
+ onAgentActivity: (callback) => {
62
+ ipcRenderer.on('agent-activity', (_event, state) => callback(state));
63
+ },
64
+ registerPlugins: (plugins) => ipcRenderer.invoke('register-plugins', plugins),
65
+ syncPluginState: (id, enabled) => ipcRenderer.invoke('sync-plugin-state', id, enabled),
66
+ onMenuTogglePlugin: (callback) => {
67
+ ipcRenderer.on('menu-toggle-plugin', (_event, id) => callback(id));
68
+ },
69
+ exportFile: (dataUrl, defaultName) => ipcRenderer.invoke('save-export-file', dataUrl, defaultName),
70
+ });