@glassly/engine 0.1.0-dev.1 → 0.1.0-dev.27

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 (277) hide show
  1. package/README.md +53 -19
  2. package/build/bluetooth-sdk/debug.d.ts +2 -0
  3. package/build/bluetooth-sdk/debug.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/debug.js +2 -0
  5. package/build/bluetooth-sdk/debug.js.map +1 -0
  6. package/build/bluetooth-sdk/index.d.ts +3 -0
  7. package/build/bluetooth-sdk/index.d.ts.map +1 -0
  8. package/build/bluetooth-sdk/index.js +3 -0
  9. package/build/bluetooth-sdk/index.js.map +1 -0
  10. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  11. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  12. package/build/bluetooth-sdk/ota-transport.js +2 -0
  13. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  14. package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
  15. package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
  16. package/build/bluetooth-sdk/photo-receiver.js +3 -0
  17. package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
  18. package/build/bluetooth-sdk/react.d.ts +2 -0
  19. package/build/bluetooth-sdk/react.d.ts.map +1 -0
  20. package/build/bluetooth-sdk/react.js +2 -0
  21. package/build/bluetooth-sdk/react.js.map +1 -0
  22. package/build/bluetooth-sdk/types.d.ts +2 -0
  23. package/build/bluetooth-sdk/types.d.ts.map +1 -0
  24. package/build/bluetooth-sdk/types.js +2 -0
  25. package/build/bluetooth-sdk/types.js.map +1 -0
  26. package/build/engine.d.ts +23 -16
  27. package/build/engine.d.ts.map +1 -1
  28. package/build/engine.js +9 -3
  29. package/build/engine.js.map +1 -1
  30. package/build/facades/dev.d.ts +1 -1
  31. package/build/facades/display.d.ts +1 -1
  32. package/build/facades/display.d.ts.map +1 -1
  33. package/build/facades/display.js +1 -1
  34. package/build/facades/display.js.map +1 -1
  35. package/build/facades/glasses.d.ts +5 -13
  36. package/build/facades/glasses.d.ts.map +1 -1
  37. package/build/facades/glasses.js +5 -3
  38. package/build/facades/glasses.js.map +1 -1
  39. package/build/facades/ota.d.ts +19 -21
  40. package/build/facades/ota.d.ts.map +1 -1
  41. package/build/facades/ota.js +23 -9
  42. package/build/facades/ota.js.map +1 -1
  43. package/build/facades/pairing.d.ts +11 -5
  44. package/build/facades/pairing.d.ts.map +1 -1
  45. package/build/facades/pairing.js +93 -17
  46. package/build/facades/pairing.js.map +1 -1
  47. package/build/facades/speakers.d.ts +1 -1
  48. package/build/facades/speakers.d.ts.map +1 -1
  49. package/build/facades/speakers.js.map +1 -1
  50. package/build/facades/speech.d.ts +7 -6
  51. package/build/facades/speech.d.ts.map +1 -1
  52. package/build/facades/speech.js +1 -1
  53. package/build/facades/speech.js.map +1 -1
  54. package/build/generated/releaseMetadata.d.ts +12 -0
  55. package/build/generated/releaseMetadata.d.ts.map +1 -0
  56. package/build/generated/releaseMetadata.js +10 -0
  57. package/build/generated/releaseMetadata.js.map +1 -0
  58. package/build/index.d.ts +7 -4
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +5 -3
  61. package/build/index.js.map +1 -1
  62. package/build/react/MentraLiveOtaFlow.d.ts +39 -0
  63. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  64. package/build/react/MentraLiveOtaFlow.js +200 -0
  65. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  66. package/build/react/index.d.ts +3 -0
  67. package/build/react/index.d.ts.map +1 -0
  68. package/build/react/index.js +3 -0
  69. package/build/react/index.js.map +1 -0
  70. package/build/react/useEngineSnapshot.d.ts +2 -0
  71. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  72. package/build/react/useEngineSnapshot.js +14 -0
  73. package/build/react/useEngineSnapshot.js.map +1 -0
  74. package/build/react/useMentraLiveOta.d.ts +80 -0
  75. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  76. package/build/react/useMentraLiveOta.js +550 -0
  77. package/build/react/useMentraLiveOta.js.map +1 -0
  78. package/build/services/AppRegistry.d.ts +8 -0
  79. package/build/services/AppRegistry.d.ts.map +1 -1
  80. package/build/services/AppRegistry.js +15 -0
  81. package/build/services/AppRegistry.js.map +1 -1
  82. package/build/services/BlobStore.d.ts +2 -0
  83. package/build/services/BlobStore.d.ts.map +1 -1
  84. package/build/services/BlobStore.js +63 -10
  85. package/build/services/BlobStore.js.map +1 -1
  86. package/build/services/CustomDashboardModel.d.ts +48 -0
  87. package/build/services/CustomDashboardModel.d.ts.map +1 -0
  88. package/build/services/CustomDashboardModel.js +73 -0
  89. package/build/services/CustomDashboardModel.js.map +1 -0
  90. package/build/services/CustomDashboardService.d.ts +61 -0
  91. package/build/services/CustomDashboardService.d.ts.map +1 -0
  92. package/build/services/CustomDashboardService.js +552 -0
  93. package/build/services/CustomDashboardService.js.map +1 -0
  94. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  95. package/build/services/DeviceEventRouter.js +44 -3
  96. package/build/services/DeviceEventRouter.js.map +1 -1
  97. package/build/services/GestureDotsService.d.ts +35 -0
  98. package/build/services/GestureDotsService.d.ts.map +1 -0
  99. package/build/services/GestureDotsService.js +149 -0
  100. package/build/services/GestureDotsService.js.map +1 -0
  101. package/build/services/GlassesStatusProjection.d.ts +15 -1
  102. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  103. package/build/services/GlassesStatusProjection.js +18 -11
  104. package/build/services/GlassesStatusProjection.js.map +1 -1
  105. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  106. package/build/services/HotspotOtaTransport.js +8 -7
  107. package/build/services/HotspotOtaTransport.js.map +1 -1
  108. package/build/services/LocalDisplayManager.d.ts +43 -1
  109. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  110. package/build/services/LocalDisplayManager.js +128 -2
  111. package/build/services/LocalDisplayManager.js.map +1 -1
  112. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  113. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  114. package/build/services/LocalMiniappRuntime.js +32 -0
  115. package/build/services/LocalMiniappRuntime.js.map +1 -1
  116. package/build/services/MiniappExitGesture.d.ts +33 -0
  117. package/build/services/MiniappExitGesture.d.ts.map +1 -0
  118. package/build/services/MiniappExitGesture.js +54 -0
  119. package/build/services/MiniappExitGesture.js.map +1 -0
  120. package/build/services/OtaAutoChain.d.ts +41 -0
  121. package/build/services/OtaAutoChain.d.ts.map +1 -0
  122. package/build/services/OtaAutoChain.js +91 -0
  123. package/build/services/OtaAutoChain.js.map +1 -0
  124. package/build/services/OtaErrorMapping.d.ts +18 -0
  125. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  126. package/build/services/OtaErrorMapping.js +63 -0
  127. package/build/services/OtaErrorMapping.js.map +1 -0
  128. package/build/services/OtaInstallCoordinator.d.ts +24 -3
  129. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  130. package/build/services/OtaInstallCoordinator.js +17 -12
  131. package/build/services/OtaInstallCoordinator.js.map +1 -1
  132. package/build/services/OtaService.d.ts.map +1 -1
  133. package/build/services/OtaService.js +1 -1
  134. package/build/services/OtaService.js.map +1 -1
  135. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  136. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  137. package/build/services/OtaUpdateCheckService.js +28 -16
  138. package/build/services/OtaUpdateCheckService.js.map +1 -1
  139. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  140. package/build/services/asg/localNetworkTransport.js +13 -12
  141. package/build/services/asg/localNetworkTransport.js.map +1 -1
  142. package/build/services/gestureClaims.d.ts +18 -0
  143. package/build/services/gestureClaims.d.ts.map +1 -0
  144. package/build/services/gestureClaims.js +24 -0
  145. package/build/services/gestureClaims.js.map +1 -0
  146. package/build/services/gestureDotGlyphs.d.ts +8 -0
  147. package/build/services/gestureDotGlyphs.d.ts.map +1 -0
  148. package/build/services/gestureDotGlyphs.js +12 -0
  149. package/build/services/gestureDotGlyphs.js.map +1 -0
  150. package/build/services/glassesClockSync.d.ts.map +1 -1
  151. package/build/services/glassesClockSync.js +5 -1
  152. package/build/services/glassesClockSync.js.map +1 -1
  153. package/build/services/otaDisplayState.d.ts +1 -1
  154. package/build/services/otaDisplayState.d.ts.map +1 -1
  155. package/build/services/otaDisplayState.js.map +1 -1
  156. package/build/services/otaInstallPolicy.d.ts +1 -1
  157. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  158. package/build/services/otaInstallPolicy.js.map +1 -1
  159. package/build/services/otaLegacyMapping.d.ts +1 -1
  160. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  161. package/build/services/otaLegacyMapping.js.map +1 -1
  162. package/build/services/otaManifestPolicy.d.ts +14 -0
  163. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  164. package/build/services/otaManifestPolicy.js +20 -0
  165. package/build/services/otaManifestPolicy.js.map +1 -0
  166. package/build/services/otaManifestUrl.d.ts +2 -1
  167. package/build/services/otaManifestUrl.d.ts.map +1 -1
  168. package/build/services/otaManifestUrl.js +24 -32
  169. package/build/services/otaManifestUrl.js.map +1 -1
  170. package/build/services/otaReleaseVersion.d.ts +10 -0
  171. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  172. package/build/services/otaReleaseVersion.js +14 -0
  173. package/build/services/otaReleaseVersion.js.map +1 -0
  174. package/build/stores/appVisibility.d.ts +3 -0
  175. package/build/stores/appVisibility.d.ts.map +1 -0
  176. package/build/stores/appVisibility.js +3 -0
  177. package/build/stores/appVisibility.js.map +1 -0
  178. package/build/stores/apps.d.ts.map +1 -1
  179. package/build/stores/apps.js +7 -1
  180. package/build/stores/apps.js.map +1 -1
  181. package/build/stores/core.d.ts +3 -3
  182. package/build/stores/core.d.ts.map +1 -1
  183. package/build/stores/core.js.map +1 -1
  184. package/build/stores/glasses.d.ts +10 -4
  185. package/build/stores/glasses.d.ts.map +1 -1
  186. package/build/stores/glasses.js.map +1 -1
  187. package/build/stores/settings.d.ts.map +1 -1
  188. package/build/stores/settings.js +87 -3
  189. package/build/stores/settings.js.map +1 -1
  190. package/build/types/applet.d.ts +2 -0
  191. package/build/types/applet.d.ts.map +1 -1
  192. package/build/types/applet.js +0 -3
  193. package/build/types/applet.js.map +1 -1
  194. package/build/types/enums.d.ts +1 -5
  195. package/build/types/enums.d.ts.map +1 -1
  196. package/build/types/enums.js +1 -8
  197. package/build/types/enums.js.map +1 -1
  198. package/build/types/hardware.d.ts +1 -1
  199. package/build/types/hardware.d.ts.map +1 -1
  200. package/build/types/hardware.js.map +1 -1
  201. package/build/types/index.d.ts +2 -6
  202. package/build/types/index.d.ts.map +1 -1
  203. package/build/types/index.js +2 -6
  204. package/build/types/index.js.map +1 -1
  205. package/build/utils/display/scene/process.d.ts.map +1 -1
  206. package/build/utils/display/scene/process.js +4 -3
  207. package/build/utils/display/scene/process.js.map +1 -1
  208. package/build/utils/display/scene/types.d.ts +6 -2
  209. package/build/utils/display/scene/types.d.ts.map +1 -1
  210. package/build/utils/display/scene/types.js.map +1 -1
  211. package/package.json +65 -33
  212. package/src/bluetooth-sdk/debug.ts +1 -0
  213. package/src/bluetooth-sdk/index.ts +2 -0
  214. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  215. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  216. package/src/bluetooth-sdk/react.ts +1 -0
  217. package/src/bluetooth-sdk/types.ts +1 -0
  218. package/src/engine.ts +9 -3
  219. package/src/facades/display.ts +1 -1
  220. package/src/facades/glasses.ts +6 -4
  221. package/src/facades/ota.ts +33 -10
  222. package/src/facades/pairing.ts +103 -23
  223. package/src/facades/speakers.ts +1 -1
  224. package/src/facades/speech.ts +21 -7
  225. package/src/generated/releaseMetadata.ts +20 -0
  226. package/src/index.ts +29 -4
  227. package/src/react/MentraLiveOtaFlow.tsx +569 -0
  228. package/src/react/index.ts +21 -0
  229. package/src/react/useEngineSnapshot.ts +16 -0
  230. package/src/react/useMentraLiveOta.ts +707 -0
  231. package/src/services/AppRegistry.ts +16 -0
  232. package/src/services/BlobStore.ts +57 -10
  233. package/src/services/CustomDashboardModel.ts +83 -0
  234. package/src/services/CustomDashboardService.ts +575 -0
  235. package/src/services/DeviceEventRouter.ts +47 -5
  236. package/src/services/GestureDotsService.ts +147 -0
  237. package/src/services/GlassesStatusProjection.ts +20 -11
  238. package/src/services/HotspotOtaTransport.ts +8 -7
  239. package/src/services/LocalDisplayManager.ts +130 -2
  240. package/src/services/LocalMiniappRuntime.ts +37 -0
  241. package/src/services/MiniappExitGesture.ts +76 -0
  242. package/src/services/OtaAutoChain.ts +130 -0
  243. package/src/services/OtaErrorMapping.ts +80 -0
  244. package/src/services/OtaInstallCoordinator.ts +19 -13
  245. package/src/services/OtaService.ts +1 -2
  246. package/src/services/OtaUpdateCheckService.ts +33 -19
  247. package/src/services/asg/localNetworkTransport.ts +20 -19
  248. package/src/services/gestureClaims.ts +28 -0
  249. package/src/services/gestureDotGlyphs.ts +14 -0
  250. package/src/services/glassesClockSync.ts +5 -1
  251. package/src/services/otaDisplayState.ts +1 -1
  252. package/src/services/otaInstallPolicy.ts +1 -1
  253. package/src/services/otaLegacyMapping.ts +1 -1
  254. package/src/services/otaManifestPolicy.ts +43 -0
  255. package/src/services/otaManifestUrl.ts +27 -36
  256. package/src/services/otaReleaseVersion.ts +22 -0
  257. package/src/stores/appVisibility.ts +3 -0
  258. package/src/stores/apps.ts +7 -1
  259. package/src/stores/core.ts +4 -4
  260. package/src/stores/glasses.ts +13 -6
  261. package/src/stores/settings.ts +87 -3
  262. package/src/types/applet.ts +2 -0
  263. package/src/types/enums.ts +1 -9
  264. package/src/types/hardware.ts +1 -1
  265. package/src/types/index.ts +2 -6
  266. package/src/utils/display/scene/process.ts +4 -3
  267. package/src/utils/display/scene/types.ts +0 -0
  268. package/build/devtools.d.ts +0 -11
  269. package/build/devtools.d.ts.map +0 -1
  270. package/build/devtools.js +0 -13
  271. package/build/devtools.js.map +0 -1
  272. package/build/internal.d.ts +0 -67
  273. package/build/internal.d.ts.map +0 -1
  274. package/build/internal.js +0 -90
  275. package/build/internal.js.map +0 -1
  276. package/src/devtools.ts +0 -13
  277. package/src/internal.ts +0 -155
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Scene API dashboard for G2 custom firmware.
3
+ *
4
+ * G2.swift marks the published firmware version after it detects the CFW
5
+ * capability advertisement. The service retains a dashboard frame, owns
6
+ * dashboard gestures while head-up is active, and leaves every other device
7
+ * on its native behavior.
8
+ *
9
+ * The dashboard is composed of user-orderable widgets (clock, calendar, apps
10
+ * button, notifications button) stacked vertically on the CFW scene canvas.
11
+ * The apps button switches to a second, dashboard-rendered app menu view.
12
+ */
13
+ import { File } from "expo-file-system";
14
+ import { BluetoothSdk } from "@glassly/bluetooth-sdk";
15
+ import { useAppStatusStore } from "../stores/apps";
16
+ import { useGlassesStore } from "../stores/glasses";
17
+ import { SETTINGS, useSettingsStore } from "../stores/settings";
18
+ import { BgTimer } from "../utils/timers";
19
+ import { appMenuActions, dashboardActions, formatDashboardClock, isCustomDashboardEligible, normalizeDashboardWidgets, orderedDashboardApps, } from "./CustomDashboardModel";
20
+ import { isGlassesConnected } from "./GlassesReadiness";
21
+ import { listPhoneCalendarEvents } from "./PhoneCalendarService";
22
+ import sceneRenderer from "./SceneRenderer";
23
+ const DASHBOARD_APP_ID = "system.custom-dashboard";
24
+ const CLOCK_REFRESH_MS = 30_000;
25
+ const CALENDAR_REFRESH_MS = 5 * 60_000;
26
+ // CFW blits the stock compositor's 576×288 canvas; used when the firmware has
27
+ // not reported scene capabilities yet.
28
+ const FALLBACK_CANVAS = { width: 576, height: 288 };
29
+ const MARGIN_X = 16;
30
+ const MARGIN_TOP = 10;
31
+ const WIDGET_GAP = 10;
32
+ /** Row height for the glasses' builtin 20px font engine. */
33
+ const LINE_H = 24;
34
+ const BUTTON_H = 34;
35
+ /** App-menu icon grid: 4 per page (G2 firmware image-container pool). */
36
+ const APPS_PER_PAGE = 4;
37
+ const APP_CELL = 72;
38
+ const APP_CELL_GAP = 16;
39
+ const APP_ICON_INSET = 4;
40
+ const APP_ICON = APP_CELL - APP_ICON_INSET * 2;
41
+ function eventLine(event) {
42
+ const when = event.allDay
43
+ ? "All day"
44
+ : new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(event.startsAt));
45
+ return `${when} ${event.title}`;
46
+ }
47
+ class CustomDashboardService {
48
+ started = false;
49
+ open = false;
50
+ view = "main";
51
+ mainIndex = 0;
52
+ appsIndex = 0;
53
+ calendarAvailable = false;
54
+ /** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */
55
+ iconCache = new Map();
56
+ iconLoads = new Set();
57
+ calendarEvents = [];
58
+ unsubs = [];
59
+ clockTimer = null;
60
+ calendarTimer = null;
61
+ /** Last observed eligibility, for transition detection. */
62
+ eligible = false;
63
+ start() {
64
+ if (this.started)
65
+ return;
66
+ this.started = true;
67
+ this.unsubs.push(useGlassesStore.subscribe((state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`, () => {
68
+ if (!this.syncEligibility())
69
+ return;
70
+ void this.refreshCalendarAndRender();
71
+ }),
72
+ // Selector: only dashboard-relevant settings retrigger a refresh. The
73
+ // whole-store form fired a calendar query on every settings write.
74
+ useSettingsStore.subscribe((state) => JSON.stringify([
75
+ state.getSetting(SETTINGS.custom_dashboard_enabled.key),
76
+ state.getSetting(SETTINGS.custom_dashboard_widgets.key),
77
+ state.getSetting(SETTINGS.custom_dashboard_clock_style.key),
78
+ state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),
79
+ state.getSetting(SETTINGS.twelve_hour_time.key),
80
+ state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),
81
+ state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),
82
+ state.getSetting(SETTINGS.menu_apps.key),
83
+ ]), () => {
84
+ if (!this.syncEligibility())
85
+ return;
86
+ void this.refreshCalendarAndRender();
87
+ }), useAppStatusStore.subscribe(() => {
88
+ if (this.isEligible())
89
+ this.render();
90
+ }));
91
+ this.clockTimer = BgTimer.setInterval(() => {
92
+ if (this.isEligible())
93
+ this.render();
94
+ }, CLOCK_REFRESH_MS);
95
+ this.calendarTimer = BgTimer.setInterval(() => {
96
+ if (this.isEligible())
97
+ void this.refreshCalendarAndRender();
98
+ }, CALENDAR_REFRESH_MS);
99
+ this.eligible = this.isEligible();
100
+ if (this.eligible) {
101
+ this.ensureContextualDashboard();
102
+ void this.refreshCalendarAndRender();
103
+ }
104
+ }
105
+ /**
106
+ * Track eligible⇄ineligible transitions from any trigger (setting toggle,
107
+ * connect/disconnect, firmware change). Entering enables the contextual
108
+ * dashboard once — not on every settings write, so a user's manual
109
+ * contextual-dashboard choice is no longer ratcheted back. Leaving closes
110
+ * the dashboard, blanks the glasses view, and drops the retained scene so a
111
+ * stale frame can't be replayed after the feature was turned off.
112
+ */
113
+ syncEligibility() {
114
+ const eligible = this.isEligible();
115
+ if (eligible === this.eligible)
116
+ return eligible;
117
+ this.eligible = eligible;
118
+ if (eligible) {
119
+ this.ensureContextualDashboard();
120
+ }
121
+ else {
122
+ this.open = false;
123
+ this.resetNavigation();
124
+ sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", []);
125
+ sceneRenderer.clearApp(DASHBOARD_APP_ID);
126
+ }
127
+ return eligible;
128
+ }
129
+ stop() {
130
+ for (const unsub of this.unsubs)
131
+ unsub();
132
+ this.unsubs = [];
133
+ if (this.clockTimer !== null)
134
+ BgTimer.clearInterval(this.clockTimer);
135
+ if (this.calendarTimer !== null)
136
+ BgTimer.clearInterval(this.calendarTimer);
137
+ this.clockTimer = null;
138
+ this.calendarTimer = null;
139
+ this.open = false;
140
+ this.eligible = false;
141
+ this.resetNavigation();
142
+ this.started = false;
143
+ sceneRenderer.clearApp(DASHBOARD_APP_ID);
144
+ }
145
+ isEligible() {
146
+ if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false)
147
+ return false;
148
+ const state = useGlassesStore.getState();
149
+ return (isGlassesConnected(state.connection) &&
150
+ isCustomDashboardEligible({
151
+ model: state.deviceModel,
152
+ firmwareVersion: state.firmwareVersion,
153
+ }));
154
+ }
155
+ get isOpen() {
156
+ return this.open;
157
+ }
158
+ setHeadUp(up) {
159
+ if (!this.isEligible()) {
160
+ this.open = false;
161
+ return;
162
+ }
163
+ if (up && !this.open)
164
+ this.resetNavigation();
165
+ this.open = up;
166
+ if (up)
167
+ void this.refreshCalendarAndRender();
168
+ }
169
+ /** Toggle the custom dashboard for an eligible device. */
170
+ toggle() {
171
+ if (!this.isEligible())
172
+ return false;
173
+ this.open = !this.open;
174
+ if (this.open) {
175
+ this.resetNavigation();
176
+ void this.refreshCalendarAndRender();
177
+ }
178
+ void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
179
+ this.open = !this.open;
180
+ console.error("CustomDashboard: toggleHeadUp failed", error);
181
+ });
182
+ return true;
183
+ }
184
+ /** Returns true when an open custom dashboard consumed the gesture. */
185
+ handleGesture(gestureName) {
186
+ if (!this.open || !this.isEligible())
187
+ return false;
188
+ if (gestureName === "double_tap") {
189
+ // In the app menu a double tap backs out to the top of the dashboard
190
+ // (there is no Back row); on the main view it closes the dashboard.
191
+ if (this.view === "apps") {
192
+ this.resetNavigation();
193
+ this.render();
194
+ return true;
195
+ }
196
+ return this.toggle();
197
+ }
198
+ if (this.view === "apps")
199
+ return this.handleAppMenuGesture(gestureName);
200
+ return this.handleMainGesture(gestureName);
201
+ }
202
+ resetNavigation() {
203
+ this.view = "main";
204
+ this.mainIndex = 0;
205
+ this.appsIndex = 0;
206
+ }
207
+ handleMainGesture(gestureName) {
208
+ const actions = dashboardActions(this.widgets());
209
+ if (this.mainIndex >= actions.length)
210
+ this.mainIndex = Math.max(0, actions.length - 1);
211
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
212
+ if (actions.length === 0)
213
+ return true;
214
+ const delta = gestureName === "swipe_down" ? 1 : -1;
215
+ this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length;
216
+ this.render();
217
+ return true;
218
+ }
219
+ if (gestureName === "single_tap") {
220
+ const action = actions[this.mainIndex];
221
+ if (action?.kind === "apps-menu") {
222
+ this.view = "apps";
223
+ this.appsIndex = 0;
224
+ this.render();
225
+ }
226
+ else if (action?.kind === "notifications") {
227
+ void this.openNotificationsPanel();
228
+ }
229
+ return true;
230
+ }
231
+ return false;
232
+ }
233
+ handleAppMenuGesture(gestureName) {
234
+ const rows = appMenuActions(this.menuApps());
235
+ if (this.appsIndex >= rows.length)
236
+ this.appsIndex = Math.max(0, rows.length - 1);
237
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
238
+ const delta = gestureName === "swipe_down" ? 1 : -1;
239
+ this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length;
240
+ this.render();
241
+ return true;
242
+ }
243
+ if (gestureName === "single_tap") {
244
+ const row = rows[this.appsIndex];
245
+ if (row?.kind === "app") {
246
+ void this.toggleApp(row.packageName);
247
+ }
248
+ return true;
249
+ }
250
+ return false;
251
+ }
252
+ menuApps() {
253
+ const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key);
254
+ return orderedDashboardApps(menu, useAppStatusStore.getState().apps);
255
+ }
256
+ widgets() {
257
+ return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key));
258
+ }
259
+ async close() {
260
+ if (!this.open)
261
+ return;
262
+ this.open = false;
263
+ await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
264
+ console.error("CustomDashboard: close failed", error);
265
+ });
266
+ }
267
+ async openNotificationsPanel() {
268
+ await this.close();
269
+ await BluetoothSdk.showNativeNotificationPanel().catch((error) => {
270
+ console.error("CustomDashboard: notification panel failed", error);
271
+ });
272
+ }
273
+ async toggleApp(packageName) {
274
+ await this.close();
275
+ const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName);
276
+ if (!app)
277
+ return;
278
+ if (app.running) {
279
+ await useAppStatusStore.getState().stop(app.packageName);
280
+ }
281
+ else {
282
+ await useAppStatusStore.getState().start(app, { skipNavigation: true });
283
+ }
284
+ }
285
+ calendarWidgetEnabled() {
286
+ return this.widgets().some((widget) => widget.id === "calendar" && widget.enabled);
287
+ }
288
+ ensureContextualDashboard() {
289
+ const settings = useSettingsStore.getState();
290
+ if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {
291
+ // DeviceManager selects its retained dashboard Scene only while this is
292
+ // enabled. Custom firmware eligibility makes that dashboard automatic.
293
+ void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false);
294
+ }
295
+ }
296
+ async refreshCalendarAndRender() {
297
+ if (!this.calendarWidgetEnabled()) {
298
+ this.calendarAvailable = false;
299
+ this.calendarEvents = [];
300
+ this.render();
301
+ return;
302
+ }
303
+ const settings = useSettingsStore.getState();
304
+ const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2);
305
+ const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3);
306
+ const startsAt = new Date();
307
+ const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000);
308
+ try {
309
+ const result = await listPhoneCalendarEvents({ startsAt, endsAt, limit: Math.max(1, Math.min(3, count)) });
310
+ this.calendarAvailable = true;
311
+ this.calendarEvents = result.events;
312
+ }
313
+ catch {
314
+ // Permission is requested from the Custom Dashboard settings screen;
315
+ // without access the widget silently stays off the glasses.
316
+ this.calendarAvailable = false;
317
+ this.calendarEvents = [];
318
+ }
319
+ this.render();
320
+ }
321
+ canvas() {
322
+ const { sceneCanvasWidth, sceneCanvasHeight } = useGlassesStore.getState();
323
+ if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {
324
+ return { width: sceneCanvasWidth, height: sceneCanvasHeight };
325
+ }
326
+ return FALLBACK_CANVAS;
327
+ }
328
+ render() {
329
+ if (!this.isEligible())
330
+ return;
331
+ const elements = this.view === "apps" ? this.appMenuElements() : this.mainElements();
332
+ sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", elements);
333
+ }
334
+ mainElements() {
335
+ const canvas = this.canvas();
336
+ const settings = useSettingsStore.getState();
337
+ const widgets = this.widgets().filter((widget) => widget.enabled);
338
+ const actions = dashboardActions(widgets);
339
+ if (this.mainIndex >= actions.length)
340
+ this.mainIndex = Math.max(0, actions.length - 1);
341
+ const selected = actions[this.mainIndex];
342
+ const clockStyle = settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) === "large"
343
+ ? "large"
344
+ : "compact";
345
+ const calendarEnabled = widgets.some((widget) => widget.id === "calendar");
346
+ // Calendar lives in the right half; everything else stacks in the left
347
+ // half (full width when the calendar widget is off).
348
+ const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2);
349
+ const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2;
350
+ const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2);
351
+ const elements = [];
352
+ let y = MARGIN_TOP;
353
+ for (const widget of widgets) {
354
+ if (widget.id === "clock") {
355
+ const fontSize = clockStyle === "large" ? 48 : 32;
356
+ const clock = formatDashboardClock(new Date(), {
357
+ twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,
358
+ showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,
359
+ });
360
+ elements.push({
361
+ type: "text",
362
+ id: "clock",
363
+ box: { x: MARGIN_X, y, w: width, h: fontSize + 6 },
364
+ text: clock.time,
365
+ style: { font: "mono", fontSize, breakMode: "strict-word" },
366
+ });
367
+ y += fontSize + 6;
368
+ if (clock.date) {
369
+ elements.push({
370
+ type: "text",
371
+ id: "clock-date",
372
+ box: { x: MARGIN_X, y, w: width, h: LINE_H },
373
+ text: clock.date,
374
+ style: { font: "builtin", breakMode: "strict-word" },
375
+ });
376
+ y += LINE_H;
377
+ }
378
+ y += WIDGET_GAP;
379
+ }
380
+ else if (widget.id === "calendar") {
381
+ // Right half, top-aligned, independent of the left stack. A missing
382
+ // calendar (permission not granted, fetch failed) shows a hint
383
+ // instead of silently vanishing.
384
+ const lines = this.calendarAvailable
385
+ ? [
386
+ "Calendar",
387
+ ...(this.calendarEvents.length
388
+ ? this.calendarEvents.slice(0, 3).map(eventLine)
389
+ : ["No upcoming events"]),
390
+ ]
391
+ : ["Calendar", "No access"];
392
+ elements.push({
393
+ type: "text",
394
+ id: "calendar",
395
+ box: { x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H },
396
+ text: lines.join("\n"),
397
+ style: { font: "builtin", breakMode: "strict-word", overflow: "ellipsis" },
398
+ });
399
+ }
400
+ else if (widget.id === "apps") {
401
+ const isSelected = selected?.kind === "apps-menu";
402
+ elements.push({
403
+ type: "text",
404
+ id: "apps-button",
405
+ box: { x: MARGIN_X, y, w: width, h: BUTTON_H },
406
+ text: "Apps",
407
+ style: { font: "builtin", border: isSelected ? 2 : 0, radius: 8 },
408
+ });
409
+ y += BUTTON_H + WIDGET_GAP;
410
+ }
411
+ else if (widget.id === "notifications") {
412
+ const isSelected = selected?.kind === "notifications";
413
+ elements.push({
414
+ type: "text",
415
+ id: "notifications",
416
+ box: { x: MARGIN_X, y, w: width, h: BUTTON_H },
417
+ text: "Notifications",
418
+ style: { font: "builtin", border: isSelected ? 2 : 0, radius: 8 },
419
+ });
420
+ y += BUTTON_H + WIDGET_GAP;
421
+ }
422
+ }
423
+ return elements;
424
+ }
425
+ /**
426
+ * Resolve a miniapp icon to base64 for the glasses. Local/bundled miniapps
427
+ * carry a file:// icon.png; store apps an https URL; the offline system
428
+ * tiles a bundler asset id (unsupported here — monogram fallback). Loads
429
+ * are async and cached; a finished load repaints the open app menu.
430
+ */
431
+ loadIcon(app) {
432
+ if (this.iconCache.has(app.packageName) || this.iconLoads.has(app.packageName))
433
+ return;
434
+ const url = typeof app.logoUrl === "string" ? app.logoUrl : "";
435
+ if (!/^(file|https?):/.test(url)) {
436
+ this.iconCache.set(app.packageName, null);
437
+ return;
438
+ }
439
+ this.iconLoads.add(app.packageName);
440
+ void (async () => {
441
+ try {
442
+ let base64;
443
+ if (url.startsWith("file:")) {
444
+ base64 = await new File(url).base64();
445
+ }
446
+ else {
447
+ const response = await fetch(url);
448
+ const blob = await response.blob();
449
+ base64 = await new Promise((resolve, reject) => {
450
+ const reader = new FileReader();
451
+ reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "");
452
+ reader.onerror = () => reject(reader.error);
453
+ reader.readAsDataURL(blob);
454
+ });
455
+ }
456
+ this.iconCache.set(app.packageName, base64 || null);
457
+ }
458
+ catch {
459
+ this.iconCache.set(app.packageName, null);
460
+ }
461
+ finally {
462
+ this.iconLoads.delete(app.packageName);
463
+ if (this.open && this.view === "apps")
464
+ this.render();
465
+ }
466
+ })();
467
+ }
468
+ appMenuElements() {
469
+ const canvas = this.canvas();
470
+ const width = canvas.width - MARGIN_X * 2;
471
+ const apps = this.menuApps();
472
+ const rows = appMenuActions(apps);
473
+ if (this.appsIndex >= rows.length)
474
+ this.appsIndex = Math.max(0, rows.length - 1);
475
+ const elements = [
476
+ {
477
+ type: "text",
478
+ id: "apps-title",
479
+ box: { x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H },
480
+ text: "Apps",
481
+ style: { font: "builtin" },
482
+ },
483
+ ];
484
+ const listTop = MARGIN_TOP + LINE_H + 16;
485
+ if (apps.length === 0) {
486
+ elements.push({
487
+ type: "text",
488
+ id: "apps-empty",
489
+ box: { x: MARGIN_X, y: listTop, w: width, h: LINE_H },
490
+ text: "No apps in Glasses Menu",
491
+ style: { font: "builtin" },
492
+ });
493
+ return elements;
494
+ }
495
+ // Icon grid, paged: the G2 firmware image-container pool is 4, so at most
496
+ // four icon images may be on a page. Selection is the bordered cell.
497
+ const perPage = APPS_PER_PAGE;
498
+ const pageStart = Math.floor(this.appsIndex / perPage) * perPage;
499
+ const page = apps.slice(pageStart, pageStart + perPage);
500
+ const totalW = page.length * APP_CELL + (page.length - 1) * APP_CELL_GAP;
501
+ const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2));
502
+ page.forEach((app, column) => {
503
+ const x = startX + column * (APP_CELL + APP_CELL_GAP);
504
+ const selected = pageStart + column === this.appsIndex;
505
+ const icon = this.iconCache.get(app.packageName);
506
+ if (icon === undefined)
507
+ this.loadIcon(app);
508
+ if (icon) {
509
+ elements.push({
510
+ type: "image",
511
+ id: `app-icon:${app.packageName}`,
512
+ box: { x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON },
513
+ data: icon,
514
+ });
515
+ }
516
+ else {
517
+ // Monogram fallback for icons that can't load (bundler-asset tiles).
518
+ elements.push({
519
+ type: "text",
520
+ id: `app-icon:${app.packageName}`,
521
+ box: { x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON },
522
+ text: (app.name.trim()[0] ?? "?").toUpperCase(),
523
+ style: { font: "mono", fontSize: 40 },
524
+ });
525
+ }
526
+ if (selected) {
527
+ elements.push({
528
+ type: "rect",
529
+ id: "apps-selection",
530
+ box: { x, y: listTop, w: APP_CELL, h: APP_CELL },
531
+ style: { border: 2, radius: 8 },
532
+ });
533
+ }
534
+ });
535
+ // Grayscale mini icons are ambiguous on their own: caption the selection.
536
+ const selectedApp = apps[this.appsIndex];
537
+ if (selectedApp) {
538
+ elements.push({
539
+ type: "text",
540
+ id: "apps-selected-name",
541
+ box: { x: MARGIN_X, y: listTop + APP_CELL + 12, w: width, h: LINE_H },
542
+ text: `${selectedApp.name}${selectedApp.running ? " ●" : ""}`,
543
+ style: { font: "builtin", breakMode: "strict-word", overflow: "ellipsis" },
544
+ });
545
+ }
546
+ return elements;
547
+ }
548
+ }
549
+ const customDashboardService = new CustomDashboardService();
550
+ export default customDashboardService;
551
+ export { CustomDashboardService };
552
+ //# sourceMappingURL=CustomDashboardService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomDashboardService.js","sourceRoot":"","sources":["../../src/services/CustomDashboardService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scene API dashboard for G2 custom firmware.\n *\n * G2.swift marks the published firmware version after it detects the CFW\n * capability advertisement. The service retains a dashboard frame, owns\n * dashboard gestures while head-up is active, and leaves every other device\n * on its native behavior.\n *\n * The dashboard is composed of user-orderable widgets (clock, calendar, apps\n * button, notifications button) stacked vertically on the CFW scene canvas.\n * The apps button switches to a second, dashboard-rendered app menu view.\n */\nimport {File} from \"expo-file-system\"\n\nimport {BluetoothSdk} from \"@glassly/bluetooth-sdk\"\nimport type {CalendarEvent} from \"@glassly/miniapp\"\n\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport type {ClientApp} from \"../types/applet\"\nimport type {SceneElementInput} from \"../utils/display/scene\"\nimport {BgTimer} from \"../utils/timers\"\nimport {\n appMenuActions,\n dashboardActions,\n formatDashboardClock,\n isCustomDashboardEligible,\n normalizeDashboardWidgets,\n orderedDashboardApps,\n type CustomDashboardClockStyle,\n type DashboardWidgetSetting,\n type MenuAppSetting,\n} from \"./CustomDashboardModel\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport {listPhoneCalendarEvents} from \"./PhoneCalendarService\"\nimport sceneRenderer from \"./SceneRenderer\"\n\nconst DASHBOARD_APP_ID = \"system.custom-dashboard\"\nconst CLOCK_REFRESH_MS = 30_000\nconst CALENDAR_REFRESH_MS = 5 * 60_000\n\n// CFW blits the stock compositor's 576×288 canvas; used when the firmware has\n// not reported scene capabilities yet.\nconst FALLBACK_CANVAS = {width: 576, height: 288}\nconst MARGIN_X = 16\nconst MARGIN_TOP = 10\nconst WIDGET_GAP = 10\n/** Row height for the glasses' builtin 20px font engine. */\nconst LINE_H = 24\nconst BUTTON_H = 34\n/** App-menu icon grid: 4 per page (G2 firmware image-container pool). */\nconst APPS_PER_PAGE = 4\nconst APP_CELL = 72\nconst APP_CELL_GAP = 16\nconst APP_ICON_INSET = 4\nconst APP_ICON = APP_CELL - APP_ICON_INSET * 2\n\nfunction eventLine(event: CalendarEvent): string {\n const when = event.allDay\n ? \"All day\"\n : new Intl.DateTimeFormat(undefined, {hour: \"numeric\", minute: \"2-digit\"}).format(new Date(event.startsAt))\n return `${when} ${event.title}`\n}\n\nclass CustomDashboardService {\n private started = false\n private open = false\n private view: \"main\" | \"apps\" = \"main\"\n private mainIndex = 0\n private appsIndex = 0\n private calendarAvailable = false\n /** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */\n private iconCache = new Map<string, string | null>()\n private iconLoads = new Set<string>()\n private calendarEvents: CalendarEvent[] = []\n private unsubs: Array<() => void> = []\n private clockTimer: number | null = null\n private calendarTimer: number | null = null\n /** Last observed eligibility, for transition detection. */\n private eligible = false\n\n start(): void {\n if (this.started) return\n this.started = true\n this.unsubs.push(\n useGlassesStore.subscribe(\n (state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n // Selector: only dashboard-relevant settings retrigger a refresh. The\n // whole-store form fired a calendar query on every settings write.\n useSettingsStore.subscribe(\n (state) =>\n JSON.stringify([\n state.getSetting(SETTINGS.custom_dashboard_enabled.key),\n state.getSetting(SETTINGS.custom_dashboard_widgets.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_style.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),\n state.getSetting(SETTINGS.twelve_hour_time.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),\n state.getSetting(SETTINGS.menu_apps.key),\n ]),\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n useAppStatusStore.subscribe(() => {\n if (this.isEligible()) this.render()\n }),\n )\n this.clockTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) this.render()\n }, CLOCK_REFRESH_MS)\n this.calendarTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) void this.refreshCalendarAndRender()\n }, CALENDAR_REFRESH_MS)\n this.eligible = this.isEligible()\n if (this.eligible) {\n this.ensureContextualDashboard()\n void this.refreshCalendarAndRender()\n }\n }\n\n /**\n * Track eligible⇄ineligible transitions from any trigger (setting toggle,\n * connect/disconnect, firmware change). Entering enables the contextual\n * dashboard once — not on every settings write, so a user's manual\n * contextual-dashboard choice is no longer ratcheted back. Leaving closes\n * the dashboard, blanks the glasses view, and drops the retained scene so a\n * stale frame can't be replayed after the feature was turned off.\n */\n private syncEligibility(): boolean {\n const eligible = this.isEligible()\n if (eligible === this.eligible) return eligible\n this.eligible = eligible\n if (eligible) {\n this.ensureContextualDashboard()\n } else {\n this.open = false\n this.resetNavigation()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", [])\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n return eligible\n }\n\n stop(): void {\n for (const unsub of this.unsubs) unsub()\n this.unsubs = []\n if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)\n if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)\n this.clockTimer = null\n this.calendarTimer = null\n this.open = false\n this.eligible = false\n this.resetNavigation()\n this.started = false\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n\n isEligible(): boolean {\n if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false\n const state = useGlassesStore.getState()\n return (\n isGlassesConnected(state.connection) &&\n isCustomDashboardEligible({\n model: state.deviceModel,\n firmwareVersion: state.firmwareVersion,\n })\n )\n }\n\n get isOpen(): boolean {\n return this.open\n }\n\n setHeadUp(up: boolean): void {\n if (!this.isEligible()) {\n this.open = false\n return\n }\n if (up && !this.open) this.resetNavigation()\n this.open = up\n if (up) void this.refreshCalendarAndRender()\n }\n\n /** Toggle the custom dashboard for an eligible device. */\n toggle(): boolean {\n if (!this.isEligible()) return false\n this.open = !this.open\n if (this.open) {\n this.resetNavigation()\n void this.refreshCalendarAndRender()\n }\n void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n this.open = !this.open\n console.error(\"CustomDashboard: toggleHeadUp failed\", error)\n })\n return true\n }\n\n /** Returns true when an open custom dashboard consumed the gesture. */\n handleGesture(gestureName: string): boolean {\n if (!this.open || !this.isEligible()) return false\n if (gestureName === \"double_tap\") {\n // In the app menu a double tap backs out to the top of the dashboard\n // (there is no Back row); on the main view it closes the dashboard.\n if (this.view === \"apps\") {\n this.resetNavigation()\n this.render()\n return true\n }\n return this.toggle()\n }\n if (this.view === \"apps\") return this.handleAppMenuGesture(gestureName)\n return this.handleMainGesture(gestureName)\n }\n\n private resetNavigation(): void {\n this.view = \"main\"\n this.mainIndex = 0\n this.appsIndex = 0\n }\n\n private handleMainGesture(gestureName: string): boolean {\n const actions = dashboardActions(this.widgets())\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n if (actions.length === 0) return true\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const action = actions[this.mainIndex]\n if (action?.kind === \"apps-menu\") {\n this.view = \"apps\"\n this.appsIndex = 0\n this.render()\n } else if (action?.kind === \"notifications\") {\n void this.openNotificationsPanel()\n }\n return true\n }\n return false\n }\n\n private handleAppMenuGesture(gestureName: string): boolean {\n const rows = appMenuActions(this.menuApps())\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const row = rows[this.appsIndex]\n if (row?.kind === \"app\") {\n void this.toggleApp(row.packageName)\n }\n return true\n }\n return false\n }\n\n private menuApps(): ClientApp[] {\n const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null\n return orderedDashboardApps(menu, useAppStatusStore.getState().apps)\n }\n\n private widgets(): DashboardWidgetSetting[] {\n return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))\n }\n\n private async close(): Promise<void> {\n if (!this.open) return\n this.open = false\n await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n console.error(\"CustomDashboard: close failed\", error)\n })\n }\n\n private async openNotificationsPanel(): Promise<void> {\n await this.close()\n await BluetoothSdk.showNativeNotificationPanel().catch((error) => {\n console.error(\"CustomDashboard: notification panel failed\", error)\n })\n }\n\n private async toggleApp(packageName: string): Promise<void> {\n await this.close()\n const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)\n if (!app) return\n if (app.running) {\n await useAppStatusStore.getState().stop(app.packageName)\n } else {\n await useAppStatusStore.getState().start(app, {skipNavigation: true})\n }\n }\n\n private calendarWidgetEnabled(): boolean {\n return this.widgets().some((widget) => widget.id === \"calendar\" && widget.enabled)\n }\n\n private ensureContextualDashboard(): void {\n const settings = useSettingsStore.getState()\n if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {\n // DeviceManager selects its retained dashboard Scene only while this is\n // enabled. Custom firmware eligibility makes that dashboard automatic.\n void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false)\n }\n }\n\n private async refreshCalendarAndRender(): Promise<void> {\n if (!this.calendarWidgetEnabled()) {\n this.calendarAvailable = false\n this.calendarEvents = []\n this.render()\n return\n }\n const settings = useSettingsStore.getState()\n const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)\n const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)\n const startsAt = new Date()\n const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)\n try {\n const result = await listPhoneCalendarEvents({startsAt, endsAt, limit: Math.max(1, Math.min(3, count))})\n this.calendarAvailable = true\n this.calendarEvents = result.events\n } catch {\n // Permission is requested from the Custom Dashboard settings screen;\n // without access the widget silently stays off the glasses.\n this.calendarAvailable = false\n this.calendarEvents = []\n }\n this.render()\n }\n\n private canvas(): {width: number; height: number} {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n }\n return FALLBACK_CANVAS\n }\n\n private render(): void {\n if (!this.isEligible()) return\n const elements = this.view === \"apps\" ? this.appMenuElements() : this.mainElements()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", elements)\n }\n\n private mainElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const settings = useSettingsStore.getState()\n const widgets = this.widgets().filter((widget) => widget.enabled)\n const actions = dashboardActions(widgets)\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n const selected = actions[this.mainIndex]\n\n const clockStyle =\n (settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === \"large\"\n ? \"large\"\n : \"compact\"\n const calendarEnabled = widgets.some((widget) => widget.id === \"calendar\")\n // Calendar lives in the right half; everything else stacks in the left\n // half (full width when the calendar widget is off).\n const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)\n const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2\n const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)\n\n const elements: SceneElementInput[] = []\n let y = MARGIN_TOP\n for (const widget of widgets) {\n if (widget.id === \"clock\") {\n const fontSize = clockStyle === \"large\" ? 48 : 32\n const clock = formatDashboardClock(new Date(), {\n twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,\n showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,\n })\n elements.push({\n type: \"text\",\n id: \"clock\",\n box: {x: MARGIN_X, y, w: width, h: fontSize + 6},\n text: clock.time,\n style: {font: \"mono\", fontSize, breakMode: \"strict-word\"},\n })\n y += fontSize + 6\n if (clock.date) {\n elements.push({\n type: \"text\",\n id: \"clock-date\",\n box: {x: MARGIN_X, y, w: width, h: LINE_H},\n text: clock.date,\n style: {font: \"builtin\", breakMode: \"strict-word\"},\n })\n y += LINE_H\n }\n y += WIDGET_GAP\n } else if (widget.id === \"calendar\") {\n // Right half, top-aligned, independent of the left stack. A missing\n // calendar (permission not granted, fetch failed) shows a hint\n // instead of silently vanishing.\n const lines = this.calendarAvailable\n ? [\n \"Calendar\",\n ...(this.calendarEvents.length\n ? this.calendarEvents.slice(0, 3).map(eventLine)\n : [\"No upcoming events\"]),\n ]\n : [\"Calendar\", \"No access\"]\n elements.push({\n type: \"text\",\n id: \"calendar\",\n box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},\n text: lines.join(\"\\n\"),\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n } else if (widget.id === \"apps\") {\n const isSelected = selected?.kind === \"apps-menu\"\n elements.push({\n type: \"text\",\n id: \"apps-button\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Apps\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n } else if (widget.id === \"notifications\") {\n const isSelected = selected?.kind === \"notifications\"\n elements.push({\n type: \"text\",\n id: \"notifications\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Notifications\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n }\n }\n return elements\n }\n\n /**\n * Resolve a miniapp icon to base64 for the glasses. Local/bundled miniapps\n * carry a file:// icon.png; store apps an https URL; the offline system\n * tiles a bundler asset id (unsupported here — monogram fallback). Loads\n * are async and cached; a finished load repaints the open app menu.\n */\n private loadIcon(app: ClientApp): void {\n if (this.iconCache.has(app.packageName) || this.iconLoads.has(app.packageName)) return\n const url = typeof app.logoUrl === \"string\" ? app.logoUrl : \"\"\n if (!/^(file|https?):/.test(url)) {\n this.iconCache.set(app.packageName, null)\n return\n }\n this.iconLoads.add(app.packageName)\n void (async () => {\n try {\n let base64: string\n if (url.startsWith(\"file:\")) {\n base64 = await new File(url).base64()\n } else {\n const response = await fetch(url)\n const blob = await response.blob()\n base64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\")\n reader.onerror = () => reject(reader.error)\n reader.readAsDataURL(blob)\n })\n }\n this.iconCache.set(app.packageName, base64 || null)\n } catch {\n this.iconCache.set(app.packageName, null)\n } finally {\n this.iconLoads.delete(app.packageName)\n if (this.open && this.view === \"apps\") this.render()\n }\n })()\n }\n\n private appMenuElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const width = canvas.width - MARGIN_X * 2\n const apps = this.menuApps()\n const rows = appMenuActions(apps)\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n\n const elements: SceneElementInput[] = [\n {\n type: \"text\",\n id: \"apps-title\",\n box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},\n text: \"Apps\",\n style: {font: \"builtin\"},\n },\n ]\n const listTop = MARGIN_TOP + LINE_H + 16\n if (apps.length === 0) {\n elements.push({\n type: \"text\",\n id: \"apps-empty\",\n box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},\n text: \"No apps in Glasses Menu\",\n style: {font: \"builtin\"},\n })\n return elements\n }\n\n // Icon grid, paged: the G2 firmware image-container pool is 4, so at most\n // four icon images may be on a page. Selection is the bordered cell.\n const perPage = APPS_PER_PAGE\n const pageStart = Math.floor(this.appsIndex / perPage) * perPage\n const page = apps.slice(pageStart, pageStart + perPage)\n const totalW = page.length * APP_CELL + (page.length - 1) * APP_CELL_GAP\n const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2))\n page.forEach((app, column) => {\n const x = startX + column * (APP_CELL + APP_CELL_GAP)\n const selected = pageStart + column === this.appsIndex\n const icon = this.iconCache.get(app.packageName)\n if (icon === undefined) this.loadIcon(app)\n if (icon) {\n elements.push({\n type: \"image\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n data: icon,\n })\n } else {\n // Monogram fallback for icons that can't load (bundler-asset tiles).\n elements.push({\n type: \"text\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n text: (app.name.trim()[0] ?? \"?\").toUpperCase(),\n style: {font: \"mono\", fontSize: 40},\n })\n }\n if (selected) {\n elements.push({\n type: \"rect\",\n id: \"apps-selection\",\n box: {x, y: listTop, w: APP_CELL, h: APP_CELL},\n style: {border: 2, radius: 8},\n })\n }\n })\n\n // Grayscale mini icons are ambiguous on their own: caption the selection.\n const selectedApp = apps[this.appsIndex]\n if (selectedApp) {\n elements.push({\n type: \"text\",\n id: \"apps-selected-name\",\n box: {x: MARGIN_X, y: listTop + APP_CELL + 12, w: width, h: LINE_H},\n text: `${selectedApp.name}${selectedApp.running ? \" ●\" : \"\"}`,\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n }\n return elements\n }\n}\n\nconst customDashboardService = new CustomDashboardService()\nexport default customDashboardService\nexport {CustomDashboardService}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceEventRouter.d.ts","sourceRoot":"","sources":["../../src/services/DeviceEventRouter.ts"],"names":[],"mappings":"AAqCA,wBAAgB,sBAAsB,IAAI,IAAI,CA+N7C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
1
+ {"version":3,"file":"DeviceEventRouter.d.ts","sourceRoot":"","sources":["../../src/services/DeviceEventRouter.ts"],"names":[],"mappings":"AAoDA,wBAAgB,sBAAsB,IAAI,IAAI,CAyP7C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C"}