@glassly/engine 0.1.0-dev.59 → 0.1.0-dev.62
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 +5 -1
- package/build/facades/ota.d.ts +3 -1
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +4 -2
- package/build/facades/ota.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +6 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +136 -14
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +5 -1
- package/build/react/useMentraLiveOta.d.ts.map +1 -1
- package/build/react/useMentraLiveOta.js +135 -58
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +3 -3
- package/build/services/DisplayProcessor.js +3 -3
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +2 -2
- package/build/services/LocalDisplayManager.js +2 -2
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +2 -0
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +21 -1
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +1 -1
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/StreamLifecycleController.d.ts +4 -0
- package/build/services/StreamLifecycleController.d.ts.map +1 -1
- package/build/services/StreamLifecycleController.js +4 -0
- package/build/services/StreamLifecycleController.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +4 -0
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +4 -0
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/types/applet.d.ts +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g2.js +2 -2
- package/build/types/capabilities/even-realities-g2.js.map +1 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +1 -0
- package/build/types/hardware.js.map +1 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +1 -1
- package/build/types/index.js.map +1 -1
- package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
- package/build/utils/display/composer/ColumnComposer.js +1 -1
- package/build/utils/display/composer/ColumnComposer.js.map +1 -1
- package/package.json +8 -7
- package/src/facades/ota.ts +4 -2
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +2 -1
- package/src/react/MentraLiveOtaFlow.tsx +226 -41
- package/src/react/useMentraLiveOta.ts +146 -60
- package/src/services/DisplayProcessor.ts +3 -3
- package/src/services/LocalDisplayManager.ts +2 -2
- package/src/services/OtaInstallCoordinator.ts +21 -0
- package/src/services/PhonePhotoCoordinator.ts +1 -1
- package/src/services/StreamLifecycleController.ts +4 -0
- package/src/services/otaInstallPolicy.ts +4 -0
- package/src/types/applet.ts +1 -1
- package/src/types/capabilities/even-realities-g2.ts +3 -2
- package/src/types/hardware.ts +1 -0
- package/src/types/index.ts +1 -1
- package/src/utils/display/composer/ColumnComposer.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnComposer.js","sourceRoot":"","sources":["../../../../src/utils/display/composer/ColumnComposer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AA+ClD,gFAAgF;AAChF,uBAAuB;AACvB,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,OAAO,cAAc;IACjB,OAAO,CAAgB;IACvB,QAAQ,CAAc;IACtB,OAAO,CAAa;IACpB,YAAY,CAAQ;IAE5B,YAAY,OAAuB,EAAE,YAAuB,qBAAqB;QAC/E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAA;QACzC,IAAI,CAAC,OAAO,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC5C,SAAS;SACV,CAAC,CAAA;QAEF,+CAA+C;QAC/C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACpD,CAAC;IAED,8EAA8E;IAC9E,aAAa;IACb,8EAA8E;IAE9E;;;;;;;;;;OAUG;IACI,qBAAqB,CAAC,QAAgB,EAAE,SAAiB,EAAE,UAA0B,EAAE;QAC5F,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;QAEzD,iCAAiC;QACjC,MAAM,eAAe,GAAgB;YACnC,UAAU,EAAE,MAAM,CAAC,iBAAiB;YACpC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAA;QAED,MAAM,gBAAgB,GAAgB;YACpC,UAAU,EAAE,MAAM,CAAC,kBAAkB;YACrC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAA;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,eAAe,CAAC,CAAA;QACrE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,gBAAgB,CAAC,CAAA;QAExE,8CAA8C;QAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QAClE,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QAEpE,qCAAqC;QACrC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;QAErE,OAAO;YACL,YAAY;YACZ,SAAS;YACT,UAAU;YACV,MAAM;SACP,CAAA;IACH,CAAC;IAED;;OAEG;IACI,sBAAsB;QAC3B,OAAO,IAAI,CAAC,eAAe,EAAE,CAAA;IAC/B,CAAC;IAED;;OAEG;IACI,YAAY,CAAC,SAAoB;QACtC,IAAI,CAAC,OAAO,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC5C,SAAS;SACV,CAAC,CAAA;IACJ,CAAC;IAED,8EAA8E;IAC9E,kBAAkB;IAClB,8EAA8E;IAE9E;;OAEG;IACK,eAAe,CAAC,SAAiC;QACvD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAA;QAEhD,2DAA2D;QAC3D,iDAAiD;QACjD,MAAM,QAAQ,GAAiB;YAC7B,iBAAiB,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC;YACjD,kBAAkB,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;YACnD,kBAAkB,EAAE,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;YAClE,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;YAC/B,gBAAgB,EAAE,CAAC;SACpB,CAAA;QAED,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,SAAS;SACb,CAAA;IACH,CAAC;IAED;;OAEG;IACK,QAAQ,CAAC,KAAe,EAAE,KAAa;QAC7C,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;QACzB,OAAO,MAAM,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IAC/B,CAAC;IAED;;;;;;;;OAQG;IACK,YAAY,CAAC,SAAmB,EAAE,UAAoB,EAAE,MAAoB;QAClF,MAAM,KAAK,GAAa,EAAE,CAAA;QAE1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;YACvD,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;YAEzD,qCAAqC;YACrC,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAA;YAEvD,0DAA0D;YAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,2BAA2B,CAAC,WAAW,EAAE,MAAM,CAAC,kBAAkB,CAAC,CAAA;YAE7F,wBAAwB;YACxB,IAAI,IAAI,GAAG,EAAE,CAAA;YAEb,gCAAgC;YAChC,IAAI,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,gBAAgB,GAAG,CAAC,EAAE,CAAC;gBAC3D,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA;YAC7C,CAAC;YAED,IAAI,IAAI,QAAQ,CAAA;YAChB,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;YAChC,IAAI,IAAI,SAAS,CAAA;YAEjB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IAED;;;;;;OAMG;IACK,2BAA2B,CAAC,cAAsB,EAAE,gBAAwB;QAClF,MAAM,YAAY,GAAG,gBAAgB,GAAG,cAAc,CAAA;QAEtD,2CAA2C;QAC3C,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAA;QACV,CAAC;QAED,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,wBAAwB;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAA;QAExE,yDAAyD;QACzD,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B,CAAC;IAED;;;OAGG;IACK,aAAa,CAAC,IAAY;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACpC,CAAC;CACF;AAED,gFAAgF;AAChF,mBAAmB;AACnB,gFAAgF;AAEhF;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAuB,EAAE,YAAuB,MAAM;IACzF,OAAO,IAAI,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;AAC/C,CAAC","sourcesContent":["/**\n * ColumnComposer\n *\n * Handles composition of multi-column layouts like double_text_wall.\n * This is the single source of truth for column layout logic, replacing\n * the duplicate implementations in native iOS (G1Text.swift) and\n * Android (G1Text.kt, G1.java).\n *\n * The composer:\n * 1. Wraps each column's text to its allocated width\n * 2. Merges columns line-by-line with pixel-precise space padding\n * 3. Returns a single pre-composed string ready for native to chunk & send\n *\n * @see cloud/issues/026-mobile-display-processor for design docs\n */\n\nimport type {DisplayProfile} from \"../profiles/types\"\nimport {TextMeasurer} from \"../measurer/TextMeasurer\"\nimport {TextWrapper} from \"../wrapper/TextWrapper\"\nimport type {WrapOptions, BreakMode} from \"../wrapper/types\"\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Configuration for column layout\n */\nexport interface ColumnConfig {\n /** Width of the left column in pixels */\n leftColumnWidthPx: number\n /** X position where the right column starts in pixels */\n rightColumnStartPx: number\n /** Width of the right column in pixels (calculated from start position) */\n rightColumnWidthPx: number\n /** Maximum number of lines to display */\n maxLines: number\n /** Left margin in spaces (for indentation) */\n leftMarginSpaces?: number\n}\n\n/**\n * Options for composing double text wall\n */\nexport interface ComposeOptions {\n /** Break mode for text wrapping */\n breakMode?: BreakMode\n /** Custom column configuration (overrides profile defaults) */\n columnConfig?: Partial<ColumnConfig>\n}\n\n/**\n * Result of column composition\n */\nexport interface ComposeResult {\n /** The fully composed text with both columns merged */\n composedText: string\n /** Lines from the left column (for debugging/preview) */\n leftLines: string[]\n /** Lines from the right column (for debugging/preview) */\n rightLines: string[]\n /** The column configuration that was used */\n config: ColumnConfig\n}\n\n// =============================================================================\n// ColumnComposer Class\n// =============================================================================\n\n/**\n * Composes multi-column layouts for smart glasses displays.\n *\n * Usage:\n * ```typescript\n * const composer = new ColumnComposer(G1_PROFILE)\n * const result = composer.composeDoubleTextWall(\"Left content\", \"Right content\")\n * // result.composedText is ready to send to native SGC\n * ```\n */\nexport class ColumnComposer {\n private profile: DisplayProfile\n private measurer: TextMeasurer\n private wrapper: TextWrapper\n private spaceWidthPx: number\n\n constructor(profile: DisplayProfile, breakMode: BreakMode = \"character-no-hyphen\") {\n this.profile = profile\n this.measurer = new TextMeasurer(profile)\n this.wrapper = new TextWrapper(this.measurer, {\n breakMode,\n })\n\n // Cache space width for alignment calculations\n this.spaceWidthPx = this.measurer.measureText(\" \")\n }\n\n // ===========================================================================\n // Public API\n // ===========================================================================\n\n /**\n * Compose a double text wall layout.\n *\n * Takes left and right text, wraps each to their column width,\n * and merges them with pixel-precise space alignment.\n *\n * @param leftText - Text for the left column\n * @param rightText - Text for the right column\n * @param options - Optional composition settings\n * @returns Composed result with merged text and metadata\n */\n public composeDoubleTextWall(leftText: string, rightText: string, options: ComposeOptions = {}): ComposeResult {\n const config = this.getColumnConfig(options.columnConfig)\n\n // Wrap each column independently\n const leftWrapOptions: WrapOptions = {\n maxWidthPx: config.leftColumnWidthPx,\n maxLines: config.maxLines,\n }\n\n const rightWrapOptions: WrapOptions = {\n maxWidthPx: config.rightColumnWidthPx,\n maxLines: config.maxLines,\n }\n\n const leftResult = this.wrapper.wrap(leftText || \"\", leftWrapOptions)\n const rightResult = this.wrapper.wrap(rightText || \"\", rightWrapOptions)\n\n // Pad arrays to have exactly maxLines entries\n const leftLines = this.padLines(leftResult.lines, config.maxLines)\n const rightLines = this.padLines(rightResult.lines, config.maxLines)\n\n // Merge columns with space alignment\n const composedText = this.mergeColumns(leftLines, rightLines, config)\n\n return {\n composedText,\n leftLines,\n rightLines,\n config,\n }\n }\n\n /**\n * Get the default column configuration for the current profile.\n */\n public getDefaultColumnConfig(): ColumnConfig {\n return this.getColumnConfig()\n }\n\n /**\n * Update the break mode for text wrapping.\n */\n public setBreakMode(breakMode: BreakMode): void {\n this.wrapper = new TextWrapper(this.measurer, {\n breakMode,\n })\n }\n\n // ===========================================================================\n // Private Methods\n // ===========================================================================\n\n /**\n * Get column configuration, merging defaults with overrides.\n */\n private getColumnConfig(overrides?: Partial<ColumnConfig>): ColumnConfig {\n const displayWidth = this.profile.displayWidthPx\n\n // Default layout: 50% for left column, right starts at 55%\n // This matches the native G1Text implementations\n const defaults: ColumnConfig = {\n leftColumnWidthPx: Math.floor(displayWidth * 0.5),\n rightColumnStartPx: Math.floor(displayWidth * 0.55),\n rightColumnWidthPx: displayWidth - Math.floor(displayWidth * 0.55),\n maxLines: this.profile.maxLines,\n leftMarginSpaces: 0,\n }\n\n return {\n ...defaults,\n ...overrides,\n }\n }\n\n /**\n * Pad lines array to exactly `count` entries.\n */\n private padLines(lines: string[], count: number): string[] {\n const result = [...lines]\n while (result.length < count) {\n result.push(\"\")\n }\n return result.slice(0, count)\n }\n\n /**\n * Merge left and right columns with pixel-precise space alignment.\n *\n * For each line:\n * 1. Take the left column text\n * 2. Calculate its pixel width\n * 3. Calculate how many spaces needed to reach rightColumnStartPx\n * 4. Append spaces and right column text\n */\n private mergeColumns(leftLines: string[], rightLines: string[], config: ColumnConfig): string {\n const lines: string[] = []\n\n for (let i = 0; i < config.maxLines; i++) {\n const leftText = this.cleanEnspaces(leftLines[i] || \"\")\n const rightText = this.cleanEnspaces(rightLines[i] || \"\")\n\n // Calculate pixel width of left text\n const leftWidthPx = this.measurer.measureText(leftText)\n\n // Calculate spaces needed to reach the right column start\n const spacesNeeded = this.calculateSpacesForAlignment(leftWidthPx, config.rightColumnStartPx)\n\n // Build the merged line\n let line = \"\"\n\n // Add left margin if configured\n if (config.leftMarginSpaces && config.leftMarginSpaces > 0) {\n line += \" \".repeat(config.leftMarginSpaces)\n }\n\n line += leftText\n line += \" \".repeat(spacesNeeded)\n line += rightText\n\n lines.push(line)\n }\n\n return lines.join(\"\\n\")\n }\n\n /**\n * Calculate the number of spaces needed to align to a target position.\n *\n * @param currentWidthPx - Current position in pixels\n * @param targetPositionPx - Target position in pixels\n * @returns Number of space characters needed\n */\n private calculateSpacesForAlignment(currentWidthPx: number, targetPositionPx: number): number {\n const pixelsNeeded = targetPositionPx - currentWidthPx\n\n // Ensure at least one space for separation\n if (pixelsNeeded <= 0) {\n return 1\n }\n\n // Calculate spaces needed. Use Math.floor (not Math.ceil) to ensure the\n // right column never starts past rightColumnStartPx. Rounding up caused\n // the right column to start 1–5px further right than planned, shrinking\n // its usable width and causing the last character to wrap onto the next\n // line's left position.\n const spaces = Math.max(1, Math.floor(pixelsNeeded / this.spaceWidthPx))\n\n // Cap at a reasonable maximum to prevent runaway spacing\n return Math.min(spaces, 100)\n }\n\n /**\n * Remove en-space characters (U+2002) that may have been added during wrapping.\n * Native implementations do this cleanup before merging.\n */\n private cleanEnspaces(text: string): string {\n return text.replace(/\\u2002/g, \"\")\n }\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Create a ColumnComposer for a given profile.\n *\n * @param profile - Display profile to use\n * @param breakMode - Break mode for text wrapping (default: \"word\")\n * @returns Configured ColumnComposer instance\n */\nexport function createColumnComposer(profile: DisplayProfile, breakMode: BreakMode = \"word\"): ColumnComposer {\n return new ColumnComposer(profile, breakMode)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ColumnComposer.js","sourceRoot":"","sources":["../../../../src/utils/display/composer/ColumnComposer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ColumnComposer\n *\n * Handles composition of multi-column layouts like double_text_wall.\n * This is the single source of truth for column layout logic, replacing\n * the duplicate implementations in native iOS (G1Text.swift) and\n * Android (G1Text.kt, G1.java).\n *\n * The composer:\n * 1. Wraps each column's text to its allocated width\n * 2. Merges columns line-by-line with pixel-precise space padding\n * 3. Returns a single pre-composed string ready for native to chunk & send\n *\n * @see notes/miniapp-display-api-redesign.md for the current display design\n */\n\nimport type {DisplayProfile} from \"../profiles/types\"\nimport {TextMeasurer} from \"../measurer/TextMeasurer\"\nimport {TextWrapper} from \"../wrapper/TextWrapper\"\nimport type {WrapOptions, BreakMode} from \"../wrapper/types\"\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Configuration for column layout\n */\nexport interface ColumnConfig {\n /** Width of the left column in pixels */\n leftColumnWidthPx: number\n /** X position where the right column starts in pixels */\n rightColumnStartPx: number\n /** Width of the right column in pixels (calculated from start position) */\n rightColumnWidthPx: number\n /** Maximum number of lines to display */\n maxLines: number\n /** Left margin in spaces (for indentation) */\n leftMarginSpaces?: number\n}\n\n/**\n * Options for composing double text wall\n */\nexport interface ComposeOptions {\n /** Break mode for text wrapping */\n breakMode?: BreakMode\n /** Custom column configuration (overrides profile defaults) */\n columnConfig?: Partial<ColumnConfig>\n}\n\n/**\n * Result of column composition\n */\nexport interface ComposeResult {\n /** The fully composed text with both columns merged */\n composedText: string\n /** Lines from the left column (for debugging/preview) */\n leftLines: string[]\n /** Lines from the right column (for debugging/preview) */\n rightLines: string[]\n /** The column configuration that was used */\n config: ColumnConfig\n}\n\n// =============================================================================\n// ColumnComposer Class\n// =============================================================================\n\n/**\n * Composes multi-column layouts for smart glasses displays.\n *\n * Usage:\n * ```typescript\n * const composer = new ColumnComposer(G1_PROFILE)\n * const result = composer.composeDoubleTextWall(\"Left content\", \"Right content\")\n * // result.composedText is ready to send to native SGC\n * ```\n */\nexport class ColumnComposer {\n private profile: DisplayProfile\n private measurer: TextMeasurer\n private wrapper: TextWrapper\n private spaceWidthPx: number\n\n constructor(profile: DisplayProfile, breakMode: BreakMode = \"character-no-hyphen\") {\n this.profile = profile\n this.measurer = new TextMeasurer(profile)\n this.wrapper = new TextWrapper(this.measurer, {\n breakMode,\n })\n\n // Cache space width for alignment calculations\n this.spaceWidthPx = this.measurer.measureText(\" \")\n }\n\n // ===========================================================================\n // Public API\n // ===========================================================================\n\n /**\n * Compose a double text wall layout.\n *\n * Takes left and right text, wraps each to their column width,\n * and merges them with pixel-precise space alignment.\n *\n * @param leftText - Text for the left column\n * @param rightText - Text for the right column\n * @param options - Optional composition settings\n * @returns Composed result with merged text and metadata\n */\n public composeDoubleTextWall(leftText: string, rightText: string, options: ComposeOptions = {}): ComposeResult {\n const config = this.getColumnConfig(options.columnConfig)\n\n // Wrap each column independently\n const leftWrapOptions: WrapOptions = {\n maxWidthPx: config.leftColumnWidthPx,\n maxLines: config.maxLines,\n }\n\n const rightWrapOptions: WrapOptions = {\n maxWidthPx: config.rightColumnWidthPx,\n maxLines: config.maxLines,\n }\n\n const leftResult = this.wrapper.wrap(leftText || \"\", leftWrapOptions)\n const rightResult = this.wrapper.wrap(rightText || \"\", rightWrapOptions)\n\n // Pad arrays to have exactly maxLines entries\n const leftLines = this.padLines(leftResult.lines, config.maxLines)\n const rightLines = this.padLines(rightResult.lines, config.maxLines)\n\n // Merge columns with space alignment\n const composedText = this.mergeColumns(leftLines, rightLines, config)\n\n return {\n composedText,\n leftLines,\n rightLines,\n config,\n }\n }\n\n /**\n * Get the default column configuration for the current profile.\n */\n public getDefaultColumnConfig(): ColumnConfig {\n return this.getColumnConfig()\n }\n\n /**\n * Update the break mode for text wrapping.\n */\n public setBreakMode(breakMode: BreakMode): void {\n this.wrapper = new TextWrapper(this.measurer, {\n breakMode,\n })\n }\n\n // ===========================================================================\n // Private Methods\n // ===========================================================================\n\n /**\n * Get column configuration, merging defaults with overrides.\n */\n private getColumnConfig(overrides?: Partial<ColumnConfig>): ColumnConfig {\n const displayWidth = this.profile.displayWidthPx\n\n // Default layout: 50% for left column, right starts at 55%\n // This matches the native G1Text implementations\n const defaults: ColumnConfig = {\n leftColumnWidthPx: Math.floor(displayWidth * 0.5),\n rightColumnStartPx: Math.floor(displayWidth * 0.55),\n rightColumnWidthPx: displayWidth - Math.floor(displayWidth * 0.55),\n maxLines: this.profile.maxLines,\n leftMarginSpaces: 0,\n }\n\n return {\n ...defaults,\n ...overrides,\n }\n }\n\n /**\n * Pad lines array to exactly `count` entries.\n */\n private padLines(lines: string[], count: number): string[] {\n const result = [...lines]\n while (result.length < count) {\n result.push(\"\")\n }\n return result.slice(0, count)\n }\n\n /**\n * Merge left and right columns with pixel-precise space alignment.\n *\n * For each line:\n * 1. Take the left column text\n * 2. Calculate its pixel width\n * 3. Calculate how many spaces needed to reach rightColumnStartPx\n * 4. Append spaces and right column text\n */\n private mergeColumns(leftLines: string[], rightLines: string[], config: ColumnConfig): string {\n const lines: string[] = []\n\n for (let i = 0; i < config.maxLines; i++) {\n const leftText = this.cleanEnspaces(leftLines[i] || \"\")\n const rightText = this.cleanEnspaces(rightLines[i] || \"\")\n\n // Calculate pixel width of left text\n const leftWidthPx = this.measurer.measureText(leftText)\n\n // Calculate spaces needed to reach the right column start\n const spacesNeeded = this.calculateSpacesForAlignment(leftWidthPx, config.rightColumnStartPx)\n\n // Build the merged line\n let line = \"\"\n\n // Add left margin if configured\n if (config.leftMarginSpaces && config.leftMarginSpaces > 0) {\n line += \" \".repeat(config.leftMarginSpaces)\n }\n\n line += leftText\n line += \" \".repeat(spacesNeeded)\n line += rightText\n\n lines.push(line)\n }\n\n return lines.join(\"\\n\")\n }\n\n /**\n * Calculate the number of spaces needed to align to a target position.\n *\n * @param currentWidthPx - Current position in pixels\n * @param targetPositionPx - Target position in pixels\n * @returns Number of space characters needed\n */\n private calculateSpacesForAlignment(currentWidthPx: number, targetPositionPx: number): number {\n const pixelsNeeded = targetPositionPx - currentWidthPx\n\n // Ensure at least one space for separation\n if (pixelsNeeded <= 0) {\n return 1\n }\n\n // Calculate spaces needed. Use Math.floor (not Math.ceil) to ensure the\n // right column never starts past rightColumnStartPx. Rounding up caused\n // the right column to start 1–5px further right than planned, shrinking\n // its usable width and causing the last character to wrap onto the next\n // line's left position.\n const spaces = Math.max(1, Math.floor(pixelsNeeded / this.spaceWidthPx))\n\n // Cap at a reasonable maximum to prevent runaway spacing\n return Math.min(spaces, 100)\n }\n\n /**\n * Remove en-space characters (U+2002) that may have been added during wrapping.\n * Native implementations do this cleanup before merging.\n */\n private cleanEnspaces(text: string): string {\n return text.replace(/\\u2002/g, \"\")\n }\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Create a ColumnComposer for a given profile.\n *\n * @param profile - Display profile to use\n * @param breakMode - Break mode for text wrapping (default: \"word\")\n * @returns Configured ColumnComposer instance\n */\nexport function createColumnComposer(profile: DisplayProfile, breakMode: BreakMode = \"word\"): ColumnComposer {\n return new ColumnComposer(profile, breakMode)\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.62",
|
|
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,16 +97,17 @@
|
|
|
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.62",
|
|
101
|
+
"@glassly/cloud-client": "0.1.0-dev.62",
|
|
102
|
+
"@glassly/cloud-protocol": "0.1.0-dev.62",
|
|
103
|
+
"@glassly/crust": "0.1.0-dev.62",
|
|
104
|
+
"@glassly/miniapp": "0.1.0-dev.62",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
|
+
"react-native-marked": "8.1.1",
|
|
107
108
|
"semver": "^7.7.4",
|
|
108
109
|
"typesafe-ts": "^1.7.1",
|
|
109
|
-
"@glassly/llm-providers": "0.1.0-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.1.0-dev.62"
|
|
110
111
|
},
|
|
111
112
|
"devDependencies": {
|
|
112
113
|
"@glassly/llm-providers": "workspace:*",
|
package/src/facades/ota.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
OtaUpdateInfo,
|
|
18
18
|
ReleaseChangelog,
|
|
19
19
|
} from "@glassly/bluetooth-sdk"
|
|
20
|
-
import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
|
|
20
|
+
import {isGlassesConnected, isGlassesReady, useGlassesStore} from "../stores/glasses"
|
|
21
21
|
import {resolveOtaManifestUrl} from "../services/otaManifestUrl"
|
|
22
22
|
import {otaInstallCoordinator, type OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
|
|
23
23
|
import {startGlassesStatusProjection} from "../services/GlassesStatusProjection"
|
|
@@ -32,10 +32,12 @@ function projectSnapshot() {
|
|
|
32
32
|
const s = useGlassesStore.getState()
|
|
33
33
|
return {
|
|
34
34
|
connected: isGlassesConnected(s.connection),
|
|
35
|
+
ready: isGlassesReady(s.connection),
|
|
35
36
|
buildNumber: s.buildNumber || null,
|
|
36
37
|
appVersion: s.appVersion || null,
|
|
37
38
|
mtkFirmwareVersion: s.mtkFirmwareVersion || null,
|
|
38
39
|
besFirmwareVersion: s.besFirmwareVersion || null,
|
|
40
|
+
batteryLevel: s.batteryLevel >= 0 ? s.batteryLevel : null,
|
|
39
41
|
hotspotOtaVersion: s.hotspotOtaVersion ?? 0,
|
|
40
42
|
wifiConnected: s.wifi.state === "connected",
|
|
41
43
|
wifiStatusKnown: s.wifiStatusKnown,
|
|
@@ -64,7 +66,7 @@ export const ota = {
|
|
|
64
66
|
// --- actions ---
|
|
65
67
|
/**
|
|
66
68
|
* Start the device-status and OTA projections without starting the authenticated
|
|
67
|
-
* cloud
|
|
69
|
+
* cloud connection or miniapp runtime. This is the explicit entry point for Bluetooth-only hosts.
|
|
68
70
|
* Idempotent and safe when the full engine runtime is already running.
|
|
69
71
|
*/
|
|
70
72
|
initialize: async () => {
|
|
@@ -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.62",
|
|
16
|
+
"releaseSetId": "glassly-0.1.0-dev.62",
|
|
17
|
+
"sourceCommit": "69eee438b882fca1c9b0ad466d71fc2c24053662",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,7 @@ export {
|
|
|
109
109
|
GLOBAL_OTA_TIMEOUT_MS,
|
|
110
110
|
PING_INTERVAL_MS,
|
|
111
111
|
QUERY_REPLY_TIMEOUT_MS,
|
|
112
|
+
BES_RESTART_TIMEOUT_MS,
|
|
112
113
|
OtaProgressMessages,
|
|
113
114
|
} from "./services/otaInstallPolicy"
|
|
114
115
|
export {deriveDisplayState} from "./services/otaDisplayState"
|
|
@@ -216,9 +217,9 @@ export {
|
|
|
216
217
|
evenRealitiesG1,
|
|
217
218
|
evenRealitiesG2,
|
|
218
219
|
mentraLive,
|
|
220
|
+
nimo,
|
|
219
221
|
vuzixZ100,
|
|
220
222
|
mentraDisplay,
|
|
221
|
-
nimo,
|
|
222
223
|
} from "./types"
|
|
223
224
|
export type {
|
|
224
225
|
HardwareRequirement,
|
|
@@ -10,10 +10,16 @@ import {
|
|
|
10
10
|
type StyleProp,
|
|
11
11
|
type ViewStyle,
|
|
12
12
|
} from "react-native"
|
|
13
|
+
import {useMarkdown, type MarkedStyles, type useMarkdownHookOptions} from "react-native-marked"
|
|
13
14
|
import {SafeAreaView} from "react-native-safe-area-context"
|
|
14
15
|
import Svg, {Path, Rect} from "react-native-svg"
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
MINIMUM_OTA_BATTERY_LEVEL,
|
|
19
|
+
useMentraLiveOta,
|
|
20
|
+
type MentraLiveOtaController,
|
|
21
|
+
type MentraLiveOtaFlowPage,
|
|
22
|
+
} from "./useMentraLiveOta"
|
|
17
23
|
|
|
18
24
|
export type {MentraLiveOtaFlowPage} from "./useMentraLiveOta"
|
|
19
25
|
|
|
@@ -65,13 +71,23 @@ const DEFAULT_THEME: MentraLiveOtaFlowTheme = {
|
|
|
65
71
|
|
|
66
72
|
const ENGLISH_COPY: Record<string, string> = {
|
|
67
73
|
"common:continue": "Continue",
|
|
74
|
+
"common:done": "Done",
|
|
68
75
|
"ota:checkingForUpdates": "Checking for updates",
|
|
69
76
|
"ota:checkingForUpdatesMessage":
|
|
70
77
|
"Connected devices will perform automatic updates. Automatic updates can be disabled in Device Settings",
|
|
78
|
+
"ota:finishingUpdate": "Finishing your update",
|
|
79
|
+
"ota:checkingAdditionalUpdates": "Checking whether your glasses need any additional updates.",
|
|
71
80
|
"ota:updateAvailable": "{{deviceName}} Update Available",
|
|
81
|
+
"ota:batteryRequiredTitle": "Charge {{deviceName}} to Update",
|
|
82
|
+
"ota:batteryRequiredMessage":
|
|
83
|
+
"{{deviceName}} is currently at {{batteryLevel}}%. Charge it to at least {{minimumBatteryLevel}}% before updating.",
|
|
84
|
+
"ota:batteryRequiredLiveUpdate": "This screen will update automatically as the battery charges.",
|
|
72
85
|
"ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
|
|
86
|
+
"ota:wifiRequiredTitle": "WiFi Needed for Update",
|
|
73
87
|
"ota:updateDescription":
|
|
74
88
|
"A new update is available for your glasses. We recommend updating now for the best experience.",
|
|
89
|
+
"ota:updateSequenceMessage":
|
|
90
|
+
"Your glasses may install more than one update and restart several times. Keep them nearby until finished.",
|
|
75
91
|
"ota:releaseTransition": "{{fromVersion}} → {{toVersion}}",
|
|
76
92
|
"ota:releaseTransitionUnknown": "Current version unknown → {{toVersion}}",
|
|
77
93
|
"ota:updatedToVersion": "Updated to {{version}}",
|
|
@@ -81,6 +97,8 @@ const ENGLISH_COPY: Record<string, string> = {
|
|
|
81
97
|
"ota:updateNow": "Update Now",
|
|
82
98
|
"ota:setupWifi": "Setup WiFi",
|
|
83
99
|
"ota:updateLater": "Later",
|
|
100
|
+
"ota:updateComplete": "Update complete",
|
|
101
|
+
"ota:whatsNew": "What's new",
|
|
84
102
|
"ota:upToDate": "Up To Date",
|
|
85
103
|
"ota:devBuild": "Development Build",
|
|
86
104
|
"ota:devBuildNoOta":
|
|
@@ -95,6 +113,10 @@ const ENGLISH_COPY: Record<string, string> = {
|
|
|
95
113
|
"ota:versionChangeRestarting": "Installing a different version…",
|
|
96
114
|
"ota:versionChangeVerifying": "Verifying your glasses…",
|
|
97
115
|
"ota:versionChangeKeepNearby": "Keep your glasses nearby and connected. They will restart on their own.",
|
|
116
|
+
"ota:restartingGlasses": "Restarting {{deviceName}}…",
|
|
117
|
+
"ota:restartingGlassesMessage":
|
|
118
|
+
"The update is installed. Keep your glasses nearby and leave this screen open while they finish starting.",
|
|
119
|
+
"ota:restartingGlassesAutomatic": "We'll continue automatically when they're ready.",
|
|
98
120
|
"ota:versionChangeComplete": "Version Change Complete",
|
|
99
121
|
"ota:versionChangeCompleteMessage":
|
|
100
122
|
"Your glasses are now on the required version. Their settings were reset and are being restored automatically.",
|
|
@@ -185,12 +207,53 @@ function OtaFlowContent({
|
|
|
185
207
|
)
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
if (state.screen === "battery_required") {
|
|
211
|
+
return (
|
|
212
|
+
<FlowPage
|
|
213
|
+
actions={
|
|
214
|
+
<>
|
|
215
|
+
<FlowButton colors={colors} disabled label={translate("ota:updateNow")} onPress={controller.install} />
|
|
216
|
+
{state.canDismiss ? (
|
|
217
|
+
<FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary />
|
|
218
|
+
) : null}
|
|
219
|
+
</>
|
|
220
|
+
}
|
|
221
|
+
colors={colors}
|
|
222
|
+
icon="alert"
|
|
223
|
+
title={translate("ota:batteryRequiredTitle", {deviceName})}>
|
|
224
|
+
<BodyText colors={colors}>
|
|
225
|
+
{translate("ota:batteryRequiredMessage", {
|
|
226
|
+
batteryLevel: String(state.batteryLevel),
|
|
227
|
+
deviceName,
|
|
228
|
+
minimumBatteryLevel: String(MINIMUM_OTA_BATTERY_LEVEL),
|
|
229
|
+
})}
|
|
230
|
+
</BodyText>
|
|
231
|
+
<BodyText colors={colors}>{translate("ota:batteryRequiredLiveUpdate")}</BodyText>
|
|
232
|
+
</FlowPage>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (state.screen === "finishing") {
|
|
237
|
+
return (
|
|
238
|
+
<FlowPage colors={colors} icon="download" title={translate("ota:finishingUpdate")}>
|
|
239
|
+
<BodyText colors={colors}>{translate("ota:checkingAdditionalUpdates")}</BodyText>
|
|
240
|
+
<ActivityIndicator size="large" color={colors.foreground} />
|
|
241
|
+
</FlowPage>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
188
245
|
if (state.screen === "update_available" || state.screen === "wifi_required") {
|
|
246
|
+
const titleKey =
|
|
247
|
+
state.screen === "wifi_required"
|
|
248
|
+
? "ota:wifiRequiredTitle"
|
|
249
|
+
: state.versionChange
|
|
250
|
+
? "ota:downgradeAvailable"
|
|
251
|
+
: "ota:updateAvailable"
|
|
189
252
|
return (
|
|
190
253
|
<FlowPage
|
|
191
254
|
colors={colors}
|
|
192
255
|
icon="download"
|
|
193
|
-
title={translate(
|
|
256
|
+
title={translate(titleKey, {deviceName})}
|
|
194
257
|
actions={
|
|
195
258
|
<>
|
|
196
259
|
<FlowButton
|
|
@@ -222,6 +285,7 @@ function OtaFlowContent({
|
|
|
222
285
|
? translate("ota:updateConnectWifi", {deviceName})
|
|
223
286
|
: translate(state.versionChange ? "ota:downgradeDescription" : "ota:updateDescription")}
|
|
224
287
|
</BodyText>
|
|
288
|
+
<BodyText colors={colors}>{translate("ota:updateSequenceMessage")}</BodyText>
|
|
225
289
|
</FlowPage>
|
|
226
290
|
)
|
|
227
291
|
}
|
|
@@ -241,17 +305,24 @@ function OtaFlowContent({
|
|
|
241
305
|
if (state.screen === "up_to_date") {
|
|
242
306
|
return (
|
|
243
307
|
<FlowPage
|
|
244
|
-
actions={
|
|
308
|
+
actions={
|
|
309
|
+
<FlowButton
|
|
310
|
+
colors={colors}
|
|
311
|
+
label={translate(state.completedUpdate ? "common:done" : "common:continue")}
|
|
312
|
+
onPress={controller.finish}
|
|
313
|
+
/>
|
|
314
|
+
}
|
|
245
315
|
colors={colors}
|
|
316
|
+
contentAlignment={state.changelogs.length > 0 ? "top" : "center"}
|
|
246
317
|
icon="check"
|
|
247
|
-
title={translate("ota:upToDate")}>
|
|
318
|
+
title={translate(state.completedUpdate ? "ota:updateComplete" : "ota:upToDate")}>
|
|
248
319
|
<BodyText colors={colors}>{translate("ota:noUpdatesAvailable")}</BodyText>
|
|
249
320
|
{state.releaseTransition ? (
|
|
250
321
|
<BodyText colors={colors}>
|
|
251
322
|
{translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
|
|
252
323
|
</BodyText>
|
|
253
324
|
) : null}
|
|
254
|
-
<ChangelogList changelogs={state.changelogs} colors={colors} />
|
|
325
|
+
<ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")} />
|
|
255
326
|
</FlowPage>
|
|
256
327
|
)
|
|
257
328
|
}
|
|
@@ -307,10 +378,10 @@ function OtaFlowContent({
|
|
|
307
378
|
state.hotspotPhase === "downloading"
|
|
308
379
|
? "Downloading update to phone..."
|
|
309
380
|
: state.hotspotPhase === "starting_hotspot"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
381
|
+
? "Starting glasses hotspot..."
|
|
382
|
+
: state.hotspotPhase === "joining_hotspot"
|
|
383
|
+
? "Connecting phone to glasses..."
|
|
384
|
+
: "Starting update..."
|
|
314
385
|
return (
|
|
315
386
|
<FlowPage colors={colors} icon="download" title={title}>
|
|
316
387
|
{state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
|
|
@@ -347,14 +418,11 @@ function OtaFlowContent({
|
|
|
347
418
|
|
|
348
419
|
if (state.screen === "restarting") {
|
|
349
420
|
return (
|
|
350
|
-
<FlowPage
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
icon="check"
|
|
356
|
-
title="Update Installed"
|
|
357
|
-
/>
|
|
421
|
+
<FlowPage colors={colors} icon="download" title={translate("ota:restartingGlasses", {deviceName})}>
|
|
422
|
+
<ActivityIndicator size="large" color={colors.foreground} />
|
|
423
|
+
<BodyText colors={colors}>{translate("ota:restartingGlassesMessage")}</BodyText>
|
|
424
|
+
<BodyText colors={colors}>{translate("ota:restartingGlassesAutomatic")}</BodyText>
|
|
425
|
+
</FlowPage>
|
|
358
426
|
)
|
|
359
427
|
}
|
|
360
428
|
|
|
@@ -362,13 +430,13 @@ function OtaFlowContent({
|
|
|
362
430
|
const title = state.versionChangeConverged
|
|
363
431
|
? translate("ota:versionChangeComplete")
|
|
364
432
|
: state.versionChange
|
|
365
|
-
|
|
366
|
-
|
|
433
|
+
? translate("ota:versionChangeFirmwarePassComplete")
|
|
434
|
+
: "Update complete!"
|
|
367
435
|
const message = state.versionChangeConverged
|
|
368
436
|
? translate("ota:versionChangeCompleteMessage")
|
|
369
437
|
: state.versionChange
|
|
370
|
-
|
|
371
|
-
|
|
438
|
+
? translate("ota:versionChangeFirmwarePassCompleteMessage")
|
|
439
|
+
: "Your glasses are up to date."
|
|
372
440
|
return (
|
|
373
441
|
<FlowPage
|
|
374
442
|
actions={
|
|
@@ -379,6 +447,7 @@ function OtaFlowContent({
|
|
|
379
447
|
/>
|
|
380
448
|
}
|
|
381
449
|
colors={colors}
|
|
450
|
+
contentAlignment={state.changelogs.length > 0 ? "top" : "center"}
|
|
382
451
|
icon="check"
|
|
383
452
|
title={title}>
|
|
384
453
|
<BodyText colors={colors}>{message}</BodyText>
|
|
@@ -387,7 +456,7 @@ function OtaFlowContent({
|
|
|
387
456
|
{translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
|
|
388
457
|
</BodyText>
|
|
389
458
|
) : null}
|
|
390
|
-
<ChangelogList changelogs={state.changelogs} colors={colors} />
|
|
459
|
+
<ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")} />
|
|
391
460
|
</FlowPage>
|
|
392
461
|
)
|
|
393
462
|
}
|
|
@@ -435,14 +504,15 @@ type FlowPageProps = {
|
|
|
435
504
|
actions?: React.ReactNode
|
|
436
505
|
children?: React.ReactNode
|
|
437
506
|
colors: MentraLiveOtaFlowTheme
|
|
507
|
+
contentAlignment?: "center" | "top"
|
|
438
508
|
icon: "alert" | "bluetooth" | "check" | "download" | "settings"
|
|
439
509
|
title: string
|
|
440
510
|
}
|
|
441
511
|
|
|
442
|
-
function FlowPage({actions, children, colors, icon, title}: FlowPageProps) {
|
|
512
|
+
function FlowPage({actions, children, colors, contentAlignment = "center", icon, title}: FlowPageProps) {
|
|
443
513
|
return (
|
|
444
514
|
<View style={styles.page} testID="mentra-live-ota-flow">
|
|
445
|
-
<View style={styles.centerContent}>
|
|
515
|
+
<View style={[styles.centerContent, contentAlignment === "top" && styles.topContent]}>
|
|
446
516
|
<FlowIcon colors={colors} name={icon} />
|
|
447
517
|
<Text style={[styles.title, {color: colors.foreground}]}>{title}</Text>
|
|
448
518
|
{children}
|
|
@@ -460,27 +530,129 @@ function PercentText({colors, percent}: {colors: MentraLiveOtaFlowTheme; percent
|
|
|
460
530
|
return <Text style={[styles.percent, {color: colors.primary}]}>{Math.round(percent)}%</Text>
|
|
461
531
|
}
|
|
462
532
|
|
|
463
|
-
function
|
|
533
|
+
function ChangelogMarkdown({colors, markdown}: {colors: MentraLiveOtaFlowTheme; markdown: string}) {
|
|
534
|
+
const markdownStyles = useMemo<MarkedStyles>(
|
|
535
|
+
() => ({
|
|
536
|
+
blockquote: {
|
|
537
|
+
borderLeftColor: colors.primary,
|
|
538
|
+
borderLeftWidth: 3,
|
|
539
|
+
marginVertical: 2,
|
|
540
|
+
opacity: 1,
|
|
541
|
+
paddingLeft: 12,
|
|
542
|
+
},
|
|
543
|
+
code: {
|
|
544
|
+
backgroundColor: colors.background,
|
|
545
|
+
borderColor: colors.border,
|
|
546
|
+
borderRadius: 8,
|
|
547
|
+
borderWidth: 1,
|
|
548
|
+
padding: 12,
|
|
549
|
+
},
|
|
550
|
+
codespan: {
|
|
551
|
+
backgroundColor: colors.border,
|
|
552
|
+
color: colors.foreground,
|
|
553
|
+
fontFamily: "monospace",
|
|
554
|
+
fontSize: 13,
|
|
555
|
+
fontStyle: "normal",
|
|
556
|
+
fontWeight: "400",
|
|
557
|
+
},
|
|
558
|
+
em: {color: colors.textDim, fontSize: 14, lineHeight: 20},
|
|
559
|
+
h1: {
|
|
560
|
+
borderBottomWidth: 0,
|
|
561
|
+
color: colors.foreground,
|
|
562
|
+
fontSize: 18,
|
|
563
|
+
fontWeight: "700",
|
|
564
|
+
lineHeight: 24,
|
|
565
|
+
marginVertical: 0,
|
|
566
|
+
paddingBottom: 0,
|
|
567
|
+
},
|
|
568
|
+
h2: {
|
|
569
|
+
borderBottomWidth: 0,
|
|
570
|
+
color: colors.foreground,
|
|
571
|
+
fontSize: 17,
|
|
572
|
+
fontWeight: "700",
|
|
573
|
+
lineHeight: 23,
|
|
574
|
+
marginVertical: 0,
|
|
575
|
+
paddingBottom: 0,
|
|
576
|
+
},
|
|
577
|
+
h3: {color: colors.foreground, fontSize: 16, fontWeight: "700", lineHeight: 22, marginVertical: 0},
|
|
578
|
+
h4: {color: colors.foreground, fontSize: 15, fontWeight: "700", lineHeight: 21, marginVertical: 0},
|
|
579
|
+
h5: {color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20, marginVertical: 0},
|
|
580
|
+
h6: {color: colors.textDim, fontSize: 14, fontWeight: "600", lineHeight: 20, marginVertical: 0},
|
|
581
|
+
hr: {borderBottomColor: colors.border, marginVertical: 2},
|
|
582
|
+
image: {borderRadius: 8},
|
|
583
|
+
li: {color: colors.textDim, fontSize: 14, lineHeight: 20},
|
|
584
|
+
link: {
|
|
585
|
+
color: colors.primary,
|
|
586
|
+
fontSize: 14,
|
|
587
|
+
fontStyle: "normal",
|
|
588
|
+
lineHeight: 20,
|
|
589
|
+
textDecorationLine: "underline",
|
|
590
|
+
},
|
|
591
|
+
paragraph: {paddingVertical: 0},
|
|
592
|
+
strikethrough: {color: colors.textDim, fontSize: 14, lineHeight: 20},
|
|
593
|
+
strong: {color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20},
|
|
594
|
+
table: {borderColor: colors.border, borderRadius: 8},
|
|
595
|
+
tableCell: {padding: 8},
|
|
596
|
+
text: {color: colors.textDim, fontSize: 14, lineHeight: 20},
|
|
597
|
+
}),
|
|
598
|
+
[colors],
|
|
599
|
+
)
|
|
600
|
+
const markdownTheme = useMemo<NonNullable<useMarkdownHookOptions["theme"]>>(
|
|
601
|
+
() => ({
|
|
602
|
+
colors: {
|
|
603
|
+
background: colors.background,
|
|
604
|
+
border: colors.border,
|
|
605
|
+
code: colors.border,
|
|
606
|
+
link: colors.primary,
|
|
607
|
+
text: colors.textDim,
|
|
608
|
+
},
|
|
609
|
+
}),
|
|
610
|
+
[colors],
|
|
611
|
+
)
|
|
612
|
+
const elements = useMarkdown(markdown, {colorScheme: "light", styles: markdownStyles, theme: markdownTheme})
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
<View style={styles.changelogMarkdownContent} testID="ota-changelog-markdown">
|
|
616
|
+
{elements}
|
|
617
|
+
</View>
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function ChangelogList({
|
|
464
622
|
changelogs,
|
|
465
623
|
colors,
|
|
624
|
+
title,
|
|
466
625
|
}: {
|
|
467
626
|
changelogs: MentraLiveOtaController["state"]["changelogs"]
|
|
468
627
|
colors: MentraLiveOtaFlowTheme
|
|
628
|
+
title: string
|
|
469
629
|
}) {
|
|
470
630
|
if (changelogs.length === 0) return null
|
|
471
631
|
return (
|
|
472
|
-
<
|
|
473
|
-
{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
632
|
+
<View style={[styles.changelogCard, {borderColor: colors.border}]} testID="ota-changelog-card">
|
|
633
|
+
<Text style={[styles.changelogTitle, {color: colors.foreground}]}>{title}</Text>
|
|
634
|
+
<ScrollView
|
|
635
|
+
contentContainerStyle={styles.changelogContent}
|
|
636
|
+
nestedScrollEnabled
|
|
637
|
+
showsVerticalScrollIndicator
|
|
638
|
+
style={styles.changelogList}
|
|
639
|
+
testID="ota-changelog-scroll">
|
|
640
|
+
{changelogs.map((entry, index) => (
|
|
641
|
+
<View
|
|
642
|
+
key={entry.version}
|
|
643
|
+
style={[
|
|
644
|
+
styles.changelogEntry,
|
|
645
|
+
index > 0 && styles.changelogEntryDivider,
|
|
646
|
+
index > 0 && {borderTopColor: colors.border},
|
|
647
|
+
]}>
|
|
648
|
+
<Text selectable style={[styles.changelogVersion, {color: colors.foreground}]}>
|
|
649
|
+
{entry.version}
|
|
650
|
+
</Text>
|
|
651
|
+
<ChangelogMarkdown colors={colors} markdown={entry.markdown} />
|
|
652
|
+
</View>
|
|
653
|
+
))}
|
|
654
|
+
</ScrollView>
|
|
655
|
+
</View>
|
|
484
656
|
)
|
|
485
657
|
}
|
|
486
658
|
|
|
@@ -544,6 +716,7 @@ const styles = StyleSheet.create({
|
|
|
544
716
|
},
|
|
545
717
|
page: {flex: 1, paddingBottom: 24, paddingHorizontal: 24},
|
|
546
718
|
centerContent: {alignItems: "center", flex: 1, gap: 16, justifyContent: "center"},
|
|
719
|
+
topContent: {justifyContent: "flex-start", paddingTop: 12},
|
|
547
720
|
actionSpacer: {height: 48},
|
|
548
721
|
actions: {gap: 12},
|
|
549
722
|
icon: {fontSize: 64, fontWeight: "500", lineHeight: 72, textAlign: "center"},
|
|
@@ -552,11 +725,23 @@ const styles = StyleSheet.create({
|
|
|
552
725
|
percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
|
|
553
726
|
progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
|
|
554
727
|
progressFill: {borderRadius: 4, height: 8},
|
|
555
|
-
|
|
556
|
-
|
|
728
|
+
changelogCard: {
|
|
729
|
+
borderRadius: 16,
|
|
730
|
+
borderWidth: 1,
|
|
731
|
+
flexShrink: 1,
|
|
732
|
+
gap: 12,
|
|
733
|
+
maxHeight: 300,
|
|
734
|
+
maxWidth: 420,
|
|
735
|
+
padding: 16,
|
|
736
|
+
width: "100%",
|
|
737
|
+
},
|
|
738
|
+
changelogTitle: {fontSize: 16, fontWeight: "700"},
|
|
739
|
+
changelogList: {flexShrink: 1, maxHeight: 232, width: "100%"},
|
|
740
|
+
changelogContent: {gap: 20, paddingBottom: 2},
|
|
557
741
|
changelogEntry: {gap: 8},
|
|
558
|
-
|
|
559
|
-
|
|
742
|
+
changelogEntryDivider: {borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20},
|
|
743
|
+
changelogVersion: {fontSize: 14, fontWeight: "600"},
|
|
744
|
+
changelogMarkdownContent: {gap: 10},
|
|
560
745
|
button: {
|
|
561
746
|
alignItems: "center",
|
|
562
747
|
borderRadius: 50,
|