@glassly/engine 0.3.5 → 0.3.6-beta.851
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.
- package/build/engine.d.ts +11 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +13 -1
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -3
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -3
- package/build/facades/ai.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -1
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +39 -30
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/memory.d.ts +10 -2
- package/build/facades/memory.d.ts.map +1 -1
- package/build/facades/memory.js +10 -0
- package/build/facades/memory.js.map +1 -1
- package/build/facades/miniapps.d.ts +2 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +2 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +3 -2
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/providerKeys.d.ts +6 -2
- package/build/facades/providerKeys.d.ts.map +1 -1
- package/build/facades/providerKeys.js +6 -2
- package/build/facades/providerKeys.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AiTierResolution.d.ts +3 -2
- package/build/services/AiTierResolution.d.ts.map +1 -1
- package/build/services/AiTierResolution.js +7 -2
- package/build/services/AiTierResolution.js.map +1 -1
- package/build/services/AppRegistry.d.ts +4 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +16 -2
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/BlobStore.js +3 -3
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/BluetoothRestoreLaunch.d.ts +8 -0
- package/build/services/BluetoothRestoreLaunch.d.ts.map +1 -0
- package/build/services/BluetoothRestoreLaunch.js +34 -0
- package/build/services/BluetoothRestoreLaunch.js.map +1 -0
- package/build/services/CloudClientService.d.ts +4 -4
- package/build/services/ForegroundLocationPermission.d.ts +1 -1
- package/build/services/ForegroundLocationPermission.d.ts.map +1 -1
- package/build/services/ForegroundLocationPermission.js.map +1 -1
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +2 -1
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +6 -4
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +81 -41
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +12 -4
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +21 -7
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalLlmModelManager.d.ts +3 -1
- package/build/services/LocalLlmModelManager.d.ts.map +1 -1
- package/build/services/LocalLlmModelManager.js +13 -5
- package/build/services/LocalLlmModelManager.js.map +1 -1
- package/build/services/LocalLlmPrompt.d.ts +10 -9
- package/build/services/LocalLlmPrompt.d.ts.map +1 -1
- package/build/services/LocalLlmPrompt.js +43 -32
- package/build/services/LocalLlmPrompt.js.map +1 -1
- package/build/services/LocalLlmProvider.d.ts +1 -0
- package/build/services/LocalLlmProvider.d.ts.map +1 -1
- package/build/services/LocalLlmProvider.js +62 -22
- package/build/services/LocalLlmProvider.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +28 -6
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +190 -148
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MemoryStore.d.ts +13 -1
- package/build/services/MemoryStore.d.ts.map +1 -1
- package/build/services/MemoryStore.js +98 -1
- package/build/services/MemoryStore.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +6 -3
- package/build/services/MiniappExitGesture.d.ts.map +1 -1
- package/build/services/MiniappExitGesture.js +4 -4
- package/build/services/MiniappExitGesture.js.map +1 -1
- package/build/services/MiniappWidgetMode.d.ts +6 -4
- package/build/services/MiniappWidgetMode.d.ts.map +1 -1
- package/build/services/MiniappWidgetMode.js +7 -6
- package/build/services/MiniappWidgetMode.js.map +1 -1
- package/build/services/ProviderKeyValidation.d.ts +2 -0
- package/build/services/ProviderKeyValidation.d.ts.map +1 -1
- package/build/services/ProviderKeyValidation.js +4 -0
- package/build/services/ProviderKeyValidation.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +3 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/ScreenshotFixture.d.ts +56 -0
- package/build/services/ScreenshotFixture.d.ts.map +1 -0
- package/build/services/ScreenshotFixture.js +72 -0
- package/build/services/ScreenshotFixture.js.map +1 -0
- package/build/services/actionInvoke.d.ts +37 -0
- package/build/services/actionInvoke.d.ts.map +1 -0
- package/build/services/actionInvoke.js +89 -0
- package/build/services/actionInvoke.js.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.js +259 -0
- package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +16 -0
- package/build/services/controllers.js.map +1 -1
- package/build/services/manifestActions.d.ts +4 -2
- package/build/services/manifestActions.d.ts.map +1 -1
- package/build/services/manifestActions.js +9 -3
- package/build/services/manifestActions.js.map +1 -1
- package/build/services/memoryBackupFile.d.ts +9 -0
- package/build/services/memoryBackupFile.d.ts.map +1 -0
- package/build/services/memoryBackupFile.js +47 -0
- package/build/services/memoryBackupFile.js.map +1 -0
- package/build/services/settingsBridge.d.ts +4 -4
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +9 -4
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/widgetPlacements.d.ts +8 -7
- package/build/services/widgetPlacements.d.ts.map +1 -1
- package/build/services/widgetPlacements.js +7 -6
- package/build/services/widgetPlacements.js.map +1 -1
- package/build/stores/apps.d.ts +6 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +29 -12
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts +2 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +17 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +42 -2
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +5 -1
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +2 -0
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +2 -1
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/build/utils/cloudClient/RnUdpAdapter.d.ts.map +1 -1
- package/build/utils/cloudClient/RnUdpAdapter.js +13 -2
- package/build/utils/cloudClient/RnUdpAdapter.js.map +1 -1
- package/build/utils/display/scene/index.d.ts +1 -1
- package/build/utils/display/scene/index.d.ts.map +1 -1
- package/build/utils/display/scene/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +20 -1
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +1 -1
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/build/utils/miniappGlobals.d.ts +6 -0
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +28 -4
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +9 -9
- package/src/engine.ts +13 -1
- package/src/facades/ai.ts +3 -3
- package/src/facades/glasses.ts +65 -42
- package/src/facades/memory.ts +11 -2
- package/src/facades/miniapps.ts +3 -0
- package/src/facades/pairing.ts +4 -2
- package/src/facades/providerKeys.ts +6 -1
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/services/AiTierResolution.ts +9 -3
- package/src/services/AppRegistry.ts +18 -3
- package/src/services/BlobStore.ts +3 -3
- package/src/services/BluetoothRestoreLaunch.ts +37 -0
- package/src/services/ForegroundLocationPermission.ts +1 -1
- package/src/services/GestureDotsService.ts +2 -1
- package/src/services/GlassesTouchInput.ts +86 -38
- package/src/services/LocalDisplayManager.ts +22 -8
- package/src/services/LocalLlmModelManager.ts +16 -6
- package/src/services/LocalLlmPrompt.ts +56 -37
- package/src/services/LocalLlmProvider.ts +74 -24
- package/src/services/LocalMiniappRuntime.ts +228 -147
- package/src/services/MemoryStore.ts +111 -2
- package/src/services/MiniappExitGesture.ts +8 -5
- package/src/services/MiniappWidgetMode.ts +9 -7
- package/src/services/ProviderKeyValidation.ts +5 -0
- package/src/services/SceneRenderer.ts +3 -1
- package/src/services/ScreenshotFixture.ts +113 -0
- package/src/services/actionInvoke.ts +105 -0
- package/src/services/appleWatch/AppleWatchLink.ts +297 -0
- package/src/services/controllers.ts +17 -0
- package/src/services/manifestActions.ts +16 -4
- package/src/services/memoryBackupFile.ts +50 -0
- package/src/services/settingsBridge.ts +9 -4
- package/src/services/widgetPlacements.ts +23 -9
- package/src/stores/apps.ts +35 -11
- package/src/stores/bluetoothSettingKeys.ts +20 -0
- package/src/stores/settings.ts +44 -2
- package/src/types/applet.ts +5 -1
- package/src/types/capabilities/rayneo-io.ts +2 -0
- package/src/types/enums.ts +1 -0
- package/src/utils/cloudClient/RnUdpAdapter.ts +13 -1
- package/src/utils/display/scene/index.ts +1 -0
- package/src/utils/display/scene/process.ts +17 -1
- package/src/utils/display/scene/types.ts +1 -0
- package/src/utils/miniappGlobals.ts +30 -4
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple Watch remote's phone half: the watch app's status context (glasses, the miniapp in
|
|
3
|
+
* front, launchable miniapps, whether Assist takes questions, the mirror setting), mirror frames
|
|
4
|
+
* while the setting is on, and the commands the watch sends back. Native adds whether the active
|
|
5
|
+
* watch is paired and keeps the newest frame for when the watch app opens. Gestures ride
|
|
6
|
+
* `touch_event` like any controller's.
|
|
7
|
+
*/
|
|
8
|
+
import type {AppleWatchCommandEvent} from "@glassly/bluetooth-sdk"
|
|
9
|
+
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
10
|
+
import type {MirrorSnapshot} from "@glassly/miniapp"
|
|
11
|
+
import {MiniappStreamType} from "@glassly/miniapp"
|
|
12
|
+
import {Platform} from "react-native"
|
|
13
|
+
|
|
14
|
+
import {useAppStatusStore} from "../../stores/apps"
|
|
15
|
+
import {useGlassesStore} from "../../stores/glasses"
|
|
16
|
+
import {SETTINGS, useSettingsStore} from "../../stores/settings"
|
|
17
|
+
import type {ClientApp} from "../../types"
|
|
18
|
+
import {BgTimer} from "../../utils/timers"
|
|
19
|
+
import {isGlassesConnected} from "../GlassesReadiness"
|
|
20
|
+
import localMiniappRuntime from "../LocalMiniappRuntime"
|
|
21
|
+
import {mirrorService} from "../MirrorService"
|
|
22
|
+
import {keyEventsForTextChange} from "../neuralBand/NeuralBandInput"
|
|
23
|
+
|
|
24
|
+
export const ASSIST_PACKAGE = "com.glassly.assistant"
|
|
25
|
+
export const ASSIST_ACTION = "ask"
|
|
26
|
+
/** WatchConnectivity messages top out near 64 KB; frames past this are skipped, not truncated. */
|
|
27
|
+
const MAX_FRAME_BYTES = 48_000
|
|
28
|
+
const MAX_IMAGE_CHARS = 16_000
|
|
29
|
+
const MAX_APPS = 24
|
|
30
|
+
const STATE_DEBOUNCE_MS = 250
|
|
31
|
+
const MIRROR_INTERVAL_MS = 500
|
|
32
|
+
|
|
33
|
+
export interface WatchStateInput {
|
|
34
|
+
glasses: {connected: boolean; model: string; battery: number}
|
|
35
|
+
apps: readonly ClientApp[]
|
|
36
|
+
assist: boolean
|
|
37
|
+
mirror: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Miniapps the watch can open: shown, compatible, and drawing on the glasses. */
|
|
41
|
+
export function launchableApps(apps: readonly ClientApp[]): {packageName: string; name: string}[] {
|
|
42
|
+
return apps
|
|
43
|
+
.filter((app) => app.type === "standard" && !app.hidden && app.compatibility?.isCompatible !== false)
|
|
44
|
+
.filter((app) => app.packageName && !app.packageName.includes("@empty"))
|
|
45
|
+
.slice(0, MAX_APPS)
|
|
46
|
+
.map((app) => ({packageName: app.packageName, name: app.name}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function watchState({glasses, apps, assist, mirror}: WatchStateInput): Record<string, unknown> {
|
|
50
|
+
const front = apps.find((app) => app.foregrounded && app.running && app.type === "standard")
|
|
51
|
+
const state: Record<string, unknown> = {
|
|
52
|
+
glasses: {connected: glasses.connected, name: glasses.model || "Glasses", battery: glasses.battery},
|
|
53
|
+
apps: launchableApps(apps),
|
|
54
|
+
assist,
|
|
55
|
+
mirror,
|
|
56
|
+
}
|
|
57
|
+
if (front) state.app = {packageName: front.packageName, name: front.name}
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type WatchElement = Record<string, string | number | boolean | number[]>
|
|
62
|
+
|
|
63
|
+
const level = (color: unknown, fallback = 15) =>
|
|
64
|
+
typeof color === "number" ? Math.max(0, Math.min(15, color)) : fallback
|
|
65
|
+
|
|
66
|
+
/** The glasses frame in the watch's drawing vocabulary; null when there is nothing to draw. */
|
|
67
|
+
export function mirrorFrame(snapshot: MirrorSnapshot): Record<string, unknown> | null {
|
|
68
|
+
const canvas = snapshot.canvas
|
|
69
|
+
if (!canvas) return null
|
|
70
|
+
const content = snapshot.content
|
|
71
|
+
if (content.kind === "bitmap") {
|
|
72
|
+
if (content.data.length > MAX_FRAME_BYTES) return null
|
|
73
|
+
return {width: canvas.width, height: canvas.height, bitmap: content.data, elements: []}
|
|
74
|
+
}
|
|
75
|
+
if (content.kind !== "scene") return null
|
|
76
|
+
const elements: WatchElement[] = []
|
|
77
|
+
for (const element of content.elements) {
|
|
78
|
+
const {x, y, w, h} = element.box
|
|
79
|
+
const style = element.style ?? {}
|
|
80
|
+
switch (element.type) {
|
|
81
|
+
case "text":
|
|
82
|
+
if (!element.text) break
|
|
83
|
+
elements.push({
|
|
84
|
+
type: "text",
|
|
85
|
+
x,
|
|
86
|
+
y,
|
|
87
|
+
w,
|
|
88
|
+
h,
|
|
89
|
+
text: element.text,
|
|
90
|
+
size: style.fontSize ?? 20,
|
|
91
|
+
color: level(style.color),
|
|
92
|
+
})
|
|
93
|
+
break
|
|
94
|
+
case "rect":
|
|
95
|
+
elements.push({
|
|
96
|
+
type: "rect",
|
|
97
|
+
x,
|
|
98
|
+
y,
|
|
99
|
+
w,
|
|
100
|
+
h,
|
|
101
|
+
fill: style.fill === true,
|
|
102
|
+
border: style.border ?? 1,
|
|
103
|
+
radius: style.radius ?? 0,
|
|
104
|
+
color: level(style.color),
|
|
105
|
+
})
|
|
106
|
+
break
|
|
107
|
+
case "circle":
|
|
108
|
+
elements.push({
|
|
109
|
+
type: "circle",
|
|
110
|
+
x,
|
|
111
|
+
y,
|
|
112
|
+
w,
|
|
113
|
+
h,
|
|
114
|
+
fill: style.fill === true,
|
|
115
|
+
border: style.border ?? 1,
|
|
116
|
+
color: level(style.color),
|
|
117
|
+
})
|
|
118
|
+
break
|
|
119
|
+
case "line":
|
|
120
|
+
case "triangle":
|
|
121
|
+
case "quad": {
|
|
122
|
+
const points = (element.points ?? []).flatMap((point) => [point.x, point.y])
|
|
123
|
+
if (points.length < 4) break
|
|
124
|
+
const closed = element.type !== "line"
|
|
125
|
+
elements.push({
|
|
126
|
+
type: "line",
|
|
127
|
+
points,
|
|
128
|
+
width: style.width ?? style.border ?? 1,
|
|
129
|
+
color: level(style.color),
|
|
130
|
+
closed,
|
|
131
|
+
fill: closed && style.fill === true,
|
|
132
|
+
})
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
case "image":
|
|
136
|
+
if (element.data && element.data.length <= MAX_IMAGE_CHARS)
|
|
137
|
+
elements.push({type: "image", x, y, w, h, data: element.data})
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const frame = {width: canvas.width, height: canvas.height, elements}
|
|
142
|
+
return JSON.stringify(frame).length > MAX_FRAME_BYTES ? null : frame
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface AppleWatchCommandDeps {
|
|
146
|
+
apps: () => readonly ClientApp[]
|
|
147
|
+
start: (app: ClientApp) => Promise<unknown>
|
|
148
|
+
stop: (packageName: string) => Promise<unknown>
|
|
149
|
+
ask: (query: string) => Promise<unknown>
|
|
150
|
+
type: (stream: string, payload: unknown) => void
|
|
151
|
+
now: () => number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const defaultDeps: AppleWatchCommandDeps = {
|
|
155
|
+
apps: () => useAppStatusStore.getState().apps,
|
|
156
|
+
start: (app) => useAppStatusStore.getState().start(app),
|
|
157
|
+
stop: (packageName) => useAppStatusStore.getState().stop(packageName),
|
|
158
|
+
ask: (query) => localMiniappRuntime.invokeActionFromHost(ASSIST_PACKAGE, ASSIST_ACTION, {query}),
|
|
159
|
+
type: (stream, payload) => localMiniappRuntime.forwardEvent(stream, payload),
|
|
160
|
+
now: () => Date.now(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function handleAppleWatchCommand(
|
|
164
|
+
event: AppleWatchCommandEvent,
|
|
165
|
+
deps: AppleWatchCommandDeps = defaultDeps,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
switch (event.command) {
|
|
168
|
+
case "launch": {
|
|
169
|
+
const app = launchableApps(deps.apps()).some((entry) => entry.packageName === event.packageName)
|
|
170
|
+
? deps.apps().find((entry) => entry.packageName === event.packageName)
|
|
171
|
+
: undefined
|
|
172
|
+
if (!app) {
|
|
173
|
+
console.warn(`AppleWatch: launch of unknown miniapp ${event.packageName}`)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
await deps.start(app)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
case "stop": {
|
|
180
|
+
const front = deps.apps().find((app) => app.foregrounded && app.running && app.type === "standard")
|
|
181
|
+
if (front) await deps.stop(front.packageName)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
case "assist": {
|
|
185
|
+
const query = event.text.trim()
|
|
186
|
+
if (query) await deps.ask(query)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
case "text":
|
|
190
|
+
for (const key of keyEventsForTextChange("", event.text, deps.now()))
|
|
191
|
+
deps.type(MiniappStreamType.KEYBOARD_EVENT, key)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let stops: (() => void)[] = []
|
|
197
|
+
let stopMirror: (() => void) | null = null
|
|
198
|
+
|
|
199
|
+
function currentState(): Record<string, unknown> {
|
|
200
|
+
const glasses = useGlassesStore.getState()
|
|
201
|
+
return watchState({
|
|
202
|
+
glasses: {
|
|
203
|
+
connected: isGlassesConnected(glasses.connection),
|
|
204
|
+
model: glasses.deviceModel,
|
|
205
|
+
battery: glasses.batteryLevel,
|
|
206
|
+
},
|
|
207
|
+
apps: useAppStatusStore.getState().apps,
|
|
208
|
+
assist: localMiniappRuntime.hostActionAvailable(ASSIST_PACKAGE, ASSIST_ACTION),
|
|
209
|
+
mirror: useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function syncMirror(enabled: boolean): void {
|
|
214
|
+
if (!enabled) {
|
|
215
|
+
if (stopMirror) {
|
|
216
|
+
stopMirror()
|
|
217
|
+
stopMirror = null
|
|
218
|
+
void BluetoothSdk.setWatchMirror(null).catch(() => {})
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (stopMirror) return
|
|
223
|
+
let lastSentAt = 0
|
|
224
|
+
let pending: MirrorSnapshot | null = null
|
|
225
|
+
let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
226
|
+
const send = (snapshot: MirrorSnapshot) => {
|
|
227
|
+
lastSentAt = Date.now()
|
|
228
|
+
void BluetoothSdk.setWatchMirror(mirrorFrame(snapshot)).catch((err) =>
|
|
229
|
+
console.warn("AppleWatch: mirror push failed:", err),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
const unsubscribe = mirrorService.subscribe((snapshot) => {
|
|
233
|
+
const wait = lastSentAt + MIRROR_INTERVAL_MS - Date.now()
|
|
234
|
+
if (wait <= 0 && !timer) {
|
|
235
|
+
send(snapshot)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
pending = snapshot
|
|
239
|
+
timer ??= BgTimer.setTimeout(
|
|
240
|
+
() => {
|
|
241
|
+
timer = null
|
|
242
|
+
if (pending) send(pending)
|
|
243
|
+
pending = null
|
|
244
|
+
},
|
|
245
|
+
Math.max(wait, 0),
|
|
246
|
+
)
|
|
247
|
+
})
|
|
248
|
+
stopMirror = () => {
|
|
249
|
+
unsubscribe()
|
|
250
|
+
if (timer) BgTimer.clearTimeout(timer)
|
|
251
|
+
timer = null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function startAppleWatchLink(): void {
|
|
256
|
+
if (Platform.OS !== "ios" || stops.length > 0) return
|
|
257
|
+
let lastKey = ""
|
|
258
|
+
let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
259
|
+
const push = () => {
|
|
260
|
+
timer = null
|
|
261
|
+
const state = currentState()
|
|
262
|
+
const key = JSON.stringify(state)
|
|
263
|
+
if (key === lastKey) return
|
|
264
|
+
lastKey = key
|
|
265
|
+
void BluetoothSdk.setWatchState(state).catch((err) => console.warn("AppleWatch: state push failed:", err))
|
|
266
|
+
}
|
|
267
|
+
const schedule = () => {
|
|
268
|
+
timer ??= BgTimer.setTimeout(push, STATE_DEBOUNCE_MS)
|
|
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))
|
|
273
|
+
})
|
|
274
|
+
stops = [
|
|
275
|
+
() => commands.remove(),
|
|
276
|
+
useGlassesStore.subscribe(schedule),
|
|
277
|
+
useAppStatusStore.subscribe(schedule),
|
|
278
|
+
useSettingsStore.subscribe(
|
|
279
|
+
(s) => s.getSetting(SETTINGS.apple_watch_mirror.key),
|
|
280
|
+
() => {
|
|
281
|
+
syncMirror(mirrorEnabled())
|
|
282
|
+
schedule()
|
|
283
|
+
},
|
|
284
|
+
),
|
|
285
|
+
() => {
|
|
286
|
+
if (timer) BgTimer.clearTimeout(timer)
|
|
287
|
+
timer = null
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
syncMirror(mirrorEnabled())
|
|
291
|
+
schedule()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function stopAppleWatchLink(): void {
|
|
295
|
+
stops.splice(0).forEach((stop) => stop())
|
|
296
|
+
syncMirror(false)
|
|
297
|
+
}
|
|
@@ -26,9 +26,26 @@ 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 = {
|
|
31
|
+
type: "swipe2d",
|
|
32
|
+
events: [
|
|
33
|
+
"TAP",
|
|
34
|
+
"DOUBLE_TAP",
|
|
35
|
+
"PRESS_HOLD",
|
|
36
|
+
"SWIPE_UP",
|
|
37
|
+
"SWIPE_DOWN",
|
|
38
|
+
"SWIPE_LEFT",
|
|
39
|
+
"SWIPE_RIGHT",
|
|
40
|
+
"DIAL_UP",
|
|
41
|
+
"DIAL_DOWN",
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
// The R1 swipes up/down like the temples it stands in for, so it adds nothing.
|
|
30
46
|
const INPUT_SURFACES: Readonly<Record<string, readonly InputSurface[]>> = {
|
|
31
47
|
[ControllerTypes.NEURAL_BAND]: [NEURAL_BAND_SURFACE],
|
|
48
|
+
[ControllerTypes.APPLE_WATCH]: [APPLE_WATCH_SURFACE],
|
|
32
49
|
[ControllerTypes.R1]: [],
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -79,29 +79,41 @@ export function normalizeManifestActions(raw: unknown): DeclaredAction[] {
|
|
|
79
79
|
return out
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const WIDGET_SIZES: readonly DeclaredWidgetSize[] = ["XS", "SM", "MD", "LG"]
|
|
82
|
+
const WIDGET_SIZES: readonly DeclaredWidgetSize[] = ["XS", "XSM", "SM", "MD", "LG"]
|
|
83
83
|
const isWidgetSize = (value: unknown): value is DeclaredWidgetSize => (WIDGET_SIZES as readonly unknown[]).includes(value)
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Normalize manifest widgets for installed and dev-sideloaded bundles: an
|
|
87
|
-
* entry without an id, a name or a usable size is dropped, repeats of a
|
|
87
|
+
* entry without an id, a name or a usable card size is dropped, repeats of a
|
|
88
88
|
* size are folded, and a default size the widget does not offer becomes
|
|
89
|
-
* its first size.
|
|
89
|
+
* its first size. Listing `EX` makes a widget expandable; it is not a card
|
|
90
|
+
* size. Taps expand it until the wearer turns that off (AppRegistry applies
|
|
91
|
+
* the choice).
|
|
90
92
|
*/
|
|
91
93
|
export function normalizeManifestWidgets(raw: unknown): DeclaredWidget[] {
|
|
92
94
|
if (!Array.isArray(raw)) return []
|
|
93
95
|
const out: DeclaredWidget[] = []
|
|
94
96
|
const seen = new Set<string>()
|
|
95
|
-
for (const w of raw as Array<{
|
|
97
|
+
for (const w of raw as Array<{
|
|
98
|
+
id?: unknown
|
|
99
|
+
name?: unknown
|
|
100
|
+
sizes?: unknown
|
|
101
|
+
defaultSize?: unknown
|
|
102
|
+
expandable?: unknown
|
|
103
|
+
}>) {
|
|
96
104
|
if (!w || typeof w.id !== "string" || !w.id || typeof w.name !== "string" || !w.name || seen.has(w.id)) continue
|
|
97
105
|
const sizes = Array.isArray(w.sizes) ? Array.from(new Set(w.sizes.filter(isWidgetSize))) : []
|
|
98
106
|
if (sizes.length === 0) continue
|
|
99
107
|
seen.add(w.id)
|
|
108
|
+
// `expandable: true` is how bundles from before `EX` said the same.
|
|
109
|
+
const expandable = (Array.isArray(w.sizes) && w.sizes.includes("EX")) || w.expandable === true
|
|
100
110
|
out.push({
|
|
101
111
|
id: w.id,
|
|
102
112
|
name: w.name,
|
|
103
113
|
sizes,
|
|
104
114
|
defaultSize: isWidgetSize(w.defaultSize) && sizes.includes(w.defaultSize) ? w.defaultSize : sizes[0],
|
|
115
|
+
expandable,
|
|
116
|
+
expandOnTap: expandable,
|
|
105
117
|
})
|
|
106
118
|
}
|
|
107
119
|
return out
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory backup files: the share-sheet export and file-picker import behind
|
|
3
|
+
* Settings > Memory and session.memory.exportFile/importFile.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {MemoryExportResult, MemoryImportResult} from "@glassly/miniapp"
|
|
7
|
+
import {File, Paths} from "expo-file-system"
|
|
8
|
+
import {Platform} from "react-native"
|
|
9
|
+
import Share from "react-native-share"
|
|
10
|
+
|
|
11
|
+
import {MemoryBackupError} from "./MemoryStore"
|
|
12
|
+
import {memoryStore} from "./memoryStoreInstance"
|
|
13
|
+
|
|
14
|
+
// Some Android document providers report .json files as octet-stream.
|
|
15
|
+
const PICK_MIME_TYPES =
|
|
16
|
+
Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
|
|
17
|
+
|
|
18
|
+
export async function exportMemoryFile(): Promise<MemoryExportResult> {
|
|
19
|
+
const backup = await memoryStore.exportBackup()
|
|
20
|
+
const filename = `glassly-memory-${localDate(backup.exportedAt)}.json`
|
|
21
|
+
const file = new File(Paths.cache, filename)
|
|
22
|
+
file.writeSync(JSON.stringify(backup))
|
|
23
|
+
try {
|
|
24
|
+
await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly memory"})
|
|
25
|
+
return {success: true}
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
28
|
+
if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves null when the user cancels the picker. */
|
|
34
|
+
export async function importMemoryFile(): Promise<MemoryImportResult | null> {
|
|
35
|
+
const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
|
|
36
|
+
if (picked.canceled) return null
|
|
37
|
+
let data: unknown
|
|
38
|
+
try {
|
|
39
|
+
data = JSON.parse(await picked.result.text())
|
|
40
|
+
} catch {
|
|
41
|
+
throw new MemoryBackupError("Not a Glassly memory backup")
|
|
42
|
+
}
|
|
43
|
+
return memoryStore.importBackup(data)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function localDate(ms: number): string {
|
|
47
|
+
const d = new Date(ms)
|
|
48
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
49
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
50
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* be tested without the native import surface the runtime pulls in.
|
|
7
7
|
*
|
|
8
8
|
* A dashboard miniapp is the usual consumer: it edits the notification,
|
|
9
|
-
* glasses-menu
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that in its storage.
|
|
9
|
+
* glasses-menu, display-position, brightness and microphone keys the phone's
|
|
10
|
+
* settings screens do, through the store, so persistence, server sync and the
|
|
11
|
+
* Bluetooth sync behave as if a phone screen wrote them. A dashboard's own
|
|
12
|
+
* look is not here; it keeps that in its storage.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type {ControllerStatus, PairedController} from "@glassly/bluetooth-sdk"
|
|
@@ -28,6 +28,11 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
|
|
|
28
28
|
// Where the glasses draw; each model's steps are its display.positionRange.
|
|
29
29
|
dashboard_depth: "write",
|
|
30
30
|
dashboard_height: "write",
|
|
31
|
+
// Device settings the dashboard's glasses Settings page edits.
|
|
32
|
+
brightness: "write",
|
|
33
|
+
auto_brightness: "write",
|
|
34
|
+
// Per paired model: the store resolves the indexed key.
|
|
35
|
+
preferred_mic: "write",
|
|
31
36
|
twelve_hour_time: "write",
|
|
32
37
|
menu_apps: "write",
|
|
33
38
|
notifications_enabled: "write",
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Who placed which widget where: the pure bookkeeping behind the runtime's
|
|
3
3
|
* widget routing. A placement is one host's box for one provider widget;
|
|
4
4
|
* a provider is placed while any host holds one of its widgets. Frames are
|
|
5
|
-
* kept per widget so a host that places one again gets the last
|
|
6
|
-
* at once.
|
|
5
|
+
* kept per widget and view so a host that places one again gets the last
|
|
6
|
+
* card and expanded frames at once.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
export type WidgetView = "card" | "expanded"
|
|
10
|
+
|
|
9
11
|
export interface WidgetBox {
|
|
10
|
-
size: "XS" | "SM" | "MD" | "LG"
|
|
12
|
+
size: "XS" | "XSM" | "SM" | "MD" | "LG"
|
|
11
13
|
width: number
|
|
12
14
|
height: number
|
|
13
15
|
}
|
|
@@ -22,8 +24,8 @@ export interface Placement {
|
|
|
22
24
|
export interface WidgetPlacements {
|
|
23
25
|
/** Keyed by `${host}|${provider}/${widgetId}`. */
|
|
24
26
|
placements: ReadonlyMap<string, Placement>
|
|
25
|
-
/** The latest frame per `${provider}/${widgetId}
|
|
26
|
-
frames: ReadonlyMap<string, unknown[]
|
|
27
|
+
/** The latest frame per `${provider}/${widgetId}` and view. */
|
|
28
|
+
frames: ReadonlyMap<string, Readonly<Partial<Record<WidgetView, unknown[]>>>>
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export const widgetKey = (providerPackageName: string, widgetId: string): string => `${providerPackageName}/${widgetId}`
|
|
@@ -71,14 +73,26 @@ export function dropProvider(state: WidgetPlacements, providerPackageName: strin
|
|
|
71
73
|
return placements.size === state.placements.size ? state : {...state, placements}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
export function setFrame(
|
|
76
|
+
export function setFrame(
|
|
77
|
+
state: WidgetPlacements,
|
|
78
|
+
providerPackageName: string,
|
|
79
|
+
widgetId: string,
|
|
80
|
+
elements: unknown[],
|
|
81
|
+
view: WidgetView = "card",
|
|
82
|
+
): WidgetPlacements {
|
|
83
|
+
const key = widgetKey(providerPackageName, widgetId)
|
|
75
84
|
const frames = new Map(state.frames)
|
|
76
|
-
frames.set(
|
|
85
|
+
frames.set(key, {...state.frames.get(key), [view]: elements})
|
|
77
86
|
return {...state, frames}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
export function frameOf(
|
|
81
|
-
|
|
89
|
+
export function frameOf(
|
|
90
|
+
state: WidgetPlacements,
|
|
91
|
+
providerPackageName: string,
|
|
92
|
+
widgetId: string,
|
|
93
|
+
view: WidgetView = "card",
|
|
94
|
+
): unknown[] | null {
|
|
95
|
+
return state.frames.get(widgetKey(providerPackageName, widgetId))?.[view] ?? null
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
export function placementOf(
|
package/src/stores/apps.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {DeviceTypes} from "../types/enums"
|
|
|
19
19
|
import {getModelCapabilities} from "../types/hardware"
|
|
20
20
|
import {HardwareCompatibility, type CompatibilityResult} from "../utils/hardware/hardware"
|
|
21
21
|
import {storage} from "../utils/storage/storage"
|
|
22
|
-
import appRegistry, {saveAlwaysOnOverride} from "../services/AppRegistry"
|
|
22
|
+
import appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from "../services/AppRegistry"
|
|
23
23
|
import localDisplayManager from "../services/LocalDisplayManager"
|
|
24
24
|
import {islandNotifications} from "../services/NotificationsEmitter"
|
|
25
25
|
import sttModelManager from "../services/STTModelManager"
|
|
@@ -103,6 +103,12 @@ interface AppStatusState {
|
|
|
103
103
|
* off leaves it running. Persists the wearer's choice beside the hidden flag.
|
|
104
104
|
*/
|
|
105
105
|
setAlwaysOn: (packageName: string, status: boolean) => Promise<void>
|
|
106
|
+
/**
|
|
107
|
+
* Whether a tap on one of the miniapp's dashboard widgets expands it to the
|
|
108
|
+
* full screen. Only widgets that list `EX` expand; false for the rest.
|
|
109
|
+
* Persists the choice and answers the setting now in force.
|
|
110
|
+
*/
|
|
111
|
+
setWidgetExpand: (packageName: string, widgetId: string, expandOnTap: boolean) => boolean
|
|
106
112
|
/** Replace the whole apps array. Hosts use this when their extra source changes. */
|
|
107
113
|
setApps: (apps: ClientApp[]) => void
|
|
108
114
|
}
|
|
@@ -256,6 +262,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
|
|
|
256
262
|
// across both passes.
|
|
257
263
|
foregrounded: app.packageName === previousState.foregroundedPackage,
|
|
258
264
|
paused: app.local && app.running && isMiniappsPaused(),
|
|
265
|
+
widgetOnly: !!app.local && app.running && miniappWidgetMode.isWidgetOnly(app.packageName),
|
|
259
266
|
}
|
|
260
267
|
const prev = previousByPackage.get(app.packageName)
|
|
261
268
|
return prev && shallowEqualApp(prev, next) ? prev : next
|
|
@@ -362,6 +369,11 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
362
369
|
return false
|
|
363
370
|
}
|
|
364
371
|
|
|
372
|
+
// An app running only for its widget is promoted before the host hook:
|
|
373
|
+
// the hook may foreground it, and setForeground sends a widget-only app
|
|
374
|
+
// back here.
|
|
375
|
+
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
376
|
+
|
|
365
377
|
// Host navigation / open-animation seam. Runs on EVERY accepted open —
|
|
366
378
|
// including re-opening an already-running app — so the animation always
|
|
367
379
|
// plays (BuiltInMiniappCatalog.navigateForApp: overlay apps setForeground
|
|
@@ -374,13 +386,12 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
374
386
|
|
|
375
387
|
// If the tapped app is already running, this is a re-open: the nav/foreground
|
|
376
388
|
// above already drove the animation, and there's nothing to spawn. Short-
|
|
377
|
-
// circuit before the spawn pipeline.
|
|
378
|
-
//
|
|
389
|
+
// circuit before the spawn pipeline. A promoted app keeps its context but
|
|
390
|
+
// still takes the foreground below.
|
|
379
391
|
if (app.running && !app.widgetOnly) {
|
|
380
392
|
saveLastOpenTime(packageName)
|
|
381
393
|
return true
|
|
382
394
|
}
|
|
383
|
-
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
384
395
|
|
|
385
396
|
// Only-one rules: one standard app in the foreground, one dashboard
|
|
386
397
|
// drawing the overlay. Starting another stops the one running. Widget-only
|
|
@@ -495,13 +506,10 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
495
506
|
apps: s.apps.map((a) => ({...a, foregrounded: a.packageName === packageName})),
|
|
496
507
|
}))
|
|
497
508
|
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
// if (!app.running) {
|
|
503
|
-
// get().start(app)
|
|
504
|
-
// }
|
|
509
|
+
// The wearer is in this app now: one running only for its widget becomes
|
|
510
|
+
// a normal run. start() promotes it, stops the standard peer and keeps the
|
|
511
|
+
// context. Fire-and-forget after the flag flip so it doesn't block paint.
|
|
512
|
+
if (app.running && app.widgetOnly) void get().start(app, {skipNavigation: true})
|
|
505
513
|
},
|
|
506
514
|
|
|
507
515
|
clearForeground: () => {
|
|
@@ -585,6 +593,22 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
585
593
|
if (status && !app.running) await get().start(app, {skipNavigation: true})
|
|
586
594
|
},
|
|
587
595
|
|
|
596
|
+
setWidgetExpand: (packageName, widgetId, expandOnTap) => {
|
|
597
|
+
const app = get().apps.find((a) => a.packageName === packageName)
|
|
598
|
+
const widget = app?.widgets?.find((w) => w.id === widgetId)
|
|
599
|
+
if (!widget?.expandable) return false
|
|
600
|
+
if (widget.expandOnTap === expandOnTap) return expandOnTap
|
|
601
|
+
saveWidgetExpandChoice(packageName, widgetId, expandOnTap)
|
|
602
|
+
set((s) => ({
|
|
603
|
+
apps: s.apps.map((a) =>
|
|
604
|
+
a.packageName === packageName
|
|
605
|
+
? {...a, widgets: a.widgets?.map((w) => (w.id === widgetId ? {...w, expandOnTap} : w))}
|
|
606
|
+
: a,
|
|
607
|
+
),
|
|
608
|
+
}))
|
|
609
|
+
return expandOnTap
|
|
610
|
+
},
|
|
611
|
+
|
|
588
612
|
setApps: (apps) => set({apps}),
|
|
589
613
|
}))
|
|
590
614
|
|
|
@@ -35,3 +35,23 @@ function isMentraLiveDevice(model: string | undefined): boolean {
|
|
|
35
35
|
export function getBluetoothSettingKeysForDevice(deviceModel: string | undefined, fullKeys: string[]): string[] {
|
|
36
36
|
return isMentraLiveDevice(deviceModel) ? MENTRA_LIVE_SETTING_KEYS : fullKeys
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Glasses Menu entries the firmware's own menu cannot open: Settings is the
|
|
41
|
+
* dashboard's glasses page, which only the dashboard's Apps menu shows.
|
|
42
|
+
*/
|
|
43
|
+
const NATIVE_MENU_EXCLUDED_PACKAGES = new Set(["com.glassly.settings"])
|
|
44
|
+
const nativeMenuAppsCache = new WeakMap<object, unknown[]>()
|
|
45
|
+
|
|
46
|
+
/** `menu_apps` as the glasses get it; the same array in gives the same array out, so the change-push stays quiet. */
|
|
47
|
+
export function nativeMenuApps(value: unknown): unknown {
|
|
48
|
+
if (!Array.isArray(value)) return value
|
|
49
|
+
let filtered = nativeMenuAppsCache.get(value)
|
|
50
|
+
if (!filtered) {
|
|
51
|
+
filtered = value.filter(
|
|
52
|
+
(item) => !NATIVE_MENU_EXCLUDED_PACKAGES.has((item as {packageName?: string} | null)?.packageName ?? ""),
|
|
53
|
+
)
|
|
54
|
+
nativeMenuAppsCache.set(value, filtered)
|
|
55
|
+
}
|
|
56
|
+
return filtered
|
|
57
|
+
}
|