From 46ac9d3123876016243e4a6c763931dd7048a4ea Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Mon, 26 Jan 2026 15:47:59 +0900 Subject: [PATCH] interface wrapping --- pi/ui/lib/screens/dashboard_screen.dart | 108 +++++++---------- pi/ui/lib/widgets/stat_box.dart | 88 ++++++++++---- pi/ui/lib/widgets/stat_box_main.dart | 82 +++++++++++++ pi/ui/lib/widgets/system_bar.dart | 151 ++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 87 deletions(-) create mode 100644 pi/ui/lib/widgets/stat_box_main.dart create mode 100644 pi/ui/lib/widgets/system_bar.dart diff --git a/pi/ui/lib/screens/dashboard_screen.dart b/pi/ui/lib/screens/dashboard_screen.dart index 7e26a4a..d2d5086 100644 --- a/pi/ui/lib/screens/dashboard_screen.dart +++ b/pi/ui/lib/screens/dashboard_screen.dart @@ -6,6 +6,8 @@ import '../services/pi_io.dart'; import '../theme/app_theme.dart'; import '../widgets/navigator_widget.dart'; import '../widgets/stat_box.dart'; +import '../widgets/stat_box_main.dart'; +import '../widgets/system_bar.dart'; // test service for triggers import '../services/test_flipflop_service.dart'; @@ -26,7 +28,11 @@ class _DashboardScreenState extends State { double? _piTemp; int _rpm = 0; double _voltage = 12.6; - int _temp = 25; + int _engineTemp = 25; + + // Placeholder values for system bar + int? _gpsSatellites; + int? _lteSignal; @override void initState() { @@ -41,7 +47,13 @@ class _DashboardScreenState extends State { // Placeholder random data - will be replaced with real sensors _rpm = 1000 + _random.nextInt(8000); _voltage = 11.5 + _random.nextDouble() * 2; - _temp = 20 + _random.nextInt(60); + _engineTemp = 20 + _random.nextInt(60); + + // Placeholder: GPS satellites (null = disconnected, 0 = no fix, 3-12 = typical) + _gpsSatellites = _random.nextBool() ? _random.nextInt(12) : null; + + // Placeholder: LTE signal (null = disconnected, 0-4 = signal bars) + _lteSignal = _random.nextBool() ? _random.nextInt(5) : null; }); }); @@ -63,7 +75,7 @@ class _DashboardScreenState extends State { return Scaffold( backgroundColor: theme.background, body: Padding( - padding: const EdgeInsets.all(32), + padding: const EdgeInsets.all(16), child: Row( children: [ // Left side: All dashboard widgets (flex: 2) @@ -72,75 +84,43 @@ class _DashboardScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Header (voltage - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - flex: 3, - child: Container(), - ), - Expanded( - flex: 3, - child: Text( - 'Chassis voltage ', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontSize: 60, - color: theme.subdued, - letterSpacing: 1, - ), - ), - ), - Expanded( - flex: 1, - child: Text( - '${_voltage.toStringAsFixed(1)}V', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontSize: 80, - color: _voltage < 11.9 ? theme.highlight : theme.foreground, - ), - ), - ) - ], + // System status bar + SystemBar( + gpsSatellites: _gpsSatellites, + lteSignal: _lteSignal, + piTemp: _piTemp, + voltage: _voltage, ), - const SizedBox(height: 20), + const SizedBox(height: 10), - // Main Pi temperature display + // Main content area - big stat boxes Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _piTemp != null ? _piTemp!.toStringAsFixed(1) : '—', - style: TextStyle( - fontSize: 250, - fontWeight: FontWeight.w200, - color: theme.foreground, - height: 1, - ), - ), - Text( - 'Pi Temp', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontSize: 80, - color: theme.subdued, - ), - ), - ], - ), + flex: 8, + child: Row( + children: [ + // Speed - placeholder, will come from GPS + StatBoxMain( + value: _rpm.toString(), + label: 'RPM', + ), + // Add second StatBoxMain here for 2-up layout: + // StatBoxMain(value: '4500', unit: 'rpm', label: 'TACH'), + ], ), ), // Bottom stats row - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - StatBox(label: 'RPM', value: _rpm.toString()), - StatBox(label: 'ENG', value: '$_temp°C'), - StatBox(label: 'GEAR', value: '—'), - ], + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + StatBox(value: _rpm.toString(), label: 'RPM'), + StatBox(value: '$_engineTemp', unit: '°C', label: 'ENG'), + const StatBox(value: '—', label: 'GEAR'), + ], + ), ), ], ), diff --git a/pi/ui/lib/widgets/stat_box.dart b/pi/ui/lib/widgets/stat_box.dart index 5cd23cc..de57787 100644 --- a/pi/ui/lib/widgets/stat_box.dart +++ b/pi/ui/lib/widgets/stat_box.dart @@ -2,38 +2,80 @@ import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; -/// A labeled stat display box for the dashboard +/// A labeled stat display box for the dashboard bottom row. class StatBox extends StatelessWidget { - final String label; final String value; + final String? unit; + final String label; + final int flex; - const StatBox({super.key, required this.label, required this.value}); + const StatBox({ + super.key, + required this.value, + this.unit, + required this.label, + this.flex = 1, + }); @override Widget build(BuildContext context) { final theme = AppTheme.of(context); return Expanded( - flex: 1, - child: Column( - children: [ - Text( - value, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontSize: 100, - color: theme.foreground, - ), - ), - Text( - label, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 80, - color: theme.subdued, - letterSpacing: 1, - ), - ), - ], - ) + flex: flex, + child: LayoutBuilder( + builder: (context, constraints) { + final baseSize = constraints.maxHeight * 0.4; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Value + optional unit + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + value, + style: TextStyle( + fontSize: baseSize, + fontWeight: FontWeight.w400, + fontFeatures: const [FontFeature.tabularFigures()], + color: theme.foreground, + height: 1, + ), + ), + SizedBox(width: baseSize * 0.1), + if (unit != null) ...[ + const SizedBox(width: 4), + Text( + unit!, + style: TextStyle( + fontSize: baseSize * 0.5, + fontWeight: FontWeight.w400, + color: theme.subdued, + height: 1, + ), + ), + ], + ], + ), + SizedBox(height: baseSize * 0.1), + // Label + Text( + label, + style: TextStyle( + fontSize: baseSize * 0.6, + fontWeight: FontWeight.w400, + color: theme.subdued, + letterSpacing: 1, + ), + ), + ], + ); + }, + ), ); } } diff --git a/pi/ui/lib/widgets/stat_box_main.dart b/pi/ui/lib/widgets/stat_box_main.dart new file mode 100644 index 0000000..920b5e1 --- /dev/null +++ b/pi/ui/lib/widgets/stat_box_main.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_theme.dart'; + +/// Large stat display for main dashboard area. +/// Fixed-size container - content changes don't affect layout. +class StatBoxMain extends StatelessWidget { + final String value; + final String? unit; + final String label; + final int flex; + + const StatBoxMain({ + super.key, + required this.value, + this.unit, + required this.label, + this.flex = 1, + }); + + @override + Widget build(BuildContext context) { + final theme = AppTheme.of(context); + + return Expanded( + flex: flex, + child: LayoutBuilder( + builder: (context, constraints) { + // Scale fonts relative to box height for consistent proportions + final baseSize = constraints.maxHeight * 0.4; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Value + optional unit row + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + value, + style: TextStyle( + fontSize: baseSize, + fontWeight: FontWeight.w300, + fontFeatures: const [FontFeature.tabularFigures()], + color: theme.foreground, + height: 1, + ), + ), + if (unit != null) ...[ + const SizedBox(width: 8), + Text( + unit!, + style: TextStyle( + fontSize: baseSize * 0.4, + fontWeight: FontWeight.w400, + color: theme.subdued, + height: 1, + ), + ), + ], + ], + ), + SizedBox(height: baseSize * 0.1), + // Label + Text( + label, + style: TextStyle( + fontSize: baseSize * 0.35, + fontWeight: FontWeight.w400, + color: theme.subdued, + letterSpacing: 2, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/pi/ui/lib/widgets/system_bar.dart b/pi/ui/lib/widgets/system_bar.dart new file mode 100644 index 0000000..b4648bd --- /dev/null +++ b/pi/ui/lib/widgets/system_bar.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_theme.dart'; + +/// Android-style persistent status bar for system indicators. +/// Shows GPS satellites, LTE signal, Pi temp, voltage at a glance. +class SystemBar extends StatelessWidget { + final int? gpsSatellites; // null = disconnected + final int? lteSignal; // null = disconnected, 0-4 bars + final double? piTemp; // null = unavailable + final double? voltage; // null = Arduino disconnected + + const SystemBar({ + super.key, + this.gpsSatellites, + this.lteSignal, + this.piTemp, + this.voltage, + }); + + @override + Widget build(BuildContext context) { + final theme = AppTheme.of(context); + + return Expanded( + flex: 1, + child: LayoutBuilder( + builder: (context, constraints) { + // Font sizes relative to bar height + final labelSize = constraints.maxHeight * 0.5; + final valueSize = constraints.maxHeight * 0.5; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.subdued.withValues(alpha: 0.3), + width: 1, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left group: GPS, LTE + _Indicator( + label: 'GPS', + value: gpsSatellites?.toString() ?? 'N/A', + isAbnormal: gpsSatellites == null || gpsSatellites == 0, + alignment: Alignment.centerLeft, + labelSize: labelSize, + valueSize: valueSize, + flex: 2, + theme: theme, + ), + _Indicator( + label: 'LTE', + value: lteSignal?.toString() ?? 'N/A', + isAbnormal: lteSignal == null, + alignment: Alignment.centerLeft, + labelSize: labelSize, + valueSize: valueSize, + flex: 2, + theme: theme, + ), + + // Right group: Pi, Chassis + _Indicator( + label: 'Pi', + value: piTemp != null ? '${piTemp!.toStringAsFixed(1)} °C' : 'N/A', + isAbnormal: piTemp == null || piTemp! > 80, + alignment: Alignment.centerRight, + labelSize: labelSize, + valueSize: valueSize, + flex: 2, + theme: theme, + ), + _Indicator( + label: 'Chassis', + value: voltage != null ? '${voltage!.toStringAsFixed(1)} V' : 'N/A', + isAbnormal: voltage == null || voltage! < 11.9, + alignment: Alignment.centerRight, + labelSize: labelSize, + valueSize: valueSize, + flex: 3, + theme: theme, + ), + ], + ), + ); + }, + ), + ); + } +} + +/// Single status indicator in a fixed-width flex slot. +class _Indicator extends StatelessWidget { + final String label; + final String value; + final bool isAbnormal; + final Alignment alignment; + final double labelSize; + final double valueSize; + final int flex; + final AppTheme theme; + + const _Indicator({ + required this.label, + required this.value, + required this.isAbnormal, + required this.alignment, + required this.labelSize, + required this.valueSize, + required this.flex, + required this.theme, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + flex: flex, + child: Align( + alignment: alignment, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '$label ', + style: TextStyle( + fontSize: labelSize, + color: theme.subdued, + ), + ), + Text( + value, + style: TextStyle( + fontSize: valueSize, + fontFeatures: const [FontFeature.tabularFigures()], + color: isAbnormal ? theme.highlight : theme.foreground, + ), + ), + ], + ), + ), + ); + } +}