@glassly/engine 0.3.1-dev.494 → 0.3.1-dev.523
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 +9 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +5 -0
- package/build/engine.js.map +1 -1
- package/build/facades/dev.js +1 -1
- package/build/facades/dev.js.map +1 -1
- package/build/facades/miniapps.d.ts +5 -5
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +5 -6
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/speech.d.ts +10 -0
- package/build/facades/speech.d.ts.map +1 -1
- package/build/facades/speech.js +10 -0
- package/build/facades/speech.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +1 -7
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +15 -26
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/DevServerBridge.d.ts +1 -1
- package/build/services/DevServerBridge.js +2 -2
- package/build/services/DevServerBridge.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +2 -0
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/JSRouter.js +1 -1
- package/build/services/JSRouter.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +21 -6
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +130 -48
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalTranslationService.d.ts +51 -0
- package/build/services/LocalTranslationService.d.ts.map +1 -0
- package/build/services/LocalTranslationService.js +141 -0
- package/build/services/LocalTranslationService.js.map +1 -0
- package/build/services/MiniappLauncher.d.ts +17 -23
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +30 -34
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/MiniappPauseCoordinator.d.ts +15 -0
- package/build/services/MiniappPauseCoordinator.d.ts.map +1 -0
- package/build/services/MiniappPauseCoordinator.js +93 -0
- package/build/services/MiniappPauseCoordinator.js.map +1 -0
- package/build/services/OfflineTranslationManager.d.ts +19 -0
- package/build/services/OfflineTranslationManager.d.ts.map +1 -0
- package/build/services/OfflineTranslationManager.js +35 -0
- package/build/services/OfflineTranslationManager.js.map +1 -0
- package/build/services/pages/OverlayPages.d.ts +5 -5
- package/build/services/pages/OverlayPages.d.ts.map +1 -1
- package/build/services/pages/OverlayPages.js +5 -5
- package/build/services/pages/OverlayPages.js.map +1 -1
- package/build/services/pages/overlayApps.d.ts +14 -11
- package/build/services/pages/overlayApps.d.ts.map +1 -1
- package/build/services/pages/overlayApps.js +21 -19
- package/build/services/pages/overlayApps.js.map +1 -1
- package/build/services/settingsBridge.d.ts +8 -8
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +12 -13
- package/build/services/settingsBridge.js.map +1 -1
- package/build/stores/apps.d.ts +0 -5
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +32 -52
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +0 -10
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +3 -1
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g1.js +1 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -1
- package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g2.js +1 -0
- package/build/types/capabilities/even-realities-g2.js.map +1 -1
- package/build/types/capabilities/mentra-display.d.ts.map +1 -1
- package/build/types/capabilities/mentra-display.js +1 -0
- package/build/types/capabilities/mentra-display.js.map +1 -1
- package/build/types/capabilities/nimo.d.ts.map +1 -1
- package/build/types/capabilities/nimo.js +1 -0
- package/build/types/capabilities/nimo.js.map +1 -1
- package/build/types/hardware.d.ts +9 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +2 -0
- package/build/types/hardware.js.map +1 -1
- package/build/utils/miniappGlobals.js +1 -1
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +5 -0
- package/src/facades/dev.ts +1 -1
- package/src/facades/miniapps.ts +7 -7
- package/src/facades/speech.ts +12 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +0 -2
- package/src/services/AppRegistry.ts +15 -27
- package/src/services/DevServerBridge.ts +2 -2
- package/src/services/DeviceEventRouter.ts +2 -0
- package/src/services/JSRouter.ts +1 -1
- package/src/services/LocalMiniappRuntime.ts +165 -66
- package/src/services/LocalTranslationService.ts +184 -0
- package/src/services/MiniappLauncher.ts +31 -34
- package/src/services/MiniappPauseCoordinator.ts +100 -0
- package/src/services/OfflineTranslationManager.ts +40 -0
- package/src/services/pages/OverlayPages.ts +5 -5
- package/src/services/pages/overlayApps.ts +19 -19
- package/src/services/settingsBridge.ts +12 -13
- package/src/stores/apps.ts +30 -58
- package/src/stores/settings.ts +0 -10
- package/src/types/applet.ts +3 -1
- package/src/types/capabilities/even-realities-g1.ts +1 -0
- package/src/types/capabilities/even-realities-g2.ts +1 -0
- package/src/types/capabilities/mentra-display.ts +1 -0
- package/src/types/capabilities/nimo.ts +1 -0
- package/src/types/hardware.ts +9 -0
- package/src/utils/miniappGlobals.ts +1 -1
- package/build/services/pages/activeDashboard.d.ts +0 -7
- package/build/services/pages/activeDashboard.d.ts.map +0 -1
- package/build/services/pages/activeDashboard.js +0 -21
- package/build/services/pages/activeDashboard.js.map +0 -1
- package/src/services/pages/activeDashboard.ts +0 -24
|
@@ -28,10 +28,9 @@ import {File} from "expo-file-system"
|
|
|
28
28
|
import {permissions} from "../facades/permissions"
|
|
29
29
|
import {decideDevLaunchRoute} from "../utils/devMiniappLaunch"
|
|
30
30
|
import {storage} from "../utils/storage/storage"
|
|
31
|
-
import appRegistry
|
|
31
|
+
import appRegistry from "./AppRegistry"
|
|
32
32
|
import devServerBridge from "./DevServerBridge"
|
|
33
33
|
import localMiniappRuntime, {type InstalledMiniappManifest} from "./LocalMiniappRuntime"
|
|
34
|
-
import {activeDashboardAmong} from "./pages/activeDashboard"
|
|
35
34
|
import type {MiniappLaunchSource} from "@glassly/miniapp"
|
|
36
35
|
import type {JSRouter} from "./JSRouter"
|
|
37
36
|
|
|
@@ -284,44 +283,43 @@ class MiniappLauncher {
|
|
|
284
283
|
}
|
|
285
284
|
|
|
286
285
|
/**
|
|
287
|
-
*
|
|
286
|
+
* Start the Always On local miniapps at boot.
|
|
288
287
|
*
|
|
289
288
|
* Miniapps run in the phone's own JS engine, so a host-process kill tears
|
|
290
|
-
* down their JSContext
|
|
291
|
-
*
|
|
292
|
-
*
|
|
289
|
+
* down their JSContext. Only Always On apps come back: whatever else was
|
|
290
|
+
* running when the app was killed stays stopped until the wearer opens it
|
|
291
|
+
* again. The spawn is headless (no foreground, no navigation) and registers
|
|
292
|
+
* the package so the home tray/switcher project it as running, exactly as a
|
|
293
|
+
* normal start would. The WebView, if any, re-attaches lazily when the user
|
|
294
|
+
* opens the app.
|
|
293
295
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* WebView, if any, re-attaches lazily when the user opens the app.
|
|
296
|
+
* One dashboard runs at a time: a dashboard already running keeps its place,
|
|
297
|
+
* else the first Always On dashboard by package name starts and the rest
|
|
298
|
+
* stay stopped. The flag is exclusive after any user action (setAlwaysOn
|
|
299
|
+
* clears it on the other dashboards), so this only arbitrates manifests
|
|
300
|
+
* that both claim it.
|
|
300
301
|
*
|
|
301
|
-
* Idempotent and best-effort: skips already-spawned contexts, and
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* resumes when they reconnect, same as mid-session.
|
|
307
|
-
*
|
|
308
|
-
* Always On apps (the dashboard) start whether or not they were running
|
|
309
|
-
* when the app was last killed, and get a switcher card like any other app.
|
|
310
|
-
* A dashboard that does not hold the role (ADR-0009) is not started by its
|
|
311
|
-
* Always On flag.
|
|
312
|
-
* Both restore paths skip apps missing required OS permissions without
|
|
313
|
-
* prompting or clearing their saved running/Always On choice.
|
|
302
|
+
* Idempotent and best-effort: skips already-spawned contexts, and skips apps
|
|
303
|
+
* missing required OS permissions without prompting or clearing their Always
|
|
304
|
+
* On choice. Not compatibility-gated: an Always On app shouldn't be dropped
|
|
305
|
+
* just because glasses are momentarily disconnected at boot — it resumes
|
|
306
|
+
* when they reconnect, same as mid-session.
|
|
314
307
|
*/
|
|
315
308
|
async autostartLocalMiniapps(): Promise<void> {
|
|
316
309
|
const apps = await appRegistry.getInstalledMiniapps()
|
|
317
|
-
const
|
|
310
|
+
const dashboards = apps.filter((app) => app.type === "dashboard")
|
|
311
|
+
const dashboard =
|
|
312
|
+
dashboards.find((app) => this.isRunning(app.packageName))?.packageName ??
|
|
313
|
+
dashboards
|
|
314
|
+
.filter((app) => app.alwaysOn)
|
|
315
|
+
.map((app) => app.packageName)
|
|
316
|
+
.sort()[0] ??
|
|
317
|
+
null
|
|
318
318
|
for (const app of apps) {
|
|
319
|
-
// Only phone-hosted miniapps have a JSContext to
|
|
320
|
-
// built-ins (`local:false, offline:true`)
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
const alwaysOn = app.alwaysOn && (app.type !== "dashboard" || app.packageName === activeDashboard)
|
|
324
|
-
if (!alwaysOn && !getLocalAppRunningState(app.packageName)) continue
|
|
319
|
+
// Only phone-hosted miniapps have a JSContext to spawn. Offline
|
|
320
|
+
// built-ins (`local:false, offline:true`) have no bundle to launch.
|
|
321
|
+
if (!app.local || !app.alwaysOn) continue
|
|
322
|
+
if (app.type === "dashboard" && app.packageName !== dashboard) continue
|
|
325
323
|
if (this.isRunning(app.packageName)) continue
|
|
326
324
|
try {
|
|
327
325
|
const missing = await permissions.missingRequired(app.permissions ?? [])
|
|
@@ -340,8 +338,7 @@ class MiniappLauncher {
|
|
|
340
338
|
try {
|
|
341
339
|
await this.ensureRunning(app.packageName)
|
|
342
340
|
} catch (e) {
|
|
343
|
-
console.warn(`MiniappLauncher: autostart failed for ${app.packageName}
|
|
344
|
-
saveLocalAppRunningState(app.packageName, false)
|
|
341
|
+
console.warn(`MiniappLauncher: autostart failed for ${app.packageName}`, e)
|
|
345
342
|
}
|
|
346
343
|
}
|
|
347
344
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiniappPauseCoordinator — pauses every local miniapp while the paired
|
|
3
|
+
* glasses are disconnected.
|
|
4
|
+
*
|
|
5
|
+
* A paused miniapp keeps its JSContext and state. The runtime tells it
|
|
6
|
+
* (`session.on("pause")`), stops fanning out streams to it, and drops the mic
|
|
7
|
+
* and transcription demand its subscriptions were holding, until the glasses
|
|
8
|
+
* are back. The pause waits out a grace window so a BLE blip does not pause
|
|
9
|
+
* and resume an app within a second; resume is immediate on reconnect.
|
|
10
|
+
*
|
|
11
|
+
* Only a phone paired to real glasses pauses. With nothing paired, or with the
|
|
12
|
+
* simulated device, there is nothing to reconnect to and miniapps keep running
|
|
13
|
+
* (the mirror and dev flows depend on that).
|
|
14
|
+
*/
|
|
15
|
+
import {useGlassesStore} from "../stores/glasses"
|
|
16
|
+
import {BgTimer} from "../utils/timers"
|
|
17
|
+
import {isGlassesConnected, type GlassesConnectionStatus} from "./GlassesReadiness"
|
|
18
|
+
import {isPairedToRealGlasses} from "./PairedGlassesLatch"
|
|
19
|
+
import {projectPairingIdentity, subscribePairingIdentity, type IdentitySnapshot} from "./PairingIdentity"
|
|
20
|
+
|
|
21
|
+
export const MINIAPP_PAUSE_GRACE_MS = 10_000
|
|
22
|
+
|
|
23
|
+
export type MiniappPauseReason = "glasses_disconnected"
|
|
24
|
+
|
|
25
|
+
type Listener = (paused: boolean) => void
|
|
26
|
+
|
|
27
|
+
let started = false
|
|
28
|
+
let paused = false
|
|
29
|
+
let graceTimer: number | null = null
|
|
30
|
+
const listeners = new Set<Listener>()
|
|
31
|
+
let unsubscribers: Array<() => void> = []
|
|
32
|
+
|
|
33
|
+
/** Paired to real glasses that are not connected. */
|
|
34
|
+
export function shouldPauseMiniapps(identity: IdentitySnapshot, connection: GlassesConnectionStatus): boolean {
|
|
35
|
+
return isPairedToRealGlasses(identity) && !isGlassesConnected(connection)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isMiniappsPaused(): boolean {
|
|
39
|
+
return paused
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Fires on pause/resume transitions only. */
|
|
43
|
+
export function onMiniappsPauseChange(listener: Listener): () => void {
|
|
44
|
+
listeners.add(listener)
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setPaused(next: boolean): void {
|
|
51
|
+
if (paused === next) return
|
|
52
|
+
paused = next
|
|
53
|
+
console.log(`MiniappPauseCoordinator: ${next ? "paused" : "resumed"} local miniapps`)
|
|
54
|
+
for (const listener of listeners) {
|
|
55
|
+
try {
|
|
56
|
+
listener(next)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn("MiniappPauseCoordinator: listener threw", e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clearGrace(): void {
|
|
64
|
+
if (graceTimer === null) return
|
|
65
|
+
BgTimer.clearTimeout(graceTimer)
|
|
66
|
+
graceTimer = null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function wanted(): boolean {
|
|
70
|
+
return shouldPauseMiniapps(projectPairingIdentity(), useGlassesStore.getState().connection)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function evaluate(): void {
|
|
74
|
+
if (!wanted()) {
|
|
75
|
+
clearGrace()
|
|
76
|
+
setPaused(false)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (paused || graceTimer !== null) return
|
|
80
|
+
graceTimer = BgTimer.setTimeout(() => {
|
|
81
|
+
graceTimer = null
|
|
82
|
+
if (wanted()) setPaused(true)
|
|
83
|
+
}, MINIAPP_PAUSE_GRACE_MS)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Start after the persisted settings are loaded so the pairing identity reads true. */
|
|
87
|
+
export function startMiniappPauseCoordinator(): void {
|
|
88
|
+
if (started) return
|
|
89
|
+
started = true
|
|
90
|
+
unsubscribers = [useGlassesStore.subscribe((s) => s.connection.state, evaluate), subscribePairingIdentity(evaluate)]
|
|
91
|
+
evaluate()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function stopMiniappPauseCoordinator(): void {
|
|
95
|
+
for (const unsubscribe of unsubscribers) unsubscribe()
|
|
96
|
+
unsubscribers = []
|
|
97
|
+
clearGrace()
|
|
98
|
+
paused = false
|
|
99
|
+
started = false
|
|
100
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {Platform} from "react-native"
|
|
2
|
+
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
3
|
+
import type {OfflineTranslationLanguage, OfflineTranslationStatus} from "@glassly/bluetooth-sdk"
|
|
4
|
+
|
|
5
|
+
export type {OfflineTranslationLanguage, OfflineTranslationStatus}
|
|
6
|
+
|
|
7
|
+
/** Headless on-device translation needs the iOS 26 Translation framework. */
|
|
8
|
+
export function offlineTranslationSupported(): boolean {
|
|
9
|
+
return Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* On-device translation language packs. The OS owns the assets: there is no
|
|
14
|
+
* download progress or removal API, only install state and a system prompt
|
|
15
|
+
* that downloads a pair.
|
|
16
|
+
*/
|
|
17
|
+
export const offlineTranslationManager = {
|
|
18
|
+
isSupported: offlineTranslationSupported,
|
|
19
|
+
|
|
20
|
+
/** Languages `source` can be translated into, with their install state. */
|
|
21
|
+
async languages(source: string): Promise<OfflineTranslationLanguage[]> {
|
|
22
|
+
if (!offlineTranslationSupported()) return []
|
|
23
|
+
return BluetoothSdk.getOfflineTranslationLanguages(source)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async status(source: string, target: string): Promise<OfflineTranslationStatus> {
|
|
27
|
+
if (!offlineTranslationSupported()) return "unavailable"
|
|
28
|
+
return BluetoothSdk.getOfflineTranslationStatus(source, target)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/** Shows the system download prompt; resolves with the pair's status afterwards. */
|
|
32
|
+
async download(source: string, target: string): Promise<OfflineTranslationStatus> {
|
|
33
|
+
if (!offlineTranslationSupported()) return "unavailable"
|
|
34
|
+
return BluetoothSdk.downloadOfflineTranslation(source, target)
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
translate(text: string, source: string, target: string): Promise<string> {
|
|
38
|
+
return BluetoothSdk.translateOffline(text, source, target)
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The overlay app's page (ADR-0006, ADR-
|
|
2
|
+
* The overlay app's page (ADR-0006, ADR-0010): one Page per dashboard that
|
|
3
3
|
* has taken the slot, the only Page implementation the page host shows. The
|
|
4
4
|
* engine keeps the slot and the eligibility watch; a page only relays:
|
|
5
5
|
* holding the slot is reported to its app as a view change, and the app's
|
|
6
6
|
* frames reach the slot through `renderOverlay` under its package name. Only
|
|
7
|
-
* the dashboard
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* the connected dashboard may show; a stopping one is removed without a
|
|
8
|
+
* word. The runtime supplies the sink that delivers to apps, so this module
|
|
9
|
+
* never imports it.
|
|
10
10
|
*/
|
|
11
11
|
import pageHost, {type Page, type PageHiddenReason} from "./PageHost"
|
|
12
12
|
import {isOverlayApp} from "./overlayApps"
|
|
@@ -45,7 +45,7 @@ class OverlayPages {
|
|
|
45
45
|
return pageHost.showing === page
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Release the slot if the app holds it
|
|
48
|
+
/** Release the slot if the app holds it. */
|
|
49
49
|
hide(packageName: string, reason: PageHiddenReason = "hidden"): void {
|
|
50
50
|
const page = this.pages.get(packageName)
|
|
51
51
|
if (page) pageHost.hide(page, reason)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The overlay app: the connected miniapp
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* The overlay app: the connected dashboard miniapp (ADR-0010). It alone may
|
|
3
|
+
* take the overlay slot through `session.display.showView("dashboard")` and
|
|
4
|
+
* has "dashboard" in its `display.views` capability while the glasses are
|
|
5
|
+
* eligible. The runtime records each dashboard miniapp (manifest type
|
|
6
|
+
* "dashboard") as it connects and stops; the first connected one holds the
|
|
7
|
+
* role until it stops, and the engine never knows what a dashboard draws.
|
|
8
|
+
* Only one dashboard runs at a time (the apps store stops the previous one
|
|
9
|
+
* before starting another), so two connected at once is a transient. Kept
|
|
10
|
+
* free of imports so the runtime and the notification policy can both read
|
|
11
|
+
* it without a cycle.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
export type DisplayView = "main" | "dashboard"
|
|
@@ -18,9 +20,9 @@ const listening = new Set<string>()
|
|
|
18
20
|
let active: string | null = null
|
|
19
21
|
const listeners = new Set<() => void>()
|
|
20
22
|
|
|
21
|
-
/** `packageName` holds the dashboard role
|
|
23
|
+
/** `packageName` holds the dashboard role. */
|
|
22
24
|
export function isOverlayApp(packageName: string): boolean {
|
|
23
|
-
return packageName === active
|
|
25
|
+
return packageName === active
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** `packageName` is a connected dashboard miniapp, whether or not it holds the role. */
|
|
@@ -29,29 +31,27 @@ export function isDashboardApp(packageName: string): boolean {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function overlayApps(): readonly string[] {
|
|
32
|
-
return active !== null
|
|
34
|
+
return active !== null ? [active] : []
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
/** The
|
|
37
|
+
/** The connected dashboard that holds the role, or null for nobody. */
|
|
36
38
|
export function activeDashboard(): string | null {
|
|
37
39
|
return active
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
if (active === packageName) return
|
|
42
|
-
active = packageName
|
|
43
|
-
emit()
|
|
44
|
-
}
|
|
45
|
-
|
|
42
|
+
/** A dashboard connected: it takes the role when nobody holds it. */
|
|
46
43
|
export function addDashboardApp(packageName: string): void {
|
|
47
44
|
if (connected.has(packageName)) return
|
|
48
45
|
connected.add(packageName)
|
|
46
|
+
if (active === null) active = packageName
|
|
49
47
|
emit()
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
/** A dashboard stopped: the role passes to another connected one, by package name, else to nobody. */
|
|
52
51
|
export function removeDashboardApp(packageName: string): void {
|
|
53
52
|
listening.delete(packageName)
|
|
54
53
|
if (!connected.delete(packageName)) return
|
|
54
|
+
if (active === packageName) active = [...connected].sort()[0] ?? null
|
|
55
55
|
emit()
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -65,7 +65,7 @@ export function setOverlayViewer(packageName: string, subscribed: boolean): void
|
|
|
65
65
|
|
|
66
66
|
/** The overlay app presents phone notifications. */
|
|
67
67
|
export function overlayViewerRunning(): boolean {
|
|
68
|
-
return active !== null &&
|
|
68
|
+
return active !== null && listening.has(active)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function onOverlayAppsChange(listener: () => void): () => void {
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* request validation. Pure, so the runtime's handlers stay thin and this can
|
|
6
6
|
* be tested without the native import surface the runtime pulls in.
|
|
7
7
|
*
|
|
8
|
-
* A dashboard miniapp is the usual consumer: it edits the notification
|
|
9
|
-
* glasses-menu keys the phone's settings screens used
|
|
10
|
-
* so persistence, server sync and the Bluetooth sync
|
|
11
|
-
* if a phone screen wrote them. A dashboard's own look is not here; it keeps
|
|
8
|
+
* A dashboard miniapp is the usual consumer: it edits the notification,
|
|
9
|
+
* glasses-menu and display-position keys the phone's settings screens used
|
|
10
|
+
* to, through the store, so persistence, server sync and the Bluetooth sync
|
|
11
|
+
* of `menu_apps` and the position behave as if a phone screen wrote them. A dashboard's own look is not here; it keeps
|
|
12
12
|
* that in its storage.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -19,6 +19,10 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
|
|
|
19
19
|
// The dashboard's master switch is a phone-side debug knob.
|
|
20
20
|
custom_dashboard_enabled: "read",
|
|
21
21
|
contextual_dashboard: "write",
|
|
22
|
+
head_up_angle: "write",
|
|
23
|
+
// Where the glasses draw; each model's steps are its display.positionRange.
|
|
24
|
+
dashboard_depth: "write",
|
|
25
|
+
dashboard_height: "write",
|
|
22
26
|
twelve_hour_time: "write",
|
|
23
27
|
menu_apps: "write",
|
|
24
28
|
notifications_enabled: "write",
|
|
@@ -33,16 +37,11 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
|
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* Read-only keys the engine computes: whether the wearer can see the phone,
|
|
36
|
-
* whether the foreground miniapp is drawing on the glasses, the ring's
|
|
37
|
-
* battery
|
|
38
|
-
*
|
|
40
|
+
* whether the foreground miniapp is drawing on the glasses, and the ring's
|
|
41
|
+
* battery. They change on their own clocks; the runtime pushes them like
|
|
42
|
+
* stored keys.
|
|
39
43
|
*/
|
|
40
|
-
export const DERIVED_SETTINGS_KEYS = [
|
|
41
|
-
"phone_active",
|
|
42
|
-
"foreground_app_on_screen",
|
|
43
|
-
"ring_battery",
|
|
44
|
-
"active_dashboard",
|
|
45
|
-
] as const
|
|
44
|
+
export const DERIVED_SETTINGS_KEYS = ["phone_active", "foreground_app_on_screen", "ring_battery"] as const
|
|
46
45
|
export type DerivedSettingsKey = (typeof DERIVED_SETTINGS_KEYS)[number]
|
|
47
46
|
|
|
48
47
|
export function isDerivedSettingsKey(key: string): key is DerivedSettingsKey {
|
package/src/stores/apps.ts
CHANGED
|
@@ -24,8 +24,8 @@ import localDisplayManager from "../services/LocalDisplayManager"
|
|
|
24
24
|
import {islandNotifications} from "../services/NotificationsEmitter"
|
|
25
25
|
import sttModelManager from "../services/STTModelManager"
|
|
26
26
|
import {miniappLauncher} from "../services/MiniappLauncher"
|
|
27
|
+
import {isMiniappsPaused, onMiniappsPauseChange} from "../services/MiniappPauseCoordinator"
|
|
27
28
|
import {miniappRunningRegistry} from "../services/MiniappRunningRegistry"
|
|
28
|
-
import {activeDashboardAmong, resolveActiveDashboard} from "../services/pages/activeDashboard"
|
|
29
29
|
import {defaultHiddenStatus, isGatedByDevTools, isGatedUntilPaired, resolveHiddenStatus} from "./appVisibility"
|
|
30
30
|
import {SETTINGS, useSettingsStore} from "./settings"
|
|
31
31
|
import BluetoothSdk from "@glassly/bluetooth-sdk"
|
|
@@ -102,7 +102,6 @@ interface AppStatusState {
|
|
|
102
102
|
* off leaves it running. Persists the wearer's choice beside the hidden flag.
|
|
103
103
|
*/
|
|
104
104
|
setAlwaysOn: (packageName: string, status: boolean) => Promise<void>
|
|
105
|
-
setActiveDashboard: (packageName: string) => Promise<void>
|
|
106
105
|
/** Replace the whole apps array. Hosts use this when their extra source changes. */
|
|
107
106
|
setApps: (apps: ClientApp[]) => void
|
|
108
107
|
}
|
|
@@ -255,6 +254,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
|
|
|
255
254
|
// through to the root router (restart). Deriving here keeps the flag stable
|
|
256
255
|
// across both passes.
|
|
257
256
|
foregrounded: app.packageName === previousState.foregroundedPackage,
|
|
257
|
+
paused: app.local && app.running && isMiniappsPaused(),
|
|
258
258
|
}
|
|
259
259
|
const prev = previousByPackage.get(app.packageName)
|
|
260
260
|
return prev && shallowEqualApp(prev, next) ? prev : next
|
|
@@ -379,12 +379,11 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
379
379
|
return true
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
for (const r of runningForeground) {
|
|
382
|
+
// Only-one rules: one standard app in the foreground, one dashboard
|
|
383
|
+
// drawing the overlay. Starting another stops the one running.
|
|
384
|
+
if (app.type === "standard" || app.type === "dashboard") {
|
|
385
|
+
const runningPeers = state.apps.filter((a) => a.running && a.type === app.type && a.packageName !== packageName)
|
|
386
|
+
for (const r of runningPeers) {
|
|
388
387
|
await get().stop(r.packageName)
|
|
389
388
|
}
|
|
390
389
|
}
|
|
@@ -413,9 +412,7 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
413
412
|
set((s) => ({
|
|
414
413
|
apps: s.apps.map((a) => (a.packageName === packageName ? {...a, running: false, loading: false} : a)),
|
|
415
414
|
}))
|
|
416
|
-
// onStart already
|
|
417
|
-
// before the spawn; run onStop to undo it so a refresh/reboot doesn't
|
|
418
|
-
// resurrect a "running" app with no JS context.
|
|
415
|
+
// onStart already ran before the spawn; run onStop to undo it.
|
|
419
416
|
await startStopApp(app, false)
|
|
420
417
|
return false
|
|
421
418
|
}
|
|
@@ -508,17 +505,9 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
508
505
|
|
|
509
506
|
uninstall: (packageName, version) => {
|
|
510
507
|
return Res.try_async(async () => {
|
|
511
|
-
const wasActiveDashboard = activeDashboardAmong(get().apps) === packageName
|
|
512
508
|
const res = await appRegistry.uninstall(packageName, version)
|
|
513
509
|
if (res.is_error()) throw res.error
|
|
514
510
|
set((s) => ({apps: s.apps.filter((a) => a.packageName !== packageName)}))
|
|
515
|
-
// The dashboard role fell back to another dashboard, which was stopped
|
|
516
|
-
// when it lost the role: start it, as launch would have.
|
|
517
|
-
if (wasActiveDashboard) {
|
|
518
|
-
const apps = get().apps
|
|
519
|
-
const heir = apps.find((a) => a.packageName === activeDashboardAmong(apps))
|
|
520
|
-
if (heir?.alwaysOn && !heir.running) await get().start(heir, {skipNavigation: true})
|
|
521
|
-
}
|
|
522
511
|
})
|
|
523
512
|
},
|
|
524
513
|
|
|
@@ -556,46 +545,49 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
556
545
|
setAlwaysOn: async (packageName: string, status: boolean) => {
|
|
557
546
|
const app = get().apps.find((a) => a.packageName === packageName)
|
|
558
547
|
if (!app) return
|
|
548
|
+
// One dashboard is Always On at a time: marking this one clears the others.
|
|
549
|
+
const cleared =
|
|
550
|
+
status && app.type === "dashboard"
|
|
551
|
+
? get().apps.filter((a) => a.type === "dashboard" && a.alwaysOn && a.packageName !== packageName)
|
|
552
|
+
: []
|
|
559
553
|
saveAlwaysOnOverride(packageName, status)
|
|
554
|
+
for (const other of cleared) saveAlwaysOnOverride(other.packageName, false)
|
|
560
555
|
set((s) => ({
|
|
561
|
-
apps: s.apps.map((a) =>
|
|
556
|
+
apps: s.apps.map((a) => {
|
|
557
|
+
if (a.packageName === packageName) return {...a, alwaysOn: status}
|
|
558
|
+
if (cleared.includes(a)) return {...a, alwaysOn: false}
|
|
559
|
+
return a
|
|
560
|
+
}),
|
|
562
561
|
}))
|
|
563
562
|
if (status && !app.running) await get().start(app, {skipNavigation: true})
|
|
564
563
|
},
|
|
565
564
|
|
|
566
|
-
// The dashboard role (ADR-0009) moves to `packageName`: the previous holder
|
|
567
|
-
// stops and the new one starts in the background.
|
|
568
|
-
setActiveDashboard: async (packageName: string) => {
|
|
569
|
-
const apps = get().apps
|
|
570
|
-
const next = apps.find((a) => a.packageName === packageName && a.type === "dashboard")
|
|
571
|
-
if (!next) return
|
|
572
|
-
const previous = apps.find((a) => a.packageName === activeDashboardAmong(apps))
|
|
573
|
-
await useSettingsStore.getState().setSetting(SETTINGS.active_dashboard.key, packageName)
|
|
574
|
-
if (previous?.running && previous.packageName !== packageName) await get().stop(previous.packageName)
|
|
575
|
-
if (!next.running) await get().start(next, {skipNavigation: true})
|
|
576
|
-
},
|
|
577
|
-
|
|
578
565
|
setApps: (apps) => set({apps}),
|
|
579
566
|
}))
|
|
580
567
|
|
|
581
568
|
// Project miniappRunningRegistry membership into the store's `running` field
|
|
582
|
-
//
|
|
583
|
-
// without waiting for the next
|
|
584
|
-
|
|
569
|
+
// (and the pause coordinator's state into `paused`) for local apps so the home
|
|
570
|
+
// tray and switcher reflect actual mount state without waiting for the next
|
|
571
|
+
// refresh() cycle.
|
|
572
|
+
function projectLocalRunningState(): void {
|
|
585
573
|
const running = new Set(miniappRunningRegistry.getAll())
|
|
574
|
+
const pausedNow = isMiniappsPaused()
|
|
586
575
|
const state = useAppStatusStore.getState()
|
|
587
576
|
let changed = false
|
|
588
577
|
const updated = state.apps.map((app) => {
|
|
589
578
|
if (!app.local) return app
|
|
590
579
|
const next = running.has(app.packageName)
|
|
591
|
-
|
|
580
|
+
const paused = next && pausedNow
|
|
581
|
+
if (app.running === next && (app.paused ?? false) === paused) return app
|
|
592
582
|
changed = true
|
|
593
|
-
return {...app, running: next}
|
|
583
|
+
return {...app, running: next, paused}
|
|
594
584
|
})
|
|
595
585
|
if (changed) {
|
|
596
586
|
useAppStatusStore.setState({apps: updated})
|
|
597
587
|
}
|
|
598
|
-
}
|
|
588
|
+
}
|
|
589
|
+
miniappRunningRegistry.subscribe(projectLocalRunningState)
|
|
590
|
+
onMiniappsPauseChange(projectLocalRunningState)
|
|
599
591
|
|
|
600
592
|
// The display-manager's core (foreground) slot is DERIVED state: always the
|
|
601
593
|
// currently-running standard app, no matter HOW it came to run — user tap
|
|
@@ -677,26 +669,6 @@ export const useRunningAlwaysOnApps = () => {
|
|
|
677
669
|
return useMemo(() => apps.filter((app) => app.alwaysOn && app.running), [apps])
|
|
678
670
|
}
|
|
679
671
|
|
|
680
|
-
/** Installed miniapps that can hold the dashboard role: manifest type "dashboard". */
|
|
681
|
-
export const useDashboards = () => {
|
|
682
|
-
const apps = useApps()
|
|
683
|
-
return useMemo(() => apps.filter((app) => app.type === "dashboard"), [apps])
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/** The miniapp that holds the dashboard role (ADR-0009), or null when none is installed. */
|
|
687
|
-
export const useActiveDashboard = (): ClientApp | null => {
|
|
688
|
-
const dashboards = useDashboards()
|
|
689
|
-
const chosen = useSettingsStore((s) => s.getSetting(SETTINGS.active_dashboard.key))
|
|
690
|
-
return useMemo(() => {
|
|
691
|
-
const packageName = resolveActiveDashboard(
|
|
692
|
-
dashboards.map((app) => app.packageName),
|
|
693
|
-
chosen,
|
|
694
|
-
SETTINGS.active_dashboard.defaultValue(),
|
|
695
|
-
)
|
|
696
|
-
return dashboards.find((app) => app.packageName === packageName) ?? null
|
|
697
|
-
}, [dashboards, chosen])
|
|
698
|
-
}
|
|
699
|
-
|
|
700
672
|
/** Apps that run without a foreground surface: background ones and dashboards. */
|
|
701
673
|
const isBackgroundApp = (app: ClientApp): boolean => app.type === "background" || app.type === "dashboard"
|
|
702
674
|
|
package/src/stores/settings.ts
CHANGED
|
@@ -806,16 +806,6 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
806
806
|
saveOnServer: false,
|
|
807
807
|
persist: true,
|
|
808
808
|
},
|
|
809
|
-
// The wearer's choice of dashboard miniapp (ADR-0009). Installs are
|
|
810
|
-
// per-phone, so the choice is device-local. The default is the bundled
|
|
811
|
-
// dashboard, which the role falls back to when the choice is not installed.
|
|
812
|
-
active_dashboard: {
|
|
813
|
-
key: "active_dashboard",
|
|
814
|
-
defaultValue: () => "com.glassly.dashboard",
|
|
815
|
-
writable: true,
|
|
816
|
-
saveOnServer: false,
|
|
817
|
-
persist: true,
|
|
818
|
-
},
|
|
819
809
|
head_up_angle: {key: "head_up_angle", defaultValue: () => 45, writable: true, saveOnServer: true, persist: true},
|
|
820
810
|
brightness: {key: "brightness", defaultValue: () => 50, writable: true, saveOnServer: true, persist: true},
|
|
821
811
|
auto_brightness: {
|
package/src/types/applet.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface AppletInterface {
|
|
|
81
81
|
* outside the manifest: offline routing, hardware compatibility result,
|
|
82
82
|
* loading/hidden/local flags, lifecycle hooks, screenshot, and the
|
|
83
83
|
* dev-miniapp escape hatches (`devUrl` / `isMiniappDev`) that surface a
|
|
84
|
-
* `glassly
|
|
84
|
+
* `glassly dev` snapshot.
|
|
85
85
|
*/
|
|
86
86
|
/**
|
|
87
87
|
* A manifest-declared action a miniapp exposes for other miniapps to invoke
|
|
@@ -116,6 +116,8 @@ export interface ClientApp extends AppletInterface {
|
|
|
116
116
|
* the wearer's choice persists beside the hidden flag.
|
|
117
117
|
*/
|
|
118
118
|
alwaysOn?: boolean
|
|
119
|
+
/** Running, but paused while the paired glasses are disconnected (MiniappPauseCoordinator). */
|
|
120
|
+
paused?: boolean
|
|
119
121
|
onStart?: () => void
|
|
120
122
|
onStop?: () => void
|
|
121
123
|
screenshot?: string
|
|
@@ -28,6 +28,7 @@ export const evenRealitiesG1: Capabilities = {
|
|
|
28
28
|
fieldOfView: {horizontal: 25},
|
|
29
29
|
maxTextLines: 5,
|
|
30
30
|
adjustBrightness: true,
|
|
31
|
+
positionRange: {height: {min: 0, max: 8}, depth: {min: 1, max: 9}},
|
|
31
32
|
|
|
32
33
|
// Scene display API — no positioned elements; scenes degrade to text walls.
|
|
33
34
|
canPosition: false,
|
|
@@ -29,6 +29,7 @@ export const evenRealitiesG2: Capabilities = {
|
|
|
29
29
|
fieldOfView: {horizontal: 25},
|
|
30
30
|
maxTextLines: 8,
|
|
31
31
|
adjustBrightness: true,
|
|
32
|
+
positionRange: {height: {min: 0, max: 12}, depth: {min: 1, max: 3}},
|
|
32
33
|
|
|
33
34
|
// Scene display API — EvenHub retained containers.
|
|
34
35
|
width: 576,
|
|
@@ -28,6 +28,7 @@ export const mentraDisplay: Capabilities = {
|
|
|
28
28
|
fieldOfView: {horizontal: 25},
|
|
29
29
|
maxTextLines: 5,
|
|
30
30
|
adjustBrightness: true,
|
|
31
|
+
positionRange: {height: {min: 0, max: 8}, depth: {min: 1, max: 4}},
|
|
31
32
|
|
|
32
33
|
// Scene display API — firmware canvas components on the 500x220 virtual
|
|
33
34
|
// screen centered in the 640x480 A6N panel (mos_display_config.c).
|
|
@@ -32,6 +32,7 @@ export const nimo: Capabilities = {
|
|
|
32
32
|
resolution: {width: 640, height: 400},
|
|
33
33
|
maxTextLines: 5,
|
|
34
34
|
adjustBrightness: true,
|
|
35
|
+
positionRange: {height: {min: 0, max: 10}, depth: {min: 0, max: 10}},
|
|
35
36
|
},
|
|
36
37
|
|
|
37
38
|
// Microphone capabilities - Nimo streams Opus audio from both arms
|