@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,713 @@
1
+ const { Capacitor } = await import('@capacitor/core').catch(() => ({ Capacitor: { isNativePlatform: () => false } }))
2
+
3
+ /**
4
+ * 懒加载 Capacitor 插件模块缓存。
5
+ * Electron 桌面环境不包含这些模块,仅在原生平台按需动态导入。
6
+ * 注意:必须使用 switch-case 中字符串字面量 import(),而不能用 import(name) 变量形式,
7
+ * 否则 Vite 无法在构建时静态分析,导致生产环境中插件模块加载失败。
8
+ */
9
+ const _capModules: Record<string, any> = {}
10
+ async function capModule(name: string): Promise<any> {
11
+ if (!(name in _capModules)) {
12
+ try {
13
+ switch (name) {
14
+ case '@capacitor/filesystem': _capModules[name] = await import('@capacitor/filesystem'); break
15
+ case '@capacitor/share': _capModules[name] = await import('@capacitor/share'); break
16
+ case '@capacitor/app': _capModules[name] = await import('@capacitor/app'); break
17
+ case '@capawesome/capacitor-file-picker': _capModules[name] = await import('@capawesome/capacitor-file-picker'); break
18
+ case '@capacitor/haptics': _capModules[name] = await import('@capacitor/haptics'); break
19
+ default: _capModules[name] = null
20
+ }
21
+ }
22
+ catch { _capModules[name] = null }
23
+ }
24
+ return _capModules[name]
25
+ }
26
+
27
+ /**
28
+ * 获取 @capacitor/filesystem 模块(含 Filesystem、Directory、Encoding)。
29
+ */
30
+ async function capFS() { return capModule('@capacitor/filesystem') }
31
+
32
+ /**
33
+ * 获取 @capacitor/share 模块。
34
+ */
35
+ async function capShare() { return capModule('@capacitor/share') }
36
+
37
+ /**
38
+ * 获取 @capawesome/capacitor-file-picker 模块。
39
+ */
40
+ async function capPicker() { return capModule('@capawesome/capacitor-file-picker') }
41
+
42
+ export interface CapacitorBridgeAPI {
43
+ openFile: () => Promise<{ path: string; content: string } | null>
44
+ openFilePath: (path: string) => Promise<{ path: string; content: string } | null>
45
+ setCurrentFile: (path: string) => void
46
+ saveFile: (content: string) => Promise<boolean>
47
+ saveFileAs: (content: string) => Promise<boolean>
48
+ exportPDF: (htmlContent?: string) => Promise<boolean>
49
+ exportHTML: (htmlContent: string) => Promise<boolean>
50
+ newSlides: () => Promise<string | null>
51
+ openAsSlides: (content: string) => Promise<boolean>
52
+ loadCustomTheme: () => Promise<{ name: string; css: string } | null>
53
+ loadThemeCSS: (fileName: string) => Promise<string | null>
54
+ getPathForFile: (_file: File) => string
55
+ openExternal: (url: string) => void
56
+ onFileChanged: (callback: (content: string) => void) => void
57
+ onNewFile: (callback: () => void) => void
58
+ onFileOpened: (callback: (data: { path: string; content: string }) => void) => void
59
+ onMenuOpen: (callback: () => void) => void
60
+ onMenuSave: (callback: () => void) => void
61
+ onMenuSaveAs: (callback: () => void) => void
62
+ onMenuExportPDF: (callback: () => void) => void
63
+ onMenuExportHTML: (callback: () => void) => void
64
+ onMenuNewSlides: (callback: () => void) => void
65
+ onMenuOpenAsSlides: (callback: () => void) => void
66
+ onNewSlidesContent: (callback: (content: string) => void) => void
67
+ onSetTheme: (callback: (theme: string) => void) => void
68
+ onSetCustomCSS: (callback: (css: string) => void) => void
69
+ exportSlides: (content: string) => Promise<boolean>
70
+ onMenuExportSlides: (callback: () => void) => void
71
+ onAgentActivity: (callback: (state: string) => void) => void
72
+ registerPlugins: (plugins: Array<{ id: string; name: string; enabled: boolean }>) => Promise<boolean>
73
+ syncPluginState: (id: string, enabled: boolean) => Promise<void>
74
+ onMenuTogglePlugin: (callback: (id: string) => void) => void
75
+ onMenuImportTheme: (callback: () => void) => void
76
+ exportFile: (dataUrl: string, defaultName: string) => Promise<boolean>
77
+ }
78
+
79
+ let currentFilePath: string | null = null
80
+ const eventListeners: Record<string, Array<(...args: any[]) => void>> = {}
81
+
82
+ /**
83
+ * 将 base64 编码字符串安全解码为 UTF-8 文本。
84
+ */
85
+ function decodeBase64UTF8(base64: string): string {
86
+ const binary = atob(base64)
87
+ const bytes = new Uint8Array(binary.length)
88
+ for (let i = 0; i < binary.length; i++) {
89
+ bytes[i] = binary.charCodeAt(i)
90
+ }
91
+ return new TextDecoder('utf-8').decode(bytes)
92
+ }
93
+
94
+ /**
95
+ * 写入文件并通过系统分享对话框分享。
96
+ */
97
+ async function writeAndShareFile(fileName: string, content: string): Promise<boolean> {
98
+ try {
99
+ const fs = await capFS()
100
+ await fs.Filesystem.writeFile({
101
+ path: fileName,
102
+ data: content,
103
+ directory: fs.Directory.Documents,
104
+ encoding: fs.Encoding.UTF8,
105
+ recursive: true,
106
+ })
107
+
108
+ const fileUri = await fs.Filesystem.getUri({
109
+ path: fileName,
110
+ directory: fs.Directory.Documents,
111
+ })
112
+
113
+ const sh = await capShare()
114
+ try {
115
+ await sh.Share.share({
116
+ title: fileName,
117
+ text: 'ColaMD Export',
118
+ files: [fileUri.uri],
119
+ dialogTitle: 'Share Export',
120
+ })
121
+ } catch { /* share dialog cancelled */ }
122
+ return true
123
+ } catch (err) {
124
+ console.error('writeAndShareFile failed:', err)
125
+ return false
126
+ }
127
+ }
128
+
129
+ function emit(event: string, ...args: any[]): void {
130
+ const listeners = eventListeners[event] || []
131
+ listeners.forEach((fn) => fn(...args))
132
+ }
133
+
134
+ function isNativePlatform(): boolean {
135
+ return Capacitor.isNativePlatform()
136
+ }
137
+
138
+ function getBaseName(path: string): string {
139
+ const parts = path.split(/[/\\]/)
140
+ return parts[parts.length - 1] || 'Untitled'
141
+ }
142
+
143
+ /**
144
+ * 从 markdown 内容中提取第一个标题作为文件名。
145
+ * 如果没有标题则使用默认名称。
146
+ */
147
+ function generateFilename(content: string, ext: string): string {
148
+ const match = content.match(/^#\s+(.+)$/m)
149
+ if (match) {
150
+ const title = match[1].replace(/[\\/:*?"<>|]/g, '').trim().slice(0, 50)
151
+ if (title) return `${title}.${ext}`
152
+ }
153
+ const ts = Date.now()
154
+ return `untitled_${ts}.${ext}`
155
+ }
156
+
157
+ /**
158
+ * 读取指定路径的文件内容。
159
+ */
160
+ async function readFileContent(filePath: string): Promise<string> {
161
+ try {
162
+ const fs = await capFS()
163
+ const result = await fs.Filesystem.readFile({
164
+ path: filePath,
165
+ directory: fs.Directory.Documents,
166
+ encoding: fs.Encoding.UTF8,
167
+ })
168
+ return result.data as string
169
+ } catch {
170
+ return ''
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 将内容写入指定路径的文件。
176
+ */
177
+ async function writeFileContent(filePath: string, content: string): Promise<boolean> {
178
+ try {
179
+ const fs = await capFS()
180
+ await fs.Filesystem.writeFile({
181
+ path: filePath,
182
+ data: content,
183
+ directory: fs.Directory.Documents,
184
+ encoding: fs.Encoding.UTF8,
185
+ recursive: true,
186
+ })
187
+ return true
188
+ } catch (err) {
189
+ console.error('writeFileContent failed:', err)
190
+ return false
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 触发原生平台触觉反馈。
196
+ * 使用已安装的 @capacitor/haptics 插件。
197
+ */
198
+ async function triggerHaptic(style: 'light' | 'medium' | 'heavy' = 'light'): Promise<void> {
199
+ if (!isNativePlatform()) return
200
+ try {
201
+ const { Haptics } = await import('@capacitor/haptics')
202
+ if (style === 'heavy') await Haptics.impact({ style: 'HEAVY' as any })
203
+ else if (style === 'medium') await Haptics.impact({ style: 'MEDIUM' as any })
204
+ else await Haptics.impact({ style: 'LIGHT' as any })
205
+ } catch { /* haptics not available */ }
206
+ }
207
+
208
+ /**
209
+ * 使用 FilePicker 插件选择并读取文件内容。
210
+ */
211
+ async function pickAndReadFile(): Promise<{ path: string; content: string } | null> {
212
+ try {
213
+ const picker = await capPicker()
214
+ const result = await picker.FilePicker.pickFiles({
215
+ types: [
216
+ 'text/markdown',
217
+ 'text/plain',
218
+ 'application/octet-stream',
219
+ 'text/x-markdown',
220
+ ],
221
+ multiple: false,
222
+ readData: true,
223
+ })
224
+
225
+ const file = result.files[0]
226
+ if (!file) return null
227
+
228
+ let content = ''
229
+ if (file.data) {
230
+ content = decodeBase64UTF8(file.data)
231
+ } else if (file.path) {
232
+ const fs = await capFS()
233
+ const readResult = await fs.Filesystem.readFile({
234
+ path: file.path,
235
+ encoding: fs.Encoding.UTF8,
236
+ })
237
+ content = readResult.data as string
238
+ }
239
+
240
+ const filePath = file.path || file.name
241
+ currentFilePath = filePath
242
+ return { path: filePath, content }
243
+ } catch (error) {
244
+ console.warn('File picker cancelled or failed:', error)
245
+ return null
246
+ }
247
+ }
248
+
249
+ /**
250
+ * 使用 FilePicker 插件选择 CSS 文件。
251
+ */
252
+ async function pickCSSFile(): Promise<{ name: string; css: string } | null> {
253
+ try {
254
+ const picker = await capPicker()
255
+ const result = await picker.FilePicker.pickFiles({
256
+ types: ['text/css'],
257
+ multiple: false,
258
+ readData: true,
259
+ })
260
+
261
+ const file = result.files[0]
262
+ if (!file) return null
263
+
264
+ let css = ''
265
+ if (file.data) {
266
+ css = decodeBase64UTF8(file.data)
267
+ } else if (file.path) {
268
+ const fs = await capFS()
269
+ const readResult = await fs.Filesystem.readFile({
270
+ path: file.path,
271
+ encoding: fs.Encoding.UTF8,
272
+ })
273
+ css = readResult.data as string
274
+ }
275
+
276
+ return { name: file.name, css }
277
+ } catch (error) {
278
+ console.warn('CSS file picker cancelled or failed:', error)
279
+ return null
280
+ }
281
+ }
282
+
283
+ export function createCapacitorAPI(): CapacitorBridgeAPI {
284
+ const api: CapacitorBridgeAPI = {
285
+
286
+ async openFile(): Promise<{ path: string; content: string } | null> {
287
+ if (!isNativePlatform()) {
288
+ const input = document.createElement('input')
289
+ input.type = 'file'
290
+ input.accept = '.md,.markdown,.mdown,.mkd,.txt'
291
+ return new Promise((resolve) => {
292
+ input.onchange = async () => {
293
+ const file = input.files?.[0]
294
+ if (!file) { resolve(null); return }
295
+ const content = await file.text()
296
+ currentFilePath = file.name
297
+ resolve({ path: file.name, content })
298
+ input.remove()
299
+ }
300
+ input.click()
301
+ })
302
+ }
303
+ return pickAndReadFile()
304
+ },
305
+
306
+ async openFilePath(path: string): Promise<{ path: string; content: string } | null> {
307
+ const content = await readFileContent(path)
308
+ if (!content && content !== '') return null
309
+ currentFilePath = path
310
+ return { path, content }
311
+ },
312
+
313
+ setCurrentFile(path: string): void {
314
+ currentFilePath = path
315
+ },
316
+
317
+ async saveFile(content: string): Promise<boolean> {
318
+ if (!currentFilePath) {
319
+ // First save: generate a filename and write directly.
320
+ // Don't call saveFileAs — its prompt() is unreliable on Android WebView.
321
+ const fileName = generateFilename(content, 'md')
322
+ const ok = await writeFileContent(fileName, content)
323
+ if (ok) {
324
+ currentFilePath = fileName
325
+ triggerHaptic('medium')
326
+ }
327
+ return ok
328
+ }
329
+ const ok = await writeFileContent(currentFilePath, content)
330
+ if (ok) triggerHaptic('light')
331
+ return ok
332
+ },
333
+
334
+ async saveFileAs(content: string): Promise<boolean> {
335
+ const baseName = currentFilePath
336
+ ? getBaseName(currentFilePath).replace(/\.md$/, '')
337
+ : 'untitled'
338
+ const fileName = `${baseName}_${Date.now()}.md`
339
+
340
+ if (!isNativePlatform()) {
341
+ const blob = new Blob([content], { type: 'text/markdown' })
342
+ const url = URL.createObjectURL(blob)
343
+ const a = document.createElement('a')
344
+ a.href = url
345
+ a.download = fileName
346
+ a.click()
347
+ URL.revokeObjectURL(url)
348
+ currentFilePath = fileName
349
+ return true
350
+ }
351
+
352
+ // Android: write file then open Share sheet so user can save to preferred location
353
+ const ok = await writeFileContent(fileName, content)
354
+ if (!ok) {
355
+ triggerHaptic('heavy')
356
+ return false
357
+ }
358
+
359
+ currentFilePath = fileName
360
+ triggerHaptic('medium')
361
+
362
+ try {
363
+ const fs = await capFS()
364
+ const fileUri = await fs.Filesystem.getUri({
365
+ path: fileName,
366
+ directory: fs.Directory.Documents,
367
+ })
368
+ const sh = await capShare()
369
+ await sh.Share.share({
370
+ title: fileName,
371
+ text: 'ColaMD Document',
372
+ files: [fileUri.uri],
373
+ dialogTitle: 'Save As',
374
+ })
375
+ } catch { /* share cancelled — file already saved */ }
376
+ return true
377
+ },
378
+
379
+ async exportPDF(htmlContent?: string): Promise<boolean> {
380
+ if (!isNativePlatform()) {
381
+ window.print()
382
+ return true
383
+ }
384
+
385
+ // Android: use native PrintManager via ColaMDNative bridge.
386
+ // This opens the system print dialog with "Save as PDF" option
387
+ // and produces proper paginated PDF output.
388
+ const bridge = (window as any).ColaMDNative
389
+ if (bridge && typeof bridge.printDocument === 'function') {
390
+ // Temporarily unconstrain the fixed-position mobile layout so
391
+ // the full document is captured, not just the visible viewport.
392
+ const orig: { el: HTMLElement; cssText: string }[] = []
393
+
394
+ function saveAndOverride(selector: string, overrides: Record<string, string>): void {
395
+ const el = document.querySelector(selector) as HTMLElement | null
396
+ if (!el) return
397
+ orig.push({ el, cssText: el.style.cssText })
398
+ for (const [prop, value] of Object.entries(overrides)) {
399
+ ;(el.style as any)[prop] = value
400
+ }
401
+ }
402
+
403
+ // Hide mobile UI chrome, unconstrain editor for full-document capture
404
+ saveAndOverride('#titlebar', { display: 'none' })
405
+ saveAndOverride('#mobile-menu', { display: 'none' })
406
+ saveAndOverride('#menu-btn', { display: 'none' })
407
+ saveAndOverride('.cola-toast', { display: 'none' })
408
+ saveAndOverride('html', { height: 'auto', overflow: 'visible' })
409
+ saveAndOverride('body', { height: 'auto', overflow: 'visible' })
410
+ saveAndOverride('#editor', {
411
+ position: 'static',
412
+ height: 'auto',
413
+ overflow: 'visible',
414
+ top: 'auto',
415
+ bottom: 'auto',
416
+ })
417
+ saveAndOverride('#editor .ProseMirror', { minHeight: 'auto' })
418
+
419
+ await new Promise(r => requestAnimationFrame(r))
420
+
421
+ const jobName = currentFilePath
422
+ ? getBaseName(currentFilePath).replace(/\.(md|markdown)$/, '')
423
+ : 'ColaMD Document'
424
+ bridge.printDocument(jobName)
425
+
426
+ // Restore layout after print adapter captures the content.
427
+ // The 3s delay gives the Android print framework time to snapshot.
428
+ setTimeout(() => {
429
+ for (const { el, cssText } of orig) {
430
+ el.style.cssText = cssText
431
+ }
432
+ }, 3000)
433
+
434
+ return true
435
+ }
436
+
437
+ // Fallback: if native bridge unavailable, share as HTML file
438
+ const content = htmlContent || ''
439
+ if (!content) return false
440
+ const defaultName = currentFilePath
441
+ ? getBaseName(currentFilePath).replace(/\.(md|markdown)$/, '-print.html')
442
+ : 'colamd-print.html'
443
+ return writeAndShareFile(defaultName, content)
444
+ },
445
+
446
+ async exportHTML(htmlContent: string): Promise<boolean> {
447
+ const defaultName = currentFilePath
448
+ ? getBaseName(currentFilePath).replace(/\.(md|markdown)$/, '.html')
449
+ : 'colamd-export.html'
450
+
451
+ if (!isNativePlatform()) {
452
+ const blob = new Blob([htmlContent], { type: 'text/html' })
453
+ const url = URL.createObjectURL(blob)
454
+ const a = document.createElement('a')
455
+ a.href = url
456
+ a.download = defaultName
457
+ a.click()
458
+ URL.revokeObjectURL(url)
459
+ return true
460
+ }
461
+
462
+ return writeAndShareFile(defaultName, htmlContent)
463
+ },
464
+
465
+ async newSlides(): Promise<string | null> {
466
+ const template = `---
467
+ kicker: ColaMD
468
+ chip: Markdown Editor · 2026
469
+ page: Your Name
470
+ ---
471
+
472
+ <!-- type: cover -->
473
+ # Welcome to ColaMD
474
+ Agent Native Markdown Editor
475
+
476
+ ---
477
+
478
+ <!-- type: statement -->
479
+ ## Start Writing
480
+ Edit this file in ColaMD and see your changes in real time.
481
+ `
482
+ return template
483
+ },
484
+
485
+ async openAsSlides(content: string): Promise<boolean> {
486
+ if (!isNativePlatform()) {
487
+ const newWindow = window.open('', '_blank')
488
+ if (newWindow) {
489
+ newWindow.document.write(`<pre>${content}</pre>`)
490
+ }
491
+ return !!newWindow
492
+ }
493
+ return writeAndShareFile('colamd-slides-preview.html', content)
494
+ },
495
+
496
+ async loadCustomTheme(): Promise<{ name: string; css: string } | null> {
497
+ if (!isNativePlatform()) {
498
+ const input = document.createElement('input')
499
+ input.type = 'file'
500
+ input.accept = '.css'
501
+ return new Promise((resolve) => {
502
+ input.onchange = async () => {
503
+ const file = input.files?.[0]
504
+ if (!file) { resolve(null); return }
505
+ const css = await file.text()
506
+ resolve({ name: file.name, css })
507
+ input.remove()
508
+ }
509
+ input.click()
510
+ })
511
+ }
512
+ return pickCSSFile()
513
+ },
514
+
515
+ async loadThemeCSS(_fileName: string): Promise<string | null> {
516
+ try {
517
+ const fs = await capFS()
518
+ const result = await fs.Filesystem.readFile({
519
+ path: `.colamd/themes/${_fileName}`,
520
+ directory: fs.Directory.Documents,
521
+ encoding: fs.Encoding.UTF8,
522
+ })
523
+ return result.data as string
524
+ } catch {
525
+ return null
526
+ }
527
+ },
528
+
529
+ getPathForFile(_file: File): string {
530
+ return _file.name || ''
531
+ },
532
+
533
+ openExternal(url: string): void {
534
+ if (isNativePlatform()) {
535
+ window.open(url, '_system', 'location=yes')
536
+ } else {
537
+ window.open(url, '_blank')
538
+ }
539
+ },
540
+
541
+ onFileChanged(callback: (content: string) => void): void {
542
+ eventListeners['file-changed'] = eventListeners['file-changed'] || []
543
+ eventListeners['file-changed'].push(callback)
544
+
545
+ if (!isNativePlatform()) return
546
+ let lastContent = ''
547
+ let watcherTimer: ReturnType<typeof setInterval> | null = null
548
+
549
+ const pollChanges = async (): Promise<void> => {
550
+ if (!currentFilePath) return
551
+ try {
552
+ const content = await readFileContent(currentFilePath)
553
+ if (content !== lastContent && content) {
554
+ lastContent = content
555
+ emit('file-changed', content)
556
+ }
557
+ } catch { /* ignore */ }
558
+ }
559
+
560
+ if (!watcherTimer) {
561
+ watcherTimer = setInterval(pollChanges, 2000)
562
+ }
563
+ },
564
+
565
+ onNewFile(callback: () => void): void {
566
+ eventListeners['new-file'] = eventListeners['new-file'] || []
567
+ eventListeners['new-file'].push(callback)
568
+ },
569
+
570
+ onFileOpened(callback: (data: { path: string; content: string }) => void): void {
571
+ eventListeners['file-opened'] = eventListeners['file-opened'] || []
572
+ eventListeners['file-opened'].push(callback)
573
+ },
574
+
575
+ onMenuOpen(callback: () => void): void {
576
+ eventListeners['menu-open'] = eventListeners['menu-open'] || []
577
+ eventListeners['menu-open'].push(callback)
578
+ },
579
+
580
+ onMenuSave(callback: () => void): void {
581
+ eventListeners['menu-save'] = eventListeners['menu-save'] || []
582
+ eventListeners['menu-save'].push(callback)
583
+ },
584
+
585
+ onMenuSaveAs(callback: () => void): void {
586
+ eventListeners['menu-save-as'] = eventListeners['menu-save-as'] || []
587
+ eventListeners['menu-save-as'].push(callback)
588
+ },
589
+
590
+ onMenuExportPDF(callback: () => void): void {
591
+ eventListeners['menu-export-pdf'] = eventListeners['menu-export-pdf'] || []
592
+ eventListeners['menu-export-pdf'].push(callback)
593
+ },
594
+
595
+ onMenuExportHTML(callback: () => void): void {
596
+ eventListeners['menu-export-html'] = eventListeners['menu-export-html'] || []
597
+ eventListeners['menu-export-html'].push(callback)
598
+ },
599
+
600
+ onMenuNewSlides(callback: () => void): void {
601
+ eventListeners['menu-new-slides'] = eventListeners['menu-new-slides'] || []
602
+ eventListeners['menu-new-slides'].push(callback)
603
+ },
604
+
605
+ onMenuOpenAsSlides(callback: () => void): void {
606
+ eventListeners['menu-open-as-slides'] = eventListeners['menu-open-as-slides'] || []
607
+ eventListeners['menu-open-as-slides'].push(callback)
608
+ },
609
+
610
+ onNewSlidesContent(callback: (content: string) => void): void {
611
+ eventListeners['new-slides-content'] = eventListeners['new-slides-content'] || []
612
+ eventListeners['new-slides-content'].push(callback)
613
+ },
614
+
615
+ onSetTheme(callback: (theme: string) => void): void {
616
+ eventListeners['set-theme'] = eventListeners['set-theme'] || []
617
+ eventListeners['set-theme'].push(callback)
618
+ },
619
+
620
+ onSetCustomCSS(callback: (css: string) => void): void {
621
+ eventListeners['set-custom-css'] = eventListeners['set-custom-css'] || []
622
+ eventListeners['set-custom-css'].push(callback)
623
+ },
624
+
625
+ async exportSlides(content: string): Promise<boolean> {
626
+ const defaultName = 'colamd-slides.html'
627
+ if (!isNativePlatform()) {
628
+ const blob = new Blob([content], { type: 'text/html' })
629
+ const url = URL.createObjectURL(blob)
630
+ const a = document.createElement('a')
631
+ a.href = url
632
+ a.download = defaultName
633
+ a.click()
634
+ URL.revokeObjectURL(url)
635
+ return true
636
+ }
637
+ return writeAndShareFile(defaultName, content)
638
+ },
639
+
640
+ onMenuExportSlides(callback: () => void): void {
641
+ eventListeners['menu-export-slides'] = eventListeners['menu-export-slides'] || []
642
+ eventListeners['menu-export-slides'].push(callback)
643
+ },
644
+
645
+ onAgentActivity(callback: (state: string) => void): void {
646
+ eventListeners['agent-activity'] = eventListeners['agent-activity'] || []
647
+ eventListeners['agent-activity'].push(callback)
648
+ },
649
+
650
+ async registerPlugins(_plugins: Array<{ id: string; name: string; enabled: boolean }>): Promise<boolean> {
651
+ localStorage.setItem('colamd-plugins', JSON.stringify(_plugins))
652
+ return true
653
+ },
654
+
655
+ async syncPluginState(_id: string, _enabled: boolean): Promise<void> {
656
+ const raw = localStorage.getItem('colamd-plugins') || '[]'
657
+ const plugins = JSON.parse(raw)
658
+ const p = plugins.find((x: { id: string }) => x.id === _id)
659
+ if (p) p.enabled = _enabled
660
+ localStorage.setItem('colamd-plugins', JSON.stringify(plugins))
661
+ },
662
+
663
+ onMenuTogglePlugin(callback: (id: string) => void): void {
664
+ eventListeners['menu-toggle-plugin'] = eventListeners['menu-toggle-plugin'] || []
665
+ eventListeners['menu-toggle-plugin'].push(callback)
666
+ },
667
+
668
+ onMenuImportTheme(callback: () => void): void {
669
+ eventListeners['menu-import-theme'] = eventListeners['menu-import-theme'] || []
670
+ eventListeners['menu-import-theme'].push(callback)
671
+ },
672
+
673
+ async exportFile(dataUrl: string, defaultName: string): Promise<boolean> {
674
+ if (!isNativePlatform()) {
675
+ const a = document.createElement('a')
676
+ a.href = dataUrl
677
+ a.download = defaultName
678
+ a.click()
679
+ return true
680
+ }
681
+
682
+ const base64Data = dataUrl.replace(/^data:[^;]+;base64,/, '')
683
+ try {
684
+ const fs = await capFS()
685
+ await fs.Filesystem.writeFile({
686
+ path: defaultName,
687
+ data: base64Data,
688
+ directory: fs.Directory.Documents,
689
+ recursive: true,
690
+ })
691
+
692
+ const fileUri = await fs.Filesystem.getUri({
693
+ path: defaultName,
694
+ directory: fs.Directory.Documents,
695
+ })
696
+
697
+ const sh = await capShare()
698
+ try {
699
+ await sh.Share.share({
700
+ title: defaultName,
701
+ files: [fileUri.uri],
702
+ dialogTitle: 'Share Export',
703
+ })
704
+ } catch { /* cancelled */ }
705
+ return true
706
+ } catch {
707
+ return false
708
+ }
709
+ },
710
+ }
711
+
712
+ return api
713
+ }