ui: tweak calibration sheet spacing
This commit is contained in:
@@ -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(
|
||||
? Chip(
|
||||
label: Text(
|
||||
'${value.round()} dBm',
|
||||
'${rssi!.round()} dBm',
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
|
||||
),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
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,7 +435,7 @@ class _CollectingPage extends ConsumerWidget {
|
||||
completedDistances: state.completedDistances,
|
||||
selectedDistance: state.selectedDistance,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Title + subtitle
|
||||
Text(
|
||||
@@ -448,10 +444,7 @@ class _CollectingPage extends ConsumerWidget {
|
||||
: state.selectedDistance != null
|
||||
? 'Step to ${_fmtDist(state.selectedDistance!)} metres'
|
||||
: 'Select a distance',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
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,11 +485,13 @@ class _CollectingPage extends ConsumerWidget {
|
||||
samples: state.samplesCollected,
|
||||
avgRssi: state.avgRssi,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Waveform
|
||||
SizedBox(
|
||||
height: 72,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.symmetric(horizontal: 8),
|
||||
child: CustomPaint(
|
||||
painter: _WaveformPainter(
|
||||
readings: state.waveform,
|
||||
@@ -504,6 +499,7 @@ class _CollectingPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -992,7 +980,13 @@ class _AsyncButtonState extends State<_AsyncButton> {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user