861 lines
26 KiB
Dart
861 lines
26 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
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 '../models/last_heard_item.dart';
|
|
import '../services/authentication_manager.dart';
|
|
import '../services/brandmeister_client.dart';
|
|
import '../services/brandmeister_websocket_client.dart';
|
|
import '../services/lastheard_websocket_client.dart';
|
|
import 'link_talkgroup_view.dart';
|
|
|
|
class DeviceDetailView extends StatefulWidget {
|
|
final Device device;
|
|
|
|
const DeviceDetailView({super.key, required this.device});
|
|
|
|
@override
|
|
State<DeviceDetailView> createState() => _DeviceDetailViewState();
|
|
}
|
|
|
|
class _DeviceDetailViewState extends State<DeviceDetailView> {
|
|
List<StaticTalkgroup> _talkgroups = [];
|
|
Map<String, String> _allTalkgroups = {};
|
|
DeviceProfile? _deviceProfile;
|
|
bool _isLoadingTalkgroups = false;
|
|
bool _isLoadingDeviceDetails = false;
|
|
String? _errorMessage;
|
|
|
|
BrandmeisterWebSocketClient? _wsClient;
|
|
StreamSubscription<Map<String, dynamic>>? _wsSubscription;
|
|
int? _autoStaticTalkgroup;
|
|
|
|
LastHeardWebSocketClient? _lhWsClient;
|
|
StreamSubscription<Map<String, dynamic>>? _lhWsSubscription;
|
|
final List<LastHeardItem> _lastHeardItems = [];
|
|
static const int _maxLastHeardItems = 20;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadData();
|
|
_connectWebSocket();
|
|
_connectLastHeardWebSocket();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_wsSubscription?.cancel();
|
|
_wsClient?.dispose();
|
|
_lhWsSubscription?.cancel();
|
|
_lhWsClient?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _connectWebSocket() async {
|
|
_wsClient = BrandmeisterWebSocketClient();
|
|
await _wsClient!.connect();
|
|
|
|
// Wait for the open packet (0{...}) to be received
|
|
await _wsClient!.ready;
|
|
|
|
// Request device status after connection is ready
|
|
_wsClient!.getDeviceStatus(widget.device.id.toString());
|
|
|
|
// Listen for device status updates
|
|
_wsSubscription = _wsClient!.messageStream.listen((message) {
|
|
if (message['event'] == 'deviceStatus' && message['data'] is List) {
|
|
_handleDeviceStatus(message['data'] as List);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _handleDeviceStatus(List deviceStatusList) {
|
|
if (deviceStatusList.isEmpty) return;
|
|
|
|
for (final status in deviceStatusList) {
|
|
if (status is Map<String, dynamic>) {
|
|
final number = status['number'];
|
|
if (number == widget.device.id) {
|
|
final values = status['values'] as List?;
|
|
if (values != null && values.length >= 20) {
|
|
final autoStaticValue = values[19];
|
|
if (autoStaticValue != null &&
|
|
autoStaticValue != 0 &&
|
|
autoStaticValue != 4000) {
|
|
setState(() {
|
|
_autoStaticTalkgroup = autoStaticValue as int;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_autoStaticTalkgroup = null;
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _connectLastHeardWebSocket() async {
|
|
try {
|
|
_lhWsClient = LastHeardWebSocketClient();
|
|
_lhWsClient!.connect();
|
|
|
|
_lhWsSubscription = _lhWsClient!.messageStream.listen((message) {
|
|
if (message['event'] == 'mqtt' && message['data'] is Map) {
|
|
_handleLastHeardMessage(message['data'] as Map<String, dynamic>);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('DeviceDetailView: Error connecting to Last Heard WebSocket: $e');
|
|
}
|
|
}
|
|
|
|
void _handleLastHeardMessage(Map<String, dynamic> data) {
|
|
try {
|
|
final topic = data['topic'] as String?;
|
|
if (topic == 'LH') {
|
|
final payloadStr = data['payload'] as String?;
|
|
if (payloadStr == null) return;
|
|
|
|
final payload = jsonDecode(payloadStr) as Map<String, dynamic>;
|
|
final destinationId = payload['DestinationID'] as int?;
|
|
|
|
if (destinationId == null) return;
|
|
|
|
// Get all linked talkgroup IDs (static + auto-static + dynamic)
|
|
final linkedTalkgroupIds = <int>{};
|
|
|
|
// Add static talkgroups
|
|
for (final tg in _deviceProfile?.staticSubscriptions ?? <StaticTalkgroup>[]) {
|
|
if (tg.talkgroup != null) {
|
|
final tgId = int.tryParse(tg.talkgroup!);
|
|
if (tgId != null) {
|
|
linkedTalkgroupIds.add(tgId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add dynamic/auto-static talkgroups from device profile
|
|
for (final tg in _deviceProfile?.dynamicSubscriptions ?? <StaticTalkgroup>[]) {
|
|
if (tg.talkgroup != null) {
|
|
final tgId = int.tryParse(tg.talkgroup!);
|
|
if (tgId != null) {
|
|
linkedTalkgroupIds.add(tgId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add auto-static talkgroup from WebSocket (if available)
|
|
if (_autoStaticTalkgroup != null) {
|
|
linkedTalkgroupIds.add(_autoStaticTalkgroup!);
|
|
}
|
|
|
|
debugPrint('DeviceDetailView: Linked talkgroups: $linkedTalkgroupIds, Destination: $destinationId');
|
|
|
|
// Only process if this message is for one of our linked talkgroups
|
|
if (linkedTalkgroupIds.contains(destinationId)) {
|
|
debugPrint('DeviceDetailView: Match found! Adding to last heard list');
|
|
final item = LastHeardItem.fromJson(payload);
|
|
|
|
debugPrint('DeviceDetailView: Item - TalkerAlias: ${item.talkerAlias}, SourceCall: ${item.sourceCall}, DestName: ${item.destinationName}, DestCall: ${item.destinationCall}');
|
|
|
|
if (item.sourceCall.isEmpty) {
|
|
debugPrint('DeviceDetailView: Ignoring item with empty SourceCall');
|
|
return;
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
// Remove any existing item with the same sourceID
|
|
_lastHeardItems.removeWhere((existing) => existing.sourceID == item.sourceID);
|
|
|
|
// Add the new item at the beginning
|
|
_lastHeardItems.insert(0, item);
|
|
|
|
// Keep only the latest 20 items
|
|
if (_lastHeardItems.length > _maxLastHeardItems) {
|
|
_lastHeardItems.removeRange(_maxLastHeardItems, _lastHeardItems.length);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('DeviceDetailView: Error handling Last Heard message: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
setState(() {
|
|
_isLoadingTalkgroups = true;
|
|
_isLoadingDeviceDetails = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final authManager = context.read<AuthenticationManager>();
|
|
|
|
final results = await Future.wait([
|
|
authManager.getTalkgroups(widget.device.id),
|
|
authManager.getAllTalkgroups(),
|
|
authManager.getDeviceProfile(widget.device.id),
|
|
]);
|
|
|
|
setState(() {
|
|
_talkgroups = results[0] as List<StaticTalkgroup>;
|
|
_allTalkgroups = results[1] as Map<String, String>;
|
|
_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<void> _unlinkTalkgroup(StaticTalkgroup talkgroup) async {
|
|
if (talkgroup.talkgroup == null || talkgroup.slot == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final authManager = context.read<AuthenticationManager>();
|
|
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()}')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _unlinkAutoStatic() async {
|
|
try {
|
|
final authManager = context.read<AuthenticationManager>();
|
|
await authManager.dropAutoStaticGroup(widget.device.id);
|
|
|
|
setState(() {
|
|
_autoStaticTalkgroup = null;
|
|
});
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Auto-static talkgroup unlinked successfully')),
|
|
);
|
|
}
|
|
} on BrandmeisterError catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Failed to unlink auto-static: ${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),
|
|
if (_lastHeardItems.isNotEmpty) ...[
|
|
_buildLastHeardSection(),
|
|
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 _buildLastHeardSection() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Last Heard on Talkgroup',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
'${_lastHeardItems.length}',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
...(_lastHeardItems.toList()
|
|
..sort((a, b) => b.timestamp.compareTo(a.timestamp)))
|
|
.map((item) => Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
|
child: Text(
|
|
'TS${item.slot}',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
|
),
|
|
),
|
|
),
|
|
title: Text(item.talkerAlias ?? item.sourceCall),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('ID: ${item.sourceID}'),
|
|
Text('TG: ${item.destinationID}'),
|
|
Text(
|
|
'${item.timeAgo} • ${item.linkTypeName}',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
trailing: item.event == 'Session-Start'
|
|
? Icon(
|
|
Icons.mic,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
size: 20,
|
|
)
|
|
: null,
|
|
isThreeLine: true,
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTalkgroupsSection() {
|
|
final hasAnyTalkgroups = _talkgroups.isNotEmpty ||
|
|
(_deviceProfile?.staticSubscriptions.isNotEmpty ?? false) ||
|
|
(_deviceProfile?.dynamicSubscriptions.isNotEmpty ?? false) ||
|
|
(_deviceProfile?.timedSubscriptions.isNotEmpty ?? false) ||
|
|
(_deviceProfile?.blockedGroups.isNotEmpty ?? false) ||
|
|
_autoStaticTalkgroup != null;
|
|
|
|
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),
|
|
],
|
|
// Auto-Static Talkgroup (from WebSocket)
|
|
if (_autoStaticTalkgroup != null) ...[
|
|
_buildAutoStaticSection(),
|
|
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 _buildAutoStaticSection() {
|
|
const color = Colors.purple;
|
|
final talkgroupName = _allTalkgroups[_autoStaticTalkgroup.toString()] ?? 'Unknown';
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.flash_on, size: 20, color: color),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Auto Static',
|
|
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: const Text(
|
|
'1',
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: color.withValues(alpha: 0.2),
|
|
child: const Icon(
|
|
Icons.flash_on,
|
|
size: 20,
|
|
color: color,
|
|
),
|
|
),
|
|
title: Text(talkgroupName),
|
|
subtitle: Text('ID: $_autoStaticTalkgroup'),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.delete_outline),
|
|
color: Colors.red,
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Unlink Auto-Static Talkgroup'),
|
|
content: Text(
|
|
'Are you sure you want to unlink auto-static talkgroup $_autoStaticTalkgroup?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
_unlinkAutoStatic();
|
|
},
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Unlink'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTalkgroupCategory(
|
|
String title,
|
|
List<StaticTalkgroup> 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,
|
|
),
|
|
);
|
|
}
|
|
}
|