@glassly/engine 0.1.0-dev.1 → 0.1.0-dev.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -19
- package/build/bluetooth-sdk/debug.d.ts +2 -0
- package/build/bluetooth-sdk/debug.d.ts.map +1 -0
- package/build/bluetooth-sdk/debug.js +2 -0
- package/build/bluetooth-sdk/debug.js.map +1 -0
- package/build/bluetooth-sdk/index.d.ts +3 -0
- package/build/bluetooth-sdk/index.d.ts.map +1 -0
- package/build/bluetooth-sdk/index.js +3 -0
- package/build/bluetooth-sdk/index.js.map +1 -0
- package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
- package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
- package/build/bluetooth-sdk/ota-transport.js +2 -0
- package/build/bluetooth-sdk/ota-transport.js.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.js +3 -0
- package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
- package/build/bluetooth-sdk/react.d.ts +2 -0
- package/build/bluetooth-sdk/react.d.ts.map +1 -0
- package/build/bluetooth-sdk/react.js +2 -0
- package/build/bluetooth-sdk/react.js.map +1 -0
- package/build/bluetooth-sdk/types.d.ts +2 -0
- package/build/bluetooth-sdk/types.d.ts.map +1 -0
- package/build/bluetooth-sdk/types.js +2 -0
- package/build/bluetooth-sdk/types.js.map +1 -0
- package/build/engine.d.ts +23 -16
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +9 -3
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts +1 -1
- package/build/facades/display.d.ts +1 -1
- package/build/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +1 -1
- package/build/facades/display.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -13
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -3
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/ota.d.ts +19 -21
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +23 -9
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +11 -5
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +93 -17
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/speakers.d.ts +1 -1
- package/build/facades/speakers.d.ts.map +1 -1
- package/build/facades/speakers.js.map +1 -1
- package/build/facades/speech.d.ts +7 -6
- package/build/facades/speech.d.ts.map +1 -1
- package/build/facades/speech.js +1 -1
- package/build/facades/speech.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +7 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +39 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +200 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +3 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +3 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/react/useMentraLiveOta.d.ts +80 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -0
- package/build/react/useMentraLiveOta.js +550 -0
- package/build/react/useMentraLiveOta.js.map +1 -0
- package/build/services/AppRegistry.d.ts +8 -0
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +15 -0
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +63 -10
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CustomDashboardModel.d.ts +48 -0
- package/build/services/CustomDashboardModel.d.ts.map +1 -0
- package/build/services/CustomDashboardModel.js +73 -0
- package/build/services/CustomDashboardModel.js.map +1 -0
- package/build/services/CustomDashboardService.d.ts +61 -0
- package/build/services/CustomDashboardService.d.ts.map +1 -0
- package/build/services/CustomDashboardService.js +552 -0
- package/build/services/CustomDashboardService.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +44 -3
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +35 -0
- package/build/services/GestureDotsService.d.ts.map +1 -0
- package/build/services/GestureDotsService.js +149 -0
- package/build/services/GestureDotsService.js.map +1 -0
- package/build/services/GlassesStatusProjection.d.ts +15 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +18 -11
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts.map +1 -1
- package/build/services/HotspotOtaTransport.js +8 -7
- package/build/services/HotspotOtaTransport.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +43 -1
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +128 -2
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +32 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +33 -0
- package/build/services/MiniappExitGesture.d.ts.map +1 -0
- package/build/services/MiniappExitGesture.js +54 -0
- package/build/services/MiniappExitGesture.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +41 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +91 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +24 -3
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +17 -12
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +1 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +4 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +28 -16
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +13 -12
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/gestureClaims.d.ts +18 -0
- package/build/services/gestureClaims.d.ts.map +1 -0
- package/build/services/gestureClaims.js +24 -0
- package/build/services/gestureClaims.js.map +1 -0
- package/build/services/gestureDotGlyphs.d.ts +8 -0
- package/build/services/gestureDotGlyphs.d.ts.map +1 -0
- package/build/services/gestureDotGlyphs.js +12 -0
- package/build/services/gestureDotGlyphs.js.map +1 -0
- package/build/services/glassesClockSync.d.ts.map +1 -1
- package/build/services/glassesClockSync.js +5 -1
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/otaDisplayState.d.ts +1 -1
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +1 -1
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaLegacyMapping.d.ts +1 -1
- package/build/services/otaLegacyMapping.d.ts.map +1 -1
- package/build/services/otaLegacyMapping.js.map +1 -1
- package/build/services/otaManifestPolicy.d.ts +14 -0
- package/build/services/otaManifestPolicy.d.ts.map +1 -0
- package/build/services/otaManifestPolicy.js +20 -0
- package/build/services/otaManifestPolicy.js.map +1 -0
- package/build/services/otaManifestUrl.d.ts +2 -1
- package/build/services/otaManifestUrl.d.ts.map +1 -1
- package/build/services/otaManifestUrl.js +24 -32
- package/build/services/otaManifestUrl.js.map +1 -1
- package/build/services/otaReleaseVersion.d.ts +10 -0
- package/build/services/otaReleaseVersion.d.ts.map +1 -0
- package/build/services/otaReleaseVersion.js +14 -0
- package/build/services/otaReleaseVersion.js.map +1 -0
- package/build/stores/appVisibility.d.ts +3 -0
- package/build/stores/appVisibility.d.ts.map +1 -0
- package/build/stores/appVisibility.js +3 -0
- package/build/stores/appVisibility.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +7 -1
- package/build/stores/apps.js.map +1 -1
- package/build/stores/core.d.ts +3 -3
- package/build/stores/core.d.ts.map +1 -1
- package/build/stores/core.js.map +1 -1
- package/build/stores/glasses.d.ts +10 -4
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +87 -3
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js +0 -3
- package/build/types/applet.js.map +1 -1
- package/build/types/enums.d.ts +1 -5
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -8
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js.map +1 -1
- package/build/types/index.d.ts +2 -6
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +2 -6
- package/build/types/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +4 -3
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +6 -2
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/package.json +65 -33
- package/src/bluetooth-sdk/debug.ts +1 -0
- package/src/bluetooth-sdk/index.ts +2 -0
- package/src/bluetooth-sdk/ota-transport.ts +1 -0
- package/src/bluetooth-sdk/photo-receiver.ts +2 -0
- package/src/bluetooth-sdk/react.ts +1 -0
- package/src/bluetooth-sdk/types.ts +1 -0
- package/src/engine.ts +9 -3
- package/src/facades/display.ts +1 -1
- package/src/facades/glasses.ts +6 -4
- package/src/facades/ota.ts +33 -10
- package/src/facades/pairing.ts +103 -23
- package/src/facades/speakers.ts +1 -1
- package/src/facades/speech.ts +21 -7
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +29 -4
- package/src/react/MentraLiveOtaFlow.tsx +569 -0
- package/src/react/index.ts +21 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/react/useMentraLiveOta.ts +707 -0
- package/src/services/AppRegistry.ts +16 -0
- package/src/services/BlobStore.ts +57 -10
- package/src/services/CustomDashboardModel.ts +83 -0
- package/src/services/CustomDashboardService.ts +575 -0
- package/src/services/DeviceEventRouter.ts +47 -5
- package/src/services/GestureDotsService.ts +147 -0
- package/src/services/GlassesStatusProjection.ts +20 -11
- package/src/services/HotspotOtaTransport.ts +8 -7
- package/src/services/LocalDisplayManager.ts +130 -2
- package/src/services/LocalMiniappRuntime.ts +37 -0
- package/src/services/MiniappExitGesture.ts +76 -0
- package/src/services/OtaAutoChain.ts +130 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +19 -13
- package/src/services/OtaService.ts +1 -2
- package/src/services/OtaUpdateCheckService.ts +33 -19
- package/src/services/asg/localNetworkTransport.ts +20 -19
- package/src/services/gestureClaims.ts +28 -0
- package/src/services/gestureDotGlyphs.ts +14 -0
- package/src/services/glassesClockSync.ts +5 -1
- package/src/services/otaDisplayState.ts +1 -1
- package/src/services/otaInstallPolicy.ts +1 -1
- package/src/services/otaLegacyMapping.ts +1 -1
- package/src/services/otaManifestPolicy.ts +43 -0
- package/src/services/otaManifestUrl.ts +27 -36
- package/src/services/otaReleaseVersion.ts +22 -0
- package/src/stores/appVisibility.ts +3 -0
- package/src/stores/apps.ts +7 -1
- package/src/stores/core.ts +4 -4
- package/src/stores/glasses.ts +13 -6
- package/src/stores/settings.ts +87 -3
- package/src/types/applet.ts +2 -0
- package/src/types/enums.ts +1 -9
- package/src/types/hardware.ts +1 -1
- package/src/types/index.ts +2 -6
- package/src/utils/display/scene/process.ts +4 -3
- package/src/utils/display/scene/types.ts +0 -0
- package/build/devtools.d.ts +0 -11
- package/build/devtools.d.ts.map +0 -1
- package/build/devtools.js +0 -13
- package/build/devtools.js.map +0 -1
- package/build/internal.d.ts +0 -67
- package/build/internal.d.ts.map +0 -1
- package/build/internal.js +0 -90
- package/build/internal.js.map +0 -1
- package/src/devtools.ts +0 -13
- package/src/internal.ts +0 -155
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene API dashboard for G2 custom firmware.
|
|
3
|
+
*
|
|
4
|
+
* G2.swift marks the published firmware version after it detects the CFW
|
|
5
|
+
* capability advertisement. The service retains a dashboard frame, owns
|
|
6
|
+
* dashboard gestures while head-up is active, and leaves every other device
|
|
7
|
+
* on its native behavior.
|
|
8
|
+
*
|
|
9
|
+
* The dashboard is composed of user-orderable widgets (clock, calendar, apps
|
|
10
|
+
* button, notifications button) stacked vertically on the CFW scene canvas.
|
|
11
|
+
* The apps button switches to a second, dashboard-rendered app menu view.
|
|
12
|
+
*/
|
|
13
|
+
import {File} from "expo-file-system"
|
|
14
|
+
|
|
15
|
+
import {BluetoothSdk} from "@glassly/bluetooth-sdk"
|
|
16
|
+
import type {CalendarEvent} from "@glassly/miniapp"
|
|
17
|
+
|
|
18
|
+
import {useAppStatusStore} from "../stores/apps"
|
|
19
|
+
import {useGlassesStore} from "../stores/glasses"
|
|
20
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
21
|
+
import type {ClientApp} from "../types/applet"
|
|
22
|
+
import type {SceneElementInput} from "../utils/display/scene"
|
|
23
|
+
import {BgTimer} from "../utils/timers"
|
|
24
|
+
import {
|
|
25
|
+
appMenuActions,
|
|
26
|
+
dashboardActions,
|
|
27
|
+
formatDashboardClock,
|
|
28
|
+
isCustomDashboardEligible,
|
|
29
|
+
normalizeDashboardWidgets,
|
|
30
|
+
orderedDashboardApps,
|
|
31
|
+
type CustomDashboardClockStyle,
|
|
32
|
+
type DashboardWidgetSetting,
|
|
33
|
+
type MenuAppSetting,
|
|
34
|
+
} from "./CustomDashboardModel"
|
|
35
|
+
import {isGlassesConnected} from "./GlassesReadiness"
|
|
36
|
+
import {listPhoneCalendarEvents} from "./PhoneCalendarService"
|
|
37
|
+
import sceneRenderer from "./SceneRenderer"
|
|
38
|
+
|
|
39
|
+
const DASHBOARD_APP_ID = "system.custom-dashboard"
|
|
40
|
+
const CLOCK_REFRESH_MS = 30_000
|
|
41
|
+
const CALENDAR_REFRESH_MS = 5 * 60_000
|
|
42
|
+
|
|
43
|
+
// CFW blits the stock compositor's 576×288 canvas; used when the firmware has
|
|
44
|
+
// not reported scene capabilities yet.
|
|
45
|
+
const FALLBACK_CANVAS = {width: 576, height: 288}
|
|
46
|
+
const MARGIN_X = 16
|
|
47
|
+
const MARGIN_TOP = 10
|
|
48
|
+
const WIDGET_GAP = 10
|
|
49
|
+
/** Row height for the glasses' builtin 20px font engine. */
|
|
50
|
+
const LINE_H = 24
|
|
51
|
+
const BUTTON_H = 34
|
|
52
|
+
/** App-menu icon grid: 4 per page (G2 firmware image-container pool). */
|
|
53
|
+
const APPS_PER_PAGE = 4
|
|
54
|
+
const APP_CELL = 72
|
|
55
|
+
const APP_CELL_GAP = 16
|
|
56
|
+
const APP_ICON_INSET = 4
|
|
57
|
+
const APP_ICON = APP_CELL - APP_ICON_INSET * 2
|
|
58
|
+
|
|
59
|
+
function eventLine(event: CalendarEvent): string {
|
|
60
|
+
const when = event.allDay
|
|
61
|
+
? "All day"
|
|
62
|
+
: new Intl.DateTimeFormat(undefined, {hour: "numeric", minute: "2-digit"}).format(new Date(event.startsAt))
|
|
63
|
+
return `${when} ${event.title}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class CustomDashboardService {
|
|
67
|
+
private started = false
|
|
68
|
+
private open = false
|
|
69
|
+
private view: "main" | "apps" = "main"
|
|
70
|
+
private mainIndex = 0
|
|
71
|
+
private appsIndex = 0
|
|
72
|
+
private calendarAvailable = false
|
|
73
|
+
/** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */
|
|
74
|
+
private iconCache = new Map<string, string | null>()
|
|
75
|
+
private iconLoads = new Set<string>()
|
|
76
|
+
private calendarEvents: CalendarEvent[] = []
|
|
77
|
+
private unsubs: Array<() => void> = []
|
|
78
|
+
private clockTimer: number | null = null
|
|
79
|
+
private calendarTimer: number | null = null
|
|
80
|
+
/** Last observed eligibility, for transition detection. */
|
|
81
|
+
private eligible = false
|
|
82
|
+
|
|
83
|
+
start(): void {
|
|
84
|
+
if (this.started) return
|
|
85
|
+
this.started = true
|
|
86
|
+
this.unsubs.push(
|
|
87
|
+
useGlassesStore.subscribe(
|
|
88
|
+
(state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,
|
|
89
|
+
() => {
|
|
90
|
+
if (!this.syncEligibility()) return
|
|
91
|
+
void this.refreshCalendarAndRender()
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
// Selector: only dashboard-relevant settings retrigger a refresh. The
|
|
95
|
+
// whole-store form fired a calendar query on every settings write.
|
|
96
|
+
useSettingsStore.subscribe(
|
|
97
|
+
(state) =>
|
|
98
|
+
JSON.stringify([
|
|
99
|
+
state.getSetting(SETTINGS.custom_dashboard_enabled.key),
|
|
100
|
+
state.getSetting(SETTINGS.custom_dashboard_widgets.key),
|
|
101
|
+
state.getSetting(SETTINGS.custom_dashboard_clock_style.key),
|
|
102
|
+
state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),
|
|
103
|
+
state.getSetting(SETTINGS.twelve_hour_time.key),
|
|
104
|
+
state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),
|
|
105
|
+
state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),
|
|
106
|
+
state.getSetting(SETTINGS.menu_apps.key),
|
|
107
|
+
]),
|
|
108
|
+
() => {
|
|
109
|
+
if (!this.syncEligibility()) return
|
|
110
|
+
void this.refreshCalendarAndRender()
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
useAppStatusStore.subscribe(() => {
|
|
114
|
+
if (this.isEligible()) this.render()
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
this.clockTimer = BgTimer.setInterval(() => {
|
|
118
|
+
if (this.isEligible()) this.render()
|
|
119
|
+
}, CLOCK_REFRESH_MS)
|
|
120
|
+
this.calendarTimer = BgTimer.setInterval(() => {
|
|
121
|
+
if (this.isEligible()) void this.refreshCalendarAndRender()
|
|
122
|
+
}, CALENDAR_REFRESH_MS)
|
|
123
|
+
this.eligible = this.isEligible()
|
|
124
|
+
if (this.eligible) {
|
|
125
|
+
this.ensureContextualDashboard()
|
|
126
|
+
void this.refreshCalendarAndRender()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Track eligible⇄ineligible transitions from any trigger (setting toggle,
|
|
132
|
+
* connect/disconnect, firmware change). Entering enables the contextual
|
|
133
|
+
* dashboard once — not on every settings write, so a user's manual
|
|
134
|
+
* contextual-dashboard choice is no longer ratcheted back. Leaving closes
|
|
135
|
+
* the dashboard, blanks the glasses view, and drops the retained scene so a
|
|
136
|
+
* stale frame can't be replayed after the feature was turned off.
|
|
137
|
+
*/
|
|
138
|
+
private syncEligibility(): boolean {
|
|
139
|
+
const eligible = this.isEligible()
|
|
140
|
+
if (eligible === this.eligible) return eligible
|
|
141
|
+
this.eligible = eligible
|
|
142
|
+
if (eligible) {
|
|
143
|
+
this.ensureContextualDashboard()
|
|
144
|
+
} else {
|
|
145
|
+
this.open = false
|
|
146
|
+
this.resetNavigation()
|
|
147
|
+
sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", [])
|
|
148
|
+
sceneRenderer.clearApp(DASHBOARD_APP_ID)
|
|
149
|
+
}
|
|
150
|
+
return eligible
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stop(): void {
|
|
154
|
+
for (const unsub of this.unsubs) unsub()
|
|
155
|
+
this.unsubs = []
|
|
156
|
+
if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)
|
|
157
|
+
if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)
|
|
158
|
+
this.clockTimer = null
|
|
159
|
+
this.calendarTimer = null
|
|
160
|
+
this.open = false
|
|
161
|
+
this.eligible = false
|
|
162
|
+
this.resetNavigation()
|
|
163
|
+
this.started = false
|
|
164
|
+
sceneRenderer.clearApp(DASHBOARD_APP_ID)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
isEligible(): boolean {
|
|
168
|
+
if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false
|
|
169
|
+
const state = useGlassesStore.getState()
|
|
170
|
+
return (
|
|
171
|
+
isGlassesConnected(state.connection) &&
|
|
172
|
+
isCustomDashboardEligible({
|
|
173
|
+
model: state.deviceModel,
|
|
174
|
+
firmwareVersion: state.firmwareVersion,
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
get isOpen(): boolean {
|
|
180
|
+
return this.open
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setHeadUp(up: boolean): void {
|
|
184
|
+
if (!this.isEligible()) {
|
|
185
|
+
this.open = false
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (up && !this.open) this.resetNavigation()
|
|
189
|
+
this.open = up
|
|
190
|
+
if (up) void this.refreshCalendarAndRender()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Toggle the custom dashboard for an eligible device. */
|
|
194
|
+
toggle(): boolean {
|
|
195
|
+
if (!this.isEligible()) return false
|
|
196
|
+
this.open = !this.open
|
|
197
|
+
if (this.open) {
|
|
198
|
+
this.resetNavigation()
|
|
199
|
+
void this.refreshCalendarAndRender()
|
|
200
|
+
}
|
|
201
|
+
void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
|
|
202
|
+
this.open = !this.open
|
|
203
|
+
console.error("CustomDashboard: toggleHeadUp failed", error)
|
|
204
|
+
})
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Returns true when an open custom dashboard consumed the gesture. */
|
|
209
|
+
handleGesture(gestureName: string): boolean {
|
|
210
|
+
if (!this.open || !this.isEligible()) return false
|
|
211
|
+
if (gestureName === "double_tap") {
|
|
212
|
+
// In the app menu a double tap backs out to the top of the dashboard
|
|
213
|
+
// (there is no Back row); on the main view it closes the dashboard.
|
|
214
|
+
if (this.view === "apps") {
|
|
215
|
+
this.resetNavigation()
|
|
216
|
+
this.render()
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
return this.toggle()
|
|
220
|
+
}
|
|
221
|
+
if (this.view === "apps") return this.handleAppMenuGesture(gestureName)
|
|
222
|
+
return this.handleMainGesture(gestureName)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private resetNavigation(): void {
|
|
226
|
+
this.view = "main"
|
|
227
|
+
this.mainIndex = 0
|
|
228
|
+
this.appsIndex = 0
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private handleMainGesture(gestureName: string): boolean {
|
|
232
|
+
const actions = dashboardActions(this.widgets())
|
|
233
|
+
if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
|
|
234
|
+
if (gestureName === "swipe_down" || gestureName === "swipe_up") {
|
|
235
|
+
if (actions.length === 0) return true
|
|
236
|
+
const delta = gestureName === "swipe_down" ? 1 : -1
|
|
237
|
+
this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length
|
|
238
|
+
this.render()
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
if (gestureName === "single_tap") {
|
|
242
|
+
const action = actions[this.mainIndex]
|
|
243
|
+
if (action?.kind === "apps-menu") {
|
|
244
|
+
this.view = "apps"
|
|
245
|
+
this.appsIndex = 0
|
|
246
|
+
this.render()
|
|
247
|
+
} else if (action?.kind === "notifications") {
|
|
248
|
+
void this.openNotificationsPanel()
|
|
249
|
+
}
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private handleAppMenuGesture(gestureName: string): boolean {
|
|
256
|
+
const rows = appMenuActions(this.menuApps())
|
|
257
|
+
if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
|
|
258
|
+
if (gestureName === "swipe_down" || gestureName === "swipe_up") {
|
|
259
|
+
const delta = gestureName === "swipe_down" ? 1 : -1
|
|
260
|
+
this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length
|
|
261
|
+
this.render()
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
if (gestureName === "single_tap") {
|
|
265
|
+
const row = rows[this.appsIndex]
|
|
266
|
+
if (row?.kind === "app") {
|
|
267
|
+
void this.toggleApp(row.packageName)
|
|
268
|
+
}
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private menuApps(): ClientApp[] {
|
|
275
|
+
const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null
|
|
276
|
+
return orderedDashboardApps(menu, useAppStatusStore.getState().apps)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private widgets(): DashboardWidgetSetting[] {
|
|
280
|
+
return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async close(): Promise<void> {
|
|
284
|
+
if (!this.open) return
|
|
285
|
+
this.open = false
|
|
286
|
+
await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
|
|
287
|
+
console.error("CustomDashboard: close failed", error)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async openNotificationsPanel(): Promise<void> {
|
|
292
|
+
await this.close()
|
|
293
|
+
await BluetoothSdk.showNativeNotificationPanel().catch((error) => {
|
|
294
|
+
console.error("CustomDashboard: notification panel failed", error)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async toggleApp(packageName: string): Promise<void> {
|
|
299
|
+
await this.close()
|
|
300
|
+
const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)
|
|
301
|
+
if (!app) return
|
|
302
|
+
if (app.running) {
|
|
303
|
+
await useAppStatusStore.getState().stop(app.packageName)
|
|
304
|
+
} else {
|
|
305
|
+
await useAppStatusStore.getState().start(app, {skipNavigation: true})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private calendarWidgetEnabled(): boolean {
|
|
310
|
+
return this.widgets().some((widget) => widget.id === "calendar" && widget.enabled)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private ensureContextualDashboard(): void {
|
|
314
|
+
const settings = useSettingsStore.getState()
|
|
315
|
+
if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {
|
|
316
|
+
// DeviceManager selects its retained dashboard Scene only while this is
|
|
317
|
+
// enabled. Custom firmware eligibility makes that dashboard automatic.
|
|
318
|
+
void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private async refreshCalendarAndRender(): Promise<void> {
|
|
323
|
+
if (!this.calendarWidgetEnabled()) {
|
|
324
|
+
this.calendarAvailable = false
|
|
325
|
+
this.calendarEvents = []
|
|
326
|
+
this.render()
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
const settings = useSettingsStore.getState()
|
|
330
|
+
const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)
|
|
331
|
+
const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)
|
|
332
|
+
const startsAt = new Date()
|
|
333
|
+
const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)
|
|
334
|
+
try {
|
|
335
|
+
const result = await listPhoneCalendarEvents({startsAt, endsAt, limit: Math.max(1, Math.min(3, count))})
|
|
336
|
+
this.calendarAvailable = true
|
|
337
|
+
this.calendarEvents = result.events
|
|
338
|
+
} catch {
|
|
339
|
+
// Permission is requested from the Custom Dashboard settings screen;
|
|
340
|
+
// without access the widget silently stays off the glasses.
|
|
341
|
+
this.calendarAvailable = false
|
|
342
|
+
this.calendarEvents = []
|
|
343
|
+
}
|
|
344
|
+
this.render()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private canvas(): {width: number; height: number} {
|
|
348
|
+
const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()
|
|
349
|
+
if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {
|
|
350
|
+
return {width: sceneCanvasWidth, height: sceneCanvasHeight}
|
|
351
|
+
}
|
|
352
|
+
return FALLBACK_CANVAS
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private render(): void {
|
|
356
|
+
if (!this.isEligible()) return
|
|
357
|
+
const elements = this.view === "apps" ? this.appMenuElements() : this.mainElements()
|
|
358
|
+
sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", elements)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private mainElements(): SceneElementInput[] {
|
|
362
|
+
const canvas = this.canvas()
|
|
363
|
+
const settings = useSettingsStore.getState()
|
|
364
|
+
const widgets = this.widgets().filter((widget) => widget.enabled)
|
|
365
|
+
const actions = dashboardActions(widgets)
|
|
366
|
+
if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)
|
|
367
|
+
const selected = actions[this.mainIndex]
|
|
368
|
+
|
|
369
|
+
const clockStyle =
|
|
370
|
+
(settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === "large"
|
|
371
|
+
? "large"
|
|
372
|
+
: "compact"
|
|
373
|
+
const calendarEnabled = widgets.some((widget) => widget.id === "calendar")
|
|
374
|
+
// Calendar lives in the right half; everything else stacks in the left
|
|
375
|
+
// half (full width when the calendar widget is off).
|
|
376
|
+
const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)
|
|
377
|
+
const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2
|
|
378
|
+
const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)
|
|
379
|
+
|
|
380
|
+
const elements: SceneElementInput[] = []
|
|
381
|
+
let y = MARGIN_TOP
|
|
382
|
+
for (const widget of widgets) {
|
|
383
|
+
if (widget.id === "clock") {
|
|
384
|
+
const fontSize = clockStyle === "large" ? 48 : 32
|
|
385
|
+
const clock = formatDashboardClock(new Date(), {
|
|
386
|
+
twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,
|
|
387
|
+
showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,
|
|
388
|
+
})
|
|
389
|
+
elements.push({
|
|
390
|
+
type: "text",
|
|
391
|
+
id: "clock",
|
|
392
|
+
box: {x: MARGIN_X, y, w: width, h: fontSize + 6},
|
|
393
|
+
text: clock.time,
|
|
394
|
+
style: {font: "mono", fontSize, breakMode: "strict-word"},
|
|
395
|
+
})
|
|
396
|
+
y += fontSize + 6
|
|
397
|
+
if (clock.date) {
|
|
398
|
+
elements.push({
|
|
399
|
+
type: "text",
|
|
400
|
+
id: "clock-date",
|
|
401
|
+
box: {x: MARGIN_X, y, w: width, h: LINE_H},
|
|
402
|
+
text: clock.date,
|
|
403
|
+
style: {font: "builtin", breakMode: "strict-word"},
|
|
404
|
+
})
|
|
405
|
+
y += LINE_H
|
|
406
|
+
}
|
|
407
|
+
y += WIDGET_GAP
|
|
408
|
+
} else if (widget.id === "calendar") {
|
|
409
|
+
// Right half, top-aligned, independent of the left stack. A missing
|
|
410
|
+
// calendar (permission not granted, fetch failed) shows a hint
|
|
411
|
+
// instead of silently vanishing.
|
|
412
|
+
const lines = this.calendarAvailable
|
|
413
|
+
? [
|
|
414
|
+
"Calendar",
|
|
415
|
+
...(this.calendarEvents.length
|
|
416
|
+
? this.calendarEvents.slice(0, 3).map(eventLine)
|
|
417
|
+
: ["No upcoming events"]),
|
|
418
|
+
]
|
|
419
|
+
: ["Calendar", "No access"]
|
|
420
|
+
elements.push({
|
|
421
|
+
type: "text",
|
|
422
|
+
id: "calendar",
|
|
423
|
+
box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},
|
|
424
|
+
text: lines.join("\n"),
|
|
425
|
+
style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
|
|
426
|
+
})
|
|
427
|
+
} else if (widget.id === "apps") {
|
|
428
|
+
const isSelected = selected?.kind === "apps-menu"
|
|
429
|
+
elements.push({
|
|
430
|
+
type: "text",
|
|
431
|
+
id: "apps-button",
|
|
432
|
+
box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
|
|
433
|
+
text: "Apps",
|
|
434
|
+
style: {font: "builtin", border: isSelected ? 2 : 0, radius: 8},
|
|
435
|
+
})
|
|
436
|
+
y += BUTTON_H + WIDGET_GAP
|
|
437
|
+
} else if (widget.id === "notifications") {
|
|
438
|
+
const isSelected = selected?.kind === "notifications"
|
|
439
|
+
elements.push({
|
|
440
|
+
type: "text",
|
|
441
|
+
id: "notifications",
|
|
442
|
+
box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
|
|
443
|
+
text: "Notifications",
|
|
444
|
+
style: {font: "builtin", border: isSelected ? 2 : 0, radius: 8},
|
|
445
|
+
})
|
|
446
|
+
y += BUTTON_H + WIDGET_GAP
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return elements
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Resolve a miniapp icon to base64 for the glasses. Local/bundled miniapps
|
|
454
|
+
* carry a file:// icon.png; store apps an https URL; the offline system
|
|
455
|
+
* tiles a bundler asset id (unsupported here — monogram fallback). Loads
|
|
456
|
+
* are async and cached; a finished load repaints the open app menu.
|
|
457
|
+
*/
|
|
458
|
+
private loadIcon(app: ClientApp): void {
|
|
459
|
+
if (this.iconCache.has(app.packageName) || this.iconLoads.has(app.packageName)) return
|
|
460
|
+
const url = typeof app.logoUrl === "string" ? app.logoUrl : ""
|
|
461
|
+
if (!/^(file|https?):/.test(url)) {
|
|
462
|
+
this.iconCache.set(app.packageName, null)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
this.iconLoads.add(app.packageName)
|
|
466
|
+
void (async () => {
|
|
467
|
+
try {
|
|
468
|
+
let base64: string
|
|
469
|
+
if (url.startsWith("file:")) {
|
|
470
|
+
base64 = await new File(url).base64()
|
|
471
|
+
} else {
|
|
472
|
+
const response = await fetch(url)
|
|
473
|
+
const blob = await response.blob()
|
|
474
|
+
base64 = await new Promise<string>((resolve, reject) => {
|
|
475
|
+
const reader = new FileReader()
|
|
476
|
+
reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "")
|
|
477
|
+
reader.onerror = () => reject(reader.error)
|
|
478
|
+
reader.readAsDataURL(blob)
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
this.iconCache.set(app.packageName, base64 || null)
|
|
482
|
+
} catch {
|
|
483
|
+
this.iconCache.set(app.packageName, null)
|
|
484
|
+
} finally {
|
|
485
|
+
this.iconLoads.delete(app.packageName)
|
|
486
|
+
if (this.open && this.view === "apps") this.render()
|
|
487
|
+
}
|
|
488
|
+
})()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private appMenuElements(): SceneElementInput[] {
|
|
492
|
+
const canvas = this.canvas()
|
|
493
|
+
const width = canvas.width - MARGIN_X * 2
|
|
494
|
+
const apps = this.menuApps()
|
|
495
|
+
const rows = appMenuActions(apps)
|
|
496
|
+
if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)
|
|
497
|
+
|
|
498
|
+
const elements: SceneElementInput[] = [
|
|
499
|
+
{
|
|
500
|
+
type: "text",
|
|
501
|
+
id: "apps-title",
|
|
502
|
+
box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},
|
|
503
|
+
text: "Apps",
|
|
504
|
+
style: {font: "builtin"},
|
|
505
|
+
},
|
|
506
|
+
]
|
|
507
|
+
const listTop = MARGIN_TOP + LINE_H + 16
|
|
508
|
+
if (apps.length === 0) {
|
|
509
|
+
elements.push({
|
|
510
|
+
type: "text",
|
|
511
|
+
id: "apps-empty",
|
|
512
|
+
box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},
|
|
513
|
+
text: "No apps in Glasses Menu",
|
|
514
|
+
style: {font: "builtin"},
|
|
515
|
+
})
|
|
516
|
+
return elements
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Icon grid, paged: the G2 firmware image-container pool is 4, so at most
|
|
520
|
+
// four icon images may be on a page. Selection is the bordered cell.
|
|
521
|
+
const perPage = APPS_PER_PAGE
|
|
522
|
+
const pageStart = Math.floor(this.appsIndex / perPage) * perPage
|
|
523
|
+
const page = apps.slice(pageStart, pageStart + perPage)
|
|
524
|
+
const totalW = page.length * APP_CELL + (page.length - 1) * APP_CELL_GAP
|
|
525
|
+
const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2))
|
|
526
|
+
page.forEach((app, column) => {
|
|
527
|
+
const x = startX + column * (APP_CELL + APP_CELL_GAP)
|
|
528
|
+
const selected = pageStart + column === this.appsIndex
|
|
529
|
+
const icon = this.iconCache.get(app.packageName)
|
|
530
|
+
if (icon === undefined) this.loadIcon(app)
|
|
531
|
+
if (icon) {
|
|
532
|
+
elements.push({
|
|
533
|
+
type: "image",
|
|
534
|
+
id: `app-icon:${app.packageName}`,
|
|
535
|
+
box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},
|
|
536
|
+
data: icon,
|
|
537
|
+
})
|
|
538
|
+
} else {
|
|
539
|
+
// Monogram fallback for icons that can't load (bundler-asset tiles).
|
|
540
|
+
elements.push({
|
|
541
|
+
type: "text",
|
|
542
|
+
id: `app-icon:${app.packageName}`,
|
|
543
|
+
box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},
|
|
544
|
+
text: (app.name.trim()[0] ?? "?").toUpperCase(),
|
|
545
|
+
style: {font: "mono", fontSize: 40},
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
if (selected) {
|
|
549
|
+
elements.push({
|
|
550
|
+
type: "rect",
|
|
551
|
+
id: "apps-selection",
|
|
552
|
+
box: {x, y: listTop, w: APP_CELL, h: APP_CELL},
|
|
553
|
+
style: {border: 2, radius: 8},
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
// Grayscale mini icons are ambiguous on their own: caption the selection.
|
|
559
|
+
const selectedApp = apps[this.appsIndex]
|
|
560
|
+
if (selectedApp) {
|
|
561
|
+
elements.push({
|
|
562
|
+
type: "text",
|
|
563
|
+
id: "apps-selected-name",
|
|
564
|
+
box: {x: MARGIN_X, y: listTop + APP_CELL + 12, w: width, h: LINE_H},
|
|
565
|
+
text: `${selectedApp.name}${selectedApp.running ? " ●" : ""}`,
|
|
566
|
+
style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
return elements
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const customDashboardService = new CustomDashboardService()
|
|
574
|
+
export default customDashboardService
|
|
575
|
+
export {CustomDashboardService}
|
|
@@ -20,7 +20,9 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
|
20
20
|
import {shallow} from "zustand/shallow"
|
|
21
21
|
|
|
22
22
|
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
23
|
+
import localDisplayManager from "./LocalDisplayManager"
|
|
23
24
|
import localSttFallbackCoordinator from "./LocalSttFallbackCoordinator"
|
|
25
|
+
import {MiniappExitGesture} from "./MiniappExitGesture"
|
|
24
26
|
import nativeNavigationService from "./NativeNavigationService"
|
|
25
27
|
import speakerLabelResolver from "./SpeakerLabelResolver"
|
|
26
28
|
import {phonePhotoCoordinator} from "./PhonePhotoCoordinator"
|
|
@@ -31,10 +33,23 @@ import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
|
31
33
|
import {useAppStatusStore} from "../stores/apps"
|
|
32
34
|
import {retirePendingSelectionOnPromotion} from "./PairingIdentity"
|
|
33
35
|
import GlobalEventEmitter from "../utils/GlobalEventEmitter"
|
|
36
|
+
import {BgTimer} from "../utils/timers"
|
|
34
37
|
import {asgCameraApi} from "./asg/asgCameraApi"
|
|
38
|
+
import customDashboardService from "./CustomDashboardService"
|
|
39
|
+
import gestureDotsService from "./GestureDotsService"
|
|
35
40
|
|
|
36
41
|
let subs: Array<{remove: () => void}> = []
|
|
37
42
|
|
|
43
|
+
const miniappExitGesture = new MiniappExitGesture({
|
|
44
|
+
getState: () => useAppStatusStore.getState(),
|
|
45
|
+
getVisiblePackageName: () => localDisplayManager.getVisibleMiniappPackageName(),
|
|
46
|
+
beginExitNotice: (packageName, text) => localDisplayManager.beginMiniappExit(packageName, text),
|
|
47
|
+
finishExitNotice: (packageName) => localDisplayManager.finishMiniappExit(packageName),
|
|
48
|
+
cancelExitNotice: (packageName) => localDisplayManager.cancelMiniappExit(packageName),
|
|
49
|
+
setTimeout: (callback, delay) => BgTimer.setTimeout(callback, delay),
|
|
50
|
+
clearTimeout: (timerId) => BgTimer.clearTimeout(timerId),
|
|
51
|
+
})
|
|
52
|
+
|
|
38
53
|
export function startDeviceEventRouter(): void {
|
|
39
54
|
if (subs.length) return
|
|
40
55
|
|
|
@@ -183,13 +198,38 @@ export function startDeviceEventRouter(): void {
|
|
|
183
198
|
)
|
|
184
199
|
subs.push(
|
|
185
200
|
BluetoothSdk.addListener("touch_event", (event) => {
|
|
201
|
+
// While the custom dashboard is visible it owns its focus/navigation
|
|
202
|
+
// gestures; do not leak those into a miniapp running underneath it.
|
|
203
|
+
if (customDashboardService.handleGesture(event.gestureName)) return
|
|
204
|
+
|
|
205
|
+
// Gesture-dots debug indicator (any device, gated by its setting). It
|
|
206
|
+
// only paints while the screen is otherwise empty; the gesture still
|
|
207
|
+
// flows to miniapps below.
|
|
208
|
+
gestureDotsService.noteGesture(event.gestureName)
|
|
209
|
+
|
|
186
210
|
localMiniappRuntime.forwardEvent("touch_event", event)
|
|
187
|
-
// Double-tap policy, moved out of the native G2 handlers:
|
|
188
|
-
//
|
|
189
|
-
// reports the gesture now.
|
|
211
|
+
// Double-tap policy, moved out of the native G2 handlers: exit a visible
|
|
212
|
+
// unclaimed miniapp; otherwise open the dashboard or toggle head-up.
|
|
213
|
+
// Native only reports the gesture now.
|
|
190
214
|
if (event.gestureName === "double_tap") {
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
// A foreground miniapp that claimed double_tap (CLAIM_GESTURES) is
|
|
216
|
+
// using it as an in-app gesture (e.g. the terminal's back-to-menu) —
|
|
217
|
+
// the event was already forwarded above; skip the system default.
|
|
218
|
+
if (localMiniappRuntime.foregroundClaimsGesture("double_tap")) return
|
|
219
|
+
|
|
220
|
+
// An unclaimed double tap exits only the miniapp whose frame is visibly
|
|
221
|
+
// on the glasses. The controller shows the bounded OS notice first,
|
|
222
|
+
// then uses the normal store stop path (and closes its phone overlay).
|
|
223
|
+
// Checked before the dashboard toggle: while an app owns the screen an
|
|
224
|
+
// unclaimed double-tap always means "leave the app" — the (custom or
|
|
225
|
+
// native) dashboard is for the empty screen, and head-up still opens
|
|
226
|
+
// the custom dashboard mid-app.
|
|
227
|
+
if (miniappExitGesture.tryExitVisibleMiniapp()) return
|
|
228
|
+
|
|
229
|
+
// Eligible G2 custom firmware replaces the native dashboard.
|
|
230
|
+
if (customDashboardService.toggle()) return
|
|
231
|
+
|
|
232
|
+
const useNativeDashboard = useSettingsStore.getState().getSetting(SETTINGS.use_native_dashboard.key) !== false
|
|
193
233
|
if (useNativeDashboard) {
|
|
194
234
|
// The modal nav UI owns the screen; showDashboard would fight it.
|
|
195
235
|
if (!nativeNavigationService.isActive) {
|
|
@@ -219,6 +259,7 @@ export function startDeviceEventRouter(): void {
|
|
|
219
259
|
// Head position — translate native {up:boolean} → SDK {position:"up"|"down"}.
|
|
220
260
|
subs.push(
|
|
221
261
|
BluetoothSdk.addListener("head_up", (event) => {
|
|
262
|
+
customDashboardService.setHeadUp(event.up)
|
|
222
263
|
localMiniappRuntime.forwardEvent("head_up", {position: event.up ? "up" : "down", timestamp: Date.now()})
|
|
223
264
|
}),
|
|
224
265
|
)
|
|
@@ -263,4 +304,5 @@ export function startDeviceEventRouter(): void {
|
|
|
263
304
|
export function stopDeviceEventRouter(): void {
|
|
264
305
|
for (const sub of subs) sub.remove()
|
|
265
306
|
subs = []
|
|
307
|
+
miniappExitGesture.reset()
|
|
266
308
|
}
|