From 2fff252e9db8835594a2b1a6c385a1fe4a338d7c Mon Sep 17 00:00:00 2001 From: dvdrw Date: Fri, 22 May 2026 15:18:37 +0200 Subject: [PATCH] ui: tweak calibration sheet spacing --- lib/features/sensors/calibration_sheet.dart | 220 ++++++++++---------- 1 file changed, 111 insertions(+), 109 deletions(-) diff --git a/lib/features/sensors/calibration_sheet.dart b/lib/features/sensors/calibration_sheet.dart index 275b364..740ff1d 100644 --- a/lib/features/sensors/calibration_sheet.dart +++ b/lib/features/sensors/calibration_sheet.dart @@ -132,15 +132,8 @@ class _CalibrationSheetState extends ConsumerState { sensorKey: _key, onNext: _commitTag, ), - _CollectingPage( - state: state, - sensorKey: _key, - onFinish: _finish, - ), - _DonePage( - state: state, - onDone: () => _done(context), - ), + _CollectingPage(state: state, sensorKey: _key, onFinish: _finish), + _DonePage(state: state, onDone: () => _done(context)), ], ), ), @@ -179,37 +172,37 @@ class _IntroPage extends StatelessWidget { ), ), const SizedBox(height: 32), - Icon( - Icons.tune, - size: 56, - color: theme.colorScheme.primary, - ), - const SizedBox(height: 24), + Icon(Icons.tune, size: 56, color: theme.colorScheme.primary), + const SizedBox(height: 18), Text( 'Calibrate sensor', - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.w600), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( 'Calibration improves distance estimation accuracy. You will hold a tag at a series of known distances from the sensor while it collects readings.', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.colorScheme.onSurfaceVariant), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( 'You will need at least two distance measurements to complete calibration. More distances improve accuracy.', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.colorScheme.onSurfaceVariant), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( 'Keep the path between the tag and sensor unobstructed during each measurement.', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.colorScheme.onSurfaceVariant), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), const Spacer(), @@ -219,10 +212,7 @@ class _IntroPage extends StatelessWidget { icon: Icons.arrow_forward, ), const SizedBox(height: 12), - TextButton( - onPressed: onCancel, - child: const Text('Cancel'), - ), + TextButton(onPressed: onCancel, child: const Text('Cancel')), ], ), ), @@ -256,7 +246,8 @@ class _TagSelectionPage extends ConsumerWidget { // Sort: tags with readings by RSSI descending (nearest first), // then tags without readings by id. - final sorted = [...tags]..sort((a, b) { + final sorted = [...tags] + ..sort((a, b) { final ra = readings[a.tagId]; final rb = readings[b.tagId]; if (ra != null && rb != null) return rb.compareTo(ra); @@ -281,15 +272,15 @@ class _TagSelectionPage extends ConsumerWidget { ), ), ), - const SizedBox(height: 20), - const Text( + const SizedBox(height: 24), + Text( 'Select your tag', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), - const SizedBox(height: 4), + const SizedBox(height: 8), Text( - 'Hold each tag near the sensor — the one you\'re using will show a stronger signal.', + 'Hold the tag near the sensor — the one you\'re using will show a stronger signal.', style: TextStyle( fontSize: 13, color: theme.colorScheme.onSurfaceVariant, @@ -303,7 +294,8 @@ class _TagSelectionPage extends ConsumerWidget { child: Text( 'No tags enrolled', style: TextStyle( - color: theme.colorScheme.onSurfaceVariant), + color: theme.colorScheme.onSurfaceVariant, + ), ), ) : ListView.builder( @@ -336,6 +328,15 @@ class _TagSelectionPage extends ConsumerWidget { } } +// TODO: use 3rd party library +String _formatLastSeen(DateTime dt) { + final diff = DateTime.now().difference(dt); + if (diff.inSeconds < 60) return 'Just now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; + if (diff.inHours < 24) return '${diff.inHours}h ago'; + return '${diff.inDays}d ago'; +} + class _TagListTile extends StatelessWidget { const _TagListTile({ super.key, @@ -363,22 +364,17 @@ class _TagListTile extends StatelessWidget { ), title: Text(tag.name), subtitle: Text( - tag.tagId, + "Last seen ${_formatLastSeen(tag.lastSeen!)}", style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), ), trailing: rssi != null - ? TweenAnimationBuilder( - tween: Tween(begin: rssi, end: rssi), - duration: const Duration(milliseconds: 300), - builder: (context, value, child) => Chip( - label: Text( - '${value.round()} dBm', - style: const TextStyle(fontFamily: 'monospace', fontSize: 12), - ), - backgroundColor: cs.primaryContainer, - side: BorderSide.none, - padding: const EdgeInsets.symmetric(horizontal: 4), + ? Chip( + label: Text( + '${rssi!.round()} dBm', + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), ), + side: BorderSide.none, + padding: const EdgeInsets.symmetric(horizontal: 4), ) : Chip( label: Text( @@ -431,7 +427,7 @@ class _CollectingPage extends ConsumerWidget { ), ), ), - const SizedBox(height: 20), + const SizedBox(height: 24), // Dot-dash stage indicator _StageIndicator( @@ -439,19 +435,16 @@ class _CollectingPage extends ConsumerWidget { completedDistances: state.completedDistances, selectedDistance: state.selectedDistance, ), - const SizedBox(height: 16), + const SizedBox(height: 18), // Title + subtitle Text( collecting ? 'Hold steady' : state.selectedDistance != null - ? 'Step to ${_fmtDist(state.selectedDistance!)} metres' - : 'Select a distance', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), + ? 'Step to ${_fmtDist(state.selectedDistance!)} metres' + : 'Select a distance', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), const SizedBox(height: 4), @@ -465,7 +458,7 @@ class _CollectingPage extends ConsumerWidget { ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), + const SizedBox(height: 18), // Distance chips _DistanceChips( @@ -475,7 +468,7 @@ class _CollectingPage extends ConsumerWidget { enabled: !collecting, onSelect: notifier.selectDistance, ), - const SizedBox(height: 24), + const SizedBox(height: 28), // Ring + pulse Center( @@ -484,7 +477,7 @@ class _CollectingPage extends ConsumerWidget { onStart: collecting ? null : notifier.startStage, ), ), - const SizedBox(height: 20), + const SizedBox(height: 28), // Stats row _StatsRow( @@ -492,15 +485,18 @@ class _CollectingPage extends ConsumerWidget { samples: state.samplesCollected, avgRssi: state.avgRssi, ), - const SizedBox(height: 16), + const SizedBox(height: 28), // Waveform SizedBox( height: 72, - child: CustomPaint( - painter: _WaveformPainter( - readings: state.waveform, - color: theme.colorScheme.primary, + child: Padding( + padding: const EdgeInsetsGeometry.symmetric(horizontal: 8), + child: CustomPaint( + painter: _WaveformPainter( + readings: state.waveform, + color: theme.colorScheme.primary, + ), ), ), ), @@ -562,21 +558,23 @@ class _DonePage extends StatelessWidget { size: 80, color: Colors.green.shade600, ), - const SizedBox(height: 20), + const SizedBox(height: 18), Text( 'Calibration complete', - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.w600), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'The sensor model has been updated with your measurements.', - style: theme.textTheme.bodyMedium - ?.copyWith(color: theme.colorScheme.onSurfaceVariant), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), - const SizedBox(height: 24), + const SizedBox(height: 16), if (rssiRef != null && exp != null) ...[ _ResultRow( label: 'RSSI at 1 m (A)', @@ -586,9 +584,9 @@ class _DonePage extends StatelessWidget { label: 'Path loss exponent (n)', value: exp.toStringAsFixed(3), ), - const SizedBox(height: 24), + // const SizedBox(height: 12), SizedBox( - height: 160, + height: 240, child: CustomPaint( painter: _ModelCurvePainter( rssiRef: rssiRef, @@ -601,15 +599,7 @@ class _DonePage extends StatelessWidget { ), ], const Spacer(), - FilledButton( - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - onPressed: onDone, - child: const Text('Done'), - ), + FilledButton(onPressed: onDone, child: const Text('Done')), ], ), ), @@ -649,8 +639,8 @@ class _StageIndicator extends StatelessWidget { color: isCompleted ? Colors.green.shade500 : isCurrent - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outlineVariant, + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outlineVariant, borderRadius: BorderRadius.circular(4), ), ), @@ -698,8 +688,8 @@ class _DistanceChips extends StatelessWidget { color: completed ? Colors.black : selected - ? cs.onPrimaryContainer - : null, + ? cs.onPrimaryContainer + : null, ), disabledColor: completed ? Colors.green.shade50 : null, shape: RoundedRectangleBorder( @@ -768,7 +758,11 @@ class _RingAreaState extends State<_RingArea> with TickerProviderStateMixin { children: [ // One independent pulse ring per in-flight animation. for (final ctrl in _pulses) - _PulseRing(controller: ctrl, color: theme.colorScheme.primary, size: size), + _PulseRing( + controller: ctrl, + color: theme.colorScheme.primary, + size: size, + ), // Ring CustomPaint( @@ -931,15 +925,9 @@ class _ResultRow extends StatelessWidget { child: Row( children: [ Expanded( - child: Text( - label, - style: Theme.of(context).textTheme.bodySmall, - ), - ), - Text( - value, - style: const TextStyle(fontFamily: 'monospace'), + child: Text(label, style: Theme.of(context).textTheme.bodySmall), ), + Text(value, style: const TextStyle(fontFamily: 'monospace')), ], ), ); @@ -984,15 +972,21 @@ class _AsyncButtonState extends State<_AsyncButton> { child: CircularProgressIndicator(strokeWidth: 2), ) : widget.icon != null - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(widget.label), - const SizedBox(width: 8), - Icon(widget.icon, size: 18), - ], - ) - : Text(widget.label, style: TextStyle(fontSize: 36, color: Theme.of(context).colorScheme.primary)); + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(widget.label), + const SizedBox(width: 8), + Icon(widget.icon, size: 18), + ], + ) + : Text( + widget.label, + style: TextStyle( + fontSize: 36, + color: Theme.of(context).colorScheme.primary, + ), + ); final canPress = !_loading && widget.enabled; @@ -1014,17 +1008,17 @@ class _AsyncButtonState extends State<_AsyncButton> { if (widget.compact) { return TextButton( - style: TextButton.styleFrom(shape: shape, textStyle: const TextStyle(fontSize: 36, color: Colors.black)), + style: TextButton.styleFrom( + shape: shape, + textStyle: const TextStyle(fontSize: 36, color: Colors.black), + ), onPressed: canPress ? _run : null, child: child, ); } return FilledButton( - style: FilledButton.styleFrom( - shape: shape, - minimumSize: const Size.fromHeight(48), - ), + style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(48)), onPressed: canPress ? _run : null, child: child, ); @@ -1116,7 +1110,10 @@ class _WaveformPainter extends CustomPainter { final path = Path(); for (var i = 0; i < readings.length; i++) { final x = size.width * i / (readings.length - 1); - final norm = ((readings[i] - _minRssi) / (_maxRssi - _minRssi)).clamp(0.0, 1.0); + final norm = ((readings[i] - _minRssi) / (_maxRssi - _minRssi)).clamp( + 0.0, + 1.0, + ); final y = size.height * (1 - norm); if (i == 0) { path.moveTo(x, y); @@ -1151,7 +1148,8 @@ class _ModelCurvePainter extends CustomPainter { static const _rssiMin = -100.0; static const _rssiMax = -20.0; - double _rssi(double d) => rssiRef - 10 * pathLossExp * math.log(d) / math.ln10; + double _rssi(double d) => + rssiRef - 10 * pathLossExp * math.log(d) / math.ln10; @override void paint(Canvas canvas, Size size) { @@ -1200,8 +1198,12 @@ class _ModelCurvePainter extends CustomPainter { for (final dist in [1.0, 3.0, 5.0, 10.0]) { final t = (dist - _dMin) / (_dMax - _dMin); final x = padding + t * plotW; - _drawText(canvas, '${dist.toInt()}m', Offset(x - 8, size.height - 14), - labelStyle); + _drawText( + canvas, + '${dist.toInt()}m', + Offset(x - 8, size.height - 14), + labelStyle, + ); } }