@glassly/engine 0.3.7 → 0.3.8-dev.1000

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 (212) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +10 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -4
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/ai.d.ts +3 -0
  7. package/build/facades/ai.d.ts.map +1 -1
  8. package/build/facades/ai.js +3 -0
  9. package/build/facades/ai.js.map +1 -1
  10. package/build/facades/backup.d.ts +14 -0
  11. package/build/facades/backup.d.ts.map +1 -0
  12. package/build/facades/backup.js +22 -0
  13. package/build/facades/backup.js.map +1 -0
  14. package/build/facades/glasses.d.ts +4 -4
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +4 -4
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/generated/releaseMetadata.js +4 -4
  19. package/build/generated/releaseMetadata.js.map +1 -1
  20. package/build/index.d.ts +2 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js.map +1 -1
  23. package/build/services/AppRegistry.d.ts +2 -1
  24. package/build/services/AppRegistry.d.ts.map +1 -1
  25. package/build/services/AppRegistry.js +38 -8
  26. package/build/services/AppRegistry.js.map +1 -1
  27. package/build/services/AppStateBackup.d.ts +98 -0
  28. package/build/services/AppStateBackup.d.ts.map +1 -0
  29. package/build/services/AppStateBackup.js +224 -0
  30. package/build/services/AppStateBackup.js.map +1 -0
  31. package/build/services/BlobStore.d.ts.map +1 -1
  32. package/build/services/BlobStore.js +5 -12
  33. package/build/services/BlobStore.js.map +1 -1
  34. package/build/services/CloudClientService.d.ts +1 -0
  35. package/build/services/CloudClientService.d.ts.map +1 -1
  36. package/build/services/CloudClientService.js +34 -2
  37. package/build/services/CloudClientService.js.map +1 -1
  38. package/build/services/DisplayDiagnostics.d.ts.map +1 -1
  39. package/build/services/DisplayDiagnostics.js +4 -2
  40. package/build/services/DisplayDiagnostics.js.map +1 -1
  41. package/build/services/DisplayProcessor.d.ts +2 -0
  42. package/build/services/DisplayProcessor.d.ts.map +1 -1
  43. package/build/services/DisplayProcessor.js +15 -1
  44. package/build/services/DisplayProcessor.js.map +1 -1
  45. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  46. package/build/services/GlassesTouchInput.js +4 -2
  47. package/build/services/GlassesTouchInput.js.map +1 -1
  48. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  49. package/build/services/JSCrashloopReportService.js +5 -0
  50. package/build/services/JSCrashloopReportService.js.map +1 -1
  51. package/build/services/JSRouter.d.ts +11 -3
  52. package/build/services/JSRouter.d.ts.map +1 -1
  53. package/build/services/JSRouter.js +36 -6
  54. package/build/services/JSRouter.js.map +1 -1
  55. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  56. package/build/services/LocalDisplayManager.js +5 -2
  57. package/build/services/LocalDisplayManager.js.map +1 -1
  58. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  59. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  60. package/build/services/LocalMiniappRuntime.js +61 -22
  61. package/build/services/LocalMiniappRuntime.js.map +1 -1
  62. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  63. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  64. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  65. package/build/services/LocalTranslationService.d.ts +2 -0
  66. package/build/services/LocalTranslationService.d.ts.map +1 -1
  67. package/build/services/LocalTranslationService.js +22 -0
  68. package/build/services/LocalTranslationService.js.map +1 -1
  69. package/build/services/MiniappEngine.d.ts +9 -0
  70. package/build/services/MiniappEngine.d.ts.map +1 -1
  71. package/build/services/MiniappEngine.js +60 -27
  72. package/build/services/MiniappEngine.js.map +1 -1
  73. package/build/services/MiniappLauncher.d.ts +2 -0
  74. package/build/services/MiniappLauncher.d.ts.map +1 -1
  75. package/build/services/MiniappLauncher.js +6 -0
  76. package/build/services/MiniappLauncher.js.map +1 -1
  77. package/build/services/NavigationService.d.ts +1 -0
  78. package/build/services/NavigationService.d.ts.map +1 -1
  79. package/build/services/NavigationService.js +20 -6
  80. package/build/services/NavigationService.js.map +1 -1
  81. package/build/services/OfflineActivity.d.ts +54 -0
  82. package/build/services/OfflineActivity.d.ts.map +1 -0
  83. package/build/services/OfflineActivity.js +86 -0
  84. package/build/services/OfflineActivity.js.map +1 -0
  85. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  86. package/build/services/PhoneCalendarService.js +8 -0
  87. package/build/services/PhoneCalendarService.js.map +1 -1
  88. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  89. package/build/services/PhoneNotificationsSync.js +7 -3
  90. package/build/services/PhoneNotificationsSync.js.map +1 -1
  91. package/build/services/STTModelManager.d.ts +1 -0
  92. package/build/services/STTModelManager.d.ts.map +1 -1
  93. package/build/services/STTModelManager.js +6 -0
  94. package/build/services/STTModelManager.js.map +1 -1
  95. package/build/services/SceneRenderer.js +1 -1
  96. package/build/services/SceneRenderer.js.map +1 -1
  97. package/build/services/appStateBackupFile.d.ts +32 -0
  98. package/build/services/appStateBackupFile.d.ts.map +1 -0
  99. package/build/services/appStateBackupFile.js +135 -0
  100. package/build/services/appStateBackupFile.js.map +1 -0
  101. package/build/services/controllers.d.ts.map +1 -1
  102. package/build/services/controllers.js +12 -3
  103. package/build/services/controllers.js.map +1 -1
  104. package/build/services/effectiveCapabilities.js +6 -1
  105. package/build/services/effectiveCapabilities.js.map +1 -1
  106. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  107. package/build/services/pages/pageEligibility.js +8 -1
  108. package/build/services/pages/pageEligibility.js.map +1 -1
  109. package/build/services/settingsBridge.d.ts.map +1 -1
  110. package/build/services/settingsBridge.js +1 -0
  111. package/build/services/settingsBridge.js.map +1 -1
  112. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  113. package/build/services/watch/WatchLink.d.ts.map +1 -0
  114. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  115. package/build/services/watch/WatchLink.js.map +1 -0
  116. package/build/stores/apps.d.ts.map +1 -1
  117. package/build/stores/apps.js +22 -10
  118. package/build/stores/apps.js.map +1 -1
  119. package/build/stores/settings.d.ts.map +1 -1
  120. package/build/stores/settings.js +63 -6
  121. package/build/stores/settings.js.map +1 -1
  122. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  123. package/build/types/capabilities/even-realities-g1.js +3 -0
  124. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  125. package/build/types/capabilities/memomind-one.d.ts +3 -0
  126. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  127. package/build/types/capabilities/memomind-one.js +42 -0
  128. package/build/types/capabilities/memomind-one.js.map +1 -0
  129. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  130. package/build/types/capabilities/rayneo-io.js +3 -2
  131. package/build/types/capabilities/rayneo-io.js.map +1 -1
  132. package/build/types/enums.d.ts +7 -2
  133. package/build/types/enums.d.ts.map +1 -1
  134. package/build/types/enums.js +5 -0
  135. package/build/types/enums.js.map +1 -1
  136. package/build/types/hardware.d.ts +3 -1
  137. package/build/types/hardware.d.ts.map +1 -1
  138. package/build/types/hardware.js +3 -1
  139. package/build/types/hardware.js.map +1 -1
  140. package/build/utils/display/index.d.ts +2 -1
  141. package/build/utils/display/index.d.ts.map +1 -1
  142. package/build/utils/display/index.js +3 -2
  143. package/build/utils/display/index.js.map +1 -1
  144. package/build/utils/display/profiles/index.d.ts +2 -1
  145. package/build/utils/display/profiles/index.d.ts.map +1 -1
  146. package/build/utils/display/profiles/index.js +3 -2
  147. package/build/utils/display/profiles/index.js.map +1 -1
  148. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  149. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  150. package/build/utils/display/profiles/rayneo-io.js +1 -1
  151. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  152. package/build/utils/display/scene/index.d.ts +1 -1
  153. package/build/utils/display/scene/index.d.ts.map +1 -1
  154. package/build/utils/display/scene/index.js +1 -1
  155. package/build/utils/display/scene/index.js.map +1 -1
  156. package/build/utils/display/scene/process.d.ts +6 -0
  157. package/build/utils/display/scene/process.d.ts.map +1 -1
  158. package/build/utils/display/scene/process.js +22 -7
  159. package/build/utils/display/scene/process.js.map +1 -1
  160. package/build/utils/miniappGlobals.d.ts +13 -17
  161. package/build/utils/miniappGlobals.d.ts.map +1 -1
  162. package/build/utils/miniappGlobals.js +24 -56
  163. package/build/utils/miniappGlobals.js.map +1 -1
  164. package/package.json +7 -7
  165. package/src/engine.ts +7 -4
  166. package/src/facades/ai.ts +4 -0
  167. package/src/facades/backup.ts +28 -0
  168. package/src/facades/glasses.ts +5 -5
  169. package/src/generated/releaseMetadata.ts +4 -4
  170. package/src/index.ts +9 -0
  171. package/src/services/AppRegistry.ts +36 -9
  172. package/src/services/AppStateBackup.ts +283 -0
  173. package/src/services/BlobStore.ts +5 -11
  174. package/src/services/CloudClientService.ts +35 -4
  175. package/src/services/DisplayDiagnostics.ts +4 -2
  176. package/src/services/DisplayProcessor.ts +20 -8
  177. package/src/services/GlassesTouchInput.ts +6 -2
  178. package/src/services/JSCrashloopReportService.ts +5 -0
  179. package/src/services/JSRouter.ts +37 -6
  180. package/src/services/LocalDisplayManager.ts +5 -2
  181. package/src/services/LocalMiniappRuntime.ts +61 -22
  182. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  183. package/src/services/LocalTranslationService.ts +20 -0
  184. package/src/services/MiniappEngine.ts +62 -27
  185. package/src/services/MiniappLauncher.ts +7 -0
  186. package/src/services/NavigationService.ts +28 -8
  187. package/src/services/OfflineActivity.ts +131 -0
  188. package/src/services/PhoneCalendarService.ts +7 -0
  189. package/src/services/PhoneNotificationsSync.ts +8 -3
  190. package/src/services/STTModelManager.ts +6 -0
  191. package/src/services/SceneRenderer.ts +1 -1
  192. package/src/services/appStateBackupFile.ts +171 -0
  193. package/src/services/controllers.ts +13 -3
  194. package/src/services/effectiveCapabilities.ts +6 -1
  195. package/src/services/pages/pageEligibility.ts +9 -1
  196. package/src/services/settingsBridge.ts +1 -0
  197. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  198. package/src/stores/apps.ts +22 -11
  199. package/src/stores/settings.ts +70 -10
  200. package/src/types/capabilities/even-realities-g1.ts +4 -0
  201. package/src/types/capabilities/memomind-one.ts +43 -0
  202. package/src/types/capabilities/rayneo-io.ts +3 -2
  203. package/src/types/enums.ts +5 -0
  204. package/src/types/hardware.ts +14 -1
  205. package/src/utils/display/index.ts +4 -2
  206. package/src/utils/display/profiles/index.ts +9 -2
  207. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  208. package/src/utils/display/scene/index.ts +1 -1
  209. package/src/utils/display/scene/process.ts +22 -7
  210. package/src/utils/miniappGlobals.ts +32 -64
  211. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  212. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
@@ -49,9 +49,16 @@ export async function listPhoneCalendarEvents(request: CalendarListRequest): Pro
49
49
  if (calendars.length === 0) return {events: [], truncated: false}
50
50
 
51
51
  const rawEvents = await Calendar.listEvents(calendars, startsAt, endsAt)
52
+ const seen = new Set<string>()
52
53
  const events = rawEvents
53
54
  .map(normalizeCalendarEvent)
54
55
  .filter((event) => calendarEventOverlapsWindow(event, startsAt, endsAt))
56
+ .filter((event) => {
57
+ const key = `${event.title}\n${event.startsAt}\n${event.endsAt}\n${event.allDay}`
58
+ if (seen.has(key)) return false
59
+ seen.add(key)
60
+ return true
61
+ })
55
62
  .sort((a, b) => Date.parse(a.startsAt) - Date.parse(b.startsAt) || Date.parse(a.endsAt) - Date.parse(b.endsAt))
56
63
 
57
64
  return {
@@ -91,6 +91,11 @@ export function nativeNotificationsSupported(): boolean {
91
91
  return getModelCapabilities(model).hasNativeNotifications === true
92
92
  }
93
93
 
94
+ function notificationRelaySupported(): boolean {
95
+ const model = useGlassesStore.getState().deviceModel as DeviceTypes
96
+ return Platform.OS === "ios" && getModelCapabilities(model).hasNotificationRelay === true
97
+ }
98
+
94
99
  /** Popup durations the firmware accepts, or an empty list when it takes any. */
95
100
  export function nativeNotificationPopupSeconds(): number[] {
96
101
  const model = useGlassesStore.getState().deviceModel as DeviceTypes
@@ -141,10 +146,10 @@ export function currentPresentationOwner(hostActive: boolean): PresentationOwner
141
146
  */
142
147
  export function captureAuthorization(): "granted" | "denied" | "unknown" | "not_needed" {
143
148
  if (Platform.OS !== "ios") return "not_needed"
144
- if (!nativeNotificationsSupported()) return "not_needed"
149
+ if (!nativeNotificationsSupported() && !notificationRelaySupported()) return "not_needed"
145
150
  const glasses = useGlassesStore.getState()
146
151
  if (!isGlassesConnected(glasses.connection)) return "unknown"
147
- return glasses.ancsAuthorized ? "granted" : "denied"
152
+ return glasses.ancsAuthorized ? "granted" : notificationRelaySupported() ? "unknown" : "denied"
148
153
  }
149
154
 
150
155
  /**
@@ -178,7 +183,7 @@ function pushListenerConfig(): void {
178
183
  }
179
184
 
180
185
  function pushGlassesConfig(): void {
181
- if (!nativeNotificationsSupported()) return
186
+ if (!nativeNotificationsSupported() && !notificationRelaySupported()) return
182
187
  if (!isGlassesConnected(useGlassesStore.getState().connection)) return
183
188
  void Promise.resolve(BluetoothSdk.setGlassesNotificationConfig(desiredGlassesNotificationConfig())).catch(
184
189
  (err: unknown) =>
@@ -228,6 +228,12 @@ class STTModelManager {
228
228
  return this.currentLanguage
229
229
  }
230
230
 
231
+ currentModelLabel(): string {
232
+ const language = this.config(this.currentLanguage)
233
+ if (!language) return this.currentLanguage
234
+ return language.type === "apple" ? `Apple · ${language.displayName}` : language.displayName
235
+ }
236
+
231
237
  setCurrentLanguage(code: string): void {
232
238
  if (this.config(code)) {
233
239
  this.currentLanguage = code
@@ -250,7 +250,7 @@ class SceneRenderer {
250
250
  return {kind: "legacy", layout, degraded, dropped}
251
251
  }
252
252
 
253
- const processed = processScene(elements, caps, displayProcessor.getProfile())
253
+ const processed = processScene(elements, caps, displayProcessor.getSceneProfile())
254
254
  // A stale baseline means the glasses were cleared since the last commit —
255
255
  // diff from empty so everything repaints (the retained frame stays valid
256
256
  // as the replay source only).
@@ -0,0 +1,171 @@
1
+ /**
2
+ * App state backup files: the share-sheet export and file-picker restore
3
+ * behind Settings > Profile > Backup. What goes in the file is decided in
4
+ * AppStateBackup.ts; this side reads and writes the live stores.
5
+ */
6
+
7
+ import type {MemoryImportResult} from "@glassly/miniapp"
8
+ import {File, Paths} from "expo-file-system"
9
+ import {Platform} from "react-native"
10
+ import Share from "react-native-share"
11
+
12
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
13
+ import {useAppStatusStore} from "../stores/apps"
14
+ import {storage} from "../utils/storage/storage"
15
+ import appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from "./AppRegistry"
16
+ import {
17
+ AppStateBackupError,
18
+ collectAppStateBackup,
19
+ miniappPrefs,
20
+ miniappsWithStorage,
21
+ parseAppStateBackup,
22
+ restorableSettings,
23
+ restoreMiniappStorage,
24
+ type AppStateBackupBackend,
25
+ } from "./AppStateBackup"
26
+ import {cloudClientService} from "./CloudClientService"
27
+ import {memoryStore} from "./memoryStoreInstance"
28
+ import {respawnMiniapp, runningMiniappPackages, stopMiniapp} from "./MiniappEngine"
29
+
30
+ export interface AppStateExportResult {
31
+ success: boolean
32
+ cancelled?: boolean
33
+ }
34
+
35
+ export interface AppStateRestoreResult {
36
+ /** Settings written. */
37
+ settings: number
38
+ /** Miniapps whose storage was restored. */
39
+ miniapps: number
40
+ memory: MemoryImportResult | null
41
+ /** Installed when the backup was made but not on this phone now. */
42
+ missing: string[]
43
+ }
44
+
45
+ export interface AppStateRestoreOptions {
46
+ /**
47
+ * Install what the backup had installed and this phone lacks. Runs before
48
+ * the restored miniapps start again, so the dashboard comes back with every
49
+ * widget provider its saved layout names: it drops unknown widgets from the
50
+ * layout the next time it saves it.
51
+ */
52
+ reinstall?: (packageNames: string[]) => Promise<void>
53
+ }
54
+
55
+ const PICK_MIME_TYPES =
56
+ Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
57
+
58
+ const backend: AppStateBackupBackend = {
59
+ get(key) {
60
+ const result = storage.load<unknown>(key)
61
+ return result.is_ok() ? result.value : null
62
+ },
63
+ set(key, value) {
64
+ const result = storage.save(key, value)
65
+ if (result.is_error()) throw result.error
66
+ },
67
+ keys: () => storage.getAllKeys(),
68
+ }
69
+
70
+ export async function exportAppStateFile(): Promise<AppStateExportResult> {
71
+ const userId = await cloudClientService.resolveUserId()
72
+ const installed: Array<{packageName: string; version: string; source?: string}> = []
73
+ for (const app of await appRegistry.getInstalledMiniapps()) {
74
+ const version = await appRegistry.getActiveVersion(app.packageName)
75
+ if (!version) continue
76
+ installed.push({
77
+ packageName: app.packageName,
78
+ version,
79
+ source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
80
+ })
81
+ }
82
+ const backup = collectAppStateBackup({
83
+ backend,
84
+ userId,
85
+ settings: Object.values(SETTINGS),
86
+ installed,
87
+ memory: await memoryStore.exportBackup(),
88
+ now: Date.now(),
89
+ })
90
+
91
+ const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`
92
+ const file = new File(Paths.cache, filename)
93
+ file.writeSync(JSON.stringify(backup))
94
+ try {
95
+ await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly backup"})
96
+ return {success: true}
97
+ } catch (error: any) {
98
+ // react-native-share rejects when the user dismisses the sheet.
99
+ if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
100
+ throw error
101
+ }
102
+ }
103
+
104
+ /** Resolves null when the user cancels the picker. */
105
+ export async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {
106
+ const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
107
+ if (picked.canceled) return null
108
+ let data: unknown
109
+ try {
110
+ data = JSON.parse(await picked.result.text())
111
+ } catch {
112
+ throw new AppStateBackupError("Not a Glassly app state backup")
113
+ }
114
+ const backup = parseAppStateBackup(data)
115
+ const userId = await cloudClientService.resolveUserId()
116
+
117
+ // First, so a memory section this build rejects fails the restore before anything is written.
118
+ const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null
119
+
120
+ const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))
121
+ const written = await useSettingsStore.getState().setManyLocally(plain)
122
+ if (written.is_error()) throw written.error
123
+ // setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
124
+ for (const [key, value] of Object.entries(indexed)) backend.set(key, value)
125
+ if (Object.keys(indexed).length > 0) {
126
+ useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))
127
+ }
128
+
129
+ // A running miniapp holds its old values in memory and could write them back over the
130
+ // restore, so the ones getting storage stop now and start again at the end.
131
+ const running = new Set(runningMiniappPackages())
132
+ const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))
133
+ for (const packageName of restarting) await stopMiniapp(packageName)
134
+
135
+ try {
136
+ const restored = restoreMiniappStorage(backend, userId, backup)
137
+
138
+ const apps = useAppStatusStore.getState()
139
+ for (const [packageName, record] of Object.entries(backup.miniapps)) {
140
+ const prefs = miniappPrefs(record)
141
+ if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)
142
+ if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)
143
+ for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
144
+ saveWidgetExpandChoice(packageName, widgetId, expand)
145
+ }
146
+ }
147
+ appRegistry.markRefreshNeeded()
148
+ await apps.refresh()
149
+
150
+ const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))
151
+ const missing = Object.entries(backup.miniapps)
152
+ .filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
153
+ .map(([packageName]) => packageName)
154
+ if (missing.length > 0) await opts.reinstall?.(missing)
155
+
156
+ return {
157
+ settings: Object.keys(plain).length + Object.keys(indexed).length,
158
+ miniapps: restored.packages.length,
159
+ memory,
160
+ missing,
161
+ }
162
+ } finally {
163
+ for (const packageName of restarting) await respawnMiniapp(packageName)
164
+ }
165
+ }
166
+
167
+ function localDate(ms: number): string {
168
+ const d = new Date(ms)
169
+ const pad = (n: number) => String(n).padStart(2, "0")
170
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
171
+ }
@@ -26,8 +26,8 @@ const NEURAL_BAND_SURFACE: InputSurface = {
26
26
  ],
27
27
  }
28
28
 
29
- /** The Apple Watch trackpad's taps and four-way swipes, and the Digital Crown as the dial. */
30
- const APPLE_WATCH_SURFACE: InputSurface = {
29
+ /** A watch trackpad's taps and four-way swipes, and the crown (Apple) or rotary bezel (Wear OS) as the dial. */
30
+ const WATCH_SURFACE: InputSurface = {
31
31
  type: "swipe2d",
32
32
  events: [
33
33
  "TAP",
@@ -42,10 +42,20 @@ const APPLE_WATCH_SURFACE: InputSurface = {
42
42
  ],
43
43
  }
44
44
 
45
+ /** The Gamebrick's D-pad is four absolute swipes; A is the press lifecycle (tap and hold) and B a double tap. */
46
+ const GAMEBRICK_SURFACE: InputSurface = {
47
+ type: "swipe2d",
48
+ events: ["TAP", "DOUBLE_TAP", "PRESS_HOLD", "SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"],
49
+ }
50
+
45
51
  // The R1 swipes up/down like the temples it stands in for, so it adds nothing.
46
52
  const INPUT_SURFACES: Readonly<Record<string, readonly InputSurface[]>> = {
47
53
  [ControllerTypes.NEURAL_BAND]: [NEURAL_BAND_SURFACE],
48
- [ControllerTypes.APPLE_WATCH]: [APPLE_WATCH_SURFACE],
54
+ [ControllerTypes.APPLE_WATCH]: [WATCH_SURFACE],
55
+ [ControllerTypes.WEAR_OS]: [WATCH_SURFACE],
56
+ [ControllerTypes.GAMEBRICK]: [GAMEBRICK_SURFACE],
57
+ [ControllerTypes.IINE_ANANKE]: [GAMEBRICK_SURFACE],
58
+ [ControllerTypes.ZERO2]: [GAMEBRICK_SURFACE],
49
59
  [ControllerTypes.R1]: [],
50
60
  }
51
61
 
@@ -61,6 +61,8 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
61
61
  // Glassly CFW supports the complete vector contract, including SVG and rotation.
62
62
  // Upstream g2flash publishes a raster canvas with sceneVector=false.
63
63
  const vector = useGlassesStore.getState().sceneVector === true
64
+ // Depth is a disparity between the lenses; one panel (MemoMind One) draws every element flat.
65
+ const stereo = (base.display.count ?? 1) >= 2
64
66
  return {
65
67
  ...(volumeGate ? withVolumeGate(base) : base),
66
68
  hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),
@@ -68,6 +70,9 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
68
70
  ...base.display,
69
71
  width: canvas.width,
70
72
  height: canvas.height,
73
+ // A Glassly CFW that publishes a canvas takes positioned elements, whatever
74
+ // the stock profile says (the G1's stock firmware degrades scenes to text walls).
75
+ canPosition: true,
71
76
  maxTextElements: Math.max(
72
77
  base.display.maxTextElements ?? 0,
73
78
  vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_TEXT_ELEMENTS,
@@ -78,7 +83,7 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
78
83
  vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,
79
84
  ),
80
85
  ...(vector
81
- ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: true}
86
+ ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: stereo}
82
87
  : {}),
83
88
  },
84
89
  }
@@ -13,7 +13,10 @@ import {isGlassesConnected} from "../GlassesReadiness"
13
13
 
14
14
  /** The SGC appends this marker after the firmware advertises Glassly CFW capabilities (G2, RayNeo iO). */
15
15
  export function isCfwFirmwareVersion(firmwareVersion: string | null | undefined): boolean {
16
- return firmwareVersion?.toLowerCase().includes("glassly cfw") === true
16
+ return (
17
+ firmwareVersion?.toLowerCase().includes("glassly cfw") === true ||
18
+ /^GLASSLYCFW\/[0-9]+$/.test(firmwareVersion ?? "")
19
+ )
17
20
  }
18
21
 
19
22
  /**
@@ -23,6 +26,11 @@ export function isCfwFirmwareVersion(firmwareVersion: string | null | undefined)
23
26
  */
24
27
  export function isCustomDashboardEligible(info: {model?: string | null; firmwareVersion?: string | null}): boolean {
25
28
  if (!info.model) return false
29
+ if (info.model === DeviceTypes.MEMOMIND_ONE) {
30
+ const match = /^GLASSLYCFW\/([0-9]+)$/.exec(info.firmwareVersion ?? "")
31
+ const revision = match ? Number(match[1]) : 0
32
+ return Number.isSafeInteger(revision) && revision >= 10
33
+ }
26
34
  return (
27
35
  getModelCapabilities(info.model as DeviceTypes).hasCustomFirmware === true &&
28
36
  isCfwFirmwareVersion(info.firmwareVersion)
@@ -38,6 +38,7 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
38
38
  notifications_enabled: "write",
39
39
  notifications_popups: "write",
40
40
  notifications_popup_seconds: "write",
41
+ notifications_batch_seconds: "write",
41
42
  notifications_hold_during_apps: "write",
42
43
  notifications_ignore_while_active: "write",
43
44
  notifications_blocklist: "write",
@@ -1,15 +1,15 @@
1
1
  /**
2
- * The Apple Watch remote's phone half: the watch app's status context (glasses, the miniapp in
2
+ * The watch remote's phone half, for the Apple Watch (iOS, WatchConnectivity) and a Wear OS
3
+ * watch (Android, Wearable Data Layer) alike: the watch app's status (glasses, the miniapp in
3
4
  * front, launchable miniapps, whether Assist takes questions, the mirror setting), mirror frames
4
5
  * while the setting is on, and the commands the watch sends back. Native adds whether the active
5
6
  * watch is paired and keeps the newest frame for when the watch app opens. Gestures ride
6
7
  * `touch_event` like any controller's.
7
8
  */
8
- import type {AppleWatchCommandEvent} from "@glassly/bluetooth-sdk"
9
+ import type {WatchCommandEvent} from "@glassly/bluetooth-sdk"
9
10
  import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
10
11
  import type {MirrorSnapshot} from "@glassly/miniapp"
11
12
  import {MiniappStreamType} from "@glassly/miniapp"
12
- import {Platform} from "react-native"
13
13
 
14
14
  import {useAppStatusStore} from "../../stores/apps"
15
15
  import {useGlassesStore} from "../../stores/glasses"
@@ -23,7 +23,7 @@ import {keyEventsForTextChange} from "../neuralBand/NeuralBandInput"
23
23
 
24
24
  export const ASSIST_PACKAGE = "com.glassly.assistant"
25
25
  export const ASSIST_ACTION = "ask"
26
- /** WatchConnectivity messages top out near 64 KB; frames past this are skipped, not truncated. */
26
+ /** WatchConnectivity messages top out near 64 KB (Data Layer messages at 100 KB); frames past this are skipped, not truncated. */
27
27
  const MAX_FRAME_BYTES = 48_000
28
28
  const MAX_IMAGE_CHARS = 16_000
29
29
  const MAX_APPS = 24
@@ -142,7 +142,7 @@ export function mirrorFrame(snapshot: MirrorSnapshot): Record<string, unknown> |
142
142
  return JSON.stringify(frame).length > MAX_FRAME_BYTES ? null : frame
143
143
  }
144
144
 
145
- export interface AppleWatchCommandDeps {
145
+ export interface WatchCommandDeps {
146
146
  apps: () => readonly ClientApp[]
147
147
  start: (app: ClientApp) => Promise<unknown>
148
148
  stop: (packageName: string) => Promise<unknown>
@@ -151,7 +151,7 @@ export interface AppleWatchCommandDeps {
151
151
  now: () => number
152
152
  }
153
153
 
154
- const defaultDeps: AppleWatchCommandDeps = {
154
+ const defaultDeps: WatchCommandDeps = {
155
155
  apps: () => useAppStatusStore.getState().apps,
156
156
  start: (app) => useAppStatusStore.getState().start(app),
157
157
  stop: (packageName) => useAppStatusStore.getState().stop(packageName),
@@ -160,9 +160,9 @@ const defaultDeps: AppleWatchCommandDeps = {
160
160
  now: () => Date.now(),
161
161
  }
162
162
 
163
- export async function handleAppleWatchCommand(
164
- event: AppleWatchCommandEvent,
165
- deps: AppleWatchCommandDeps = defaultDeps,
163
+ export async function handleWatchCommand(
164
+ event: WatchCommandEvent,
165
+ deps: WatchCommandDeps = defaultDeps,
166
166
  ): Promise<void> {
167
167
  switch (event.command) {
168
168
  case "launch": {
@@ -170,7 +170,7 @@ export async function handleAppleWatchCommand(
170
170
  ? deps.apps().find((entry) => entry.packageName === event.packageName)
171
171
  : undefined
172
172
  if (!app) {
173
- console.warn(`AppleWatch: launch of unknown miniapp ${event.packageName}`)
173
+ console.warn(`Watch: launch of unknown miniapp ${event.packageName}`)
174
174
  return
175
175
  }
176
176
  await deps.start(app)
@@ -206,7 +206,7 @@ function currentState(): Record<string, unknown> {
206
206
  },
207
207
  apps: useAppStatusStore.getState().apps,
208
208
  assist: localMiniappRuntime.hostActionAvailable(ASSIST_PACKAGE, ASSIST_ACTION),
209
- mirror: useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true,
209
+ mirror: useSettingsStore.getState().getSetting(SETTINGS.watch_mirror.key) === true,
210
210
  })
211
211
  }
212
212
 
@@ -226,7 +226,7 @@ function syncMirror(enabled: boolean): void {
226
226
  const send = (snapshot: MirrorSnapshot) => {
227
227
  lastSentAt = Date.now()
228
228
  void BluetoothSdk.setWatchMirror(mirrorFrame(snapshot)).catch((err) =>
229
- console.warn("AppleWatch: mirror push failed:", err),
229
+ console.warn("Watch: mirror push failed:", err),
230
230
  )
231
231
  }
232
232
  const unsubscribe = mirrorService.subscribe((snapshot) => {
@@ -252,8 +252,8 @@ function syncMirror(enabled: boolean): void {
252
252
  }
253
253
  }
254
254
 
255
- export function startAppleWatchLink(): void {
256
- if (Platform.OS !== "ios" || stops.length > 0) return
255
+ export function startWatchLink(): void {
256
+ if (stops.length > 0) return
257
257
  let lastKey = ""
258
258
  let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
259
259
  const push = () => {
@@ -262,21 +262,21 @@ export function startAppleWatchLink(): void {
262
262
  const key = JSON.stringify(state)
263
263
  if (key === lastKey) return
264
264
  lastKey = key
265
- void BluetoothSdk.setWatchState(state).catch((err) => console.warn("AppleWatch: state push failed:", err))
265
+ void BluetoothSdk.setWatchState(state).catch((err) => console.warn("Watch: state push failed:", err))
266
266
  }
267
267
  const schedule = () => {
268
268
  timer ??= BgTimer.setTimeout(push, STATE_DEBOUNCE_MS)
269
269
  }
270
- const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true
271
- const commands = BluetoothSdk.addListener("apple_watch_command", (event) => {
272
- handleAppleWatchCommand(event).catch((err) => console.warn(`AppleWatch: ${event.command} failed:`, err))
270
+ const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.watch_mirror.key) === true
271
+ const commands = BluetoothSdk.addListener("watch_command", (event) => {
272
+ handleWatchCommand(event).catch((err) => console.warn(`Watch: ${event.command} failed:`, err))
273
273
  })
274
274
  stops = [
275
275
  () => commands.remove(),
276
276
  useGlassesStore.subscribe(schedule),
277
277
  useAppStatusStore.subscribe(schedule),
278
278
  useSettingsStore.subscribe(
279
- (s) => s.getSetting(SETTINGS.apple_watch_mirror.key),
279
+ (s) => s.getSetting(SETTINGS.watch_mirror.key),
280
280
  () => {
281
281
  syncMirror(mirrorEnabled())
282
282
  schedule()
@@ -291,7 +291,7 @@ export function startAppleWatchLink(): void {
291
291
  schedule()
292
292
  }
293
293
 
294
- export function stopAppleWatchLink(): void {
294
+ export function stopWatchLink(): void {
295
295
  stops.splice(0).forEach((stop) => stop())
296
296
  syncMirror(false)
297
297
  }
@@ -217,6 +217,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
217
217
  DeviceTypes.NONE
218
218
  const capabilities = getModelCapabilities(defaultWearable)
219
219
  const devToolsEnabled = useSettingsStore.getState().getSetting(SETTINGS.dev_tools.key) === true
220
+ const persistScreenshots = useSettingsStore.getState().getSetting(SETTINGS.persist_miniapp_screenshots.key) !== false
220
221
  const hasPairedGlasses = useSettingsStore.getState().getSetting(SETTINGS.has_paired_glasses.key) === true
221
222
 
222
223
  // Single pass: dedupe → screenshot carry-over → compat → hidden, all into
@@ -246,7 +247,10 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
246
247
  .map((app) => {
247
248
  const next: ClientApp = {
248
249
  ...app,
249
- screenshot: previousByPackage.get(app.packageName)?.screenshot ?? app.screenshot,
250
+ // The registry's screenshot is the one restored from disk; only an
251
+ // in-memory capture carries over when persistence is off.
252
+ screenshot:
253
+ previousByPackage.get(app.packageName)?.screenshot ?? (persistScreenshots ? app.screenshot : undefined),
250
254
  compatibility: HardwareCompatibility.checkCompatibility(app.hardwareRequirements, capabilities),
251
255
  // A registry source can force an app off the primary home screen.
252
256
  // Compose that gate with the user's persisted home-screen layout instead
@@ -384,15 +388,6 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
384
388
  // owns the settings store).
385
389
  useSettingsStore.getState().setSetting(SETTINGS.has_ever_activated_app.key, true)
386
390
 
387
- // If the tapped app is already running, this is a re-open: the nav/foreground
388
- // above already drove the animation, and there's nothing to spawn. Short-
389
- // circuit before the spawn pipeline. A promoted app keeps its context but
390
- // still takes the foreground below.
391
- if (app.running && !app.widgetOnly) {
392
- saveLastOpenTime(packageName)
393
- return true
394
- }
395
-
396
391
  // Only-one rules: one standard app in the foreground, one dashboard
397
392
  // drawing the overlay. Starting another stops the one running. Widget-only
398
393
  // peers hold no foreground, so they stay.
@@ -405,6 +400,17 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
405
400
  }
406
401
  }
407
402
 
403
+ // If the tapped app is already running, this is a re-open: the nav/foreground
404
+ // above already drove the animation, and there's nothing to spawn. Short-
405
+ // circuit before the spawn pipeline. A promoted app keeps its context but
406
+ // still takes the foreground below.
407
+ if (app.running && !app.widgetOnly) {
408
+ saveLastOpenTime(packageName)
409
+ // The running background only knows the source of its spawn.
410
+ if (app.local) miniappLauncher.notifyLaunch(packageName, opts?.launchSource ?? "phone")
411
+ return true
412
+ }
413
+
408
414
  set((s) => ({
409
415
  apps: s.apps.map((a) => (a.packageName === packageName ? {...a, running: true, loading: false} : a)),
410
416
  }))
@@ -543,7 +549,12 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
543
549
  },
544
550
 
545
551
  saveScreenshot: async (packageName: string, screenshot: string) => {
546
- storage.save(`${packageName}_screenshot`, screenshot)
552
+ const key = `${packageName}_screenshot`
553
+ if (useSettingsStore.getState().getSetting(SETTINGS.persist_miniapp_screenshots.key) !== false) {
554
+ storage.save(key, screenshot)
555
+ } else {
556
+ storage.remove(key)
557
+ }
547
558
  set((s) => ({
548
559
  apps: s.apps.map((a) => (a.packageName === packageName ? {...a, screenshot} : a)),
549
560
  }))