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,261 @@
import 'package:flutter/material.dart';
import '../../../domain/entities/wall_config.dart';
import '../../blocs/wall/wall_state.dart';
/// Overview screen showing all wall sections in spatial layout (matching D6)
class WallOverview extends StatelessWidget {
final WallConfig config;
final WallState wallState;
final Function(String sectionId) onSectionTap;
const WallOverview({
super.key,
required this.config,
required this.wallState,
required this.onSectionTap,
});
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFF5A5A5A), // D6 background color
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: _buildSpatialLayout(constraints),
);
},
),
);
}
List<Widget> _buildSpatialLayout(BoxConstraints constraints) {
final widgets = <Widget>[];
final width = constraints.maxWidth;
final height = constraints.maxHeight;
// Position sections based on D6 layout
// The D6 app shows:
// - "4. Vrchní část" (top) at the top center
// - "1. Levá část" (left), "2. Střed stěny" (center), "3. Pravá část" (right) in middle row
// - "5. Stupínek" (bottom) at the bottom center
for (final section in config.sections) {
final position = _getSectionPosition(section.id, width, height);
final size = _getSectionSize(section.id, width, height);
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: size.width,
height: size.height,
child: _SectionTile(
section: section,
wallState: wallState,
onTap: () => onSectionTap(section.id),
),
),
);
}
return widgets;
}
Offset _getSectionPosition(String sectionId, double width, double height) {
// Layout matching D6 screenshot
switch (sectionId) {
case 'top': // 4. Vrchní část - top center
return Offset(width * 0.35, height * 0.05);
case 'left': // 1. Levá část - middle left
return Offset(width * 0.08, height * 0.35);
case 'center': // 2. Střed stěny - middle center
return Offset(width * 0.33, height * 0.35);
case 'right': // 3. Pravá část - middle right
return Offset(width * 0.58, height * 0.35);
case 'bottom': // 5. Stupínek - bottom center
return Offset(width * 0.33, height * 0.68);
default:
return Offset.zero;
}
}
Size _getSectionSize(String sectionId, double width, double height) {
// Sizes proportional to D6 layout
switch (sectionId) {
case 'top':
return Size(width * 0.30, height * 0.22);
case 'left':
return Size(width * 0.22, height * 0.25);
case 'center':
return Size(width * 0.22, height * 0.25);
case 'right':
return Size(width * 0.22, height * 0.25);
case 'bottom':
return Size(width * 0.30, height * 0.25);
default:
return Size(width * 0.2, height * 0.2);
}
}
}
class _SectionTile extends StatelessWidget {
final WallSection section;
final WallState wallState;
final VoidCallback onTap;
const _SectionTile({
required this.section,
required this.wallState,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section label
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'${_getSectionNumber(section.id)}. ${section.name}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
// Mini monitors grid
Expanded(
child: _MiniMonitorsGrid(
section: section,
wallState: wallState,
),
),
],
),
);
}
int _getSectionNumber(String id) {
switch (id) {
case 'left': return 1;
case 'center': return 2;
case 'right': return 3;
case 'top': return 4;
case 'bottom': return 5;
default: return 0;
}
}
}
class _MiniMonitorsGrid extends StatelessWidget {
final WallSection section;
final WallState wallState;
const _MiniMonitorsGrid({
required this.section,
required this.wallState,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final monitors = section.monitors;
final gridCols = section.columns;
final gridRows = section.rows;
// Calculate cell dimensions based on grid size
final cellWidth = constraints.maxWidth / gridCols;
final cellHeight = constraints.maxHeight / gridRows;
// Position monitors using their explicit row/col values (matching detail view)
return Stack(
children: monitors.map((monitor) {
// Convert 1-based row/col to 0-based for positioning
final row = monitor.row - 1;
final col = monitor.col - 1;
return Positioned(
left: col * cellWidth,
top: row * cellHeight,
width: monitor.colSpan * cellWidth,
height: monitor.rowSpan * cellHeight,
child: _MiniPhysicalMonitor(
monitor: monitor,
wallState: wallState,
),
);
}).toList(),
);
},
);
}
}
class _MiniPhysicalMonitor extends StatelessWidget {
final PhysicalMonitor monitor;
final WallState wallState;
const _MiniPhysicalMonitor({
required this.monitor,
required this.wallState,
});
@override
Widget build(BuildContext context) {
final viewers = monitor.viewerIds;
final gridCols = monitor.colSpan;
final gridRows = monitor.rowSpan;
// Overview: no cyan borders, just dark grid lines between viewers
return Container(
color: const Color(0xFF4A4A4A), // Dark background shows as grid lines
child: Column(
children: List.generate(gridRows, (row) {
return Expanded(
child: Row(
children: List.generate(gridCols, (col) {
final index = row * gridCols + col;
if (index >= viewers.length) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(0.5),
child: Container(
color: const Color(0xFF6A6A6A),
),
),
);
}
final viewerId = viewers[index];
final viewerState = wallState.getViewerState(viewerId);
Color tileColor;
if (viewerState.hasAlarm) {
tileColor = const Color(0xFFDC2626);
} else {
tileColor = const Color(0xFF6A6A6A);
}
return Expanded(
child: Padding(
padding: const EdgeInsets.all(0.5),
child: Container(
color: tileColor,
),
),
);
}),
),
);
}),
),
);
}
}