196 lines
6.8 KiB
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),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} |