Files
TelemetryMonitor/lib/ui/toolbar.dart

196 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
import '../session/view_state.dart';
import 'app_scope.dart';
import 'dialogs/clear_confirm_dialog.dart';
import 'dialogs/layout_dialog.dart';
import 'dialogs/settings_dialog.dart';
class Toolbar extends StatelessWidget {
const Toolbar({super.key});
@override
Widget build(BuildContext context) {
final scope = AppScope.of(context);
return Material(
color: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
const Text(
'Telemetry Monitor',
style: TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(width: 12),
const _Sep(),
ListenableBuilder(
listenable: scope.session.viewState,
builder: (_, __) => OutlinedButton.icon(
onPressed: () {
final newestUs = scope.session.packets.newest?.timestampUs
.toInt() ??
DateTime.now().microsecondsSinceEpoch;
scope.session.viewState.togglePause(newestUs: newestUs);
},
icon: Icon(scope.session.viewState.userPaused
? Icons.play_arrow
: Icons.pause),
label: Text(
scope.session.viewState.userPaused ? 'Resume' : 'Pause'),
),
),
const SizedBox(width: 6),
ListenableBuilder(
listenable: scope.session.viewState,
builder: (_, __) => OutlinedButton.icon(
onPressed: scope.session.viewState.anchorMode ==
ViewAnchorMode.followLive
? null
: scope.session.viewState.goLive,
icon: const Icon(Icons.fast_forward),
label: const Text('Go live'),
),
),
const SizedBox(width: 6),
OutlinedButton.icon(
onPressed: () => scope.session.viewState.resetView(),
icon: const Icon(Icons.fit_screen),
label: const Text('Reset view'),
),
const SizedBox(width: 6),
ValueListenableBuilder<bool>(
valueListenable: scope.session.cursorEnabled,
builder: (_, on, __) {
final scheme = Theme.of(context).colorScheme;
return OutlinedButton.icon(
onPressed: () =>
scope.session.cursorEnabled.value = !on,
icon: const Icon(Icons.my_location),
label: const Text('Cursor'),
style: on
? OutlinedButton.styleFrom(
backgroundColor: scheme.primaryContainer,
foregroundColor: scheme.onPrimaryContainer,
)
: null,
);
},
),
const _Sep(),
OutlinedButton.icon(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => scope.wrap(const LayoutDialog()),
),
icon: const Icon(Icons.grid_view),
label: const Text('Layout'),
),
const SizedBox(width: 6),
OutlinedButton.icon(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => scope.wrap(const SettingsDialog()),
),
icon: const Icon(Icons.settings),
label: const Text('Settings'),
),
const _Sep(),
OutlinedButton.icon(
onPressed: () => _exportData(context),
icon: const Icon(Icons.download),
label: const Text('Export data'),
),
const SizedBox(width: 6),
OutlinedButton.icon(
onPressed: () => _confirmClear(context),
icon: const Icon(Icons.delete_outline),
label: const Text('Clear all'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
),
const Spacer(),
const _ZoomReadout(),
],
),
),
);
}
Future<void> _exportData(BuildContext context) async {
final scope = AppScope.of(context);
final messenger = ScaffoldMessenger.of(context);
try {
final result = await scope.exporter.exportData(
buffer: scope.session.packets,
layout: scope.layout,
);
messenger.showSnackBar(
SnackBar(content: Text('Exported to ${result.path}')),
);
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Export failed: $e')),
);
}
}
Future<void> _confirmClear(BuildContext context) async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => const ClearConfirmDialog(),
);
if (ok == true && context.mounted) {
AppScope.of(context).session.clearAll();
}
}
}
class _Sep extends StatelessWidget {
const _Sep();
@override
Widget build(BuildContext context) => Container(
width: 1,
height: 22,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: Theme.of(context).colorScheme.outlineVariant,
);
}
class _ZoomReadout extends StatelessWidget {
const _ZoomReadout();
@override
Widget build(BuildContext context) {
final scope = AppScope.of(context);
return ValueListenableBuilder<int>(
valueListenable: scope.session.frameTick,
builder: (_, __, ___) {
final view = scope.session.viewState;
final newest = scope.session.packets.newest;
final nowUs = newest?.timestampUs.toInt() ??
DateTime.now().microsecondsSinceEpoch;
final win = view.currentWindow(
nowUs: nowUs,
oldestUs: scope.session.packets.oldest?.timestampUs.toInt() ?? nowUs,
newestUs: nowUs,
);
final widthMs = view.windowWidth.inMilliseconds;
final centerUs = (win.startUs + win.endUs) ~/ 2;
final dt = DateTime.fromMicrosecondsSinceEpoch(centerUs);
final widthStr = widthMs >= 1000
? '${(widthMs / 1000).toStringAsFixed(2)} s'
: '$widthMs ms';
final hh = dt.hour.toString().padLeft(2, '0');
final mm = dt.minute.toString().padLeft(2, '0');
final ss = dt.second.toString().padLeft(2, '0');
final ms = dt.millisecond.toString().padLeft(3, '0');
return Text(
'Window: $widthStr · Center: $hh:$mm:$ss.$ms',
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12, color: Colors.grey),
);
},
);
}
}