Files
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

181 lines
5.1 KiB
Dart

import 'package:equatable/equatable.dart';
import '../../../domain/entities/wall_config.dart';
/// State for a single viewer
class ViewerState extends Equatable {
final int viewerId;
final int currentCameraId;
final bool isLive;
final bool hasAlarm;
final bool isLocked;
final String? lockedBy;
const ViewerState({
required this.viewerId,
this.currentCameraId = 0,
this.isLive = true,
this.hasAlarm = false,
this.isLocked = false,
this.lockedBy,
});
bool get hasCamera => currentCameraId > 0;
bool get isLockedByOther => isLocked && lockedBy != null;
ViewerState copyWith({
int? currentCameraId,
bool? isLive,
bool? hasAlarm,
bool? isLocked,
String? lockedBy,
}) {
return ViewerState(
viewerId: viewerId,
currentCameraId: currentCameraId ?? this.currentCameraId,
isLive: isLive ?? this.isLive,
hasAlarm: hasAlarm ?? this.hasAlarm,
isLocked: isLocked ?? this.isLocked,
lockedBy: lockedBy ?? this.lockedBy,
);
}
@override
List<Object?> get props =>
[viewerId, currentCameraId, isLive, hasAlarm, isLocked, lockedBy];
}
/// Main wall bloc state
class WallState extends Equatable {
final WallConfig? config;
final bool isLoading;
final String? error;
// Selection state
final int? selectedViewerId;
final int? selectedPhysicalMonitorId;
// Camera input state
final int cameraPrefix; // 500, 501, 502
final String cameraNumberInput; // Up to 6 digits typed by user
final bool isEditing; // Whether camera input is active
// Viewer states (keyed by viewer ID)
final Map<int, ViewerState> viewerStates;
// Expanded sections
final Set<String> expandedSections;
const WallState({
this.config,
this.isLoading = false,
this.error,
this.selectedViewerId,
this.selectedPhysicalMonitorId,
this.cameraPrefix = 500,
this.cameraNumberInput = '',
this.isEditing = false,
this.viewerStates = const {},
this.expandedSections = const {},
});
static const int _maxLength = 6;
static const List<int> _prefixes = [500, 501, 502];
/// Compose camera number with prefix (legacy CameraNumber.GetCameraNumberWithPrefix).
/// If digits > prefix length: use digits as-is, right-pad with zeros.
/// If digits <= prefix length: prefix + left-padded digits.
int? get fullCameraNumber {
if (cameraNumberInput.isEmpty) return null;
final prefix = cameraPrefix.toString();
final String composed;
if (cameraNumberInput.length > prefix.length) {
composed = cameraNumberInput.padRight(_maxLength, '0');
} else {
composed = prefix +
cameraNumberInput.padLeft(_maxLength - prefix.length, '0');
}
return int.tryParse(composed);
}
/// Display string: typed digits only (no prefix shown in field).
String get cameraInputDisplay {
if (!isEditing || cameraNumberInput.isEmpty) return '';
return cameraNumberInput;
}
/// Check if a viewer is selected
bool isViewerSelected(int viewerId) => selectedViewerId == viewerId;
/// Check if a physical monitor is selected (any of its viewers)
bool isPhysicalMonitorSelected(PhysicalMonitor monitor) =>
selectedPhysicalMonitorId == monitor.id;
/// Get viewer state
ViewerState getViewerState(int viewerId) {
return viewerStates[viewerId] ?? ViewerState(viewerId: viewerId);
}
/// Check if section is expanded
bool isSectionExpanded(String sectionId) =>
expandedSections.contains(sectionId);
/// Check if CrossSwitch can be executed
bool get canExecuteCrossSwitch {
if (selectedViewerId == null) return false;
if (!isEditing || cameraNumberInput.isEmpty) return false;
if (fullCameraNumber == null) return false;
final viewerState = getViewerState(selectedViewerId!);
if (viewerState.hasAlarm) return false;
return true;
}
WallState copyWith({
WallConfig? config,
bool? isLoading,
String? error,
int? selectedViewerId,
int? selectedPhysicalMonitorId,
int? cameraPrefix,
String? cameraNumberInput,
bool? isEditing,
Map<int, ViewerState>? viewerStates,
Set<String>? expandedSections,
bool clearSelection = false,
bool clearError = false,
}) {
return WallState(
config: config ?? this.config,
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
selectedViewerId:
clearSelection ? null : (selectedViewerId ?? this.selectedViewerId),
selectedPhysicalMonitorId: clearSelection
? null
: (selectedPhysicalMonitorId ?? this.selectedPhysicalMonitorId),
cameraPrefix: cameraPrefix ?? this.cameraPrefix,
cameraNumberInput: cameraNumberInput ?? this.cameraNumberInput,
isEditing: isEditing ?? this.isEditing,
viewerStates: viewerStates ?? this.viewerStates,
expandedSections: expandedSections ?? this.expandedSections,
);
}
@override
List<Object?> get props => [
config,
isLoading,
error,
selectedViewerId,
selectedPhysicalMonitorId,
cameraPrefix,
cameraNumberInput,
isEditing,
viewerStates,
expandedSections,
];
}