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

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 (343) hide show
  1. package/README.md +57 -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 +30 -18
  27. package/build/engine.d.ts.map +1 -1
  28. package/build/engine.js +12 -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 +11 -4
  32. package/build/facades/display.d.ts.map +1 -1
  33. package/build/facades/display.js +12 -2
  34. package/build/facades/display.js.map +1 -1
  35. package/build/facades/displayMirror.d.ts +2 -6
  36. package/build/facades/displayMirror.d.ts.map +1 -1
  37. package/build/facades/displayMirror.js +7 -14
  38. package/build/facades/displayMirror.js.map +1 -1
  39. package/build/facades/glasses.d.ts +6 -13
  40. package/build/facades/glasses.d.ts.map +1 -1
  41. package/build/facades/glasses.js +7 -3
  42. package/build/facades/glasses.js.map +1 -1
  43. package/build/facades/ota.d.ts +21 -21
  44. package/build/facades/ota.d.ts.map +1 -1
  45. package/build/facades/ota.js +26 -10
  46. package/build/facades/ota.js.map +1 -1
  47. package/build/facades/pairing.d.ts +11 -5
  48. package/build/facades/pairing.d.ts.map +1 -1
  49. package/build/facades/pairing.js +93 -17
  50. package/build/facades/pairing.js.map +1 -1
  51. package/build/facades/phoneNotifications.d.ts +12 -2
  52. package/build/facades/phoneNotifications.d.ts.map +1 -1
  53. package/build/facades/phoneNotifications.js +30 -10
  54. package/build/facades/phoneNotifications.js.map +1 -1
  55. package/build/facades/speakers.d.ts +1 -1
  56. package/build/facades/speakers.d.ts.map +1 -1
  57. package/build/facades/speakers.js.map +1 -1
  58. package/build/facades/speech.d.ts +7 -6
  59. package/build/facades/speech.d.ts.map +1 -1
  60. package/build/facades/speech.js +1 -1
  61. package/build/facades/speech.js.map +1 -1
  62. package/build/generated/releaseMetadata.d.ts +12 -0
  63. package/build/generated/releaseMetadata.d.ts.map +1 -0
  64. package/build/generated/releaseMetadata.js +10 -0
  65. package/build/generated/releaseMetadata.js.map +1 -0
  66. package/build/index.d.ts +9 -6
  67. package/build/index.d.ts.map +1 -1
  68. package/build/index.js +7 -5
  69. package/build/index.js.map +1 -1
  70. package/build/react/MentraLiveOtaFlow.d.ts +44 -0
  71. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  72. package/build/react/MentraLiveOtaFlow.js +322 -0
  73. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  74. package/build/react/index.d.ts +3 -0
  75. package/build/react/index.d.ts.map +1 -0
  76. package/build/react/index.js +3 -0
  77. package/build/react/index.js.map +1 -0
  78. package/build/react/useEngineSnapshot.d.ts +2 -0
  79. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  80. package/build/react/useEngineSnapshot.js +14 -0
  81. package/build/react/useEngineSnapshot.js.map +1 -0
  82. package/build/react/useMentraLiveOta.d.ts +84 -0
  83. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  84. package/build/react/useMentraLiveOta.js +627 -0
  85. package/build/react/useMentraLiveOta.js.map +1 -0
  86. package/build/services/AppRegistry.d.ts +8 -0
  87. package/build/services/AppRegistry.d.ts.map +1 -1
  88. package/build/services/AppRegistry.js +15 -0
  89. package/build/services/AppRegistry.js.map +1 -1
  90. package/build/services/BlobStore.d.ts +2 -0
  91. package/build/services/BlobStore.d.ts.map +1 -1
  92. package/build/services/BlobStore.js +63 -10
  93. package/build/services/BlobStore.js.map +1 -1
  94. package/build/services/CustomDashboardModel.d.ts +48 -0
  95. package/build/services/CustomDashboardModel.d.ts.map +1 -0
  96. package/build/services/CustomDashboardModel.js +73 -0
  97. package/build/services/CustomDashboardModel.js.map +1 -0
  98. package/build/services/CustomDashboardService.d.ts +55 -0
  99. package/build/services/CustomDashboardService.d.ts.map +1 -0
  100. package/build/services/CustomDashboardService.js +520 -0
  101. package/build/services/CustomDashboardService.js.map +1 -0
  102. package/build/services/DashboardCoordinator.d.ts +26 -0
  103. package/build/services/DashboardCoordinator.d.ts.map +1 -0
  104. package/build/services/DashboardCoordinator.js +125 -0
  105. package/build/services/DashboardCoordinator.js.map +1 -0
  106. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  107. package/build/services/DeviceEventRouter.js +45 -6
  108. package/build/services/DeviceEventRouter.js.map +1 -1
  109. package/build/services/DisplayProcessor.d.ts +3 -3
  110. package/build/services/DisplayProcessor.js +3 -3
  111. package/build/services/DisplayProcessor.js.map +1 -1
  112. package/build/services/GestureDotsService.d.ts +40 -0
  113. package/build/services/GestureDotsService.d.ts.map +1 -0
  114. package/build/services/GestureDotsService.js +167 -0
  115. package/build/services/GestureDotsService.js.map +1 -0
  116. package/build/services/GlassesStatusProjection.d.ts +15 -1
  117. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  118. package/build/services/GlassesStatusProjection.js +18 -11
  119. package/build/services/GlassesStatusProjection.js.map +1 -1
  120. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  121. package/build/services/HotspotOtaTransport.js +8 -7
  122. package/build/services/HotspotOtaTransport.js.map +1 -1
  123. package/build/services/LocalDisplayManager.d.ts +55 -3
  124. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  125. package/build/services/LocalDisplayManager.js +196 -16
  126. package/build/services/LocalDisplayManager.js.map +1 -1
  127. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  128. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  129. package/build/services/LocalMiniappRuntime.js +32 -0
  130. package/build/services/LocalMiniappRuntime.js.map +1 -1
  131. package/build/services/MiniappExitGesture.d.ts +33 -0
  132. package/build/services/MiniappExitGesture.d.ts.map +1 -0
  133. package/build/services/MiniappExitGesture.js +54 -0
  134. package/build/services/MiniappExitGesture.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +41 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +91 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +26 -3
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +38 -13
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +1 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +28 -16
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  155. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  156. package/build/services/PhoneNotificationsSync.js +101 -9
  157. package/build/services/PhoneNotificationsSync.js.map +1 -1
  158. package/build/services/PhonePhotoCoordinator.js +1 -1
  159. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  160. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  161. package/build/services/PhoneStreamCoordinator.js +9 -6
  162. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  163. package/build/services/SceneRenderer.d.ts.map +1 -1
  164. package/build/services/SceneRenderer.js +5 -0
  165. package/build/services/SceneRenderer.js.map +1 -1
  166. package/build/services/StreamLifecycleController.d.ts +4 -0
  167. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  168. package/build/services/StreamLifecycleController.js +4 -0
  169. package/build/services/StreamLifecycleController.js.map +1 -1
  170. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  171. package/build/services/asg/localNetworkTransport.js +13 -12
  172. package/build/services/asg/localNetworkTransport.js.map +1 -1
  173. package/build/services/effectiveCapabilities.d.ts +1 -1
  174. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  175. package/build/services/effectiveCapabilities.js +26 -7
  176. package/build/services/effectiveCapabilities.js.map +1 -1
  177. package/build/services/gestureClaims.d.ts +18 -0
  178. package/build/services/gestureClaims.d.ts.map +1 -0
  179. package/build/services/gestureClaims.js +24 -0
  180. package/build/services/gestureClaims.js.map +1 -0
  181. package/build/services/gestureDotGlyphs.d.ts +8 -0
  182. package/build/services/gestureDotGlyphs.d.ts.map +1 -0
  183. package/build/services/gestureDotGlyphs.js +13 -0
  184. package/build/services/gestureDotGlyphs.js.map +1 -0
  185. package/build/services/glassesClockSync.d.ts.map +1 -1
  186. package/build/services/glassesClockSync.js +5 -1
  187. package/build/services/glassesClockSync.js.map +1 -1
  188. package/build/services/iosNotificationAppCatalog.d.ts +13 -0
  189. package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
  190. package/build/services/iosNotificationAppCatalog.js +28 -0
  191. package/build/services/iosNotificationAppCatalog.js.map +1 -0
  192. package/build/services/otaDisplayState.d.ts +1 -1
  193. package/build/services/otaDisplayState.d.ts.map +1 -1
  194. package/build/services/otaDisplayState.js.map +1 -1
  195. package/build/services/otaInstallPolicy.d.ts +5 -1
  196. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  197. package/build/services/otaInstallPolicy.js +4 -0
  198. package/build/services/otaInstallPolicy.js.map +1 -1
  199. package/build/services/otaLegacyMapping.d.ts +1 -1
  200. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  201. package/build/services/otaLegacyMapping.js.map +1 -1
  202. package/build/services/otaManifestPolicy.d.ts +14 -0
  203. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  204. package/build/services/otaManifestPolicy.js +20 -0
  205. package/build/services/otaManifestPolicy.js.map +1 -0
  206. package/build/services/otaManifestUrl.d.ts +2 -1
  207. package/build/services/otaManifestUrl.d.ts.map +1 -1
  208. package/build/services/otaManifestUrl.js +24 -32
  209. package/build/services/otaManifestUrl.js.map +1 -1
  210. package/build/services/otaReleaseVersion.d.ts +10 -0
  211. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  212. package/build/services/otaReleaseVersion.js +14 -0
  213. package/build/services/otaReleaseVersion.js.map +1 -0
  214. package/build/stores/appVisibility.d.ts +3 -0
  215. package/build/stores/appVisibility.d.ts.map +1 -0
  216. package/build/stores/appVisibility.js +3 -0
  217. package/build/stores/appVisibility.js.map +1 -0
  218. package/build/stores/apps.d.ts.map +1 -1
  219. package/build/stores/apps.js +7 -1
  220. package/build/stores/apps.js.map +1 -1
  221. package/build/stores/core.d.ts +3 -3
  222. package/build/stores/core.d.ts.map +1 -1
  223. package/build/stores/core.js.map +1 -1
  224. package/build/stores/display.d.ts +4 -4
  225. package/build/stores/display.d.ts.map +1 -1
  226. package/build/stores/display.js +12 -36
  227. package/build/stores/display.js.map +1 -1
  228. package/build/stores/glasses.d.ts +10 -4
  229. package/build/stores/glasses.d.ts.map +1 -1
  230. package/build/stores/glasses.js +2 -0
  231. package/build/stores/glasses.js.map +1 -1
  232. package/build/stores/settings.d.ts.map +1 -1
  233. package/build/stores/settings.js +140 -5
  234. package/build/stores/settings.js.map +1 -1
  235. package/build/types/applet.d.ts +3 -1
  236. package/build/types/applet.d.ts.map +1 -1
  237. package/build/types/applet.js +0 -3
  238. package/build/types/applet.js.map +1 -1
  239. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  240. package/build/types/capabilities/even-realities-g2.js +2 -2
  241. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  242. package/build/types/enums.d.ts +1 -5
  243. package/build/types/enums.d.ts.map +1 -1
  244. package/build/types/enums.js +1 -8
  245. package/build/types/enums.js.map +1 -1
  246. package/build/types/hardware.d.ts +1 -1
  247. package/build/types/hardware.d.ts.map +1 -1
  248. package/build/types/hardware.js +1 -0
  249. package/build/types/hardware.js.map +1 -1
  250. package/build/types/index.d.ts +3 -7
  251. package/build/types/index.d.ts.map +1 -1
  252. package/build/types/index.js +3 -7
  253. package/build/types/index.js.map +1 -1
  254. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  255. package/build/utils/display/composer/ColumnComposer.js +1 -1
  256. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  257. package/build/utils/display/scene/process.d.ts.map +1 -1
  258. package/build/utils/display/scene/process.js +4 -3
  259. package/build/utils/display/scene/process.js.map +1 -1
  260. package/build/utils/display/scene/types.d.ts +6 -2
  261. package/build/utils/display/scene/types.d.ts.map +1 -1
  262. package/build/utils/display/scene/types.js.map +1 -1
  263. package/package.json +69 -35
  264. package/src/bluetooth-sdk/debug.ts +1 -0
  265. package/src/bluetooth-sdk/index.ts +2 -0
  266. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  267. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  268. package/src/bluetooth-sdk/react.ts +1 -0
  269. package/src/bluetooth-sdk/types.ts +1 -0
  270. package/src/engine.ts +12 -3
  271. package/src/facades/display.ts +12 -2
  272. package/src/facades/displayMirror.ts +7 -15
  273. package/src/facades/glasses.ts +8 -4
  274. package/src/facades/ota.ts +36 -11
  275. package/src/facades/pairing.ts +103 -23
  276. package/src/facades/phoneNotifications.ts +32 -10
  277. package/src/facades/speakers.ts +1 -1
  278. package/src/facades/speech.ts +21 -7
  279. package/src/generated/releaseMetadata.ts +20 -0
  280. package/src/index.ts +31 -5
  281. package/src/react/MentraLiveOtaFlow.tsx +754 -0
  282. package/src/react/index.ts +21 -0
  283. package/src/react/useEngineSnapshot.ts +16 -0
  284. package/src/react/useMentraLiveOta.ts +793 -0
  285. package/src/services/AppRegistry.ts +16 -0
  286. package/src/services/BlobStore.ts +57 -10
  287. package/src/services/CustomDashboardModel.ts +83 -0
  288. package/src/services/CustomDashboardService.ts +544 -0
  289. package/src/services/DashboardCoordinator.ts +121 -0
  290. package/src/services/DeviceEventRouter.ts +48 -8
  291. package/src/services/DisplayProcessor.ts +3 -3
  292. package/src/services/GestureDotsService.ts +164 -0
  293. package/src/services/GlassesStatusProjection.ts +20 -11
  294. package/src/services/HotspotOtaTransport.ts +8 -7
  295. package/src/services/LocalDisplayManager.ts +198 -18
  296. package/src/services/LocalMiniappRuntime.ts +37 -0
  297. package/src/services/MiniappExitGesture.ts +76 -0
  298. package/src/services/OtaAutoChain.ts +130 -0
  299. package/src/services/OtaErrorMapping.ts +80 -0
  300. package/src/services/OtaInstallCoordinator.ts +40 -13
  301. package/src/services/OtaService.ts +1 -2
  302. package/src/services/OtaUpdateCheckService.ts +33 -19
  303. package/src/services/PhoneNotificationsSync.ts +125 -9
  304. package/src/services/PhonePhotoCoordinator.ts +1 -1
  305. package/src/services/PhoneStreamCoordinator.ts +10 -6
  306. package/src/services/SceneRenderer.ts +4 -0
  307. package/src/services/StreamLifecycleController.ts +4 -0
  308. package/src/services/asg/localNetworkTransport.ts +20 -19
  309. package/src/services/effectiveCapabilities.ts +27 -7
  310. package/src/services/gestureClaims.ts +28 -0
  311. package/src/services/gestureDotGlyphs.ts +15 -0
  312. package/src/services/glassesClockSync.ts +5 -1
  313. package/src/services/iosNotificationAppCatalog.ts +39 -0
  314. package/src/services/otaDisplayState.ts +1 -1
  315. package/src/services/otaInstallPolicy.ts +5 -1
  316. package/src/services/otaLegacyMapping.ts +1 -1
  317. package/src/services/otaManifestPolicy.ts +43 -0
  318. package/src/services/otaManifestUrl.ts +27 -36
  319. package/src/services/otaReleaseVersion.ts +22 -0
  320. package/src/stores/appVisibility.ts +3 -0
  321. package/src/stores/apps.ts +7 -1
  322. package/src/stores/core.ts +4 -4
  323. package/src/stores/display.ts +16 -48
  324. package/src/stores/glasses.ts +15 -6
  325. package/src/stores/settings.ts +140 -5
  326. package/src/types/applet.ts +3 -1
  327. package/src/types/capabilities/even-realities-g2.ts +3 -2
  328. package/src/types/enums.ts +1 -9
  329. package/src/types/hardware.ts +2 -1
  330. package/src/types/index.ts +3 -7
  331. package/src/utils/display/composer/ColumnComposer.ts +1 -1
  332. package/src/utils/display/scene/process.ts +4 -3
  333. package/src/utils/display/scene/types.ts +0 -0
  334. package/build/devtools.d.ts +0 -11
  335. package/build/devtools.d.ts.map +0 -1
  336. package/build/devtools.js +0 -13
  337. package/build/devtools.js.map +0 -1
  338. package/build/internal.d.ts +0 -67
  339. package/build/internal.d.ts.map +0 -1
  340. package/build/internal.js +0 -90
  341. package/build/internal.js.map +0 -1
  342. package/src/devtools.ts +0 -13
  343. package/src/internal.ts +0 -155
@@ -0,0 +1,544 @@
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. DashboardCoordinator owns open/close and display
6
+ * switching; this service renders the widgets and handles gestures while open.
7
+ *
8
+ * The dashboard is composed of user-orderable widgets (clock, calendar, apps
9
+ * button, notifications button) stacked vertically on the CFW scene canvas.
10
+ * The apps button switches to a second, dashboard-rendered app menu view.
11
+ */
12
+ import {BluetoothSdk} from "@glassly/bluetooth-sdk"
13
+ import {MiniappErrorCode, type CalendarEvent} from "@glassly/miniapp"
14
+
15
+ import {useAppStatusStore} from "../stores/apps"
16
+ import {useGlassesStore} from "../stores/glasses"
17
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
18
+ import type {ClientApp} from "../types/applet"
19
+ import {TextMeasurer} from "../utils/display/measurer"
20
+ import type {SceneElementInput} from "../utils/display/scene"
21
+ import {BgTimer} from "../utils/timers"
22
+ import {
23
+ appMenuActions,
24
+ dashboardActions,
25
+ formatDashboardClock,
26
+ isCustomDashboardEligible,
27
+ normalizeDashboardWidgets,
28
+ orderedDashboardApps,
29
+ type CustomDashboardClockStyle,
30
+ type DashboardWidgetSetting,
31
+ type MenuAppSetting,
32
+ } from "./CustomDashboardModel"
33
+ import displayProcessor from "./DisplayProcessor"
34
+ import {isGlassesConnected} from "./GlassesReadiness"
35
+ import localDisplayManager from "./LocalDisplayManager"
36
+ import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarService"
37
+ import sceneRenderer from "./SceneRenderer"
38
+
39
+ const DASHBOARD_APP_ID = "system.custom-dashboard"
40
+ const CLOCK_REFRESH_MS = 30_000
41
+ const CALENDAR_REFRESH_MS = 5 * 60_000
42
+ /** Matches the boot message's window; stops have no unmount-driven end signal. */
43
+ const STOP_NOTICE_MS = 1500
44
+
45
+ // CFW blits the stock compositor's 576×288 canvas; used when the firmware has
46
+ // not reported scene capabilities yet.
47
+ const FALLBACK_CANVAS = {width: 576, height: 288}
48
+ const MARGIN_X = 16
49
+ const MARGIN_TOP = 10
50
+ const WIDGET_GAP = 10
51
+ /** Row height for the glasses' builtin 20px font engine. */
52
+ const LINE_H = 24
53
+ const BUTTON_H = 34
54
+ /**
55
+ * Selection marker: a text-hugging bar under the focused label instead of a
56
+ * boxed border. Drawn as a rect (h 3, border 2 — the strokes overlap into a
57
+ * solid bar) because mode 15 renders only glyphs; the bar must be in the
58
+ * raster or the firmware never shows it.
59
+ */
60
+ const UNDERLINE_H = 3
61
+ /** Row-top → underline-top: just under the builtin font's 27px line. */
62
+ const UNDERLINE_OFFSET_Y = BUTTON_H - 4
63
+
64
+ function sameLocalDay(a: Date, b: Date): boolean {
65
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
66
+ }
67
+
68
+ /** Local midnight after tomorrow: the widget never shows a "day after tomorrow". */
69
+ function endOfTomorrow(now: Date): Date {
70
+ const end = new Date(now)
71
+ end.setHours(0, 0, 0, 0)
72
+ end.setDate(end.getDate() + 2)
73
+ return end
74
+ }
75
+
76
+ /** Up to three lines per entry: title, location (when set), then the time — "tmr @ …" for tomorrow. */
77
+ function eventLines(event: CalendarEvent, now: Date): string[] {
78
+ const lines = [event.title]
79
+ const location = event.location?.trim()
80
+ if (location) lines.push(location)
81
+ const start = new Date(event.startsAt)
82
+ const tomorrow = new Date(now)
83
+ tomorrow.setDate(tomorrow.getDate() + 1)
84
+ const isTomorrow = sameLocalDay(start, tomorrow)
85
+ if (event.allDay) {
86
+ lines.push(isTomorrow ? "tmr, all day" : "All day")
87
+ } else {
88
+ const time = new Intl.DateTimeFormat(undefined, {hour: "numeric", minute: "2-digit"}).format(start)
89
+ lines.push(isTomorrow ? `tmr @ ${time}` : time)
90
+ }
91
+ return lines
92
+ }
93
+
94
+ class CustomDashboardService {
95
+ private started = false
96
+ private open = false
97
+ /** Set by DashboardCoordinator; asks it to close (and restore main content). */
98
+ public onRequestClose: (() => void) | null = null
99
+ private view: "main" | "apps" = "main"
100
+ private mainIndex = 0
101
+ private appsIndex = 0
102
+ /** First visible row of the app list's scrolling window. */
103
+ private appsScroll = 0
104
+ /** "loading" until the first fetch resolves; "no-access" only for a real permission denial. */
105
+ private calendarStatus: "loading" | "ok" | "no-access" | "unavailable" = "loading"
106
+ private calendarEvents: CalendarEvent[] = []
107
+ private unsubs: Array<() => void> = []
108
+ private clockTimer: number | null = null
109
+ private calendarTimer: number | null = null
110
+ /** Last observed eligibility, for transition detection. */
111
+ private eligible = false
112
+
113
+ start(): void {
114
+ if (this.started) return
115
+ this.started = true
116
+ this.unsubs.push(
117
+ useGlassesStore.subscribe(
118
+ (state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,
119
+ () => {
120
+ if (!this.syncEligibility()) return
121
+ void this.refreshCalendarAndRender()
122
+ },
123
+ ),
124
+ // Selector: only dashboard-relevant settings retrigger a refresh. The
125
+ // whole-store form fired a calendar query on every settings write.
126
+ useSettingsStore.subscribe(
127
+ (state) =>
128
+ JSON.stringify([
129
+ state.getSetting(SETTINGS.custom_dashboard_enabled.key),
130
+ state.getSetting(SETTINGS.custom_dashboard_widgets.key),
131
+ state.getSetting(SETTINGS.custom_dashboard_clock_style.key),
132
+ state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),
133
+ state.getSetting(SETTINGS.twelve_hour_time.key),
134
+ state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),
135
+ state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),
136
+ state.getSetting(SETTINGS.menu_apps.key),
137
+ ]),
138
+ () => {
139
+ if (!this.syncEligibility()) return
140
+ void this.refreshCalendarAndRender()
141
+ },
142
+ ),
143
+ useAppStatusStore.subscribe(() => {
144
+ if (this.isEligible()) this.render()
145
+ }),
146
+ )
147
+ this.clockTimer = BgTimer.setInterval(() => {
148
+ if (this.isEligible()) this.render()
149
+ }, CLOCK_REFRESH_MS)
150
+ this.calendarTimer = BgTimer.setInterval(() => {
151
+ if (this.isEligible()) void this.refreshCalendarAndRender()
152
+ }, CALENDAR_REFRESH_MS)
153
+ this.eligible = this.isEligible()
154
+ }
155
+
156
+ /**
157
+ * Track eligible⇄ineligible transitions from any trigger (setting toggle,
158
+ * connect/disconnect, firmware change). Leaving closes the dashboard via the
159
+ * coordinator (which restores main content) and drops the retained scene so
160
+ * a stale frame can't be replayed after the feature was turned off.
161
+ */
162
+ private syncEligibility(): boolean {
163
+ const eligible = this.isEligible()
164
+ if (eligible === this.eligible) return eligible
165
+ this.eligible = eligible
166
+ if (!eligible) {
167
+ if (this.open) this.onRequestClose?.()
168
+ this.open = false
169
+ this.resetNavigation()
170
+ sceneRenderer.clearApp(DASHBOARD_APP_ID)
171
+ }
172
+ return eligible
173
+ }
174
+
175
+ stop(): void {
176
+ for (const unsub of this.unsubs) unsub()
177
+ this.unsubs = []
178
+ if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)
179
+ if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)
180
+ this.clockTimer = null
181
+ this.calendarTimer = null
182
+ if (this.open) this.onRequestClose?.()
183
+ this.open = false
184
+ this.eligible = false
185
+ this.resetNavigation()
186
+ this.started = false
187
+ sceneRenderer.clearApp(DASHBOARD_APP_ID)
188
+ }
189
+
190
+ isEligible(): boolean {
191
+ if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false
192
+ const state = useGlassesStore.getState()
193
+ return (
194
+ isGlassesConnected(state.connection) &&
195
+ isCustomDashboardEligible({
196
+ model: state.deviceModel,
197
+ firmwareVersion: state.firmwareVersion,
198
+ })
199
+ )
200
+ }
201
+
202
+ get isOpen(): boolean {
203
+ return this.open
204
+ }
205
+
206
+ /** Coordinator opened the dashboard: render fresh from the top. */
207
+ show(): void {
208
+ this.open = true
209
+ this.resetNavigation()
210
+ void this.refreshCalendarAndRender()
211
+ }
212
+
213
+ /** Coordinator closed the dashboard. */
214
+ hide(): void {
215
+ this.open = false
216
+ this.resetNavigation()
217
+ }
218
+
219
+ /** Returns true when an open custom dashboard consumed the gesture. */
220
+ handleGesture(gestureName: string): boolean {
221
+ if (!this.open || !this.isEligible()) return false
222
+ if (gestureName === "double_tap") {
223
+ // In the app menu a double tap backs out to the top of the dashboard
224
+ // (there is no Back row); on the main view it closes the dashboard.
225
+ if (this.view === "apps") {
226
+ this.resetNavigation()
227
+ this.render()
228
+ return true
229
+ }
230
+ this.onRequestClose?.()
231
+ return true
232
+ }
233
+ if (this.view === "apps") return this.handleAppMenuGesture(gestureName)
234
+ return this.handleMainGesture(gestureName)
235
+ }
236
+
237
+ private resetNavigation(): void {
238
+ this.view = "main"
239
+ this.mainIndex = 0
240
+ this.appsIndex = 0
241
+ this.appsScroll = 0
242
+ }
243
+
244
+ private handleMainGesture(gestureName: string): boolean {
245
+ const actions = dashboardActions(this.widgets())
246
+ if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
247
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
248
+ if (actions.length === 0) return true
249
+ const delta = gestureName === "swipe_down" ? 1 : -1
250
+ this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length
251
+ this.render()
252
+ return true
253
+ }
254
+ if (gestureName === "single_tap") {
255
+ const action = actions[this.mainIndex]
256
+ if (action?.kind === "apps-menu") {
257
+ this.view = "apps"
258
+ this.appsIndex = 0
259
+ this.appsScroll = 0
260
+ this.render()
261
+ } else if (action?.kind === "notifications") {
262
+ void this.openNotificationsPanel()
263
+ }
264
+ return true
265
+ }
266
+ return false
267
+ }
268
+
269
+ private handleAppMenuGesture(gestureName: string): boolean {
270
+ const rows = appMenuActions(this.menuApps())
271
+ if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
272
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
273
+ const delta = gestureName === "swipe_down" ? 1 : -1
274
+ this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length
275
+ this.render()
276
+ return true
277
+ }
278
+ if (gestureName === "single_tap") {
279
+ const row = rows[this.appsIndex]
280
+ if (row?.kind === "app") {
281
+ void this.toggleApp(row.packageName)
282
+ }
283
+ return true
284
+ }
285
+ return false
286
+ }
287
+
288
+ private menuApps(): ClientApp[] {
289
+ const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null
290
+ return orderedDashboardApps(menu, useAppStatusStore.getState().apps)
291
+ }
292
+
293
+ private widgets(): DashboardWidgetSetting[] {
294
+ return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))
295
+ }
296
+
297
+ private async openNotificationsPanel(): Promise<void> {
298
+ this.onRequestClose?.()
299
+ await BluetoothSdk.showNativeNotificationPanel().catch((error) => {
300
+ console.error("CustomDashboard: notification panel failed", error)
301
+ })
302
+ }
303
+
304
+ private async toggleApp(packageName: string): Promise<void> {
305
+ this.onRequestClose?.()
306
+ const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)
307
+ if (!app) return
308
+ if (app.running) {
309
+ // Mirror the "Starting X…" boot message: without this the dashboard just
310
+ // closes and the stop is invisible on the glasses.
311
+ localDisplayManager.showSystemNotice(`Stopping ${app.name}…`, STOP_NOTICE_MS)
312
+ await useAppStatusStore.getState().stop(app.packageName)
313
+ } else {
314
+ await useAppStatusStore.getState().start(app, {skipNavigation: true})
315
+ }
316
+ }
317
+
318
+ private calendarWidgetEnabled(): boolean {
319
+ return this.widgets().some((widget) => widget.id === "calendar" && widget.enabled)
320
+ }
321
+
322
+ private async refreshCalendarAndRender(): Promise<void> {
323
+ if (!this.open) return
324
+ if (!this.calendarWidgetEnabled()) {
325
+ this.calendarStatus = "loading"
326
+ this.calendarEvents = []
327
+ this.render()
328
+ return
329
+ }
330
+ const settings = useSettingsStore.getState()
331
+ const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)
332
+ const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)
333
+ const startsAt = new Date()
334
+ // Clamped to local end-of-tomorrow: the widget only ever shows today and
335
+ // tomorrow ("tmr"), whatever the days-ahead setting says.
336
+ const daysEnd = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)
337
+ const windowEnd = endOfTomorrow(startsAt)
338
+ const endsAt = daysEnd < windowEnd ? daysEnd : windowEnd
339
+ try {
340
+ const result = await listPhoneCalendarEvents({
341
+ startsAt: startsAt.toISOString(),
342
+ endsAt: endsAt.toISOString(),
343
+ limit: Math.max(1, Math.min(3, count)),
344
+ })
345
+ this.calendarStatus = "ok"
346
+ this.calendarEvents = result.events
347
+ } catch (error) {
348
+ // Permission is requested from the Custom Dashboard settings screen;
349
+ // only a real denial should read as one — any other failure is
350
+ // "Unavailable" (and logged) so it can't masquerade as a permission
351
+ // problem.
352
+ const denied = error instanceof PhoneCalendarError && error.code === MiniappErrorCode.PERMISSION_DENIED
353
+ this.calendarStatus = denied ? "no-access" : "unavailable"
354
+ this.calendarEvents = []
355
+ if (!denied) console.warn("CustomDashboard: calendar fetch failed", error)
356
+ }
357
+ this.render()
358
+ }
359
+
360
+ /** Underline hugging a builtin-font label. x/rowY are the label box's origin. */
361
+ private labelUnderline(id: string, x: number, rowY: number, label: string, maxW?: number): SceneElementInput {
362
+ // Same metrics processScene uses to ellipsize builtin text; the raster
363
+ // insets text by 2px, so the bar starts under the first glyph.
364
+ const textW = new TextMeasurer(displayProcessor.getProfile()).measureText(label)
365
+ const w = maxW === undefined ? textW + 4 : Math.min(textW + 4, maxW)
366
+ return {
367
+ type: "rect",
368
+ id,
369
+ box: {x: x + 2, y: rowY + UNDERLINE_OFFSET_Y, w, h: UNDERLINE_H},
370
+ style: {border: 2},
371
+ }
372
+ }
373
+
374
+ private canvas(): {width: number; height: number} {
375
+ const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()
376
+ if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {
377
+ return {width: sceneCanvasWidth, height: sceneCanvasHeight}
378
+ }
379
+ return FALLBACK_CANVAS
380
+ }
381
+
382
+ private render(): void {
383
+ // Only while open: emitting into the single native view while closed
384
+ // would clobber the main content.
385
+ if (!this.open || !this.isEligible()) return
386
+ const elements = this.view === "apps" ? this.appMenuElements() : this.mainElements()
387
+ sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", elements)
388
+ }
389
+
390
+ private mainElements(): SceneElementInput[] {
391
+ const canvas = this.canvas()
392
+ const settings = useSettingsStore.getState()
393
+ const widgets = this.widgets().filter((widget) => widget.enabled)
394
+ const actions = dashboardActions(widgets)
395
+ if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
396
+ const selected = actions[this.mainIndex]
397
+
398
+ const clockStyle =
399
+ (settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === "large"
400
+ ? "large"
401
+ : "compact"
402
+ const calendarEnabled = widgets.some((widget) => widget.id === "calendar")
403
+ // Calendar lives in the right half; everything else stacks in the left
404
+ // half (full width when the calendar widget is off).
405
+ const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)
406
+ const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2
407
+ const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)
408
+
409
+ const elements: SceneElementInput[] = []
410
+ let y = MARGIN_TOP
411
+ for (const widget of widgets) {
412
+ if (widget.id === "clock") {
413
+ const fontSize = clockStyle === "large" ? 48 : 32
414
+ const clock = formatDashboardClock(new Date(), {
415
+ twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,
416
+ showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,
417
+ })
418
+ elements.push({
419
+ type: "text",
420
+ id: "clock",
421
+ box: {x: MARGIN_X, y, w: width, h: fontSize + 6},
422
+ text: clock.time,
423
+ style: {font: "mono", fontSize, breakMode: "strict-word"},
424
+ })
425
+ y += fontSize + 6
426
+ if (clock.date) {
427
+ elements.push({
428
+ type: "text",
429
+ id: "clock-date",
430
+ box: {x: MARGIN_X, y, w: width, h: LINE_H},
431
+ text: clock.date,
432
+ style: {font: "builtin", breakMode: "strict-word"},
433
+ })
434
+ y += LINE_H
435
+ }
436
+ y += WIDGET_GAP
437
+ } else if (widget.id === "calendar") {
438
+ // Right half, top-aligned, independent of the left stack. A missing
439
+ // calendar shows a hint instead of silently vanishing — "No access"
440
+ // strictly for permission denial, "Unavailable" for any other failure.
441
+ const now = new Date()
442
+ const lines =
443
+ this.calendarStatus === "ok"
444
+ ? [
445
+ "Calendar",
446
+ ...(this.calendarEvents.length
447
+ ? this.calendarEvents.slice(0, 3).flatMap((event) => eventLines(event, now))
448
+ : ["No upcoming events"]),
449
+ ]
450
+ : this.calendarStatus === "loading"
451
+ ? ["Calendar"]
452
+ : ["Calendar", this.calendarStatus === "no-access" ? "No access" : "Unavailable"]
453
+ elements.push({
454
+ type: "text",
455
+ id: "calendar",
456
+ box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},
457
+ text: lines.join("\n"),
458
+ style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
459
+ })
460
+ } else if (widget.id === "apps") {
461
+ elements.push({
462
+ type: "text",
463
+ id: "apps-button",
464
+ box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
465
+ text: "Apps",
466
+ style: {font: "builtin"},
467
+ })
468
+ if (selected?.kind === "apps-menu") elements.push(this.labelUnderline("apps-underline", MARGIN_X, y, "Apps"))
469
+ y += BUTTON_H + WIDGET_GAP
470
+ } else if (widget.id === "notifications") {
471
+ elements.push({
472
+ type: "text",
473
+ id: "notifications",
474
+ box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
475
+ text: "Notifications",
476
+ style: {font: "builtin"},
477
+ })
478
+ if (selected?.kind === "notifications") {
479
+ elements.push(this.labelUnderline("notifications-underline", MARGIN_X, y, "Notifications"))
480
+ }
481
+ y += BUTTON_H + WIDGET_GAP
482
+ }
483
+ }
484
+ return elements
485
+ }
486
+
487
+ private appMenuElements(): SceneElementInput[] {
488
+ const canvas = this.canvas()
489
+ const width = canvas.width - MARGIN_X * 2
490
+ const apps = this.menuApps()
491
+ const rows = appMenuActions(apps)
492
+ if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
493
+
494
+ const elements: SceneElementInput[] = [
495
+ {
496
+ type: "text",
497
+ id: "apps-title",
498
+ box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},
499
+ text: "Apps",
500
+ style: {font: "builtin"},
501
+ },
502
+ ]
503
+ const listTop = MARGIN_TOP + LINE_H + 16
504
+ if (apps.length === 0) {
505
+ elements.push({
506
+ type: "text",
507
+ id: "apps-empty",
508
+ box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},
509
+ text: "No apps in Glasses Menu",
510
+ style: {font: "builtin"},
511
+ })
512
+ return elements
513
+ }
514
+
515
+ // Vertical side menu down the left half, stock-firmware style. The list
516
+ // scrolls as a window: the selection stays visible and rows shift under it.
517
+ const listW = Math.floor(canvas.width / 2) - MARGIN_X
518
+ const visibleRows = Math.max(1, Math.floor((canvas.height - listTop) / BUTTON_H))
519
+ const maxScroll = Math.max(0, apps.length - visibleRows)
520
+ if (this.appsScroll > maxScroll) this.appsScroll = maxScroll
521
+ if (this.appsIndex < this.appsScroll) this.appsScroll = this.appsIndex
522
+ else if (this.appsIndex >= this.appsScroll + visibleRows) this.appsScroll = this.appsIndex - visibleRows + 1
523
+
524
+ apps.slice(this.appsScroll, this.appsScroll + visibleRows).forEach((app, row) => {
525
+ const y = listTop + row * BUTTON_H
526
+ const label = app.running ? `${app.name} (running)` : app.name
527
+ elements.push({
528
+ type: "text",
529
+ id: `app-row:${app.packageName}`,
530
+ box: {x: MARGIN_X, y, w: listW, h: BUTTON_H},
531
+ text: label,
532
+ style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
533
+ })
534
+ if (this.appsScroll + row === this.appsIndex) {
535
+ elements.push(this.labelUnderline("apps-selection", MARGIN_X, y, label, listW - 2))
536
+ }
537
+ })
538
+ return elements
539
+ }
540
+ }
541
+
542
+ const customDashboardService = new CustomDashboardService()
543
+ export default customDashboardService
544
+ export {CustomDashboardService}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Phone-side dashboard switching. Native retains a single display state; this
3
+ * service decides when the dashboard covers the main content (head-up,
4
+ * double-tap) and which renderer draws it: the CFW custom dashboard, or the
5
+ * legacy time/battery wall for stock devices.
6
+ */
7
+ import {useGlassesStore} from "../stores/glasses"
8
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
9
+ import {BgTimer} from "../utils/timers"
10
+ import customDashboardService from "./CustomDashboardService"
11
+ import {isGlassesConnected} from "./GlassesReadiness"
12
+ import localDisplayManager from "./LocalDisplayManager"
13
+
14
+ const LEGACY_CLOCK_REFRESH_MS = 30_000
15
+ const LEGACY_DASHBOARD_PKG = "system.dashboard"
16
+
17
+ class DashboardCoordinator {
18
+ private opened = false
19
+ private renderer: "custom" | "legacy" | null = null
20
+ private legacyTimer: number | null = null
21
+
22
+ start(): void {
23
+ customDashboardService.onRequestClose = () => this.close()
24
+ }
25
+
26
+ stop(): void {
27
+ this.close()
28
+ customDashboardService.onRequestClose = null
29
+ }
30
+
31
+ get isOpen(): boolean {
32
+ return this.opened
33
+ }
34
+
35
+ /** Head position (real or simulated). Opening respects contextual_dashboard. */
36
+ setHeadUp(up: boolean): void {
37
+ if (up === this.opened) return
38
+ if (up) {
39
+ if (useSettingsStore.getState().getSetting(SETTINGS.contextual_dashboard.key) === false) return
40
+ this.open()
41
+ } else {
42
+ this.close()
43
+ }
44
+ }
45
+
46
+ /** Double-tap on a CFW-eligible device. False when not eligible. */
47
+ toggleCustom(): boolean {
48
+ if (!customDashboardService.isEligible()) return false
49
+ if (this.opened) this.close()
50
+ else this.open()
51
+ return true
52
+ }
53
+
54
+ /** Double-tap fallback for devices without a firmware dashboard. */
55
+ toggleLegacy(): void {
56
+ if (this.opened) this.close()
57
+ else this.open()
58
+ }
59
+
60
+ /** True when an open dashboard consumed the gesture. */
61
+ handleGesture(gestureName: string): boolean {
62
+ if (!this.opened) return false
63
+ if (this.renderer === "custom") return customDashboardService.handleGesture(gestureName)
64
+ if (gestureName === "double_tap") {
65
+ this.close()
66
+ return true
67
+ }
68
+ return false
69
+ }
70
+
71
+ /** Show the dashboard over the main content (ignores contextual_dashboard). */
72
+ open(): void {
73
+ if (this.opened) return
74
+ if (!isGlassesConnected(useGlassesStore.getState().connection)) return
75
+ this.opened = true
76
+ localDisplayManager.setDashboardActive(true)
77
+ if (customDashboardService.isEligible()) {
78
+ this.renderer = "custom"
79
+ customDashboardService.show()
80
+ } else {
81
+ this.renderer = "legacy"
82
+ this.renderLegacy()
83
+ this.legacyTimer = BgTimer.setInterval(() => this.renderLegacy(), LEGACY_CLOCK_REFRESH_MS)
84
+ }
85
+ }
86
+
87
+ /** Close and restore the retained main content. */
88
+ close(): void {
89
+ if (!this.opened) return
90
+ this.opened = false
91
+ if (this.legacyTimer !== null) {
92
+ BgTimer.clearInterval(this.legacyTimer)
93
+ this.legacyTimer = null
94
+ }
95
+ if (this.renderer === "custom") customDashboardService.hide()
96
+ this.renderer = null
97
+ localDisplayManager.setDashboardActive(false)
98
+ }
99
+
100
+ /** Parity with the retired native head-up wall: time, date, battery. */
101
+ private renderLegacy(): void {
102
+ if (!this.opened) return
103
+ const now = new Date()
104
+ const time = new Intl.DateTimeFormat(undefined, {
105
+ hour: "2-digit",
106
+ minute: "2-digit",
107
+ hour12: useSettingsStore.getState().getSetting(SETTINGS.twelve_hour_time.key) === true,
108
+ }).format(now)
109
+ const date = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}`
110
+ const level = useGlassesStore.getState().batteryLevel
111
+ const batt = typeof level === "number" && level >= 0 ? ` ${level}%` : ""
112
+ localDisplayManager.renderDashboard(LEGACY_DASHBOARD_PKG, {
113
+ view: "dashboard",
114
+ layout: {layoutType: "text_wall", text: `${time} ${date}${batt} Connected`},
115
+ })
116
+ }
117
+ }
118
+
119
+ const dashboardCoordinator = new DashboardCoordinator()
120
+ export default dashboardCoordinator
121
+ export {DashboardCoordinator}