import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../blocs/geviscope/geviscope_bloc.dart'; import '../../blocs/geviscope/geviscope_event.dart'; import '../../blocs/geviscope/geviscope_state.dart'; import '../../widgets/app_drawer.dart'; class GeViScopeScreen extends StatefulWidget { const GeViScopeScreen({super.key}); @override State createState() => _GeViScopeScreenState(); } class _GeViScopeScreenState extends State { // Connection form controllers final _addressController = TextEditingController(text: 'localhost'); final _usernameController = TextEditingController(text: 'sysadmin'); final _passwordController = TextEditingController(text: 'masterkey'); // CrossSwitch form controllers final _videoInputController = TextEditingController(text: '1'); final _videoOutputController = TextEditingController(text: '1'); // PTZ controls final _cameraController = TextEditingController(text: '1'); final _ptzSpeedController = TextEditingController(text: '50'); final _presetController = TextEditingController(text: '1'); // Digital I/O controller final _contactIdController = TextEditingController(text: '1'); // Custom action controllers final _customActionTypeIdController = TextEditingController(text: '1'); final _customActionTextController = TextEditingController(text: 'Test message'); // Raw action controller final _rawActionController = TextEditingController(); bool _didCheckStatus = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_didCheckStatus) { _didCheckStatus = true; context.read().add(const CheckGeViScopeStatusEvent()); } } @override void dispose() { _addressController.dispose(); _usernameController.dispose(); _passwordController.dispose(); _videoInputController.dispose(); _videoOutputController.dispose(); _cameraController.dispose(); _ptzSpeedController.dispose(); _presetController.dispose(); _contactIdController.dispose(); _customActionTypeIdController.dispose(); _customActionTextController.dispose(); _rawActionController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('GeViScope Control'), actions: [ BlocBuilder( builder: (context, state) { return Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: _getStatusColor(state.connectionStatus).withOpacity(0.2), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _getStatusIcon(state.connectionStatus), size: 16, color: _getStatusColor(state.connectionStatus), ), const SizedBox(width: 6), Text( _getStatusText(state.connectionStatus), style: TextStyle( color: _getStatusColor(state.connectionStatus), fontWeight: FontWeight.bold, ), ), if (state.channelCount > 0) ...[ const SizedBox(width: 8), Text( '(${state.channelCount} channels)', style: TextStyle( color: _getStatusColor(state.connectionStatus), fontSize: 12, ), ), ], ], ), ), const SizedBox(width: 16), ], ); }, ), ], ), drawer: const AppDrawer(currentRoute: '/geviscope'), body: BlocConsumer( listener: (context, state) { if (state.lastActionResult != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.lastActionResult!), backgroundColor: state.lastActionSuccess ? Colors.green : Colors.red, duration: const Duration(seconds: 2), ), ); context.read().add(const ClearGeViScopeActionResultEvent()); } }, builder: (context, state) { return Row( children: [ // Left panel - Controls Expanded( flex: 2, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildConnectionCard(context, state), const SizedBox(height: 16), if (state.isConnected) ...[ _buildPTZControlCard(context, state), const SizedBox(height: 16), _buildVideoControlCard(context, state), const SizedBox(height: 16), _buildDigitalIOCard(context, state), const SizedBox(height: 16), _buildCustomActionCard(context, state), const SizedBox(height: 16), _buildRawActionCard(context, state), ], ], ), ), ), // Right panel - Channels & Message log Expanded( flex: 1, child: _buildRightPanel(context, state), ), ], ); }, ), ); } Widget _buildConnectionCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.power_settings_new, color: state.isConnected ? Colors.green : Colors.grey, ), const SizedBox(width: 8), Text( 'Connection', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), if (!state.isConnected) ...[ Row( children: [ Expanded( child: TextField( controller: _addressController, decoration: const InputDecoration( labelText: 'Server Address', hintText: 'localhost', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', hintText: 'sysadmin', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _passwordController, obscureText: true, decoration: const InputDecoration( labelText: 'Password', hintText: 'masterkey', border: OutlineInputBorder(), isDense: true, ), ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( ConnectGeViScopeEvent( address: _addressController.text, username: _usernameController.text, password: _passwordController.text, ), ); }, icon: state.isLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.link), label: Text(state.isLoading ? 'Connecting...' : 'Connect'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), ), ] else ...[ ListTile( leading: const Icon(Icons.videocam, color: Colors.green), title: Text('Connected to ${state.serverAddress}'), subtitle: Text('User: ${state.username} | ${state.channelCount} channels'), contentPadding: EdgeInsets.zero, ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( const DisconnectGeViScopeEvent(), ); }, icon: const Icon(Icons.link_off), label: const Text('Disconnect'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), ), ), ], ], ), ), ); } Widget _buildPTZControlCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.control_camera, color: Colors.orange), const SizedBox(width: 8), Text( 'PTZ Camera Control', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), Row( children: [ Expanded( child: TextField( controller: _cameraController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Camera #', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _ptzSpeedController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Speed (1-100)', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _presetController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Preset #', border: OutlineInputBorder(), isDense: true, ), ), ), ], ), const SizedBox(height: 16), // PTZ Direction pad Center( child: SizedBox( width: 200, height: 200, child: Stack( children: [ // Up Positioned( top: 0, left: 70, child: _ptzButton( context, Icons.arrow_upward, 'Up', () => _sendPTZ(context, 'tilt', 'up'), ), ), // Down Positioned( bottom: 0, left: 70, child: _ptzButton( context, Icons.arrow_downward, 'Down', () => _sendPTZ(context, 'tilt', 'down'), ), ), // Left Positioned( left: 0, top: 70, child: _ptzButton( context, Icons.arrow_back, 'Left', () => _sendPTZ(context, 'pan', 'left'), ), ), // Right Positioned( right: 0, top: 70, child: _ptzButton( context, Icons.arrow_forward, 'Right', () => _sendPTZ(context, 'pan', 'right'), ), ), // Stop (center) Positioned( left: 70, top: 70, child: SizedBox( width: 60, height: 60, child: ElevatedButton( onPressed: state.isLoading ? null : () { final camera = int.tryParse(_cameraController.text) ?? 1; context.read().add( CameraStopEvent(camera: camera), ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, shape: const CircleBorder(), padding: EdgeInsets.zero, ), child: const Icon(Icons.stop, size: 30), ), ), ), ], ), ), ), const SizedBox(height: 16), // Zoom controls Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton.icon( onPressed: state.isLoading ? null : () => _sendPTZ(context, 'zoom', 'out'), icon: const Icon(Icons.zoom_out), label: const Text('Zoom Out'), ), const SizedBox(width: 16), ElevatedButton.icon( onPressed: state.isLoading ? null : () => _sendPTZ(context, 'zoom', 'in'), icon: const Icon(Icons.zoom_in), label: const Text('Zoom In'), ), ], ), const SizedBox(height: 16), // Preset Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: state.isLoading ? null : () { final camera = int.tryParse(_cameraController.text) ?? 1; final preset = int.tryParse(_presetController.text) ?? 1; context.read().add( CameraPresetEvent(camera: camera, preset: preset), ); }, icon: const Icon(Icons.bookmark), label: const Text('Go to Preset'), style: ElevatedButton.styleFrom( backgroundColor: Colors.purple, foregroundColor: Colors.white, ), ), ), ], ), ], ), ), ); } Widget _ptzButton( BuildContext context, IconData icon, String tooltip, VoidCallback onPressed, ) { final state = context.read().state; return SizedBox( width: 60, height: 60, child: ElevatedButton( onPressed: state.isLoading ? null : onPressed, style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, shape: const CircleBorder(), padding: EdgeInsets.zero, ), child: Icon(icon, size: 30), ), ); } void _sendPTZ(BuildContext context, String type, String direction) { final camera = int.tryParse(_cameraController.text) ?? 1; final speed = int.tryParse(_ptzSpeedController.text) ?? 50; switch (type) { case 'pan': context.read().add( CameraPanEvent(camera: camera, direction: direction, speed: speed), ); break; case 'tilt': context.read().add( CameraTiltEvent(camera: camera, direction: direction, speed: speed), ); break; case 'zoom': context.read().add( CameraZoomEvent(camera: camera, direction: direction, speed: speed), ); break; } } Widget _buildVideoControlCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.videocam, color: Colors.blue), const SizedBox(width: 8), Text( 'Video CrossSwitch', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), Row( children: [ Expanded( child: TextField( controller: _videoInputController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Video Input', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), const Icon(Icons.arrow_forward, color: Colors.grey), const SizedBox(width: 8), Expanded( child: TextField( controller: _videoOutputController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Video Output', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( SendGeViScopeCrossSwitchEvent( videoInput: int.tryParse(_videoInputController.text) ?? 1, videoOutput: int.tryParse(_videoOutputController.text) ?? 1, ), ); }, icon: const Icon(Icons.swap_horiz), label: const Text('Switch'), ), ], ), ], ), ), ); } Widget _buildDigitalIOCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.toggle_on, color: Colors.purple), const SizedBox(width: 8), Text( 'Digital I/O', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), Row( children: [ Expanded( flex: 2, child: TextField( controller: _contactIdController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Contact ID', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( GeViScopeCloseContactEvent( contactId: int.tryParse(_contactIdController.text) ?? 1, ), ); }, icon: const Icon(Icons.lock), label: const Text('Close'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( GeViScopeOpenContactEvent( contactId: int.tryParse(_contactIdController.text) ?? 1, ), ); }, icon: const Icon(Icons.lock_open), label: const Text('Open'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), ), ], ), ], ), ), ); } Widget _buildCustomActionCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.send, color: Colors.teal), const SizedBox(width: 8), Text( 'Custom Action', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), Row( children: [ Expanded( child: TextField( controller: _customActionTypeIdController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Type ID', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), Expanded( flex: 3, child: TextField( controller: _customActionTextController, decoration: const InputDecoration( labelText: 'Text', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: state.isLoading ? null : () { context.read().add( SendGeViScopeCustomActionEvent( typeId: int.tryParse(_customActionTypeIdController.text) ?? 1, text: _customActionTextController.text, ), ); }, icon: const Icon(Icons.send), label: const Text('Send'), ), ], ), ], ), ), ); } Widget _buildRawActionCard(BuildContext context, GeViScopeState state) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.code, color: Colors.indigo), const SizedBox(width: 8), Text( 'Raw Action', style: Theme.of(context).textTheme.titleLarge, ), ], ), const Divider(), Row( children: [ Expanded( child: TextField( controller: _rawActionController, decoration: const InputDecoration( labelText: 'Action String', hintText: 'e.g., CrossSwitch(1, 2, 0)', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: state.isLoading || _rawActionController.text.isEmpty ? null : () { context.read().add( SendGeViScopeActionEvent( action: _rawActionController.text, ), ); _rawActionController.clear(); }, icon: const Icon(Icons.send), label: const Text('Send'), ), ], ), ], ), ), ); } Widget _buildRightPanel(BuildContext context, GeViScopeState state) { return Container( decoration: BoxDecoration( border: Border( left: BorderSide(color: Colors.grey.shade300), ), ), child: Column( children: [ // Channels section if (state.isConnected && state.channels.isNotEmpty) ...[ Container( padding: const EdgeInsets.all(12), color: Colors.blue.shade50, child: Row( children: [ const Icon(Icons.videocam, size: 18, color: Colors.blue), const SizedBox(width: 8), Text( 'Channels (${state.channels.length})', style: const TextStyle(fontWeight: FontWeight.bold), ), const Spacer(), IconButton( icon: const Icon(Icons.refresh, size: 18), onPressed: () { context.read().add(const RefreshChannelsEvent()); }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), SizedBox( height: 150, child: ListView.builder( padding: const EdgeInsets.all(8), itemCount: state.channels.length, itemBuilder: (context, index) { final channel = state.channels[index]; return ListTile( dense: true, leading: Icon( Icons.camera_alt, size: 16, color: channel.isActive ? Colors.green : Colors.grey, ), title: Text( channel.name.isNotEmpty ? channel.name : 'Channel ${channel.channelId}', style: const TextStyle(fontSize: 12), ), subtitle: Text( 'ID: ${channel.channelId} | Global: ${channel.globalNumber}', style: const TextStyle(fontSize: 10), ), contentPadding: EdgeInsets.zero, ); }, ), ), const Divider(height: 1), ], // Message log section Container( padding: const EdgeInsets.all(12), color: Colors.grey.shade100, child: Row( children: [ const Icon(Icons.list_alt, size: 18), const SizedBox(width: 8), const Text( 'Message Log', style: TextStyle(fontWeight: FontWeight.bold), ), const Spacer(), Text( '${state.messageLog.length}', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), ], ), ), Expanded( child: state.messageLog.isEmpty ? const Center( child: Text( 'No messages yet', style: TextStyle(color: Colors.grey), ), ) : ListView.builder( padding: const EdgeInsets.all(8), itemCount: state.messageLog.length, itemBuilder: (context, index) { final reversedIndex = state.messageLog.length - 1 - index; return Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), margin: const EdgeInsets.only(bottom: 4), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(4), ), child: Text( state.messageLog[reversedIndex], style: const TextStyle( fontFamily: 'monospace', fontSize: 11, ), ), ); }, ), ), ], ), ); } Color _getStatusColor(GeViScopeConnectionStatus status) { switch (status) { case GeViScopeConnectionStatus.connected: return Colors.green; case GeViScopeConnectionStatus.connecting: return Colors.orange; case GeViScopeConnectionStatus.error: return Colors.red; case GeViScopeConnectionStatus.disconnected: return Colors.grey; } } IconData _getStatusIcon(GeViScopeConnectionStatus status) { switch (status) { case GeViScopeConnectionStatus.connected: return Icons.check_circle; case GeViScopeConnectionStatus.connecting: return Icons.sync; case GeViScopeConnectionStatus.error: return Icons.error; case GeViScopeConnectionStatus.disconnected: return Icons.cancel; } } String _getStatusText(GeViScopeConnectionStatus status) { switch (status) { case GeViScopeConnectionStatus.connected: return 'Connected'; case GeViScopeConnectionStatus.connecting: return 'Connecting...'; case GeViScopeConnectionStatus.error: return 'Error'; case GeViScopeConnectionStatus.disconnected: return 'Disconnected'; } } }