@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
@@ -879,6 +879,22 @@ class AppRegistry {
879
879
  this.notify()
880
880
  }
881
881
 
882
+ /**
883
+ * Update whether a registered offline app is projected into launcher UIs.
884
+ *
885
+ * Offline apps are registered once at host startup, so settings-backed
886
+ * visibility changes need to update the projection without reinstalling or
887
+ * duplicating the app. No-op when the app is missing or already matches.
888
+ */
889
+ public setOfflineAppHidden(packageName: string, hidden: boolean): void {
890
+ const app = this.offlineApps.find((candidate) => candidate.packageName === packageName)
891
+ if (!app || app.hidden === hidden) return
892
+
893
+ app.hidden = hidden
894
+ this.refreshNeeded = true
895
+ this.notify()
896
+ }
897
+
882
898
  /**
883
899
  * Registered as needing the on-device STT model before it can start (the
884
900
  * host declares this per offline app; the apps store gates start() on it).
@@ -40,8 +40,15 @@ const LOG_TAG = "LocalMiniappRuntime"
40
40
  /** Per-app blob quota: 1 GB. */
41
41
  export const BLOB_QUOTA_BYTES = 1024 * 1024 * 1024
42
42
  const ROOT_DIR_NAME = "glassly_blobs"
43
- /** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one unique subdir per share). */
43
+ /** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one subdir per stored blob). */
44
44
  const SHARE_DIR_NAME = "blob_share"
45
+ /**
46
+ * Keep prepared share handoffs alive long enough for recipient apps to read
47
+ * them asynchronously (for example, an email composer attaching the file).
48
+ * Stale copies are collected before the next share; the OS may evict cache
49
+ * files sooner under storage pressure.
50
+ */
51
+ const SHARE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
45
52
  const META_KEY_ROOT = "blobmeta_"
46
53
  /** Cap md5 computation so we don't block the JS thread hashing a huge file. */
47
54
  const MD5_MAX_BYTES = 50 * 1024 * 1024
@@ -662,6 +669,32 @@ export class BlobStore {
662
669
 
663
670
  // ---- share ---------------------------------------------------------------
664
671
 
672
+ /** Best-effort GC for prepared share handoffs retained in the OS cache. */
673
+ private cleanupShareCache(now = Date.now()): void {
674
+ try {
675
+ const root = new Directory(Paths.cache, SHARE_DIR_NAME)
676
+ if (!root.exists) return
677
+ const cutoff = now - SHARE_CACHE_MAX_AGE_MS
678
+ for (const entry of root.list()) {
679
+ if (!(entry instanceof Directory)) continue
680
+ try {
681
+ const info = entry.info()
682
+ // modificationTime is available on both platforms; creationTime can
683
+ // be absent on Android versions before API 26. The directory id
684
+ // begins with Date.now().toString(36), which is a final fallback.
685
+ const encoded = entry.name.split("-", 1)[0]
686
+ const encodedTime = /^[0-9a-z]+$/.test(encoded) ? Number.parseInt(encoded, 36) : Number.NaN
687
+ const timestamp = info.modificationTime ?? info.creationTime ?? encodedTime
688
+ if (Number.isFinite(timestamp) && timestamp < cutoff) entry.delete()
689
+ } catch {
690
+ // Keep scanning if one cache entry disappears or is unreadable.
691
+ }
692
+ }
693
+ } catch {
694
+ // Cache cleanup must never prevent a share.
695
+ }
696
+ }
697
+
665
698
  async handleShare(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
666
699
  const key = payload.key as string
667
700
  const meta = this.readMeta(packageName, key)
@@ -679,23 +712,33 @@ export class BlobStore {
679
712
  // applies to base64/data payloads, not file:// URLs), so sharing `file.uri`
680
713
  // directly would hand the recipient a machine-named file. Copy to a temp
681
714
  // file that carries the real display name + extension, share that, then
682
- // clean it up.
715
+ // clean it up after the recipient has had time to consume it.
683
716
  const shareName = shareFileName(meta)
684
- // Copy into a per-share unique subdir so two concurrent shares that resolve
685
- // to the same display name can't clobber each other's temp file while
686
- // Share.open still references it. The file keeps `shareName` as its basename
687
- // so the OS share sheet shows the right name + extension.
717
+ // Key the cache directory by the immutable stored filename. Repeated or
718
+ // concurrent shares of the same blob can safely reference the same bytes,
719
+ // avoiding an extra full-sized cache copy for every share. Different blobs
720
+ // still get isolated directories even when they use the same display name.
721
+ // The file keeps `shareName` as its basename so the OS share sheet shows the
722
+ // right name + extension.
688
723
  let tempDir: Directory | null = null
724
+ let cacheReady = false
689
725
  try {
690
- tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, this.makeId())
691
- tempDir.create({intermediates: true})
726
+ this.cleanupShareCache()
727
+ tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, sanitizeSegment(meta.fileName))
728
+ if (!tempDir.exists) tempDir.create({intermediates: true})
692
729
  const temp = new File(tempDir, shareName)
693
- file.copy(temp)
730
+ if (!temp.exists || temp.size !== meta.bytes) {
731
+ if (temp.exists) temp.delete()
732
+ file.copy(temp)
733
+ }
734
+ cacheReady = true
694
735
  await Share.open({
695
736
  url: temp.uri,
696
737
  type: meta.mimeType || OCTET,
697
738
  filename: shareName,
698
739
  })
740
+ // Share.open resolves once the OS accepts the handoff. Recipient apps may
741
+ // still read the URI asynchronously, so retain this cache copy for GC.
699
742
  this.hooks.sendResult(packageName, requestId, true, {success: true})
700
743
  } catch (error: any) {
701
744
  if (error?.message?.includes("User did not share")) {
@@ -705,7 +748,11 @@ export class BlobStore {
705
748
  this.hooks.sendResult(packageName, requestId, true, {success: false})
706
749
  }
707
750
  } finally {
708
- if (tempDir) {
751
+ // Once a reusable cache entry is ready, retain it even if this particular
752
+ // sheet is cancelled or fails: another concurrent/recent share may still
753
+ // be reading the same file. Only remove an entry whose preparation did
754
+ // not finish; ready entries are collected after the retention window.
755
+ if (tempDir && !cacheReady) {
709
756
  try {
710
757
  if (tempDir.exists) tempDir.delete()
711
758
  } catch {
@@ -0,0 +1,83 @@
1
+ import type {ClientApp} from "../types/applet"
2
+ import {DeviceTypes} from "../types/enums"
3
+
4
+ /** Canonical widget order; also the default layout order. */
5
+ export const DASHBOARD_WIDGET_IDS = ["clock", "calendar", "apps", "notifications"] as const
6
+ export type DashboardWidgetId = (typeof DASHBOARD_WIDGET_IDS)[number]
7
+ export type DashboardWidgetSetting = {id: DashboardWidgetId; enabled: boolean}
8
+
9
+ export type DashboardAction = {kind: "apps-menu"} | {kind: "notifications"}
10
+ export type AppMenuAction = {kind: "app"; packageName: string}
11
+
12
+ export type MenuAppSetting = {packageName: string; name: string; running?: boolean}
13
+ export type CustomDashboardClockStyle = "compact" | "large"
14
+
15
+ /** G2.swift appends this marker after the firmware advertises EVENCFW capabilities. */
16
+ export function isGlasslyCfwFirmwareVersion(firmwareVersion: string | null | undefined): boolean {
17
+ return firmwareVersion?.toLowerCase().includes("glassly cfw") === true
18
+ }
19
+
20
+ /** Stock G2 and NIMO are intentionally excluded. */
21
+ export function isCustomDashboardEligible(info: {model?: string | null; firmwareVersion?: string | null}): boolean {
22
+ return info.model === DeviceTypes.G2 && isGlasslyCfwFirmwareVersion(info.firmwareVersion)
23
+ }
24
+
25
+ export function formatDashboardClock(
26
+ date: Date,
27
+ options: {twelveHour: boolean; showDate: boolean},
28
+ ): {time: string; date?: string} {
29
+ const time = new Intl.DateTimeFormat(undefined, {
30
+ hour: "numeric",
31
+ minute: "2-digit",
32
+ hour12: options.twelveHour,
33
+ }).format(date)
34
+ if (!options.showDate) return {time}
35
+ return {
36
+ time,
37
+ date: new Intl.DateTimeFormat(undefined, {weekday: "short", month: "short", day: "numeric"}).format(date),
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Saved widget order, sanitized: unknown/duplicate entries dropped, widgets
43
+ * missing from the saved value appended enabled in canonical order.
44
+ */
45
+ export function normalizeDashboardWidgets(raw: unknown): DashboardWidgetSetting[] {
46
+ const widgets: DashboardWidgetSetting[] = []
47
+ if (Array.isArray(raw)) {
48
+ for (const entry of raw) {
49
+ if (!entry || typeof entry !== "object") continue
50
+ const id = (entry as {id?: unknown}).id as DashboardWidgetId
51
+ if (!DASHBOARD_WIDGET_IDS.includes(id)) continue
52
+ if (widgets.some((widget) => widget.id === id)) continue
53
+ widgets.push({id, enabled: (entry as {enabled?: unknown}).enabled !== false})
54
+ }
55
+ }
56
+ for (const id of DASHBOARD_WIDGET_IDS) {
57
+ if (!widgets.some((widget) => widget.id === id)) widgets.push({id, enabled: true})
58
+ }
59
+ return widgets
60
+ }
61
+
62
+ export function orderedDashboardApps(menu: MenuAppSetting[] | null | undefined, apps: ClientApp[]): ClientApp[] {
63
+ if (!Array.isArray(menu)) return []
64
+ return menu
65
+ .map((item) => apps.find((app) => app.packageName === item.packageName))
66
+ .filter((app): app is ClientApp => Boolean(app && !app.hidden && app.compatibility?.isCompatible !== false))
67
+ }
68
+
69
+ /** Focusable main-view actions, following the user's widget order. */
70
+ export function dashboardActions(widgets: DashboardWidgetSetting[]): DashboardAction[] {
71
+ const actions: DashboardAction[] = []
72
+ for (const widget of widgets) {
73
+ if (!widget.enabled) continue
74
+ if (widget.id === "apps") actions.push({kind: "apps-menu"})
75
+ else if (widget.id === "notifications") actions.push({kind: "notifications"})
76
+ }
77
+ return actions
78
+ }
79
+
80
+ /** App-menu view rows: the Glasses Menu apps in order (double tap leaves the view). */
81
+ export function appMenuActions(apps: ClientApp[]): AppMenuAction[] {
82
+ return apps.map((app): AppMenuAction => ({kind: "app", packageName: app.packageName}))
83
+ }