Files
COPILOT/copilot_keyboard/lib/presentation/widgets/alarm_panel.dart
klas 40143734fc Initial commit: COPILOT D6 Flutter keyboard controller
Flutter web app replacing legacy WPF CCTV surveillance keyboard controller.
Includes wall overview, section view with monitor grid, camera input,
PTZ control, alarm/lock/sequence BLoCs, and legacy-matching UI styling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:57:38 +01:00

254 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/entities/alarm_state.dart';
import '../blocs/alarm/alarm_bloc.dart';
import '../blocs/alarm/alarm_event.dart';
import '../blocs/alarm/alarm_state.dart';
class AlarmPanel extends StatelessWidget {
final int maxDisplayed;
const AlarmPanel({super.key, this.maxDisplayed = 5});
@override
Widget build(BuildContext context) {
return BlocBuilder<AlarmBloc, AlarmBlocState>(
builder: (context, state) {
final alarms = state.activeAlarms.take(maxDisplayed).toList();
final hasMore = state.activeAlarms.length > maxDisplayed;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'ACTIVE ALARMS',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
if (state.blockingAlarmCount > 0)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.blockingAlarmCount}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const Spacer(),
if (state.isLoading)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
IconButton(
icon: const Icon(Icons.refresh, size: 20),
onPressed: () =>
context.read<AlarmBloc>().add(const RefreshAlarms()),
tooltip: 'Refresh alarms',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
const SizedBox(height: 8),
if (alarms.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.green.shade600,
),
const SizedBox(width: 8),
Text(
'No active alarms',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
)
else
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.red.shade200,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
...alarms.asMap().entries.map((entry) {
final index = entry.key;
final alarm = entry.value;
return _AlarmTile(
alarm: alarm,
isLast: index == alarms.length - 1 && !hasMore,
);
}),
if (hasMore)
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(7),
bottomRight: Radius.circular(7),
),
),
child: Center(
child: Text(
'+${state.activeAlarms.length - maxDisplayed} more alarms',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
),
if (state.error != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
state.error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
if (state.lastSync != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Last sync: ${_formatTime(state.lastSync!)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
),
),
),
],
);
},
);
}
String _formatTime(DateTime time) {
return '${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}:'
'${time.second.toString().padLeft(2, '0')}';
}
}
class _AlarmTile extends StatelessWidget {
final AlarmState alarm;
final bool isLast;
const _AlarmTile({
required this.alarm,
required this.isLast,
});
@override
Widget build(BuildContext context) {
final isBlocking = alarm.blocksMonitor;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isBlocking ? Colors.red.shade50 : null,
border: isLast
? null
: Border(
bottom: BorderSide(
color: Colors.red.shade200,
width: 1,
),
),
),
child: Row(
children: [
Icon(
isBlocking ? Icons.warning : Icons.info_outline,
color: isBlocking ? Colors.red : Colors.orange,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
alarm.eventName.isNotEmpty
? alarm.eventName
: 'Event ${alarm.eventId}',
style: TextStyle(
fontWeight: isBlocking ? FontWeight.bold : FontWeight.normal,
),
),
if (alarm.foreignKey > 0)
Text(
'Camera/Contact: ${alarm.foreignKey}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Text(
_formatTime(alarm.startedAt),
style: Theme.of(context).textTheme.bodySmall,
),
if (alarm.associatedMonitor != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'M${alarm.associatedMonitor}',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
],
],
),
);
}
String _formatTime(DateTime time) {
return '${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}:'
'${time.second.toString().padLeft(2, '0')}';
}
}