import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:vibration/vibration.dart'; import '../providers/providers.dart'; import '../services/navigate_notifier.dart'; import '../theme/app_theme.dart'; /// Terminal navigation screen with screenshot display and key grid. class NavigateScreen extends ConsumerStatefulWidget { const NavigateScreen({super.key}); @override ConsumerState createState() => _NavigateScreenState(); } class _NavigateScreenState extends ConsumerState { @override Widget build(BuildContext context) { final screenshot = ref.watch(latestScreenshotProvider); final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( appBar: AppBar( title: const Text('Navigate'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _requestScreenshot, tooltip: 'Refresh screenshot', ), ], ), body: Column( children: [ // Screenshot display Expanded( child: screenshot != null ? Padding( padding: const EdgeInsets.all(8), child: InteractiveViewer( minScale: 1.0, maxScale: 3.0, child: Image.memory( base64Decode( screenshot.contains(',') ? screenshot.split(',').last : screenshot, ), fit: BoxFit.contain, errorBuilder: (_, e, st) => const Center( child: Text('Screenshot decode error'), ), ), ), ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.screenshot_monitor, size: 48, color: isDark ? AppColors.darkTextTertiary : Colors.grey.shade400, ), const SizedBox(height: 12), Text( 'No screenshot yet', style: TextStyle( color: isDark ? AppColors.darkTextTertiary : Colors.grey.shade500, ), ), const SizedBox(height: 8), TextButton( onPressed: _requestScreenshot, child: const Text('Request Screenshot'), ), ], ), ), ), // Key grid Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, border: Border( top: BorderSide( color: isDark ? Colors.white10 : Colors.black12, ), ), ), child: SafeArea( top: false, child: _buildKeyGrid(context), ), ), ], ), ); } Widget _buildKeyGrid(BuildContext context) { return Column( children: [ // Row 1: 0, Up, G Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _keyButton('0', '0'), _keyButton('\u2191', 'k', label: 'k'), _keyButton('G', 'G'), ], ), const SizedBox(height: 8), // Row 2: Left, Down, Right Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _keyButton('\u2190', 'h', label: 'h'), _keyButton('\u2193', 'j', label: 'j'), _keyButton('\u2192', 'l', label: 'l'), ], ), const SizedBox(height: 8), // Row 3: dd, Esc, Tab Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _keyButton('dd', 'dd'), _keyButton('Esc', 'Escape'), _keyButton('Tab', 'Tab'), ], ), const SizedBox(height: 8), // Row 4: Enter (wide), ^C Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: _keyButton('Enter', 'Return', wide: true), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: _keyButton('^C', 'ctrl+c'), ), ), ], ), ], ); } Widget _keyButton(String display, String key, {String? label, bool wide = false}) { final isDark = Theme.of(context).brightness == Brightness.dark; return SizedBox( width: wide ? null : 72, height: 44, child: Material( color: isDark ? AppColors.darkInputBg : AppColors.lightInputBg, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: () => _sendKey(key), borderRadius: BorderRadius.circular(8), child: Center( child: Text( display, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary, ), ), ), ), ), ); } void _sendKey(String key) { _haptic(); // Send via MQTT - the chat screen's MQTT service is in the provider final activeSessionId = ref.read(activeSessionIdProvider); // Send a key press to the AIBroker daemon via the MQTT service. // navigateNotifierProvider bridges the navigate screen to the chat screen's MQTT service. ref.read(navigateNotifierProvider)?.sendKey(key, activeSessionId); // Request updated screenshot after key Future.delayed(const Duration(milliseconds: 500), _requestScreenshot); } void _requestScreenshot() { final activeSessionId = ref.read(activeSessionIdProvider); ref.read(navigateNotifierProvider)?.requestScreenshot(activeSessionId); } Future _haptic() async { try { final hasVibrator = await Vibration.hasVibrator(); if (hasVibrator) { Vibration.vibrate(duration: 15); } } catch (_) {} } }