import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/device.dart'; import '../models/static_talkgroup.dart'; import '../models/device_profile.dart'; import '../services/authentication_manager.dart'; import '../services/brandmeister_client.dart'; import 'link_talkgroup_view.dart'; class DeviceDetailView extends StatefulWidget { final Device device; const DeviceDetailView({super.key, required this.device}); @override State createState() => _DeviceDetailViewState(); } class _DeviceDetailViewState extends State { List _talkgroups = []; Map _allTalkgroups = {}; DeviceProfile? _deviceProfile; bool _isLoadingTalkgroups = false; bool _isLoadingDeviceDetails = false; String? _errorMessage; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _isLoadingTalkgroups = true; _isLoadingDeviceDetails = true; _errorMessage = null; }); try { final authManager = context.read(); final results = await Future.wait([ authManager.getTalkgroups(widget.device.id), authManager.getAllTalkgroups(), authManager.getDeviceProfile(widget.device.id), ]); setState(() { _talkgroups = results[0] as List; _allTalkgroups = results[1] as Map; _deviceProfile = results[2] as DeviceProfile; _isLoadingTalkgroups = false; _isLoadingDeviceDetails = false; }); } on BrandmeisterError catch (e) { setState(() { _errorMessage = e.message; _isLoadingTalkgroups = false; _isLoadingDeviceDetails = false; }); } catch (e) { setState(() { _errorMessage = e.toString(); _isLoadingTalkgroups = false; _isLoadingDeviceDetails = false; }); } } Future _unlinkTalkgroup(StaticTalkgroup talkgroup) async { if (talkgroup.talkgroup == null || talkgroup.slot == null) { return; } try { final authManager = context.read(); await authManager.unlinkTalkgroup( talkgroupId: talkgroup.talkgroup!, dmrId: widget.device.id, timeslot: talkgroup.slot!, ); await _loadData(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Talkgroup unlinked successfully')), ); } } on BrandmeisterError catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to unlink: ${e.message}')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${e.toString()}')), ); } } } void _showLinkTalkgroupSheet() { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: LinkTalkgroupView( device: widget.device, onSuccess: () { Navigator.pop(context); _loadData(); }, ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.device.callsign ?? 'Device Details'), ), floatingActionButton: FloatingActionButton( onPressed: _showLinkTalkgroupSheet, child: const Icon(Icons.add), ), body: RefreshIndicator( onRefresh: _loadData, child: _buildBody(), ), ); } Widget _buildBody() { if ((_isLoadingTalkgroups || _isLoadingDeviceDetails) && _talkgroups.isEmpty) { return const Center( child: CircularProgressIndicator(), ); } if (_errorMessage != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: Colors.red[300], ), const SizedBox(height: 16), Text( 'Error', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( _errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey[600]), ), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _loadData, icon: const Icon(Icons.refresh), label: const Text('Retry'), ), ], ), ); } return ListView( children: [ _buildDeviceInfoSection(), const Divider(height: 1), _buildTalkgroupsSection(), ], ); } Widget _buildDeviceInfoSection() { return Container( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Device Information', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), _InfoRow( label: 'DMR ID', value: widget.device.id.toString(), ), if (widget.device.callsign != null) _InfoRow( label: 'Callsign', value: widget.device.callsign!, ), if (widget.device.name != null) _InfoRow( label: 'Name', value: widget.device.name!, ), _InfoRow( label: 'Location', value: widget.device.displayLocation, ), if (widget.device.hardware != null) _InfoRow( label: 'Hardware', value: widget.device.hardware!, ), if (widget.device.firmware != null) _InfoRow( label: 'Firmware', value: widget.device.firmware!, ), ], ), ); } Widget _buildTalkgroupsSection() { final hasAnyTalkgroups = _talkgroups.isNotEmpty || (_deviceProfile?.staticSubscriptions.isNotEmpty ?? false) || (_deviceProfile?.dynamicSubscriptions.isNotEmpty ?? false) || (_deviceProfile?.timedSubscriptions.isNotEmpty ?? false) || (_deviceProfile?.blockedGroups.isNotEmpty ?? false); return Container( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Talkgroup Subscriptions', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), if (_isLoadingTalkgroups) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), const SizedBox(height: 16), if (!hasAnyTalkgroups) Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( children: [ Icon( Icons.speaker_group, size: 48, color: Colors.grey[400], ), const SizedBox(height: 8), Text( 'No talkgroups configured', style: TextStyle(color: Colors.grey[600]), ), ], ), ), ) else ...[ // Static Subscriptions if (_deviceProfile?.staticSubscriptions.isNotEmpty ?? false) ...[ _buildTalkgroupCategory( 'Static', _deviceProfile!.staticSubscriptions, Colors.blue, Icons.link, canDelete: true, ), const SizedBox(height: 16), ], // Dynamic Subscriptions (Autostatic) if (_deviceProfile?.dynamicSubscriptions.isNotEmpty ?? false) ...[ _buildTalkgroupCategory( 'Dynamic / Autostatic', _deviceProfile!.dynamicSubscriptions, Colors.green, Icons.autorenew, canDelete: false, ), const SizedBox(height: 16), ], // Timed Subscriptions if (_deviceProfile?.timedSubscriptions.isNotEmpty ?? false) ...[ _buildTalkgroupCategory( 'Timed', _deviceProfile!.timedSubscriptions, Colors.orange, Icons.schedule, canDelete: false, ), const SizedBox(height: 16), ], // Blocked Groups if (_deviceProfile?.blockedGroups.isNotEmpty ?? false) ...[ _buildTalkgroupCategory( 'Blocked', _deviceProfile!.blockedGroups, Colors.red, Icons.block, canDelete: false, ), ], ], ], ), ); } Widget _buildTalkgroupCategory( String title, List talkgroups, Color color, IconData icon, {bool canDelete = false} ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 20, color: color), const SizedBox(width: 8), Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( '${talkgroups.length}', style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 8), ...talkgroups.map((tg) => _TalkgroupRow( talkgroup: tg, talkgroupName: _allTalkgroups[tg.talkgroup], onDelete: canDelete ? () => _unlinkTalkgroup(tg) : null, categoryColor: color, )), ], ); } } class _InfoRow extends StatelessWidget { final String label; final String value; const _InfoRow({ required this.label, required this.value, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text( label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, color: Colors.grey[600], ), ), ), Expanded( child: Text( value, style: Theme.of(context).textTheme.bodyMedium, ), ), ], ), ); } } class _TalkgroupRow extends StatelessWidget { final StaticTalkgroup talkgroup; final String? talkgroupName; final VoidCallback? onDelete; final Color? categoryColor; const _TalkgroupRow({ required this.talkgroup, this.talkgroupName, this.onDelete, this.categoryColor, }); @override Widget build(BuildContext context) { final color = categoryColor ?? Theme.of(context).colorScheme.primary; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( backgroundColor: color.withValues(alpha: 0.2), child: Text( talkgroup.displaySlot, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: color, ), ), ), title: Text(talkgroupName ?? talkgroup.displayId), subtitle: Text('ID: ${talkgroup.displayId}'), trailing: onDelete != null ? IconButton( icon: const Icon(Icons.delete_outline), color: Colors.red, onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Unlink Talkgroup'), content: Text( 'Are you sure you want to unlink talkgroup ${talkgroup.displayId}?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), TextButton( onPressed: () { Navigator.pop(context); onDelete!(); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Unlink'), ), ], ), ); }, ) : null, ), ); } }