import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/entities/wall_config.dart'; import '../../blocs/wall/wall_bloc.dart'; import '../../blocs/wall/wall_event.dart'; import '../../blocs/wall/wall_state.dart'; /// Full section view matching D6 app design class SectionView extends StatelessWidget { final WallSection section; final WallState wallState; final VoidCallback onBack; final Function(int viewerId) onViewerTap; const SectionView({ super.key, required this.section, required this.wallState, required this.onBack, required this.onViewerTap, }); @override Widget build(BuildContext context) { return Container( color: const Color(0xFF555555), // D6 background color child: Column( children: [ // Header bar _HeaderBar( section: section, wallState: wallState, ), // Monitors grid - takes all available space Expanded( child: _MonitorsGrid( section: section, wallState: wallState, onViewerTap: onViewerTap, ), ), // Bottom toolbar with circular icons _BottomIconBar( wallState: wallState, onSegmentsTap: onBack, ), ], ), ); } } class _HeaderBar extends StatelessWidget { final WallSection section; final WallState wallState; const _HeaderBar({ required this.section, required this.wallState, }); @override Widget build(BuildContext context) { // Get section number from id final sectionNum = _getSectionNumber(section.id); return Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 8), color: const Color(0xFF555555), child: Row( children: [ // Video camera icon (matching D6 app) const Icon( Icons.videocam, color: Colors.white, size: 24, ), const SizedBox(width: 8), // Section name Text( '$sectionNum | ${section.name}', style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w500, ), ), const Spacer(), // Status message (red when server unavailable) const Text( 'Aplikační server není dostupný, některé funkce nejsou k dispozici', style: TextStyle( color: Color(0xFFFF4444), fontSize: 18, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 8), // Status icons const Icon(Icons.dns, color: Color(0xFFFF4444), size: 28), const SizedBox(width: 6), const Icon(Icons.lan, color: Color(0xFF24FF00), size: 28), ], ), ); } 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 _MonitorsGrid extends StatelessWidget { final WallSection section; final WallState wallState; final Function(int viewerId) onViewerTap; const _MonitorsGrid({ required this.section, required this.wallState, required this.onViewerTap, }); @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; // Build a map of which physical monitor each grid cell belongs to final cellToMonitor = {}; for (final monitor in monitors) { for (int r = 0; r < monitor.rowSpan; r++) { for (int c = 0; c < monitor.colSpan; c++) { final gridRow = monitor.row - 1 + r; final gridCol = monitor.col - 1 + c; cellToMonitor['$gridRow,$gridCol'] = monitor; } } } // Position monitors using their explicit row/col values return Stack( children: [ // First layer: monitor content without borders ...monitors.map((monitor) { 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: _PhysicalMonitorContent( monitor: monitor, wallState: wallState, onViewerTap: onViewerTap, ), ); }), // Second layer: border overlay Positioned.fill( child: IgnorePointer( child: CustomPaint( painter: _GridBorderPainter( monitors: monitors, gridCols: gridCols, gridRows: gridRows, cellToMonitor: cellToMonitor, ), ), ), ), ], ); }, ); } } /// Custom painter that draws all grid borders with correct colors class _GridBorderPainter extends CustomPainter { final List monitors; final int gridCols; final int gridRows; final Map cellToMonitor; static const _borderWidth = 2.0; static const _cyanColor = Color(0xFF00BFFF); static const _darkColor = Color(0xFF4A4A4A); _GridBorderPainter({ required this.monitors, required this.gridCols, required this.gridRows, required this.cellToMonitor, }); @override void paint(Canvas canvas, Size size) { final cellWidth = size.width / gridCols; final cellHeight = size.height / gridRows; final cyanPaint = Paint() ..color = _cyanColor ..strokeWidth = _borderWidth ..style = PaintingStyle.stroke; final darkPaint = Paint() ..color = _darkColor ..strokeWidth = _borderWidth ..style = PaintingStyle.stroke; // Collect all border segments, draw dark first then cyan on top final darkLines = <_LineSegment>[]; final cyanLines = <_LineSegment>[]; // Collect horizontal lines for (int row = 0; row <= gridRows; row++) { for (int col = 0; col < gridCols; col++) { final x1 = col * cellWidth; final x2 = (col + 1) * cellWidth; final y = row * cellHeight; final cellAbove = row > 0 ? cellToMonitor['${row - 1},$col'] : null; final cellBelow = row < gridRows ? cellToMonitor['$row,$col'] : null; // Only draw border if at least one side has a physical monitor if (cellAbove == null && cellBelow == null) continue; // Cyan if: edge of physical monitor (one side empty or different monitor) final isCyan = cellAbove == null || cellBelow == null || cellAbove.id != cellBelow.id; // Skip internal borders for single-viewer monitors if (!isCyan && cellAbove != null && cellAbove.viewerIds.length == 1) { continue; } final segment = _LineSegment(Offset(x1, y), Offset(x2, y)); if (isCyan) { cyanLines.add(segment); } else { darkLines.add(segment); } } } // Collect vertical lines for (int col = 0; col <= gridCols; col++) { for (int row = 0; row < gridRows; row++) { final x = col * cellWidth; final y1 = row * cellHeight; final y2 = (row + 1) * cellHeight; final cellLeft = col > 0 ? cellToMonitor['$row,${col - 1}'] : null; final cellRight = col < gridCols ? cellToMonitor['$row,$col'] : null; // Only draw border if at least one side has a physical monitor if (cellLeft == null && cellRight == null) continue; // Cyan if: edge of physical monitor (one side empty or different monitor) final isCyan = cellLeft == null || cellRight == null || cellLeft.id != cellRight.id; // Skip internal borders for single-viewer monitors if (!isCyan && cellLeft != null && cellLeft.viewerIds.length == 1) { continue; } final segment = _LineSegment(Offset(x, y1), Offset(x, y2)); if (isCyan) { cyanLines.add(segment); } else { darkLines.add(segment); } } } // Draw dark borders first (behind) for (final line in darkLines) { canvas.drawLine(line.start, line.end, darkPaint); } // Draw cyan borders on top (in front) for (final line in cyanLines) { canvas.drawLine(line.start, line.end, cyanPaint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class _LineSegment { final Offset start; final Offset end; _LineSegment(this.start, this.end); } /// Physical monitor content without borders (borders drawn by overlay) class _PhysicalMonitorContent extends StatelessWidget { final PhysicalMonitor monitor; final WallState wallState; final Function(int viewerId) onViewerTap; const _PhysicalMonitorContent({ required this.monitor, required this.wallState, required this.onViewerTap, }); @override Widget build(BuildContext context) { final viewers = monitor.viewerIds; // Single viewer fills entire monitor space if (viewers.length == 1) { final viewerId = viewers.first; final isSelected = wallState.isViewerSelected(viewerId); return _ViewerTile( viewerId: viewerId, viewerState: wallState.getViewerState(viewerId), isSelected: isSelected, cameraInputDisplay: isSelected ? wallState.cameraInputDisplay : null, onTap: () => onViewerTap(viewerId), ); } // Multiple viewers: determine grid based on viewer count and monitor shape final gridCols = monitor.colSpan; final gridRows = monitor.rowSpan; return 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: Container( color: const Color(0xFF6A6A6A), ), ); } final viewerId = viewers[index]; final isSelected = wallState.isViewerSelected(viewerId); return Expanded( child: _ViewerTile( viewerId: viewerId, viewerState: wallState.getViewerState(viewerId), isSelected: isSelected, cameraInputDisplay: isSelected ? wallState.cameraInputDisplay : null, onTap: () => onViewerTap(viewerId), ), ); }), ), ); }), ); } } class _ViewerTile extends StatelessWidget { final int viewerId; final ViewerState viewerState; final bool isSelected; final String? cameraInputDisplay; final VoidCallback onTap; const _ViewerTile({ required this.viewerId, required this.viewerState, required this.isSelected, this.cameraInputDisplay, required this.onTap, }); @override Widget build(BuildContext context) { // D6 style: selected = cyan fill, alarm = red, normal = gray Color bgColor; if (isSelected) { bgColor = const Color(0xFF00D4FF); // Cyan fill for selected } else if (viewerState.hasAlarm) { bgColor = const Color(0xFFDC2626); // Red for alarm } else { bgColor = const Color(0xFF6A6A6A); // Gray for normal } return GestureDetector( onTap: onTap, child: Container( color: bgColor, child: Padding( padding: const EdgeInsets.only(top: 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Viewer ID at top (smaller) Text( '$viewerId', style: TextStyle( color: isSelected ? Colors.white : Colors.white, fontSize: 14, fontWeight: FontWeight.w400, ), ), // Camera number - large white text, matching D6 style if (isSelected && cameraInputDisplay != null) Padding( padding: const EdgeInsets.only(top: 2), child: Text( cameraInputDisplay!, style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, ), ), ), ], ), ), ), ); } } class _ViewerTileWithBorder extends StatelessWidget { final int viewerId; final ViewerState viewerState; final bool isSelected; final String? cameraInputDisplay; final VoidCallback onTap; final Border border; const _ViewerTileWithBorder({ required this.viewerId, required this.viewerState, required this.isSelected, this.cameraInputDisplay, required this.onTap, required this.border, }); @override Widget build(BuildContext context) { // D6 style: selected = cyan fill, alarm = red, normal = gray Color bgColor; if (isSelected) { bgColor = const Color(0xFF00D4FF); // Cyan fill for selected } else if (viewerState.hasAlarm) { bgColor = const Color(0xFFDC2626); // Red for alarm } else { bgColor = const Color(0xFF6A6A6A); // Gray for normal } return GestureDetector( onTap: onTap, child: Container( decoration: BoxDecoration( color: bgColor, border: border, ), child: Padding( padding: const EdgeInsets.only(top: 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Viewer ID at top (smaller) Text( '$viewerId', style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w400, ), ), // Camera number - large white text, matching D6 style if (isSelected && cameraInputDisplay != null) Padding( padding: const EdgeInsets.only(top: 2), child: Text( cameraInputDisplay!, style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, ), ), ), ], ), ), ), ); } } class _BottomIconBar extends StatelessWidget { final WallState wallState; final VoidCallback onSegmentsTap; const _BottomIconBar({ required this.wallState, required this.onSegmentsTap, }); @override Widget build(BuildContext context) { final hasSelection = wallState.selectedViewerId != null; return Container( height: 86, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: const Color(0xFF555555), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // Search _CircleIconButton( icon: Icons.search, isActive: hasSelection, onTap: () {}, ), // Lock _CircleIconButton( icon: Icons.lock_outline, onTap: () {}, ), // Quad view _CircleIconButton( icon: Icons.grid_view, onTap: () {}, ), // Segments - navigate to overview _CircleIconButton( icon: Icons.apps, isActive: true, onTap: onSegmentsTap, ), // Image/Camera _CircleIconButton( icon: Icons.image_outlined, onTap: () {}, ), // Alarm (red) _CircleIconButton( icon: Icons.notification_important, iconColor: const Color(0xFFCC4444), onTap: () {}, ), // History _CircleIconButton( icon: Icons.history, onTap: () {}, ), // Monitor _CircleIconButton( icon: Icons.tv, onTap: () {}, ), // Prefix selector _PrefixButton( prefix: wallState.cameraPrefix, onTap: () { // Cycle through prefixes final nextPrefix = wallState.cameraPrefix == 500 ? 501 : wallState.cameraPrefix == 501 ? 502 : 500; context.read().add(SetCameraPrefix(nextPrefix)); }, ), ], ), ); } } class _CircleIconButton extends StatelessWidget { final IconData icon; final bool isActive; final Color? iconColor; final VoidCallback onTap; const _CircleIconButton({ required this.icon, this.isActive = false, this.iconColor, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: isActive ? Colors.white : Colors.white38, width: 2, ), color: isActive ? const Color(0xFF333333) : Colors.transparent, ), child: Icon( icon, color: iconColor ?? (isActive ? Colors.white : Colors.white60), size: 38, ), ), ); } } class _PrefixButton extends StatelessWidget { final int prefix; final VoidCallback onTap; const _PrefixButton({ required this.prefix, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), color: const Color(0xFF333333), ), child: Center( child: Text( '$prefix', style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), ), ); } }