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,
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<double>(
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,
);
}
}