@glassly/engine 0.1.0-dev.31 → 0.1.0-dev.34
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/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/CustomDashboardService.d.ts +2 -1
- package/build/services/CustomDashboardService.d.ts.map +1 -1
- package/build/services/CustomDashboardService.js +52 -29
- package/build/services/CustomDashboardService.js.map +1 -1
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +19 -5
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/package.json +6 -6
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/CustomDashboardService.ts +58 -36
- package/src/services/effectiveCapabilities.ts +20 -5
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const ENGINE_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.1.0",
|
|
4
|
-
"releaseIdentity": "0.1.0-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.1.0-dev.34",
|
|
5
|
+
"releaseSetId": "glassly-0.1.0-dev.34",
|
|
6
|
+
"sourceCommit": "ac0207b0f8b55aac462fac1017a22214ebb17cd7",
|
|
7
7
|
"otaManifestUrl": null,
|
|
8
8
|
"otaManifestSha256": null
|
|
9
9
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,uBAAuB,GAAoC,MAAM,CAAC,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,cAAc;IACjC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface EngineReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,uBAAuB,GAAoC,MAAM,CAAC,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,cAAc;IACjC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface EngineReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.34\",\n \"releaseSetId\": \"glassly-0.1.0-dev.34\",\n \"sourceCommit\": \"ac0207b0f8b55aac462fac1017a22214ebb17cd7\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -4,7 +4,8 @@ declare class CustomDashboardService {
|
|
|
4
4
|
private view;
|
|
5
5
|
private mainIndex;
|
|
6
6
|
private appsIndex;
|
|
7
|
-
|
|
7
|
+
/** "loading" until the first fetch resolves; "no-access" only for a real permission denial. */
|
|
8
|
+
private calendarStatus;
|
|
8
9
|
/** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */
|
|
9
10
|
private iconCache;
|
|
10
11
|
private iconLoads;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CustomDashboardService.d.ts","sourceRoot":"","sources":["../../src/services/CustomDashboardService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CustomDashboardService.d.ts","sourceRoot":"","sources":["../../src/services/CustomDashboardService.ts"],"names":[],"mappings":"AAwEA,cAAM,sBAAsB;IAC1B,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,SAAS,CAAI;IACrB,+FAA+F;IAC/F,OAAO,CAAC,cAAc,CAA4D;IAClF,kGAAkG;IAClG,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,aAAa,CAAsB;IAC3C,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAQ;IAExB,KAAK,IAAI,IAAI;IA+Cb;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAevB,IAAI,IAAI,IAAI;IAcZ,UAAU,IAAI,OAAO;IAYrB,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAU5B,0DAA0D;IAC1D,MAAM,IAAI,OAAO;IAcjB,uEAAuE;IACvE,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAgB3C,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,oBAAoB;IAmB5B,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,OAAO;YAID,KAAK;YAQL,sBAAsB;YAOtB,SAAS;IAWvB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,yBAAyB;YASnB,wBAAwB;IAiCtC,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,YAAY;IA8FpB;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IAiChB,OAAO,CAAC,eAAe;CAmFxB;AAED,QAAA,MAAM,sBAAsB,wBAA+B,CAAA;AAC3D,eAAe,sBAAsB,CAAA;AACrC,OAAO,EAAC,sBAAsB,EAAC,CAAA"}
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { File } from "expo-file-system";
|
|
14
14
|
import { BluetoothSdk } from "@glassly/bluetooth-sdk";
|
|
15
|
+
import { MiniappErrorCode } from "@glassly/miniapp";
|
|
15
16
|
import { useAppStatusStore } from "../stores/apps";
|
|
16
17
|
import { useGlassesStore } from "../stores/glasses";
|
|
17
18
|
import { SETTINGS, useSettingsStore } from "../stores/settings";
|
|
18
19
|
import { BgTimer } from "../utils/timers";
|
|
19
20
|
import { appMenuActions, dashboardActions, formatDashboardClock, isCustomDashboardEligible, normalizeDashboardWidgets, orderedDashboardApps, } from "./CustomDashboardModel";
|
|
20
21
|
import { isGlassesConnected } from "./GlassesReadiness";
|
|
21
|
-
import { listPhoneCalendarEvents } from "./PhoneCalendarService";
|
|
22
|
+
import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarService";
|
|
22
23
|
import sceneRenderer from "./SceneRenderer";
|
|
23
24
|
const DASHBOARD_APP_ID = "system.custom-dashboard";
|
|
24
25
|
const CLOCK_REFRESH_MS = 30_000;
|
|
@@ -32,11 +33,18 @@ const WIDGET_GAP = 10;
|
|
|
32
33
|
/** Row height for the glasses' builtin 20px font engine. */
|
|
33
34
|
const LINE_H = 24;
|
|
34
35
|
const BUTTON_H = 34;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
/**
|
|
37
|
+
* App-menu icon grid: 2 rows × 4 columns per page. CFW rasterizes scenes
|
|
38
|
+
* phone-side, so the stock firmware's 4-image container pool does not bound
|
|
39
|
+
* the page (getEffectiveCapabilities lifts the element caps while the CFW
|
|
40
|
+
* canvas is active).
|
|
41
|
+
*/
|
|
42
|
+
const APP_GRID_COLS = 4;
|
|
43
|
+
const APP_GRID_ROWS = 2;
|
|
44
|
+
const APPS_PER_PAGE = APP_GRID_COLS * APP_GRID_ROWS;
|
|
45
|
+
const APP_CELL = 36;
|
|
46
|
+
const APP_CELL_GAP = 12;
|
|
47
|
+
const APP_ICON_INSET = 2;
|
|
40
48
|
const APP_ICON = APP_CELL - APP_ICON_INSET * 2;
|
|
41
49
|
function eventLine(event) {
|
|
42
50
|
const when = event.allDay
|
|
@@ -50,7 +58,8 @@ class CustomDashboardService {
|
|
|
50
58
|
view = "main";
|
|
51
59
|
mainIndex = 0;
|
|
52
60
|
appsIndex = 0;
|
|
53
|
-
|
|
61
|
+
/** "loading" until the first fetch resolves; "no-access" only for a real permission denial. */
|
|
62
|
+
calendarStatus = "loading";
|
|
54
63
|
/** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */
|
|
55
64
|
iconCache = new Map();
|
|
56
65
|
iconLoads = new Set();
|
|
@@ -295,7 +304,7 @@ class CustomDashboardService {
|
|
|
295
304
|
}
|
|
296
305
|
async refreshCalendarAndRender() {
|
|
297
306
|
if (!this.calendarWidgetEnabled()) {
|
|
298
|
-
this.
|
|
307
|
+
this.calendarStatus = "loading";
|
|
299
308
|
this.calendarEvents = [];
|
|
300
309
|
this.render();
|
|
301
310
|
return;
|
|
@@ -306,15 +315,24 @@ class CustomDashboardService {
|
|
|
306
315
|
const startsAt = new Date();
|
|
307
316
|
const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000);
|
|
308
317
|
try {
|
|
309
|
-
const result = await listPhoneCalendarEvents({
|
|
310
|
-
|
|
318
|
+
const result = await listPhoneCalendarEvents({
|
|
319
|
+
startsAt: startsAt.toISOString(),
|
|
320
|
+
endsAt: endsAt.toISOString(),
|
|
321
|
+
limit: Math.max(1, Math.min(3, count)),
|
|
322
|
+
});
|
|
323
|
+
this.calendarStatus = "ok";
|
|
311
324
|
this.calendarEvents = result.events;
|
|
312
325
|
}
|
|
313
|
-
catch {
|
|
326
|
+
catch (error) {
|
|
314
327
|
// Permission is requested from the Custom Dashboard settings screen;
|
|
315
|
-
//
|
|
316
|
-
|
|
328
|
+
// only a real denial should read as one — any other failure is
|
|
329
|
+
// "Unavailable" (and logged) so it can't masquerade as a permission
|
|
330
|
+
// problem.
|
|
331
|
+
const denied = error instanceof PhoneCalendarError && error.code === MiniappErrorCode.PERMISSION_DENIED;
|
|
332
|
+
this.calendarStatus = denied ? "no-access" : "unavailable";
|
|
317
333
|
this.calendarEvents = [];
|
|
334
|
+
if (!denied)
|
|
335
|
+
console.warn("CustomDashboard: calendar fetch failed", error);
|
|
318
336
|
}
|
|
319
337
|
this.render();
|
|
320
338
|
}
|
|
@@ -379,16 +397,18 @@ class CustomDashboardService {
|
|
|
379
397
|
}
|
|
380
398
|
else if (widget.id === "calendar") {
|
|
381
399
|
// Right half, top-aligned, independent of the left stack. A missing
|
|
382
|
-
// calendar
|
|
383
|
-
//
|
|
384
|
-
const lines = this.
|
|
400
|
+
// calendar shows a hint instead of silently vanishing — "No access"
|
|
401
|
+
// strictly for permission denial, "Unavailable" for any other failure.
|
|
402
|
+
const lines = this.calendarStatus === "ok"
|
|
385
403
|
? [
|
|
386
404
|
"Calendar",
|
|
387
405
|
...(this.calendarEvents.length
|
|
388
406
|
? this.calendarEvents.slice(0, 3).map(eventLine)
|
|
389
407
|
: ["No upcoming events"]),
|
|
390
408
|
]
|
|
391
|
-
:
|
|
409
|
+
: this.calendarStatus === "loading"
|
|
410
|
+
? ["Calendar"]
|
|
411
|
+
: ["Calendar", this.calendarStatus === "no-access" ? "No access" : "Unavailable"];
|
|
392
412
|
elements.push({
|
|
393
413
|
type: "text",
|
|
394
414
|
id: "calendar",
|
|
@@ -492,16 +512,17 @@ class CustomDashboardService {
|
|
|
492
512
|
});
|
|
493
513
|
return elements;
|
|
494
514
|
}
|
|
495
|
-
// Icon grid, paged
|
|
496
|
-
// four icon images may be on a page. Selection is the bordered cell.
|
|
515
|
+
// Icon grid, paged, row-major. Selection is the bordered cell.
|
|
497
516
|
const perPage = APPS_PER_PAGE;
|
|
498
517
|
const pageStart = Math.floor(this.appsIndex / perPage) * perPage;
|
|
499
518
|
const page = apps.slice(pageStart, pageStart + perPage);
|
|
500
|
-
const
|
|
519
|
+
const cols = Math.min(page.length, APP_GRID_COLS);
|
|
520
|
+
const totalW = cols * APP_CELL + (cols - 1) * APP_CELL_GAP;
|
|
501
521
|
const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2));
|
|
502
|
-
page.forEach((app,
|
|
503
|
-
const x = startX +
|
|
504
|
-
const
|
|
522
|
+
page.forEach((app, cell) => {
|
|
523
|
+
const x = startX + (cell % APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP);
|
|
524
|
+
const y = listTop + Math.floor(cell / APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP);
|
|
525
|
+
const selected = pageStart + cell === this.appsIndex;
|
|
505
526
|
const icon = this.iconCache.get(app.packageName);
|
|
506
527
|
if (icon === undefined)
|
|
507
528
|
this.loadIcon(app);
|
|
@@ -509,7 +530,7 @@ class CustomDashboardService {
|
|
|
509
530
|
elements.push({
|
|
510
531
|
type: "image",
|
|
511
532
|
id: `app-icon:${app.packageName}`,
|
|
512
|
-
box: { x: x + APP_ICON_INSET, y:
|
|
533
|
+
box: { x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON },
|
|
513
534
|
data: icon,
|
|
514
535
|
});
|
|
515
536
|
}
|
|
@@ -518,27 +539,29 @@ class CustomDashboardService {
|
|
|
518
539
|
elements.push({
|
|
519
540
|
type: "text",
|
|
520
541
|
id: `app-icon:${app.packageName}`,
|
|
521
|
-
box: { x: x + APP_ICON_INSET, y:
|
|
542
|
+
box: { x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON },
|
|
522
543
|
text: (app.name.trim()[0] ?? "?").toUpperCase(),
|
|
523
|
-
style: { font: "mono", fontSize:
|
|
544
|
+
style: { font: "mono", fontSize: 20 },
|
|
524
545
|
});
|
|
525
546
|
}
|
|
526
547
|
if (selected) {
|
|
527
548
|
elements.push({
|
|
528
549
|
type: "rect",
|
|
529
550
|
id: "apps-selection",
|
|
530
|
-
box: { x, y
|
|
531
|
-
style: { border: 2, radius:
|
|
551
|
+
box: { x, y, w: APP_CELL, h: APP_CELL },
|
|
552
|
+
style: { border: 2, radius: 6 },
|
|
532
553
|
});
|
|
533
554
|
}
|
|
534
555
|
});
|
|
535
556
|
// Grayscale mini icons are ambiguous on their own: caption the selection.
|
|
557
|
+
const gridRows = Math.ceil(page.length / APP_GRID_COLS);
|
|
558
|
+
const gridH = gridRows * APP_CELL + (gridRows - 1) * APP_CELL_GAP;
|
|
536
559
|
const selectedApp = apps[this.appsIndex];
|
|
537
560
|
if (selectedApp) {
|
|
538
561
|
elements.push({
|
|
539
562
|
type: "text",
|
|
540
563
|
id: "apps-selected-name",
|
|
541
|
-
box: { x: MARGIN_X, y: listTop +
|
|
564
|
+
box: { x: MARGIN_X, y: listTop + gridH + 12, w: width, h: LINE_H },
|
|
542
565
|
text: `${selectedApp.name}${selectedApp.running ? " ●" : ""}`,
|
|
543
566
|
style: { font: "builtin", breakMode: "strict-word", overflow: "ellipsis" },
|
|
544
567
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CustomDashboardService.js","sourceRoot":"","sources":["../../src/services/CustomDashboardService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scene API dashboard for G2 custom firmware.\n *\n * G2.swift marks the published firmware version after it detects the CFW\n * capability advertisement. The service retains a dashboard frame, owns\n * dashboard gestures while head-up is active, and leaves every other device\n * on its native behavior.\n *\n * The dashboard is composed of user-orderable widgets (clock, calendar, apps\n * button, notifications button) stacked vertically on the CFW scene canvas.\n * The apps button switches to a second, dashboard-rendered app menu view.\n */\nimport {File} from \"expo-file-system\"\n\nimport {BluetoothSdk} from \"@glassly/bluetooth-sdk\"\nimport type {CalendarEvent} from \"@glassly/miniapp\"\n\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport type {ClientApp} from \"../types/applet\"\nimport type {SceneElementInput} from \"../utils/display/scene\"\nimport {BgTimer} from \"../utils/timers\"\nimport {\n appMenuActions,\n dashboardActions,\n formatDashboardClock,\n isCustomDashboardEligible,\n normalizeDashboardWidgets,\n orderedDashboardApps,\n type CustomDashboardClockStyle,\n type DashboardWidgetSetting,\n type MenuAppSetting,\n} from \"./CustomDashboardModel\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport {listPhoneCalendarEvents} from \"./PhoneCalendarService\"\nimport sceneRenderer from \"./SceneRenderer\"\n\nconst DASHBOARD_APP_ID = \"system.custom-dashboard\"\nconst CLOCK_REFRESH_MS = 30_000\nconst CALENDAR_REFRESH_MS = 5 * 60_000\n\n// CFW blits the stock compositor's 576×288 canvas; used when the firmware has\n// not reported scene capabilities yet.\nconst FALLBACK_CANVAS = {width: 576, height: 288}\nconst MARGIN_X = 16\nconst MARGIN_TOP = 10\nconst WIDGET_GAP = 10\n/** Row height for the glasses' builtin 20px font engine. */\nconst LINE_H = 24\nconst BUTTON_H = 34\n/** App-menu icon grid: 4 per page (G2 firmware image-container pool). */\nconst APPS_PER_PAGE = 4\nconst APP_CELL = 72\nconst APP_CELL_GAP = 16\nconst APP_ICON_INSET = 4\nconst APP_ICON = APP_CELL - APP_ICON_INSET * 2\n\nfunction eventLine(event: CalendarEvent): string {\n const when = event.allDay\n ? \"All day\"\n : new Intl.DateTimeFormat(undefined, {hour: \"numeric\", minute: \"2-digit\"}).format(new Date(event.startsAt))\n return `${when} ${event.title}`\n}\n\nclass CustomDashboardService {\n private started = false\n private open = false\n private view: \"main\" | \"apps\" = \"main\"\n private mainIndex = 0\n private appsIndex = 0\n private calendarAvailable = false\n /** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */\n private iconCache = new Map<string, string | null>()\n private iconLoads = new Set<string>()\n private calendarEvents: CalendarEvent[] = []\n private unsubs: Array<() => void> = []\n private clockTimer: number | null = null\n private calendarTimer: number | null = null\n /** Last observed eligibility, for transition detection. */\n private eligible = false\n\n start(): void {\n if (this.started) return\n this.started = true\n this.unsubs.push(\n useGlassesStore.subscribe(\n (state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n // Selector: only dashboard-relevant settings retrigger a refresh. The\n // whole-store form fired a calendar query on every settings write.\n useSettingsStore.subscribe(\n (state) =>\n JSON.stringify([\n state.getSetting(SETTINGS.custom_dashboard_enabled.key),\n state.getSetting(SETTINGS.custom_dashboard_widgets.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_style.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),\n state.getSetting(SETTINGS.twelve_hour_time.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),\n state.getSetting(SETTINGS.menu_apps.key),\n ]),\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n useAppStatusStore.subscribe(() => {\n if (this.isEligible()) this.render()\n }),\n )\n this.clockTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) this.render()\n }, CLOCK_REFRESH_MS)\n this.calendarTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) void this.refreshCalendarAndRender()\n }, CALENDAR_REFRESH_MS)\n this.eligible = this.isEligible()\n if (this.eligible) {\n this.ensureContextualDashboard()\n void this.refreshCalendarAndRender()\n }\n }\n\n /**\n * Track eligible⇄ineligible transitions from any trigger (setting toggle,\n * connect/disconnect, firmware change). Entering enables the contextual\n * dashboard once — not on every settings write, so a user's manual\n * contextual-dashboard choice is no longer ratcheted back. Leaving closes\n * the dashboard, blanks the glasses view, and drops the retained scene so a\n * stale frame can't be replayed after the feature was turned off.\n */\n private syncEligibility(): boolean {\n const eligible = this.isEligible()\n if (eligible === this.eligible) return eligible\n this.eligible = eligible\n if (eligible) {\n this.ensureContextualDashboard()\n } else {\n this.open = false\n this.resetNavigation()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", [])\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n return eligible\n }\n\n stop(): void {\n for (const unsub of this.unsubs) unsub()\n this.unsubs = []\n if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)\n if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)\n this.clockTimer = null\n this.calendarTimer = null\n this.open = false\n this.eligible = false\n this.resetNavigation()\n this.started = false\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n\n isEligible(): boolean {\n if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false\n const state = useGlassesStore.getState()\n return (\n isGlassesConnected(state.connection) &&\n isCustomDashboardEligible({\n model: state.deviceModel,\n firmwareVersion: state.firmwareVersion,\n })\n )\n }\n\n get isOpen(): boolean {\n return this.open\n }\n\n setHeadUp(up: boolean): void {\n if (!this.isEligible()) {\n this.open = false\n return\n }\n if (up && !this.open) this.resetNavigation()\n this.open = up\n if (up) void this.refreshCalendarAndRender()\n }\n\n /** Toggle the custom dashboard for an eligible device. */\n toggle(): boolean {\n if (!this.isEligible()) return false\n this.open = !this.open\n if (this.open) {\n this.resetNavigation()\n void this.refreshCalendarAndRender()\n }\n void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n this.open = !this.open\n console.error(\"CustomDashboard: toggleHeadUp failed\", error)\n })\n return true\n }\n\n /** Returns true when an open custom dashboard consumed the gesture. */\n handleGesture(gestureName: string): boolean {\n if (!this.open || !this.isEligible()) return false\n if (gestureName === \"double_tap\") {\n // In the app menu a double tap backs out to the top of the dashboard\n // (there is no Back row); on the main view it closes the dashboard.\n if (this.view === \"apps\") {\n this.resetNavigation()\n this.render()\n return true\n }\n return this.toggle()\n }\n if (this.view === \"apps\") return this.handleAppMenuGesture(gestureName)\n return this.handleMainGesture(gestureName)\n }\n\n private resetNavigation(): void {\n this.view = \"main\"\n this.mainIndex = 0\n this.appsIndex = 0\n }\n\n private handleMainGesture(gestureName: string): boolean {\n const actions = dashboardActions(this.widgets())\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n if (actions.length === 0) return true\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const action = actions[this.mainIndex]\n if (action?.kind === \"apps-menu\") {\n this.view = \"apps\"\n this.appsIndex = 0\n this.render()\n } else if (action?.kind === \"notifications\") {\n void this.openNotificationsPanel()\n }\n return true\n }\n return false\n }\n\n private handleAppMenuGesture(gestureName: string): boolean {\n const rows = appMenuActions(this.menuApps())\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const row = rows[this.appsIndex]\n if (row?.kind === \"app\") {\n void this.toggleApp(row.packageName)\n }\n return true\n }\n return false\n }\n\n private menuApps(): ClientApp[] {\n const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null\n return orderedDashboardApps(menu, useAppStatusStore.getState().apps)\n }\n\n private widgets(): DashboardWidgetSetting[] {\n return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))\n }\n\n private async close(): Promise<void> {\n if (!this.open) return\n this.open = false\n await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n console.error(\"CustomDashboard: close failed\", error)\n })\n }\n\n private async openNotificationsPanel(): Promise<void> {\n await this.close()\n await BluetoothSdk.showNativeNotificationPanel().catch((error) => {\n console.error(\"CustomDashboard: notification panel failed\", error)\n })\n }\n\n private async toggleApp(packageName: string): Promise<void> {\n await this.close()\n const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)\n if (!app) return\n if (app.running) {\n await useAppStatusStore.getState().stop(app.packageName)\n } else {\n await useAppStatusStore.getState().start(app, {skipNavigation: true})\n }\n }\n\n private calendarWidgetEnabled(): boolean {\n return this.widgets().some((widget) => widget.id === \"calendar\" && widget.enabled)\n }\n\n private ensureContextualDashboard(): void {\n const settings = useSettingsStore.getState()\n if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {\n // DeviceManager selects its retained dashboard Scene only while this is\n // enabled. Custom firmware eligibility makes that dashboard automatic.\n void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false)\n }\n }\n\n private async refreshCalendarAndRender(): Promise<void> {\n if (!this.calendarWidgetEnabled()) {\n this.calendarAvailable = false\n this.calendarEvents = []\n this.render()\n return\n }\n const settings = useSettingsStore.getState()\n const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)\n const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)\n const startsAt = new Date()\n const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)\n try {\n const result = await listPhoneCalendarEvents({startsAt, endsAt, limit: Math.max(1, Math.min(3, count))})\n this.calendarAvailable = true\n this.calendarEvents = result.events\n } catch {\n // Permission is requested from the Custom Dashboard settings screen;\n // without access the widget silently stays off the glasses.\n this.calendarAvailable = false\n this.calendarEvents = []\n }\n this.render()\n }\n\n private canvas(): {width: number; height: number} {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n }\n return FALLBACK_CANVAS\n }\n\n private render(): void {\n if (!this.isEligible()) return\n const elements = this.view === \"apps\" ? this.appMenuElements() : this.mainElements()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", elements)\n }\n\n private mainElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const settings = useSettingsStore.getState()\n const widgets = this.widgets().filter((widget) => widget.enabled)\n const actions = dashboardActions(widgets)\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n const selected = actions[this.mainIndex]\n\n const clockStyle =\n (settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === \"large\"\n ? \"large\"\n : \"compact\"\n const calendarEnabled = widgets.some((widget) => widget.id === \"calendar\")\n // Calendar lives in the right half; everything else stacks in the left\n // half (full width when the calendar widget is off).\n const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)\n const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2\n const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)\n\n const elements: SceneElementInput[] = []\n let y = MARGIN_TOP\n for (const widget of widgets) {\n if (widget.id === \"clock\") {\n const fontSize = clockStyle === \"large\" ? 48 : 32\n const clock = formatDashboardClock(new Date(), {\n twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,\n showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,\n })\n elements.push({\n type: \"text\",\n id: \"clock\",\n box: {x: MARGIN_X, y, w: width, h: fontSize + 6},\n text: clock.time,\n style: {font: \"mono\", fontSize, breakMode: \"strict-word\"},\n })\n y += fontSize + 6\n if (clock.date) {\n elements.push({\n type: \"text\",\n id: \"clock-date\",\n box: {x: MARGIN_X, y, w: width, h: LINE_H},\n text: clock.date,\n style: {font: \"builtin\", breakMode: \"strict-word\"},\n })\n y += LINE_H\n }\n y += WIDGET_GAP\n } else if (widget.id === \"calendar\") {\n // Right half, top-aligned, independent of the left stack. A missing\n // calendar (permission not granted, fetch failed) shows a hint\n // instead of silently vanishing.\n const lines = this.calendarAvailable\n ? [\n \"Calendar\",\n ...(this.calendarEvents.length\n ? this.calendarEvents.slice(0, 3).map(eventLine)\n : [\"No upcoming events\"]),\n ]\n : [\"Calendar\", \"No access\"]\n elements.push({\n type: \"text\",\n id: \"calendar\",\n box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},\n text: lines.join(\"\\n\"),\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n } else if (widget.id === \"apps\") {\n const isSelected = selected?.kind === \"apps-menu\"\n elements.push({\n type: \"text\",\n id: \"apps-button\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Apps\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n } else if (widget.id === \"notifications\") {\n const isSelected = selected?.kind === \"notifications\"\n elements.push({\n type: \"text\",\n id: \"notifications\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Notifications\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n }\n }\n return elements\n }\n\n /**\n * Resolve a miniapp icon to base64 for the glasses. Local/bundled miniapps\n * carry a file:// icon.png; store apps an https URL; the offline system\n * tiles a bundler asset id (unsupported here — monogram fallback). Loads\n * are async and cached; a finished load repaints the open app menu.\n */\n private loadIcon(app: ClientApp): void {\n if (this.iconCache.has(app.packageName) || this.iconLoads.has(app.packageName)) return\n const url = typeof app.logoUrl === \"string\" ? app.logoUrl : \"\"\n if (!/^(file|https?):/.test(url)) {\n this.iconCache.set(app.packageName, null)\n return\n }\n this.iconLoads.add(app.packageName)\n void (async () => {\n try {\n let base64: string\n if (url.startsWith(\"file:\")) {\n base64 = await new File(url).base64()\n } else {\n const response = await fetch(url)\n const blob = await response.blob()\n base64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\")\n reader.onerror = () => reject(reader.error)\n reader.readAsDataURL(blob)\n })\n }\n this.iconCache.set(app.packageName, base64 || null)\n } catch {\n this.iconCache.set(app.packageName, null)\n } finally {\n this.iconLoads.delete(app.packageName)\n if (this.open && this.view === \"apps\") this.render()\n }\n })()\n }\n\n private appMenuElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const width = canvas.width - MARGIN_X * 2\n const apps = this.menuApps()\n const rows = appMenuActions(apps)\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n\n const elements: SceneElementInput[] = [\n {\n type: \"text\",\n id: \"apps-title\",\n box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},\n text: \"Apps\",\n style: {font: \"builtin\"},\n },\n ]\n const listTop = MARGIN_TOP + LINE_H + 16\n if (apps.length === 0) {\n elements.push({\n type: \"text\",\n id: \"apps-empty\",\n box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},\n text: \"No apps in Glasses Menu\",\n style: {font: \"builtin\"},\n })\n return elements\n }\n\n // Icon grid, paged: the G2 firmware image-container pool is 4, so at most\n // four icon images may be on a page. Selection is the bordered cell.\n const perPage = APPS_PER_PAGE\n const pageStart = Math.floor(this.appsIndex / perPage) * perPage\n const page = apps.slice(pageStart, pageStart + perPage)\n const totalW = page.length * APP_CELL + (page.length - 1) * APP_CELL_GAP\n const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2))\n page.forEach((app, column) => {\n const x = startX + column * (APP_CELL + APP_CELL_GAP)\n const selected = pageStart + column === this.appsIndex\n const icon = this.iconCache.get(app.packageName)\n if (icon === undefined) this.loadIcon(app)\n if (icon) {\n elements.push({\n type: \"image\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n data: icon,\n })\n } else {\n // Monogram fallback for icons that can't load (bundler-asset tiles).\n elements.push({\n type: \"text\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: listTop + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n text: (app.name.trim()[0] ?? \"?\").toUpperCase(),\n style: {font: \"mono\", fontSize: 40},\n })\n }\n if (selected) {\n elements.push({\n type: \"rect\",\n id: \"apps-selection\",\n box: {x, y: listTop, w: APP_CELL, h: APP_CELL},\n style: {border: 2, radius: 8},\n })\n }\n })\n\n // Grayscale mini icons are ambiguous on their own: caption the selection.\n const selectedApp = apps[this.appsIndex]\n if (selectedApp) {\n elements.push({\n type: \"text\",\n id: \"apps-selected-name\",\n box: {x: MARGIN_X, y: listTop + APP_CELL + 12, w: width, h: LINE_H},\n text: `${selectedApp.name}${selectedApp.running ? \" ●\" : \"\"}`,\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n }\n return elements\n }\n}\n\nconst customDashboardService = new CustomDashboardService()\nexport default customDashboardService\nexport {CustomDashboardService}\n"]}
|
|
1
|
+
{"version":3,"file":"CustomDashboardService.js","sourceRoot":"","sources":["../../src/services/CustomDashboardService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scene API dashboard for G2 custom firmware.\n *\n * G2.swift marks the published firmware version after it detects the CFW\n * capability advertisement. The service retains a dashboard frame, owns\n * dashboard gestures while head-up is active, and leaves every other device\n * on its native behavior.\n *\n * The dashboard is composed of user-orderable widgets (clock, calendar, apps\n * button, notifications button) stacked vertically on the CFW scene canvas.\n * The apps button switches to a second, dashboard-rendered app menu view.\n */\nimport {File} from \"expo-file-system\"\n\nimport {BluetoothSdk} from \"@glassly/bluetooth-sdk\"\nimport {MiniappErrorCode, type CalendarEvent} from \"@glassly/miniapp\"\n\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport type {ClientApp} from \"../types/applet\"\nimport type {SceneElementInput} from \"../utils/display/scene\"\nimport {BgTimer} from \"../utils/timers\"\nimport {\n appMenuActions,\n dashboardActions,\n formatDashboardClock,\n isCustomDashboardEligible,\n normalizeDashboardWidgets,\n orderedDashboardApps,\n type CustomDashboardClockStyle,\n type DashboardWidgetSetting,\n type MenuAppSetting,\n} from \"./CustomDashboardModel\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport {listPhoneCalendarEvents, PhoneCalendarError} from \"./PhoneCalendarService\"\nimport sceneRenderer from \"./SceneRenderer\"\n\nconst DASHBOARD_APP_ID = \"system.custom-dashboard\"\nconst CLOCK_REFRESH_MS = 30_000\nconst CALENDAR_REFRESH_MS = 5 * 60_000\n\n// CFW blits the stock compositor's 576×288 canvas; used when the firmware has\n// not reported scene capabilities yet.\nconst FALLBACK_CANVAS = {width: 576, height: 288}\nconst MARGIN_X = 16\nconst MARGIN_TOP = 10\nconst WIDGET_GAP = 10\n/** Row height for the glasses' builtin 20px font engine. */\nconst LINE_H = 24\nconst BUTTON_H = 34\n/**\n * App-menu icon grid: 2 rows × 4 columns per page. CFW rasterizes scenes\n * phone-side, so the stock firmware's 4-image container pool does not bound\n * the page (getEffectiveCapabilities lifts the element caps while the CFW\n * canvas is active).\n */\nconst APP_GRID_COLS = 4\nconst APP_GRID_ROWS = 2\nconst APPS_PER_PAGE = APP_GRID_COLS * APP_GRID_ROWS\nconst APP_CELL = 36\nconst APP_CELL_GAP = 12\nconst APP_ICON_INSET = 2\nconst APP_ICON = APP_CELL - APP_ICON_INSET * 2\n\nfunction eventLine(event: CalendarEvent): string {\n const when = event.allDay\n ? \"All day\"\n : new Intl.DateTimeFormat(undefined, {hour: \"numeric\", minute: \"2-digit\"}).format(new Date(event.startsAt))\n return `${when} ${event.title}`\n}\n\nclass CustomDashboardService {\n private started = false\n private open = false\n private view: \"main\" | \"apps\" = \"main\"\n private mainIndex = 0\n private appsIndex = 0\n /** \"loading\" until the first fetch resolves; \"no-access\" only for a real permission denial. */\n private calendarStatus: \"loading\" | \"ok\" | \"no-access\" | \"unavailable\" = \"loading\"\n /** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */\n private iconCache = new Map<string, string | null>()\n private iconLoads = new Set<string>()\n private calendarEvents: CalendarEvent[] = []\n private unsubs: Array<() => void> = []\n private clockTimer: number | null = null\n private calendarTimer: number | null = null\n /** Last observed eligibility, for transition detection. */\n private eligible = false\n\n start(): void {\n if (this.started) return\n this.started = true\n this.unsubs.push(\n useGlassesStore.subscribe(\n (state) => `${state.connection.state}:${state.deviceModel}:${state.firmwareVersion}`,\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n // Selector: only dashboard-relevant settings retrigger a refresh. The\n // whole-store form fired a calendar query on every settings write.\n useSettingsStore.subscribe(\n (state) =>\n JSON.stringify([\n state.getSetting(SETTINGS.custom_dashboard_enabled.key),\n state.getSetting(SETTINGS.custom_dashboard_widgets.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_style.key),\n state.getSetting(SETTINGS.custom_dashboard_clock_show_date.key),\n state.getSetting(SETTINGS.twelve_hour_time.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key),\n state.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key),\n state.getSetting(SETTINGS.menu_apps.key),\n ]),\n () => {\n if (!this.syncEligibility()) return\n void this.refreshCalendarAndRender()\n },\n ),\n useAppStatusStore.subscribe(() => {\n if (this.isEligible()) this.render()\n }),\n )\n this.clockTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) this.render()\n }, CLOCK_REFRESH_MS)\n this.calendarTimer = BgTimer.setInterval(() => {\n if (this.isEligible()) void this.refreshCalendarAndRender()\n }, CALENDAR_REFRESH_MS)\n this.eligible = this.isEligible()\n if (this.eligible) {\n this.ensureContextualDashboard()\n void this.refreshCalendarAndRender()\n }\n }\n\n /**\n * Track eligible⇄ineligible transitions from any trigger (setting toggle,\n * connect/disconnect, firmware change). Entering enables the contextual\n * dashboard once — not on every settings write, so a user's manual\n * contextual-dashboard choice is no longer ratcheted back. Leaving closes\n * the dashboard, blanks the glasses view, and drops the retained scene so a\n * stale frame can't be replayed after the feature was turned off.\n */\n private syncEligibility(): boolean {\n const eligible = this.isEligible()\n if (eligible === this.eligible) return eligible\n this.eligible = eligible\n if (eligible) {\n this.ensureContextualDashboard()\n } else {\n this.open = false\n this.resetNavigation()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", [])\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n return eligible\n }\n\n stop(): void {\n for (const unsub of this.unsubs) unsub()\n this.unsubs = []\n if (this.clockTimer !== null) BgTimer.clearInterval(this.clockTimer)\n if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)\n this.clockTimer = null\n this.calendarTimer = null\n this.open = false\n this.eligible = false\n this.resetNavigation()\n this.started = false\n sceneRenderer.clearApp(DASHBOARD_APP_ID)\n }\n\n isEligible(): boolean {\n if (useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_enabled.key) === false) return false\n const state = useGlassesStore.getState()\n return (\n isGlassesConnected(state.connection) &&\n isCustomDashboardEligible({\n model: state.deviceModel,\n firmwareVersion: state.firmwareVersion,\n })\n )\n }\n\n get isOpen(): boolean {\n return this.open\n }\n\n setHeadUp(up: boolean): void {\n if (!this.isEligible()) {\n this.open = false\n return\n }\n if (up && !this.open) this.resetNavigation()\n this.open = up\n if (up) void this.refreshCalendarAndRender()\n }\n\n /** Toggle the custom dashboard for an eligible device. */\n toggle(): boolean {\n if (!this.isEligible()) return false\n this.open = !this.open\n if (this.open) {\n this.resetNavigation()\n void this.refreshCalendarAndRender()\n }\n void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n this.open = !this.open\n console.error(\"CustomDashboard: toggleHeadUp failed\", error)\n })\n return true\n }\n\n /** Returns true when an open custom dashboard consumed the gesture. */\n handleGesture(gestureName: string): boolean {\n if (!this.open || !this.isEligible()) return false\n if (gestureName === \"double_tap\") {\n // In the app menu a double tap backs out to the top of the dashboard\n // (there is no Back row); on the main view it closes the dashboard.\n if (this.view === \"apps\") {\n this.resetNavigation()\n this.render()\n return true\n }\n return this.toggle()\n }\n if (this.view === \"apps\") return this.handleAppMenuGesture(gestureName)\n return this.handleMainGesture(gestureName)\n }\n\n private resetNavigation(): void {\n this.view = \"main\"\n this.mainIndex = 0\n this.appsIndex = 0\n }\n\n private handleMainGesture(gestureName: string): boolean {\n const actions = dashboardActions(this.widgets())\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n if (actions.length === 0) return true\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.mainIndex = (this.mainIndex + delta + actions.length) % actions.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const action = actions[this.mainIndex]\n if (action?.kind === \"apps-menu\") {\n this.view = \"apps\"\n this.appsIndex = 0\n this.render()\n } else if (action?.kind === \"notifications\") {\n void this.openNotificationsPanel()\n }\n return true\n }\n return false\n }\n\n private handleAppMenuGesture(gestureName: string): boolean {\n const rows = appMenuActions(this.menuApps())\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n if (gestureName === \"swipe_down\" || gestureName === \"swipe_up\") {\n const delta = gestureName === \"swipe_down\" ? 1 : -1\n this.appsIndex = (this.appsIndex + delta + rows.length) % rows.length\n this.render()\n return true\n }\n if (gestureName === \"single_tap\") {\n const row = rows[this.appsIndex]\n if (row?.kind === \"app\") {\n void this.toggleApp(row.packageName)\n }\n return true\n }\n return false\n }\n\n private menuApps(): ClientApp[] {\n const menu = useSettingsStore.getState().getSetting(SETTINGS.menu_apps.key) as MenuAppSetting[] | null\n return orderedDashboardApps(menu, useAppStatusStore.getState().apps)\n }\n\n private widgets(): DashboardWidgetSetting[] {\n return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))\n }\n\n private async close(): Promise<void> {\n if (!this.open) return\n this.open = false\n await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {\n console.error(\"CustomDashboard: close failed\", error)\n })\n }\n\n private async openNotificationsPanel(): Promise<void> {\n await this.close()\n await BluetoothSdk.showNativeNotificationPanel().catch((error) => {\n console.error(\"CustomDashboard: notification panel failed\", error)\n })\n }\n\n private async toggleApp(packageName: string): Promise<void> {\n await this.close()\n const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)\n if (!app) return\n if (app.running) {\n await useAppStatusStore.getState().stop(app.packageName)\n } else {\n await useAppStatusStore.getState().start(app, {skipNavigation: true})\n }\n }\n\n private calendarWidgetEnabled(): boolean {\n return this.widgets().some((widget) => widget.id === \"calendar\" && widget.enabled)\n }\n\n private ensureContextualDashboard(): void {\n const settings = useSettingsStore.getState()\n if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {\n // DeviceManager selects its retained dashboard Scene only while this is\n // enabled. Custom firmware eligibility makes that dashboard automatic.\n void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false)\n }\n }\n\n private async refreshCalendarAndRender(): Promise<void> {\n if (!this.calendarWidgetEnabled()) {\n this.calendarStatus = \"loading\"\n this.calendarEvents = []\n this.render()\n return\n }\n const settings = useSettingsStore.getState()\n const count = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_event_count.key) ?? 2)\n const days = Number(settings.getSetting(SETTINGS.custom_dashboard_calendar_days_ahead.key) ?? 3)\n const startsAt = new Date()\n const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)\n try {\n const result = await listPhoneCalendarEvents({\n startsAt: startsAt.toISOString(),\n endsAt: endsAt.toISOString(),\n limit: Math.max(1, Math.min(3, count)),\n })\n this.calendarStatus = \"ok\"\n this.calendarEvents = result.events\n } catch (error) {\n // Permission is requested from the Custom Dashboard settings screen;\n // only a real denial should read as one — any other failure is\n // \"Unavailable\" (and logged) so it can't masquerade as a permission\n // problem.\n const denied = error instanceof PhoneCalendarError && error.code === MiniappErrorCode.PERMISSION_DENIED\n this.calendarStatus = denied ? \"no-access\" : \"unavailable\"\n this.calendarEvents = []\n if (!denied) console.warn(\"CustomDashboard: calendar fetch failed\", error)\n }\n this.render()\n }\n\n private canvas(): {width: number; height: number} {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n }\n return FALLBACK_CANVAS\n }\n\n private render(): void {\n if (!this.isEligible()) return\n const elements = this.view === \"apps\" ? this.appMenuElements() : this.mainElements()\n sceneRenderer.emitScene(DASHBOARD_APP_ID, \"dashboard\", elements)\n }\n\n private mainElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const settings = useSettingsStore.getState()\n const widgets = this.widgets().filter((widget) => widget.enabled)\n const actions = dashboardActions(widgets)\n if (this.mainIndex >= actions.length) this.mainIndex = Math.max(0, actions.length - 1)\n const selected = actions[this.mainIndex]\n\n const clockStyle =\n (settings.getSetting(SETTINGS.custom_dashboard_clock_style.key) as CustomDashboardClockStyle) === \"large\"\n ? \"large\"\n : \"compact\"\n const calendarEnabled = widgets.some((widget) => widget.id === \"calendar\")\n // Calendar lives in the right half; everything else stacks in the left\n // half (full width when the calendar widget is off).\n const halfW = Math.floor(canvas.width / 2) - MARGIN_X - Math.floor(WIDGET_GAP / 2)\n const width = calendarEnabled ? halfW : canvas.width - MARGIN_X * 2\n const rightX = Math.floor(canvas.width / 2) + Math.floor(WIDGET_GAP / 2)\n\n const elements: SceneElementInput[] = []\n let y = MARGIN_TOP\n for (const widget of widgets) {\n if (widget.id === \"clock\") {\n const fontSize = clockStyle === \"large\" ? 48 : 32\n const clock = formatDashboardClock(new Date(), {\n twelveHour: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,\n showDate: settings.getSetting(SETTINGS.custom_dashboard_clock_show_date.key) !== false,\n })\n elements.push({\n type: \"text\",\n id: \"clock\",\n box: {x: MARGIN_X, y, w: width, h: fontSize + 6},\n text: clock.time,\n style: {font: \"mono\", fontSize, breakMode: \"strict-word\"},\n })\n y += fontSize + 6\n if (clock.date) {\n elements.push({\n type: \"text\",\n id: \"clock-date\",\n box: {x: MARGIN_X, y, w: width, h: LINE_H},\n text: clock.date,\n style: {font: \"builtin\", breakMode: \"strict-word\"},\n })\n y += LINE_H\n }\n y += WIDGET_GAP\n } else if (widget.id === \"calendar\") {\n // Right half, top-aligned, independent of the left stack. A missing\n // calendar shows a hint instead of silently vanishing — \"No access\"\n // strictly for permission denial, \"Unavailable\" for any other failure.\n const lines =\n this.calendarStatus === \"ok\"\n ? [\n \"Calendar\",\n ...(this.calendarEvents.length\n ? this.calendarEvents.slice(0, 3).map(eventLine)\n : [\"No upcoming events\"]),\n ]\n : this.calendarStatus === \"loading\"\n ? [\"Calendar\"]\n : [\"Calendar\", this.calendarStatus === \"no-access\" ? \"No access\" : \"Unavailable\"]\n elements.push({\n type: \"text\",\n id: \"calendar\",\n box: {x: rightX, y: MARGIN_TOP, w: halfW, h: lines.length * LINE_H},\n text: lines.join(\"\\n\"),\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n } else if (widget.id === \"apps\") {\n const isSelected = selected?.kind === \"apps-menu\"\n elements.push({\n type: \"text\",\n id: \"apps-button\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Apps\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n } else if (widget.id === \"notifications\") {\n const isSelected = selected?.kind === \"notifications\"\n elements.push({\n type: \"text\",\n id: \"notifications\",\n box: {x: MARGIN_X, y, w: width, h: BUTTON_H},\n text: \"Notifications\",\n style: {font: \"builtin\", border: isSelected ? 2 : 0, radius: 8},\n })\n y += BUTTON_H + WIDGET_GAP\n }\n }\n return elements\n }\n\n /**\n * Resolve a miniapp icon to base64 for the glasses. Local/bundled miniapps\n * carry a file:// icon.png; store apps an https URL; the offline system\n * tiles a bundler asset id (unsupported here — monogram fallback). Loads\n * are async and cached; a finished load repaints the open app menu.\n */\n private loadIcon(app: ClientApp): void {\n if (this.iconCache.has(app.packageName) || this.iconLoads.has(app.packageName)) return\n const url = typeof app.logoUrl === \"string\" ? app.logoUrl : \"\"\n if (!/^(file|https?):/.test(url)) {\n this.iconCache.set(app.packageName, null)\n return\n }\n this.iconLoads.add(app.packageName)\n void (async () => {\n try {\n let base64: string\n if (url.startsWith(\"file:\")) {\n base64 = await new File(url).base64()\n } else {\n const response = await fetch(url)\n const blob = await response.blob()\n base64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\")\n reader.onerror = () => reject(reader.error)\n reader.readAsDataURL(blob)\n })\n }\n this.iconCache.set(app.packageName, base64 || null)\n } catch {\n this.iconCache.set(app.packageName, null)\n } finally {\n this.iconLoads.delete(app.packageName)\n if (this.open && this.view === \"apps\") this.render()\n }\n })()\n }\n\n private appMenuElements(): SceneElementInput[] {\n const canvas = this.canvas()\n const width = canvas.width - MARGIN_X * 2\n const apps = this.menuApps()\n const rows = appMenuActions(apps)\n if (this.appsIndex >= rows.length) this.appsIndex = Math.max(0, rows.length - 1)\n\n const elements: SceneElementInput[] = [\n {\n type: \"text\",\n id: \"apps-title\",\n box: {x: MARGIN_X, y: MARGIN_TOP, w: width, h: LINE_H},\n text: \"Apps\",\n style: {font: \"builtin\"},\n },\n ]\n const listTop = MARGIN_TOP + LINE_H + 16\n if (apps.length === 0) {\n elements.push({\n type: \"text\",\n id: \"apps-empty\",\n box: {x: MARGIN_X, y: listTop, w: width, h: LINE_H},\n text: \"No apps in Glasses Menu\",\n style: {font: \"builtin\"},\n })\n return elements\n }\n\n // Icon grid, paged, row-major. Selection is the bordered cell.\n const perPage = APPS_PER_PAGE\n const pageStart = Math.floor(this.appsIndex / perPage) * perPage\n const page = apps.slice(pageStart, pageStart + perPage)\n const cols = Math.min(page.length, APP_GRID_COLS)\n const totalW = cols * APP_CELL + (cols - 1) * APP_CELL_GAP\n const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2))\n page.forEach((app, cell) => {\n const x = startX + (cell % APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP)\n const y = listTop + Math.floor(cell / APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP)\n const selected = pageStart + cell === this.appsIndex\n const icon = this.iconCache.get(app.packageName)\n if (icon === undefined) this.loadIcon(app)\n if (icon) {\n elements.push({\n type: \"image\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n data: icon,\n })\n } else {\n // Monogram fallback for icons that can't load (bundler-asset tiles).\n elements.push({\n type: \"text\",\n id: `app-icon:${app.packageName}`,\n box: {x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},\n text: (app.name.trim()[0] ?? \"?\").toUpperCase(),\n style: {font: \"mono\", fontSize: 20},\n })\n }\n if (selected) {\n elements.push({\n type: \"rect\",\n id: \"apps-selection\",\n box: {x, y, w: APP_CELL, h: APP_CELL},\n style: {border: 2, radius: 6},\n })\n }\n })\n\n // Grayscale mini icons are ambiguous on their own: caption the selection.\n const gridRows = Math.ceil(page.length / APP_GRID_COLS)\n const gridH = gridRows * APP_CELL + (gridRows - 1) * APP_CELL_GAP\n const selectedApp = apps[this.appsIndex]\n if (selectedApp) {\n elements.push({\n type: \"text\",\n id: \"apps-selected-name\",\n box: {x: MARGIN_X, y: listTop + gridH + 12, w: width, h: LINE_H},\n text: `${selectedApp.name}${selectedApp.running ? \" ●\" : \"\"}`,\n style: {font: \"builtin\", breakMode: \"strict-word\", overflow: \"ellipsis\"},\n })\n }\n return elements\n }\n}\n\nconst customDashboardService = new CustomDashboardService()\nexport default customDashboardService\nexport {CustomDashboardService}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAWA,OAAO,EAAuB,KAAK,YAAY,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAA;AAKlF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAI3D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAczE"}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Effective device capabilities — the static model profile plus what the
|
|
3
|
-
* connected firmware reports at runtime.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* connected firmware reports at runtime. The runtime override is the scene
|
|
4
|
+
* canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into
|
|
5
|
+
* the 640×480 panel, so the drawable canvas grows past stock's 576×288.
|
|
6
|
+
* Native publishes the canvas into the glasses status (0 = none). Because
|
|
7
|
+
* that path is phone-side rasterization, the stock firmware's container
|
|
8
|
+
* pools (6 text / 4 image) don't bound a CFW frame either — element caps are
|
|
9
|
+
* lifted alongside the canvas, bounded only to keep frames sane.
|
|
7
10
|
*/
|
|
8
11
|
import { useGlassesStore } from "../stores/glasses";
|
|
9
12
|
import { getModelCapabilities } from "../types";
|
|
13
|
+
const CFW_MAX_TEXT_ELEMENTS = 24;
|
|
14
|
+
const CFW_MAX_IMAGE_ELEMENTS = 16;
|
|
10
15
|
export function runtimeSceneCanvas() {
|
|
11
16
|
const { sceneCanvasWidth, sceneCanvasHeight } = useGlassesStore.getState();
|
|
12
17
|
if (!sceneCanvasWidth || !sceneCanvasHeight || sceneCanvasWidth <= 0 || sceneCanvasHeight <= 0)
|
|
@@ -18,6 +23,15 @@ export function getEffectiveCapabilities(model) {
|
|
|
18
23
|
const canvas = runtimeSceneCanvas();
|
|
19
24
|
if (!base.display || !canvas)
|
|
20
25
|
return base;
|
|
21
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
...base,
|
|
28
|
+
display: {
|
|
29
|
+
...base.display,
|
|
30
|
+
width: canvas.width,
|
|
31
|
+
height: canvas.height,
|
|
32
|
+
maxTextElements: Math.max(base.display.maxTextElements ?? 0, CFW_MAX_TEXT_ELEMENTS),
|
|
33
|
+
maxImageElements: Math.max(base.display.maxImageElements ?? 0, CFW_MAX_IMAGE_ELEMENTS),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
22
36
|
}
|
|
23
37
|
//# sourceMappingURL=effectiveCapabilities.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAC,oBAAoB,EAAsC,MAAM,UAAU,CAAA;AAElF,MAAM,qBAAqB,GAAG,EAAE,CAAA;AAChC,MAAM,sBAAsB,GAAG,EAAE,CAAA;AAOjC,MAAM,UAAU,kBAAkB;IAChC,MAAM,EAAC,gBAAgB,EAAE,iBAAiB,EAAC,GAAG,eAAe,CAAC,QAAQ,EAAE,CAAA;IACxE,IAAI,CAAC,gBAAgB,IAAI,CAAC,iBAAiB,IAAI,gBAAgB,IAAI,CAAC,IAAI,iBAAiB,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3G,OAAO,EAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,iBAAiB,EAAC,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAkB;IACzD,MAAM,IAAI,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACzC,OAAO;QACL,GAAG,IAAI;QACP,OAAO,EAAE;YACP,GAAG,IAAI,CAAC,OAAO;YACf,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,IAAI,CAAC,EAAE,qBAAqB,CAAC;YACnF,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,IAAI,CAAC,EAAE,sBAAsB,CAAC;SACvF;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Effective device capabilities — the static model profile plus what the\n * connected firmware reports at runtime. The runtime override is the scene\n * canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into\n * the 640×480 panel, so the drawable canvas grows past stock's 576×288.\n * Native publishes the canvas into the glasses status (0 = none). Because\n * that path is phone-side rasterization, the stock firmware's container\n * pools (6 text / 4 image) don't bound a CFW frame either — element caps are\n * lifted alongside the canvas, bounded only to keep frames sane.\n */\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {getModelCapabilities, type Capabilities, type DeviceTypes} from \"../types\"\n\nconst CFW_MAX_TEXT_ELEMENTS = 24\nconst CFW_MAX_IMAGE_ELEMENTS = 16\n\nexport interface SceneCanvasSize {\n width: number\n height: number\n}\n\nexport function runtimeSceneCanvas(): SceneCanvasSize | null {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (!sceneCanvasWidth || !sceneCanvasHeight || sceneCanvasWidth <= 0 || sceneCanvasHeight <= 0) return null\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n}\n\nexport function getEffectiveCapabilities(model: DeviceTypes): Capabilities {\n const base = getModelCapabilities(model)\n const canvas = runtimeSceneCanvas()\n if (!base.display || !canvas) return base\n return {\n ...base,\n display: {\n ...base.display,\n width: canvas.width,\n height: canvas.height,\n maxTextElements: Math.max(base.display.maxTextElements ?? 0, CFW_MAX_TEXT_ELEMENTS),\n maxImageElements: Math.max(base.display.maxImageElements ?? 0, CFW_MAX_IMAGE_ELEMENTS),\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.34",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,11 +97,11 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.1.0-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.1.0-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.1.0-dev.
|
|
103
|
-
"@glassly/crust": "0.1.0-dev.
|
|
104
|
-
"@glassly/miniapp": "0.1.0-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.1.0-dev.34",
|
|
101
|
+
"@glassly/cloud-client": "0.1.0-dev.34",
|
|
102
|
+
"@glassly/cloud-protocol": "0.1.0-dev.34",
|
|
103
|
+
"@glassly/crust": "0.1.0-dev.34",
|
|
104
|
+
"@glassly/miniapp": "0.1.0-dev.34",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"semver": "^7.7.4",
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.1.0",
|
|
15
|
-
"releaseIdentity": "0.1.0-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.1.0-dev.34",
|
|
16
|
+
"releaseSetId": "glassly-0.1.0-dev.34",
|
|
17
|
+
"sourceCommit": "ac0207b0f8b55aac462fac1017a22214ebb17cd7",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import {File} from "expo-file-system"
|
|
14
14
|
|
|
15
15
|
import {BluetoothSdk} from "@glassly/bluetooth-sdk"
|
|
16
|
-
import type
|
|
16
|
+
import {MiniappErrorCode, type CalendarEvent} from "@glassly/miniapp"
|
|
17
17
|
|
|
18
18
|
import {useAppStatusStore} from "../stores/apps"
|
|
19
19
|
import {useGlassesStore} from "../stores/glasses"
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
type MenuAppSetting,
|
|
34
34
|
} from "./CustomDashboardModel"
|
|
35
35
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
36
|
-
import {listPhoneCalendarEvents} from "./PhoneCalendarService"
|
|
36
|
+
import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarService"
|
|
37
37
|
import sceneRenderer from "./SceneRenderer"
|
|
38
38
|
|
|
39
39
|
const DASHBOARD_APP_ID = "system.custom-dashboard"
|
|
@@ -49,11 +49,18 @@ const WIDGET_GAP = 10
|
|
|
49
49
|
/** Row height for the glasses' builtin 20px font engine. */
|
|
50
50
|
const LINE_H = 24
|
|
51
51
|
const BUTTON_H = 34
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
/**
|
|
53
|
+
* App-menu icon grid: 2 rows × 4 columns per page. CFW rasterizes scenes
|
|
54
|
+
* phone-side, so the stock firmware's 4-image container pool does not bound
|
|
55
|
+
* the page (getEffectiveCapabilities lifts the element caps while the CFW
|
|
56
|
+
* canvas is active).
|
|
57
|
+
*/
|
|
58
|
+
const APP_GRID_COLS = 4
|
|
59
|
+
const APP_GRID_ROWS = 2
|
|
60
|
+
const APPS_PER_PAGE = APP_GRID_COLS * APP_GRID_ROWS
|
|
61
|
+
const APP_CELL = 36
|
|
62
|
+
const APP_CELL_GAP = 12
|
|
63
|
+
const APP_ICON_INSET = 2
|
|
57
64
|
const APP_ICON = APP_CELL - APP_ICON_INSET * 2
|
|
58
65
|
|
|
59
66
|
function eventLine(event: CalendarEvent): string {
|
|
@@ -69,7 +76,8 @@ class CustomDashboardService {
|
|
|
69
76
|
private view: "main" | "apps" = "main"
|
|
70
77
|
private mainIndex = 0
|
|
71
78
|
private appsIndex = 0
|
|
72
|
-
|
|
79
|
+
/** "loading" until the first fetch resolves; "no-access" only for a real permission denial. */
|
|
80
|
+
private calendarStatus: "loading" | "ok" | "no-access" | "unavailable" = "loading"
|
|
73
81
|
/** packageName → base64 icon bytes, or null when the icon can't be loaded (monogram fallback). */
|
|
74
82
|
private iconCache = new Map<string, string | null>()
|
|
75
83
|
private iconLoads = new Set<string>()
|
|
@@ -321,7 +329,7 @@ class CustomDashboardService {
|
|
|
321
329
|
|
|
322
330
|
private async refreshCalendarAndRender(): Promise<void> {
|
|
323
331
|
if (!this.calendarWidgetEnabled()) {
|
|
324
|
-
this.
|
|
332
|
+
this.calendarStatus = "loading"
|
|
325
333
|
this.calendarEvents = []
|
|
326
334
|
this.render()
|
|
327
335
|
return
|
|
@@ -332,14 +340,22 @@ class CustomDashboardService {
|
|
|
332
340
|
const startsAt = new Date()
|
|
333
341
|
const endsAt = new Date(startsAt.getTime() + Math.max(1, days) * 24 * 60 * 60 * 1000)
|
|
334
342
|
try {
|
|
335
|
-
const result = await listPhoneCalendarEvents({
|
|
336
|
-
|
|
343
|
+
const result = await listPhoneCalendarEvents({
|
|
344
|
+
startsAt: startsAt.toISOString(),
|
|
345
|
+
endsAt: endsAt.toISOString(),
|
|
346
|
+
limit: Math.max(1, Math.min(3, count)),
|
|
347
|
+
})
|
|
348
|
+
this.calendarStatus = "ok"
|
|
337
349
|
this.calendarEvents = result.events
|
|
338
|
-
} catch {
|
|
350
|
+
} catch (error) {
|
|
339
351
|
// Permission is requested from the Custom Dashboard settings screen;
|
|
340
|
-
//
|
|
341
|
-
|
|
352
|
+
// only a real denial should read as one — any other failure is
|
|
353
|
+
// "Unavailable" (and logged) so it can't masquerade as a permission
|
|
354
|
+
// problem.
|
|
355
|
+
const denied = error instanceof PhoneCalendarError && error.code === MiniappErrorCode.PERMISSION_DENIED
|
|
356
|
+
this.calendarStatus = denied ? "no-access" : "unavailable"
|
|
342
357
|
this.calendarEvents = []
|
|
358
|
+
if (!denied) console.warn("CustomDashboard: calendar fetch failed", error)
|
|
343
359
|
}
|
|
344
360
|
this.render()
|
|
345
361
|
}
|
|
@@ -407,16 +423,19 @@ class CustomDashboardService {
|
|
|
407
423
|
y += WIDGET_GAP
|
|
408
424
|
} else if (widget.id === "calendar") {
|
|
409
425
|
// Right half, top-aligned, independent of the left stack. A missing
|
|
410
|
-
// calendar
|
|
411
|
-
//
|
|
412
|
-
const lines =
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
426
|
+
// calendar shows a hint instead of silently vanishing — "No access"
|
|
427
|
+
// strictly for permission denial, "Unavailable" for any other failure.
|
|
428
|
+
const lines =
|
|
429
|
+
this.calendarStatus === "ok"
|
|
430
|
+
? [
|
|
431
|
+
"Calendar",
|
|
432
|
+
...(this.calendarEvents.length
|
|
433
|
+
? this.calendarEvents.slice(0, 3).map(eventLine)
|
|
434
|
+
: ["No upcoming events"]),
|
|
435
|
+
]
|
|
436
|
+
: this.calendarStatus === "loading"
|
|
437
|
+
? ["Calendar"]
|
|
438
|
+
: ["Calendar", this.calendarStatus === "no-access" ? "No access" : "Unavailable"]
|
|
420
439
|
elements.push({
|
|
421
440
|
type: "text",
|
|
422
441
|
id: "calendar",
|
|
@@ -516,23 +535,24 @@ class CustomDashboardService {
|
|
|
516
535
|
return elements
|
|
517
536
|
}
|
|
518
537
|
|
|
519
|
-
// Icon grid, paged
|
|
520
|
-
// four icon images may be on a page. Selection is the bordered cell.
|
|
538
|
+
// Icon grid, paged, row-major. Selection is the bordered cell.
|
|
521
539
|
const perPage = APPS_PER_PAGE
|
|
522
540
|
const pageStart = Math.floor(this.appsIndex / perPage) * perPage
|
|
523
541
|
const page = apps.slice(pageStart, pageStart + perPage)
|
|
524
|
-
const
|
|
542
|
+
const cols = Math.min(page.length, APP_GRID_COLS)
|
|
543
|
+
const totalW = cols * APP_CELL + (cols - 1) * APP_CELL_GAP
|
|
525
544
|
const startX = Math.max(MARGIN_X, Math.round((canvas.width - totalW) / 2))
|
|
526
|
-
page.forEach((app,
|
|
527
|
-
const x = startX +
|
|
528
|
-
const
|
|
545
|
+
page.forEach((app, cell) => {
|
|
546
|
+
const x = startX + (cell % APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP)
|
|
547
|
+
const y = listTop + Math.floor(cell / APP_GRID_COLS) * (APP_CELL + APP_CELL_GAP)
|
|
548
|
+
const selected = pageStart + cell === this.appsIndex
|
|
529
549
|
const icon = this.iconCache.get(app.packageName)
|
|
530
550
|
if (icon === undefined) this.loadIcon(app)
|
|
531
551
|
if (icon) {
|
|
532
552
|
elements.push({
|
|
533
553
|
type: "image",
|
|
534
554
|
id: `app-icon:${app.packageName}`,
|
|
535
|
-
box: {x: x + APP_ICON_INSET, y:
|
|
555
|
+
box: {x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},
|
|
536
556
|
data: icon,
|
|
537
557
|
})
|
|
538
558
|
} else {
|
|
@@ -540,28 +560,30 @@ class CustomDashboardService {
|
|
|
540
560
|
elements.push({
|
|
541
561
|
type: "text",
|
|
542
562
|
id: `app-icon:${app.packageName}`,
|
|
543
|
-
box: {x: x + APP_ICON_INSET, y:
|
|
563
|
+
box: {x: x + APP_ICON_INSET, y: y + APP_ICON_INSET, w: APP_ICON, h: APP_ICON},
|
|
544
564
|
text: (app.name.trim()[0] ?? "?").toUpperCase(),
|
|
545
|
-
style: {font: "mono", fontSize:
|
|
565
|
+
style: {font: "mono", fontSize: 20},
|
|
546
566
|
})
|
|
547
567
|
}
|
|
548
568
|
if (selected) {
|
|
549
569
|
elements.push({
|
|
550
570
|
type: "rect",
|
|
551
571
|
id: "apps-selection",
|
|
552
|
-
box: {x, y
|
|
553
|
-
style: {border: 2, radius:
|
|
572
|
+
box: {x, y, w: APP_CELL, h: APP_CELL},
|
|
573
|
+
style: {border: 2, radius: 6},
|
|
554
574
|
})
|
|
555
575
|
}
|
|
556
576
|
})
|
|
557
577
|
|
|
558
578
|
// Grayscale mini icons are ambiguous on their own: caption the selection.
|
|
579
|
+
const gridRows = Math.ceil(page.length / APP_GRID_COLS)
|
|
580
|
+
const gridH = gridRows * APP_CELL + (gridRows - 1) * APP_CELL_GAP
|
|
559
581
|
const selectedApp = apps[this.appsIndex]
|
|
560
582
|
if (selectedApp) {
|
|
561
583
|
elements.push({
|
|
562
584
|
type: "text",
|
|
563
585
|
id: "apps-selected-name",
|
|
564
|
-
box: {x: MARGIN_X, y: listTop +
|
|
586
|
+
box: {x: MARGIN_X, y: listTop + gridH + 12, w: width, h: LINE_H},
|
|
565
587
|
text: `${selectedApp.name}${selectedApp.running ? " ●" : ""}`,
|
|
566
588
|
style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
|
|
567
589
|
})
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Effective device capabilities — the static model profile plus what the
|
|
3
|
-
* connected firmware reports at runtime.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* connected firmware reports at runtime. The runtime override is the scene
|
|
4
|
+
* canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into
|
|
5
|
+
* the 640×480 panel, so the drawable canvas grows past stock's 576×288.
|
|
6
|
+
* Native publishes the canvas into the glasses status (0 = none). Because
|
|
7
|
+
* that path is phone-side rasterization, the stock firmware's container
|
|
8
|
+
* pools (6 text / 4 image) don't bound a CFW frame either — element caps are
|
|
9
|
+
* lifted alongside the canvas, bounded only to keep frames sane.
|
|
7
10
|
*/
|
|
8
11
|
import {useGlassesStore} from "../stores/glasses"
|
|
9
12
|
import {getModelCapabilities, type Capabilities, type DeviceTypes} from "../types"
|
|
10
13
|
|
|
14
|
+
const CFW_MAX_TEXT_ELEMENTS = 24
|
|
15
|
+
const CFW_MAX_IMAGE_ELEMENTS = 16
|
|
16
|
+
|
|
11
17
|
export interface SceneCanvasSize {
|
|
12
18
|
width: number
|
|
13
19
|
height: number
|
|
@@ -23,5 +29,14 @@ export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
|
|
|
23
29
|
const base = getModelCapabilities(model)
|
|
24
30
|
const canvas = runtimeSceneCanvas()
|
|
25
31
|
if (!base.display || !canvas) return base
|
|
26
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
...base,
|
|
34
|
+
display: {
|
|
35
|
+
...base.display,
|
|
36
|
+
width: canvas.width,
|
|
37
|
+
height: canvas.height,
|
|
38
|
+
maxTextElements: Math.max(base.display.maxTextElements ?? 0, CFW_MAX_TEXT_ELEMENTS),
|
|
39
|
+
maxImageElements: Math.max(base.display.maxImageElements ?? 0, CFW_MAX_IMAGE_ELEMENTS),
|
|
40
|
+
},
|
|
41
|
+
}
|
|
27
42
|
}
|