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

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 +492 -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 +516 -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,516 @@
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 eventLine(event: CalendarEvent): string {
65
+ const when = event.allDay
66
+ ? "All day"
67
+ : new Intl.DateTimeFormat(undefined, {hour: "numeric", minute: "2-digit"}).format(new Date(event.startsAt))
68
+ return `${when} ${event.title}`
69
+ }
70
+
71
+ class CustomDashboardService {
72
+ private started = false
73
+ private open = false
74
+ /** Set by DashboardCoordinator; asks it to close (and restore main content). */
75
+ public onRequestClose: (() => void) | null = null
76
+ private view: "main" | "apps" = "main"
77
+ private mainIndex = 0
78
+ private appsIndex = 0
79
+ /** First visible row of the app list's scrolling window. */
80
+ private appsScroll = 0
81
+ /** "loading" until the first fetch resolves; "no-access" only for a real permission denial. */
82
+ private calendarStatus: "loading" | "ok" | "no-access" | "unavailable" = "loading"
83
+ private calendarEvents: CalendarEvent[] = []
84
+ private unsubs: Array<() => void> = []
85
+ private clockTimer: number | null = null
86
+ private calendarTimer: number | null = null
87
+ /** Last observed eligibility, for transition detection. */
88
+ private eligible = false
89
+
90
+ start(): void {
91
+ if (this.started) return
92
+ this.started = true
93
+ this.unsubs.push(
94
+ useGlassesStore.subscribe(
95
+ (state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,
96
+ () => {
97
+ if (!this.syncEligibility()) return
98
+ void this.refreshCalendarAndRender()
99
+ },
100
+ ),
101
+ // Selector: only dashboard-relevant settings retrigger a refresh. The
102
+ // whole-store form fired a calendar query on every settings write.
103
+ useSettingsStore.subscribe(
104
+ (state) =>
105
+ JSON.stringify([
106
+ state.getSetting(SETTINGS.custom_dashboard_enabled.key),
107
+ state.getSetting(SETTINGS.custom_dashboard_widgets.key),
108
+ state.getSetting(SETTINGS.custom_dashboard_clock_style.key),
109
+ state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),
110
+ state.getSetting(SETTINGS.twelve_hour_time.key),
111
+ state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),
112
+ state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),
113
+ state.getSetting(SETTINGS.menu_apps.key),
114
+ ]),
115
+ () => {
116
+ if (!this.syncEligibility()) return
117
+ void this.refreshCalendarAndRender()
118
+ },
119
+ ),
120
+ useAppStatusStore.subscribe(() => {
121
+ if (this.isEligible()) this.render()
122
+ }),
123
+ )
124
+ this.clockTimer = BgTimer.setInterval(() => {
125
+ if (this.isEligible()) this.render()
126
+ }, CLOCK_REFRESH_MS)
127
+ this.calendarTimer = BgTimer.setInterval(() => {
128
+ if (this.isEligible()) void this.refreshCalendarAndRender()
129
+ }, CALENDAR_REFRESH_MS)
130
+ this.eligible = this.isEligible()
131
+ }
132
+
133
+ /**
134
+ * Track eligible⇄ineligible transitions from any trigger (setting toggle,
135
+ * connect/disconnect, firmware change). Leaving closes the dashboard via the
136
+ * coordinator (which restores main content) and drops the retained scene so
137
+ * a stale frame can't be replayed after the feature was turned off.
138
+ */
139
+ private syncEligibility(): boolean {
140
+ const eligible = this.isEligible()
141
+ if (eligible === this.eligible) return eligible
142
+ this.eligible = eligible
143
+ if (!eligible) {
144
+ if (this.open) this.onRequestClose?.()
145
+ this.open = false
146
+ this.resetNavigation()
147
+ sceneRenderer.clearApp(DASHBOARD_APP_ID)
148
+ }
149
+ return eligible
150
+ }
151
+
152
+ stop(): void {
153
+ for (const unsub of this.unsubs) unsub()
154
+ this.unsubs = []
155
+ if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)
156
+ if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)
157
+ this.clockTimer = null
158
+ this.calendarTimer = null
159
+ if (this.open) this.onRequestClose?.()
160
+ this.open = false
161
+ this.eligible = false
162
+ this.resetNavigation()
163
+ this.started = false
164
+ sceneRenderer.clearApp(DASHBOARD_APP_ID)
165
+ }
166
+
167
+ isEligible(): boolean {
168
+ if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false
169
+ const state = useGlassesStore.getState()
170
+ return (
171
+ isGlassesConnected(state.connection) &&
172
+ isCustomDashboardEligible({
173
+ model: state.deviceModel,
174
+ firmwareVersion: state.firmwareVersion,
175
+ })
176
+ )
177
+ }
178
+
179
+ get isOpen(): boolean {
180
+ return this.open
181
+ }
182
+
183
+ /** Coordinator opened the dashboard: render fresh from the top. */
184
+ show(): void {
185
+ this.open = true
186
+ this.resetNavigation()
187
+ void this.refreshCalendarAndRender()
188
+ }
189
+
190
+ /** Coordinator closed the dashboard. */
191
+ hide(): void {
192
+ this.open = false
193
+ this.resetNavigation()
194
+ }
195
+
196
+ /** Returns true when an open custom dashboard consumed the gesture. */
197
+ handleGesture(gestureName: string): boolean {
198
+ if (!this.open || !this.isEligible()) return false
199
+ if (gestureName === "double_tap") {
200
+ // In the app menu a double tap backs out to the top of the dashboard
201
+ // (there is no Back row); on the main view it closes the dashboard.
202
+ if (this.view === "apps") {
203
+ this.resetNavigation()
204
+ this.render()
205
+ return true
206
+ }
207
+ this.onRequestClose?.()
208
+ return true
209
+ }
210
+ if (this.view === "apps") return this.handleAppMenuGesture(gestureName)
211
+ return this.handleMainGesture(gestureName)
212
+ }
213
+
214
+ private resetNavigation(): void {
215
+ this.view = "main"
216
+ this.mainIndex = 0
217
+ this.appsIndex = 0
218
+ this.appsScroll = 0
219
+ }
220
+
221
+ private handleMainGesture(gestureName: string): boolean {
222
+ const actions = dashboardActions(this.widgets())
223
+ if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
224
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
225
+ if (actions.length === 0) return true
226
+ const delta = gestureName === "swipe_down" ? 1 : -1
227
+ this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length
228
+ this.render()
229
+ return true
230
+ }
231
+ if (gestureName === "single_tap") {
232
+ const action = actions[this.mainIndex]
233
+ if (action?.kind === "apps-menu") {
234
+ this.view = "apps"
235
+ this.appsIndex = 0
236
+ this.appsScroll = 0
237
+ this.render()
238
+ } else if (action?.kind === "notifications") {
239
+ void this.openNotificationsPanel()
240
+ }
241
+ return true
242
+ }
243
+ return false
244
+ }
245
+
246
+ private handleAppMenuGesture(gestureName: string): boolean {
247
+ const rows = appMenuActions(this.menuApps())
248
+ if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
249
+ if (gestureName === "swipe_down" || gestureName === "swipe_up") {
250
+ const delta = gestureName === "swipe_down" ? 1 : -1
251
+ this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length
252
+ this.render()
253
+ return true
254
+ }
255
+ if (gestureName === "single_tap") {
256
+ const row = rows[this.appsIndex]
257
+ if (row?.kind === "app") {
258
+ void this.toggleApp(row.packageName)
259
+ }
260
+ return true
261
+ }
262
+ return false
263
+ }
264
+
265
+ private menuApps(): ClientApp[] {
266
+ const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null
267
+ return orderedDashboardApps(menu, useAppStatusStore.getState().apps)
268
+ }
269
+
270
+ private widgets(): DashboardWidgetSetting[] {
271
+ return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))
272
+ }
273
+
274
+ private async openNotificationsPanel(): Promise<void> {
275
+ this.onRequestClose?.()
276
+ await BluetoothSdk.showNativeNotificationPanel().catch((error) => {
277
+ console.error("CustomDashboard: notification panel failed", error)
278
+ })
279
+ }
280
+
281
+ private async toggleApp(packageName: string): Promise<void> {
282
+ this.onRequestClose?.()
283
+ const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)
284
+ if (!app) return
285
+ if (app.running) {
286
+ // Mirror the "Starting X…" boot message: without this the dashboard just
287
+ // closes and the stop is invisible on the glasses.
288
+ localDisplayManager.showSystemNotice(`Stopping ${app.name}…`, STOP_NOTICE_MS)
289
+ await useAppStatusStore.getState().stop(app.packageName)
290
+ } else {
291
+ await useAppStatusStore.getState().start(app, {skipNavigation: true})
292
+ }
293
+ }
294
+
295
+ private calendarWidgetEnabled(): boolean {
296
+ return this.widgets().some((widget) => widget.id === "calendar" && widget.enabled)
297
+ }
298
+
299
+ private async refreshCalendarAndRender(): Promise<void> {
300
+ if (!this.open) return
301
+ if (!this.calendarWidgetEnabled()) {
302
+ this.calendarStatus = "loading"
303
+ this.calendarEvents = []
304
+ this.render()
305
+ return
306
+ }
307
+ const settings = useSettingsStore.getState()
308
+ const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)
309
+ const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)
310
+ const startsAt = new Date()
311
+ const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)
312
+ try {
313
+ const result = await listPhoneCalendarEvents({
314
+ startsAt: startsAt.toISOString(),
315
+ endsAt: endsAt.toISOString(),
316
+ limit: Math.max(1, Math.min(3, count)),
317
+ })
318
+ this.calendarStatus = "ok"
319
+ this.calendarEvents = result.events
320
+ } catch (error) {
321
+ // Permission is requested from the Custom Dashboard settings screen;
322
+ // only a real denial should read as one — any other failure is
323
+ // "Unavailable" (and logged) so it can't masquerade as a permission
324
+ // problem.
325
+ const denied = error instanceof PhoneCalendarError && error.code === MiniappErrorCode.PERMISSION_DENIED
326
+ this.calendarStatus = denied ? "no-access" : "unavailable"
327
+ this.calendarEvents = []
328
+ if (!denied) console.warn("CustomDashboard: calendar fetch failed", error)
329
+ }
330
+ this.render()
331
+ }
332
+
333
+ /** Underline hugging a builtin-font label. x/rowY are the label box's origin. */
334
+ private labelUnderline(id: string, x: number, rowY: number, label: string, maxW?: number): SceneElementInput {
335
+ // Same metrics processScene uses to ellipsize builtin text; the raster
336
+ // insets text by 2px, so the bar starts under the first glyph.
337
+ const textW = new TextMeasurer(displayProcessor.getProfile()).measureText(label)
338
+ const w = maxW === undefined ? textW + 4 : Math.min(textW + 4, maxW)
339
+ return {
340
+ type: "rect",
341
+ id,
342
+ box: {x: x + 2, y: rowY + UNDERLINE_OFFSET_Y, w, h: UNDERLINE_H},
343
+ style: {border: 2},
344
+ }
345
+ }
346
+
347
+ private canvas(): {width: number; height: number} {
348
+ const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()
349
+ if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {
350
+ return {width: sceneCanvasWidth, height: sceneCanvasHeight}
351
+ }
352
+ return FALLBACK_CANVAS
353
+ }
354
+
355
+ private render(): void {
356
+ // Only while open: emitting into the single native view while closed
357
+ // would clobber the main content.
358
+ if (!this.open || !this.isEligible()) return
359
+ const elements = this.view === "apps" ? this.appMenuElements() : this.mainElements()
360
+ sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", elements)
361
+ }
362
+
363
+ private mainElements(): SceneElementInput[] {
364
+ const canvas = this.canvas()
365
+ const settings = useSettingsStore.getState()
366
+ const widgets = this.widgets().filter((widget) => widget.enabled)
367
+ const actions = dashboardActions(widgets)
368
+ if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
369
+ const selected = actions[this.mainIndex]
370
+
371
+ const clockStyle =
372
+ (settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === "large"
373
+ ? "large"
374
+ : "compact"
375
+ const calendarEnabled = widgets.some((widget) => widget.id === "calendar")
376
+ // Calendar lives in the right half; everything else stacks in the left
377
+ // half (full width when the calendar widget is off).
378
+ const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)
379
+ const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2
380
+ const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)
381
+
382
+ const elements: SceneElementInput[] = []
383
+ let y = MARGIN_TOP
384
+ for (const widget of widgets) {
385
+ if (widget.id === "clock") {
386
+ const fontSize = clockStyle === "large" ? 48 : 32
387
+ const clock = formatDashboardClock(new Date(), {
388
+ twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,
389
+ showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,
390
+ })
391
+ elements.push({
392
+ type: "text",
393
+ id: "clock",
394
+ box: {x: MARGIN_X, y, w: width, h: fontSize + 6},
395
+ text: clock.time,
396
+ style: {font: "mono", fontSize, breakMode: "strict-word"},
397
+ })
398
+ y += fontSize + 6
399
+ if (clock.date) {
400
+ elements.push({
401
+ type: "text",
402
+ id: "clock-date",
403
+ box: {x: MARGIN_X, y, w: width, h: LINE_H},
404
+ text: clock.date,
405
+ style: {font: "builtin", breakMode: "strict-word"},
406
+ })
407
+ y += LINE_H
408
+ }
409
+ y += WIDGET_GAP
410
+ } else if (widget.id === "calendar") {
411
+ // Right half, top-aligned, independent of the left stack. A missing
412
+ // calendar shows a hint instead of silently vanishing — "No access"
413
+ // strictly for permission denial, "Unavailable" for any other failure.
414
+ const lines =
415
+ this.calendarStatus === "ok"
416
+ ? [
417
+ "Calendar",
418
+ ...(this.calendarEvents.length
419
+ ? this.calendarEvents.slice(0, 3).map(eventLine)
420
+ : ["No upcoming events"]),
421
+ ]
422
+ : this.calendarStatus === "loading"
423
+ ? ["Calendar"]
424
+ : ["Calendar", this.calendarStatus === "no-access" ? "No access" : "Unavailable"]
425
+ elements.push({
426
+ type: "text",
427
+ id: "calendar",
428
+ box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},
429
+ text: lines.join("\n"),
430
+ style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
431
+ })
432
+ } else if (widget.id === "apps") {
433
+ elements.push({
434
+ type: "text",
435
+ id: "apps-button",
436
+ box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
437
+ text: "Apps",
438
+ style: {font: "builtin"},
439
+ })
440
+ if (selected?.kind === "apps-menu") elements.push(this.labelUnderline("apps-underline", MARGIN_X, y, "Apps"))
441
+ y += BUTTON_H + WIDGET_GAP
442
+ } else if (widget.id === "notifications") {
443
+ elements.push({
444
+ type: "text",
445
+ id: "notifications",
446
+ box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
447
+ text: "Notifications",
448
+ style: {font: "builtin"},
449
+ })
450
+ if (selected?.kind === "notifications") {
451
+ elements.push(this.labelUnderline("notifications-underline", MARGIN_X, y, "Notifications"))
452
+ }
453
+ y += BUTTON_H + WIDGET_GAP
454
+ }
455
+ }
456
+ return elements
457
+ }
458
+
459
+ private appMenuElements(): SceneElementInput[] {
460
+ const canvas = this.canvas()
461
+ const width = canvas.width - MARGIN_X * 2
462
+ const apps = this.menuApps()
463
+ const rows = appMenuActions(apps)
464
+ if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
465
+
466
+ const elements: SceneElementInput[] = [
467
+ {
468
+ type: "text",
469
+ id: "apps-title",
470
+ box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},
471
+ text: "Apps",
472
+ style: {font: "builtin"},
473
+ },
474
+ ]
475
+ const listTop = MARGIN_TOP + LINE_H + 16
476
+ if (apps.length === 0) {
477
+ elements.push({
478
+ type: "text",
479
+ id: "apps-empty",
480
+ box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},
481
+ text: "No apps in Glasses Menu",
482
+ style: {font: "builtin"},
483
+ })
484
+ return elements
485
+ }
486
+
487
+ // Vertical side menu down the left half, stock-firmware style. The list
488
+ // scrolls as a window: the selection stays visible and rows shift under it.
489
+ const listW = Math.floor(canvas.width / 2) - MARGIN_X
490
+ const visibleRows = Math.max(1, Math.floor((canvas.height - listTop) / BUTTON_H))
491
+ const maxScroll = Math.max(0, apps.length - visibleRows)
492
+ if (this.appsScroll > maxScroll) this.appsScroll = maxScroll
493
+ if (this.appsIndex < this.appsScroll) this.appsScroll = this.appsIndex
494
+ else if (this.appsIndex >= this.appsScroll + visibleRows) this.appsScroll = this.appsIndex - visibleRows + 1
495
+
496
+ apps.slice(this.appsScroll, this.appsScroll + visibleRows).forEach((app, row) => {
497
+ const y = listTop + row * BUTTON_H
498
+ const label = app.running ? `${app.name} (running)` : app.name
499
+ elements.push({
500
+ type: "text",
501
+ id: `app-row:${app.packageName}`,
502
+ box: {x: MARGIN_X, y, w: listW, h: BUTTON_H},
503
+ text: label,
504
+ style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
505
+ })
506
+ if (this.appsScroll + row === this.appsIndex) {
507
+ elements.push(this.labelUnderline("apps-selection", MARGIN_X, y, label, listW - 2))
508
+ }
509
+ })
510
+ return elements
511
+ }
512
+ }
513
+
514
+ const customDashboardService = new CustomDashboardService()
515
+ export default customDashboardService
516
+ 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}