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>
181 lines
5.1 KiB
Dart
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,
|
|
];
|
|
}
|