Move things from tabs, add me view

This commit is contained in:
2026-01-20 17:52:54 +01:00
parent 4fc8570d66
commit 7416f7c29b
5 changed files with 453 additions and 21 deletions

View File

@@ -32,6 +32,8 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
BrandmeisterWebSocketClient? _wsClient;
StreamSubscription<Map<String, dynamic>>? _wsSubscription;
int? _autoStaticTalkgroup;
Map<String, int> _dynamicTimeouts = {};
Timer? _countdownTimer;
LastHeardWebSocketClient? _lhWsClient;
StreamSubscription<Map<String, dynamic>>? _lhWsSubscription;
@@ -52,6 +54,7 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
_wsClient?.dispose();
_lhWsSubscription?.cancel();
_lhWsClient?.dispose();
_countdownTimer?.cancel();
super.dispose();
}
@@ -101,6 +104,41 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
}
}
void _startCountdownTimer() {
_countdownTimer?.cancel();
if (_dynamicTimeouts.isEmpty) return;
// Update countdown every second
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final keysToRemove = <String>[];
_dynamicTimeouts.forEach((key, value) {
if (value > 0) {
_dynamicTimeouts[key] = value - 1;
} else {
keysToRemove.add(key);
}
});
// If any entries expired, reload data to get updated list from server
if (keysToRemove.isNotEmpty) {
for (final key in keysToRemove) {
_dynamicTimeouts.remove(key);
}
// Reload data to get the updated device profile
_loadData();
} else {
// Just update UI for countdown
setState(() {});
}
// Stop timer if no more timeouts
if (_dynamicTimeouts.isEmpty) {
timer.cancel();
}
});
}
Future<void> _connectLastHeardWebSocket() async {
try {
_lhWsClient = LastHeardWebSocketClient();
@@ -211,6 +249,27 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
_talkgroups = results[0] as List<StaticTalkgroup>;
_allTalkgroups = results[1] as Map<String, String>;
_deviceProfile = results[2] as DeviceProfile;
// Initialize dynamic timeouts from device profile
_dynamicTimeouts.clear();
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; // Current Unix timestamp in seconds
for (final tg in _deviceProfile!.dynamicSubscriptions) {
if (tg.talkgroup != null && tg.timeout != null && tg.timeout! > 0) {
final key = '${tg.talkgroup}_${tg.slot}';
// Convert Unix timestamp to remaining seconds
final remainingSeconds = tg.timeout! - now;
if (remainingSeconds > 0) {
_dynamicTimeouts[key] = remainingSeconds;
}
}
}
// Start countdown timer if we have any dynamic timeouts
if (_dynamicTimeouts.isNotEmpty) {
_startCountdownTimer();
}
_isLoadingTalkgroups = false;
_isLoadingDeviceDetails = false;
});
@@ -581,11 +640,12 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
// Dynamic Subscriptions (Autostatic)
if (_deviceProfile?.dynamicSubscriptions.isNotEmpty ?? false) ...[
_buildTalkgroupCategory(
'Dynamic / Autostatic',
'Dynamic',
_deviceProfile!.dynamicSubscriptions,
Colors.green,
Icons.autorenew,
canDelete: false,
canDelete: true,
showTimeout: true,
),
const SizedBox(height: 16),
],
@@ -701,12 +761,27 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
);
}
String _formatTimeout(int seconds) {
final duration = Duration(seconds: seconds);
if (duration.inHours > 0) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
return '${hours}h ${minutes}m';
} else if (duration.inMinutes > 0) {
final minutes = duration.inMinutes;
final secs = duration.inSeconds.remainder(60);
return '${minutes}m ${secs}s';
} else {
return '${duration.inSeconds}s';
}
}
Widget _buildTalkgroupCategory(
String title,
List<StaticTalkgroup> talkgroups,
Color color,
IconData icon,
{bool canDelete = false}
{bool canDelete = false, bool showTimeout = false}
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -741,12 +816,18 @@ class _DeviceDetailViewState extends State<DeviceDetailView> {
],
),
const SizedBox(height: 8),
...talkgroups.map((tg) => _TalkgroupRow(
talkgroup: tg,
talkgroupName: _allTalkgroups[tg.talkgroup],
onDelete: canDelete ? () => _unlinkTalkgroup(tg) : null,
categoryColor: color,
)),
...talkgroups.map((tg) {
final key = '${tg.talkgroup}_${tg.slot}';
final timeout = showTimeout ? _dynamicTimeouts[key] : null;
return _TalkgroupRow(
talkgroup: tg,
talkgroupName: _allTalkgroups[tg.talkgroup],
onDelete: canDelete ? () => _unlinkTalkgroup(tg) : null,
categoryColor: color,
timeout: timeout,
formatTimeout: _formatTimeout,
);
}),
],
);
}
@@ -795,17 +876,22 @@ class _TalkgroupRow extends StatelessWidget {
final String? talkgroupName;
final VoidCallback? onDelete;
final Color? categoryColor;
final int? timeout;
final String Function(int)? formatTimeout;
const _TalkgroupRow({
required this.talkgroup,
this.talkgroupName,
this.onDelete,
this.categoryColor,
this.timeout,
this.formatTimeout,
});
@override
Widget build(BuildContext context) {
final color = categoryColor ?? Theme.of(context).colorScheme.primary;
final hasTimeout = timeout != null && timeout! > 0 && formatTimeout != null;
return Card(
margin: const EdgeInsets.only(bottom: 8),
@@ -822,7 +908,21 @@ class _TalkgroupRow extends StatelessWidget {
),
),
title: Text(talkgroupName ?? talkgroup.displayId),
subtitle: Text('ID: ${talkgroup.displayId}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${talkgroup.displayId}'),
if (hasTimeout)
Text(
'Expires in ${formatTimeout!(timeout!)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
trailing: onDelete != null
? IconButton(
icon: const Icon(Icons.delete_outline),
@@ -854,6 +954,7 @@ class _TalkgroupRow extends StatelessWidget {
},
)
: null,
isThreeLine: hasTimeout,
),
);
}