ui: tweak calibration sheet spacing

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