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:
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user