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( 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( 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( 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 _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 _confirmClear(BuildContext context) async { final ok = await showDialog( 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( 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), ); }, ); } }