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>
This commit is contained in:
klas
2026-02-12 14:57:38 +01:00
commit 40143734fc
125 changed files with 65073 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/sequence/sequence_bloc.dart';
import '../blocs/sequence/sequence_event.dart';
import '../blocs/sequence/sequence_state.dart';
/// Panel for starting/stopping camera rotation sequences.
/// Shown as a dialog from the toolbar.
class SequencePanel extends StatelessWidget {
final int viewerId;
const SequencePanel({super.key, required this.viewerId});
@override
Widget build(BuildContext context) {
return BlocBuilder<SequenceBloc, SequenceState>(
builder: (context, state) {
if (state.isLoading) {
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()),
);
}
final isRunning = state.isRunningOnViewer(viewerId);
final runningSeq = state.getRunning(viewerId);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Running sequence indicator
if (isRunning && runningSeq != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF38A169).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFF38A169)),
),
child: Row(
children: [
const Icon(Icons.play_circle, color: Color(0xFF38A169)),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getSequenceName(state, runningSeq.sequenceId),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
'Běží na monitoru $viewerId',
style: const TextStyle(
color: Colors.white70, fontSize: 12),
),
],
),
),
ElevatedButton.icon(
onPressed: () {
context
.read<SequenceBloc>()
.add(StopSequence(viewerId));
},
icon: const Icon(Icons.stop, size: 16),
label: const Text('Zastavit'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFDC2626),
foregroundColor: Colors.white,
),
),
],
),
),
const SizedBox(height: 12),
],
// Category filter chips
if (state.categories.isNotEmpty) ...[
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('Vše'),
selected: state.selectedCategoryId == null,
onSelected: (_) =>
context.read<SequenceBloc>().add(SelectCategory(null)),
selectedColor: const Color(0xFF2B6CB0),
labelStyle: TextStyle(
color: state.selectedCategoryId == null
? Colors.white
: Colors.white70,
),
backgroundColor: const Color(0xFF2D3748),
),
...state.categories.map((cat) => FilterChip(
label: Text(cat.name),
selected: state.selectedCategoryId == cat.id,
onSelected: (_) => context
.read<SequenceBloc>()
.add(SelectCategory(cat.id)),
selectedColor: const Color(0xFF2B6CB0),
labelStyle: TextStyle(
color: state.selectedCategoryId == cat.id
? Colors.white
: Colors.white70,
),
backgroundColor: const Color(0xFF2D3748),
)),
],
),
const SizedBox(height: 12),
],
// Sequence list
if (state.filteredSequences.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Žádné sekvence k dispozici.',
style: TextStyle(color: Colors.white54),
),
),
)
else
...state.filteredSequences.map((seq) {
final isThisRunning =
runningSeq?.sequenceId == seq.id && isRunning;
return ListTile(
dense: true,
leading: Icon(
isThisRunning ? Icons.play_circle : Icons.loop,
color: isThisRunning
? const Color(0xFF38A169)
: const Color(0xFF718096),
),
title: Text(
seq.name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
'${seq.cameras.length} kamer, ${seq.intervalSeconds}s interval',
style:
const TextStyle(color: Colors.white54, fontSize: 12),
),
trailing: isThisRunning
? TextButton(
onPressed: () => context
.read<SequenceBloc>()
.add(StopSequence(viewerId)),
child: const Text('Zastavit',
style: TextStyle(color: Color(0xFFDC2626))),
)
: TextButton(
onPressed: () => context
.read<SequenceBloc>()
.add(StartSequence(
viewerId: viewerId, sequenceId: seq.id)),
child: const Text('Spustit'),
),
);
}),
],
);
},
);
}
String _getSequenceName(SequenceState state, int sequenceId) {
final seq =
state.sequences.where((s) => s.id == sequenceId).firstOrNull;
return seq?.name ?? 'Sekvence #$sequenceId';
}
}