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:
137
copilot_keyboard/lib/domain/entities/alarm_state.dart
Normal file
137
copilot_keyboard/lib/domain/entities/alarm_state.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Alarm state enumeration matching SDK PlcViewerAlarmState
|
||||
enum AlarmStatus {
|
||||
newAlarm(0, 'vasNewAlarm'),
|
||||
presented(1, 'vasPresented'),
|
||||
stacked(2, 'vasStacked'),
|
||||
confirmed(3, 'vasConfirmed'),
|
||||
removed(4, 'vasRemoved'),
|
||||
lastConfirmed(5, 'vasLastConfirmed'),
|
||||
lastRemoved(6, 'vasLastRemoved');
|
||||
|
||||
final int value;
|
||||
final String name;
|
||||
const AlarmStatus(this.value, this.name);
|
||||
|
||||
static AlarmStatus fromValue(int value) {
|
||||
return AlarmStatus.values.firstWhere(
|
||||
(s) => s.value == value,
|
||||
orElse: () => AlarmStatus.newAlarm,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if this status blocks the monitor
|
||||
bool get blocksMonitor =>
|
||||
this == AlarmStatus.newAlarm || this == AlarmStatus.presented;
|
||||
}
|
||||
|
||||
/// State of a single alarm/event
|
||||
class AlarmState extends Equatable {
|
||||
final int eventId;
|
||||
final String eventName;
|
||||
final int typeId;
|
||||
final int foreignKey; // Camera or contact ID
|
||||
final String? serverId;
|
||||
final DateTime startedAt;
|
||||
final DateTime? stoppedAt;
|
||||
final bool isActive;
|
||||
final AlarmStatus status;
|
||||
final int? associatedMonitor;
|
||||
|
||||
const AlarmState({
|
||||
required this.eventId,
|
||||
required this.eventName,
|
||||
required this.typeId,
|
||||
required this.foreignKey,
|
||||
this.serverId,
|
||||
required this.startedAt,
|
||||
this.stoppedAt,
|
||||
required this.isActive,
|
||||
this.status = AlarmStatus.newAlarm,
|
||||
this.associatedMonitor,
|
||||
});
|
||||
|
||||
/// Check if this alarm blocks a monitor
|
||||
bool get blocksMonitor => isActive && status.blocksMonitor;
|
||||
|
||||
/// Create a stopped alarm
|
||||
AlarmState stopped() {
|
||||
return AlarmState(
|
||||
eventId: eventId,
|
||||
eventName: eventName,
|
||||
typeId: typeId,
|
||||
foreignKey: foreignKey,
|
||||
serverId: serverId,
|
||||
startedAt: startedAt,
|
||||
stoppedAt: DateTime.now(),
|
||||
isActive: false,
|
||||
status: AlarmStatus.removed,
|
||||
associatedMonitor: associatedMonitor,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create alarm with updated status
|
||||
AlarmState withStatus(AlarmStatus newStatus) {
|
||||
return AlarmState(
|
||||
eventId: eventId,
|
||||
eventName: eventName,
|
||||
typeId: typeId,
|
||||
foreignKey: foreignKey,
|
||||
serverId: serverId,
|
||||
startedAt: startedAt,
|
||||
stoppedAt: stoppedAt,
|
||||
isActive: isActive,
|
||||
status: newStatus,
|
||||
associatedMonitor: associatedMonitor,
|
||||
);
|
||||
}
|
||||
|
||||
factory AlarmState.fromJson(Map<String, dynamic> json) {
|
||||
return AlarmState(
|
||||
eventId: json['event_id'] as int? ?? 0,
|
||||
eventName: json['event_name'] as String? ?? '',
|
||||
typeId: json['type_id'] as int? ?? 0,
|
||||
foreignKey: json['foreign_key'] as int? ?? 0,
|
||||
serverId: json['server_id'] as String?,
|
||||
startedAt: json['started_at'] != null
|
||||
? DateTime.parse(json['started_at'] as String)
|
||||
: DateTime.now(),
|
||||
stoppedAt: json['stopped_at'] != null
|
||||
? DateTime.parse(json['stopped_at'] as String)
|
||||
: null,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
status: AlarmStatus.fromValue(json['status'] as int? ?? 0),
|
||||
associatedMonitor: json['associated_monitor'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'event_id': eventId,
|
||||
'event_name': eventName,
|
||||
'type_id': typeId,
|
||||
'foreign_key': foreignKey,
|
||||
'server_id': serverId,
|
||||
'started_at': startedAt.toIso8601String(),
|
||||
'stopped_at': stoppedAt?.toIso8601String(),
|
||||
'is_active': isActive,
|
||||
'status': status.value,
|
||||
'associated_monitor': associatedMonitor,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
eventId,
|
||||
eventName,
|
||||
typeId,
|
||||
foreignKey,
|
||||
serverId,
|
||||
startedAt,
|
||||
stoppedAt,
|
||||
isActive,
|
||||
status,
|
||||
associatedMonitor,
|
||||
];
|
||||
}
|
||||
123
copilot_keyboard/lib/domain/entities/camera_lock.dart
Normal file
123
copilot_keyboard/lib/domain/entities/camera_lock.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
/// Camera lock entity matching the coordinator's CameraLock model.
|
||||
class CameraLock {
|
||||
final int cameraId;
|
||||
final CameraLockPriority priority;
|
||||
final String ownerName;
|
||||
final DateTime ownedSince;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const CameraLock({
|
||||
required this.cameraId,
|
||||
required this.priority,
|
||||
required this.ownerName,
|
||||
required this.ownedSince,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory CameraLock.fromJson(Map<String, dynamic> json) {
|
||||
return CameraLock(
|
||||
cameraId: json['cameraId'] as int? ?? json['CameraId'] as int? ?? 0,
|
||||
priority: CameraLockPriority.fromString(
|
||||
json['priority'] as String? ?? json['Priority'] as String? ?? 'Low'),
|
||||
ownerName:
|
||||
json['ownerName'] as String? ?? json['OwnerName'] as String? ?? '',
|
||||
ownedSince: DateTime.parse(
|
||||
json['ownedSince'] as String? ?? json['OwnedSince'] as String? ?? ''),
|
||||
expiresAt: DateTime.parse(
|
||||
json['expiresAt'] as String? ?? json['ExpiresAt'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
Duration get timeRemaining {
|
||||
final remaining = expiresAt.difference(DateTime.now().toUtc());
|
||||
return remaining.isNegative ? Duration.zero : remaining;
|
||||
}
|
||||
|
||||
bool get isExpiringSoon => timeRemaining.inSeconds <= 60;
|
||||
bool get isExpired => timeRemaining == Duration.zero;
|
||||
bool isOwnedBy(String keyboardId) =>
|
||||
ownerName.toLowerCase() == keyboardId.toLowerCase();
|
||||
}
|
||||
|
||||
enum CameraLockPriority {
|
||||
none,
|
||||
high,
|
||||
low;
|
||||
|
||||
static CameraLockPriority fromString(String value) {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'high':
|
||||
return CameraLockPriority.high;
|
||||
case 'low':
|
||||
return CameraLockPriority.low;
|
||||
default:
|
||||
return CameraLockPriority.none;
|
||||
}
|
||||
}
|
||||
|
||||
String get name {
|
||||
switch (this) {
|
||||
case CameraLockPriority.high:
|
||||
return 'High';
|
||||
case CameraLockPriority.low:
|
||||
return 'Low';
|
||||
case CameraLockPriority.none:
|
||||
return 'None';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CameraLockNotificationType {
|
||||
acquired,
|
||||
takenOver,
|
||||
confirmTakeOver,
|
||||
confirmed,
|
||||
rejected,
|
||||
expireSoon,
|
||||
unlocked;
|
||||
|
||||
static CameraLockNotificationType fromString(String value) {
|
||||
switch (value) {
|
||||
case 'Acquired':
|
||||
return CameraLockNotificationType.acquired;
|
||||
case 'TakenOver':
|
||||
return CameraLockNotificationType.takenOver;
|
||||
case 'ConfirmTakeOver':
|
||||
return CameraLockNotificationType.confirmTakeOver;
|
||||
case 'Confirmed':
|
||||
return CameraLockNotificationType.confirmed;
|
||||
case 'Rejected':
|
||||
return CameraLockNotificationType.rejected;
|
||||
case 'ExpireSoon':
|
||||
return CameraLockNotificationType.expireSoon;
|
||||
case 'Unlocked':
|
||||
return CameraLockNotificationType.unlocked;
|
||||
default:
|
||||
return CameraLockNotificationType.acquired;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock notification from the coordinator (sent via WebSocket)
|
||||
class CameraLockNotification {
|
||||
final CameraLockNotificationType type;
|
||||
final int cameraId;
|
||||
final String copilotName;
|
||||
|
||||
const CameraLockNotification({
|
||||
required this.type,
|
||||
required this.cameraId,
|
||||
required this.copilotName,
|
||||
});
|
||||
|
||||
factory CameraLockNotification.fromJson(Map<String, dynamic> json) {
|
||||
return CameraLockNotification(
|
||||
type: CameraLockNotificationType.fromString(
|
||||
json['type'] as String? ?? json['Type'] as String? ?? ''),
|
||||
cameraId: json['cameraId'] as int? ?? json['CameraId'] as int? ?? 0,
|
||||
copilotName: json['copilotName'] as String? ??
|
||||
json['CopilotName'] as String? ??
|
||||
'',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/// Configuration for function buttons (F1-F7) per wall.
|
||||
/// Loaded from the "functionButtons" section of the config file.
|
||||
class FunctionButtonConfig {
|
||||
final Map<String, Map<String, List<FunctionButtonAction>>> walls;
|
||||
|
||||
const FunctionButtonConfig({this.walls = const {}});
|
||||
|
||||
/// Get actions for a specific wall and button.
|
||||
List<FunctionButtonAction> getActions(String wallId, String buttonKey) {
|
||||
return walls[wallId]?[buttonKey] ?? [];
|
||||
}
|
||||
|
||||
/// Check if a button has any actions configured for this wall.
|
||||
bool hasActions(String wallId, String buttonKey) {
|
||||
return getActions(wallId, buttonKey).isNotEmpty;
|
||||
}
|
||||
|
||||
factory FunctionButtonConfig.fromJson(Map<String, dynamic> json) {
|
||||
final wallsJson = json['walls'] as Map<String, dynamic>? ?? {};
|
||||
final walls = <String, Map<String, List<FunctionButtonAction>>>{};
|
||||
|
||||
for (final wallEntry in wallsJson.entries) {
|
||||
final buttonsJson = wallEntry.value as Map<String, dynamic>? ?? {};
|
||||
final buttons = <String, List<FunctionButtonAction>>{};
|
||||
|
||||
for (final buttonEntry in buttonsJson.entries) {
|
||||
final actionsJson = buttonEntry.value as List<dynamic>? ?? [];
|
||||
buttons[buttonEntry.key] = actionsJson
|
||||
.map((a) =>
|
||||
FunctionButtonAction.fromJson(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
walls[wallEntry.key] = buttons;
|
||||
}
|
||||
|
||||
return FunctionButtonConfig(walls: walls);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single action triggered by a function button press.
|
||||
class FunctionButtonAction {
|
||||
final FunctionButtonActionType type;
|
||||
final int viewerId;
|
||||
final int sourceId;
|
||||
|
||||
const FunctionButtonAction({
|
||||
required this.type,
|
||||
required this.viewerId,
|
||||
required this.sourceId,
|
||||
});
|
||||
|
||||
factory FunctionButtonAction.fromJson(Map<String, dynamic> json) {
|
||||
return FunctionButtonAction(
|
||||
type: FunctionButtonActionType.fromString(
|
||||
json['actionType'] as String? ?? ''),
|
||||
viewerId: json['viewerId'] as int? ?? 0,
|
||||
sourceId: json['sourceId'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum FunctionButtonActionType {
|
||||
crossSwitch,
|
||||
sequenceStart;
|
||||
|
||||
static FunctionButtonActionType fromString(String value) {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'crossswitch':
|
||||
return FunctionButtonActionType.crossSwitch;
|
||||
case 'sequencestart':
|
||||
return FunctionButtonActionType.sequenceStart;
|
||||
default:
|
||||
return FunctionButtonActionType.crossSwitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
copilot_keyboard/lib/domain/entities/monitor_state.dart
Normal file
127
copilot_keyboard/lib/domain/entities/monitor_state.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Play mode enumeration matching SDK values
|
||||
enum PlayMode {
|
||||
unknown(0),
|
||||
playStop(1),
|
||||
playForward(2),
|
||||
playBackward(3),
|
||||
fastForward(4),
|
||||
fastBackward(5),
|
||||
stepForward(6),
|
||||
stepBackward(7),
|
||||
playBOD(8),
|
||||
playEOD(9),
|
||||
quasiLive(10),
|
||||
live(11),
|
||||
nextEvent(12),
|
||||
prevEvent(13),
|
||||
peekLivePicture(14),
|
||||
nextDetectedMotion(17),
|
||||
prevDetectedMotion(18);
|
||||
|
||||
final int value;
|
||||
const PlayMode(this.value);
|
||||
|
||||
static PlayMode fromValue(int value) {
|
||||
return PlayMode.values.firstWhere(
|
||||
(m) => m.value == value,
|
||||
orElse: () => PlayMode.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// State of a single monitor/viewer
|
||||
class MonitorState extends Equatable {
|
||||
final int viewerId;
|
||||
final int currentChannel;
|
||||
final PlayMode playMode;
|
||||
final String? serverId;
|
||||
final DateTime lastUpdated;
|
||||
final bool hasAlarm;
|
||||
|
||||
const MonitorState({
|
||||
required this.viewerId,
|
||||
required this.currentChannel,
|
||||
required this.playMode,
|
||||
this.serverId,
|
||||
required this.lastUpdated,
|
||||
this.hasAlarm = false,
|
||||
});
|
||||
|
||||
/// Check if monitor is currently displaying a camera
|
||||
bool get isActive => currentChannel > 0;
|
||||
|
||||
/// Check if monitor is in live mode
|
||||
bool get isLive => playMode == PlayMode.live || playMode == PlayMode.quasiLive;
|
||||
|
||||
/// Create a cleared state
|
||||
MonitorState cleared() {
|
||||
return MonitorState(
|
||||
viewerId: viewerId,
|
||||
currentChannel: 0,
|
||||
playMode: PlayMode.unknown,
|
||||
serverId: serverId,
|
||||
lastUpdated: DateTime.now(),
|
||||
hasAlarm: hasAlarm,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create updated state with new camera
|
||||
MonitorState withCamera(int channel, PlayMode mode) {
|
||||
return MonitorState(
|
||||
viewerId: viewerId,
|
||||
currentChannel: channel,
|
||||
playMode: mode,
|
||||
serverId: serverId,
|
||||
lastUpdated: DateTime.now(),
|
||||
hasAlarm: hasAlarm,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create state with alarm flag updated
|
||||
MonitorState withAlarm(bool alarm) {
|
||||
return MonitorState(
|
||||
viewerId: viewerId,
|
||||
currentChannel: currentChannel,
|
||||
playMode: playMode,
|
||||
serverId: serverId,
|
||||
lastUpdated: lastUpdated,
|
||||
hasAlarm: alarm,
|
||||
);
|
||||
}
|
||||
|
||||
factory MonitorState.fromJson(Map<String, dynamic> json) {
|
||||
return MonitorState(
|
||||
viewerId: json['viewer_id'] as int? ?? 0,
|
||||
currentChannel: json['current_channel'] as int? ?? 0,
|
||||
playMode: PlayMode.fromValue(json['play_mode'] as int? ?? 0),
|
||||
serverId: json['server_id'] as String?,
|
||||
lastUpdated: json['last_updated'] != null
|
||||
? DateTime.parse(json['last_updated'] as String)
|
||||
: DateTime.now(),
|
||||
hasAlarm: json['has_alarm'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'viewer_id': viewerId,
|
||||
'current_channel': currentChannel,
|
||||
'play_mode': playMode.value,
|
||||
'server_id': serverId,
|
||||
'last_updated': lastUpdated.toIso8601String(),
|
||||
'has_alarm': hasAlarm,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
viewerId,
|
||||
currentChannel,
|
||||
playMode,
|
||||
serverId,
|
||||
lastUpdated,
|
||||
hasAlarm,
|
||||
];
|
||||
}
|
||||
72
copilot_keyboard/lib/domain/entities/sequence.dart
Normal file
72
copilot_keyboard/lib/domain/entities/sequence.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Sequence definition loaded from the coordinator.
|
||||
class SequenceDefinition {
|
||||
final int id;
|
||||
final String name;
|
||||
final int categoryId;
|
||||
final List<int> cameras;
|
||||
final int intervalSeconds;
|
||||
|
||||
const SequenceDefinition({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.categoryId,
|
||||
required this.cameras,
|
||||
required this.intervalSeconds,
|
||||
});
|
||||
|
||||
factory SequenceDefinition.fromJson(Map<String, dynamic> json) {
|
||||
return SequenceDefinition(
|
||||
id: json['id'] as int? ?? json['Id'] as int? ?? 0,
|
||||
name: json['name'] as String? ?? json['Name'] as String? ?? '',
|
||||
categoryId:
|
||||
json['categoryId'] as int? ?? json['CategoryId'] as int? ?? 0,
|
||||
cameras: (json['cameras'] as List<dynamic>? ??
|
||||
json['Cameras'] as List<dynamic>? ??
|
||||
[])
|
||||
.cast<int>(),
|
||||
intervalSeconds: json['intervalSeconds'] as int? ??
|
||||
json['IntervalSeconds'] as int? ??
|
||||
5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequence category for grouping sequences.
|
||||
class SequenceCategory {
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
const SequenceCategory({required this.id, required this.name});
|
||||
|
||||
factory SequenceCategory.fromJson(Map<String, dynamic> json) {
|
||||
return SequenceCategory(
|
||||
id: json['id'] as int? ?? json['Id'] as int? ?? 0,
|
||||
name: json['name'] as String? ?? json['Name'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence currently running on a viewer.
|
||||
class RunningSequence {
|
||||
final int viewerId;
|
||||
final int sequenceId;
|
||||
final DateTime startedAt;
|
||||
|
||||
const RunningSequence({
|
||||
required this.viewerId,
|
||||
required this.sequenceId,
|
||||
required this.startedAt,
|
||||
});
|
||||
|
||||
factory RunningSequence.fromJson(Map<String, dynamic> json) {
|
||||
return RunningSequence(
|
||||
viewerId: json['viewerId'] as int? ?? json['ViewerId'] as int? ?? 0,
|
||||
sequenceId:
|
||||
json['sequenceId'] as int? ?? json['SequenceId'] as int? ?? 0,
|
||||
startedAt: DateTime.tryParse(json['startedAt'] as String? ??
|
||||
json['StartedAt'] as String? ??
|
||||
'') ??
|
||||
DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
copilot_keyboard/lib/domain/entities/server_config.dart
Normal file
94
copilot_keyboard/lib/domain/entities/server_config.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Server type enumeration
|
||||
enum ServerType { geviscope, gcore, geviserver }
|
||||
|
||||
/// Configuration for a single recording server
|
||||
class ServerConfig extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final ServerType type;
|
||||
final bool enabled;
|
||||
final String address;
|
||||
final int port;
|
||||
final String username;
|
||||
final String bridgeUrl;
|
||||
final String? websocketUrl;
|
||||
final int cameraRangeStart;
|
||||
final int cameraRangeEnd;
|
||||
final int monitorRangeStart;
|
||||
final int monitorRangeEnd;
|
||||
|
||||
const ServerConfig({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.enabled,
|
||||
required this.address,
|
||||
required this.port,
|
||||
required this.username,
|
||||
required this.bridgeUrl,
|
||||
this.websocketUrl,
|
||||
required this.cameraRangeStart,
|
||||
required this.cameraRangeEnd,
|
||||
required this.monitorRangeStart,
|
||||
required this.monitorRangeEnd,
|
||||
});
|
||||
|
||||
/// Check if this server owns a camera ID
|
||||
bool ownsCamera(int cameraId) {
|
||||
return cameraId >= cameraRangeStart && cameraId <= cameraRangeEnd;
|
||||
}
|
||||
|
||||
/// Check if this server owns a monitor ID
|
||||
bool ownsMonitor(int monitorId) {
|
||||
return monitorId >= monitorRangeStart && monitorId <= monitorRangeEnd;
|
||||
}
|
||||
|
||||
factory ServerConfig.fromJson(Map<String, dynamic> json) {
|
||||
final typeStr = json['type'] as String;
|
||||
final type = ServerType.values.firstWhere(
|
||||
(t) => t.name == typeStr,
|
||||
orElse: () => ServerType.geviscope,
|
||||
);
|
||||
|
||||
final connection = json['connection'] as Map<String, dynamic>? ?? {};
|
||||
final bridge = json['bridge'] as Map<String, dynamic>? ?? {};
|
||||
final resources = json['resources'] as Map<String, dynamic>? ?? {};
|
||||
final cameraRange = resources['cameraRange'] as Map<String, dynamic>? ?? {};
|
||||
final monitorRange = resources['monitorRange'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return ServerConfig(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String? ?? json['id'] as String,
|
||||
type: type,
|
||||
enabled: json['enabled'] as bool? ?? true,
|
||||
address: connection['address'] as String? ?? '',
|
||||
port: connection['port'] as int? ?? 7700,
|
||||
username: connection['username'] as String? ?? '',
|
||||
bridgeUrl: bridge['url'] as String? ?? '',
|
||||
websocketUrl: bridge['websocket'] as String?,
|
||||
cameraRangeStart: cameraRange['start'] as int? ?? 0,
|
||||
cameraRangeEnd: cameraRange['end'] as int? ?? 0,
|
||||
monitorRangeStart: monitorRange['start'] as int? ?? 0,
|
||||
monitorRangeEnd: monitorRange['end'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
enabled,
|
||||
address,
|
||||
port,
|
||||
username,
|
||||
bridgeUrl,
|
||||
websocketUrl,
|
||||
cameraRangeStart,
|
||||
cameraRangeEnd,
|
||||
monitorRangeStart,
|
||||
monitorRangeEnd,
|
||||
];
|
||||
}
|
||||
439
copilot_keyboard/lib/domain/entities/wall_config.dart
Normal file
439
copilot_keyboard/lib/domain/entities/wall_config.dart
Normal file
@@ -0,0 +1,439 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Configuration for a physical monitor that can display 1-4 viewers
|
||||
class PhysicalMonitor extends Equatable {
|
||||
final int id;
|
||||
final String? name;
|
||||
final List<int> viewerIds; // 1-4 viewer IDs in this physical monitor
|
||||
final bool isQuadView;
|
||||
final int row; // Grid row position (1-based)
|
||||
final int col; // Grid column position (1-based)
|
||||
final int rowSpan; // How many rows this monitor spans
|
||||
final int colSpan; // How many columns this monitor spans
|
||||
|
||||
const PhysicalMonitor({
|
||||
required this.id,
|
||||
this.name,
|
||||
required this.viewerIds,
|
||||
this.isQuadView = false,
|
||||
this.row = 1,
|
||||
this.col = 1,
|
||||
this.rowSpan = 1,
|
||||
this.colSpan = 1,
|
||||
});
|
||||
|
||||
/// Whether this monitor has multiple viewers (quad view)
|
||||
bool get hasMultipleViewers => viewerIds.length > 1;
|
||||
|
||||
/// Get the primary viewer ID (first one)
|
||||
int get primaryViewerId => viewerIds.isNotEmpty ? viewerIds.first : 0;
|
||||
|
||||
factory PhysicalMonitor.fromJson(Map<String, dynamic> json) {
|
||||
final viewers = json['viewer_ids'] as List<dynamic>?;
|
||||
return PhysicalMonitor(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String?,
|
||||
viewerIds: viewers?.map((v) => v as int).toList() ?? [],
|
||||
isQuadView: json['is_quad_view'] as bool? ?? false,
|
||||
row: json['row'] as int? ?? 1,
|
||||
col: json['col'] as int? ?? 1,
|
||||
rowSpan: json['row_span'] as int? ?? 1,
|
||||
colSpan: json['col_span'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'viewer_ids': viewerIds,
|
||||
'is_quad_view': isQuadView,
|
||||
'row': row,
|
||||
'col': col,
|
||||
'row_span': rowSpan,
|
||||
'col_span': colSpan,
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, viewerIds, isQuadView, row, col, rowSpan, colSpan];
|
||||
}
|
||||
|
||||
/// A section of the video wall (e.g., "Vrchní část", "Pravá část")
|
||||
class WallSection extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<PhysicalMonitor> monitors;
|
||||
final int columns; // Grid layout columns for this section
|
||||
final int rows; // Grid layout rows for this section
|
||||
|
||||
const WallSection({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.monitors,
|
||||
this.columns = 8,
|
||||
this.rows = 4,
|
||||
});
|
||||
|
||||
factory WallSection.fromJson(Map<String, dynamic> json) {
|
||||
final monitors = json['monitors'] as List<dynamic>?;
|
||||
return WallSection(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
monitors: monitors
|
||||
?.map((m) => PhysicalMonitor.fromJson(m as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
columns: json['columns'] as int? ?? 8,
|
||||
rows: json['rows'] as int? ?? 4,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'monitors': monitors.map((m) => m.toJson()).toList(),
|
||||
'columns': columns,
|
||||
'rows': rows,
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, monitors, columns, rows];
|
||||
}
|
||||
|
||||
/// Complete wall configuration with all sections
|
||||
class WallConfig extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<WallSection> sections;
|
||||
final List<int> alarmMonitorIds; // Monitor IDs designated for alarms
|
||||
|
||||
const WallConfig({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sections,
|
||||
this.alarmMonitorIds = const [],
|
||||
});
|
||||
|
||||
/// Get all viewer IDs across all sections
|
||||
List<int> get allViewerIds {
|
||||
final ids = <int>[];
|
||||
for (final section in sections) {
|
||||
for (final monitor in section.monitors) {
|
||||
ids.addAll(monitor.viewerIds);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/// Get all physical monitors across all sections
|
||||
List<PhysicalMonitor> get allMonitors {
|
||||
final monitors = <PhysicalMonitor>[];
|
||||
for (final section in sections) {
|
||||
monitors.addAll(section.monitors);
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
|
||||
/// Find physical monitor containing a viewer ID
|
||||
PhysicalMonitor? findMonitorByViewerId(int viewerId) {
|
||||
for (final section in sections) {
|
||||
for (final monitor in section.monitors) {
|
||||
if (monitor.viewerIds.contains(viewerId)) {
|
||||
return monitor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Find section containing a viewer ID
|
||||
WallSection? findSectionByViewerId(int viewerId) {
|
||||
for (final section in sections) {
|
||||
for (final monitor in section.monitors) {
|
||||
if (monitor.viewerIds.contains(viewerId)) {
|
||||
return section;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
factory WallConfig.fromJson(Map<String, dynamic> json) {
|
||||
final sections = json['sections'] as List<dynamic>?;
|
||||
final alarmIds = json['alarm_monitor_ids'] as List<dynamic>?;
|
||||
return WallConfig(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
sections: sections
|
||||
?.map((s) => WallSection.fromJson(s as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
alarmMonitorIds: alarmIds?.map((i) => i as int).toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'sections': sections.map((s) => s.toJson()).toList(),
|
||||
'alarm_monitor_ids': alarmMonitorIds,
|
||||
};
|
||||
|
||||
/// Create sample wall config matching D6 app structure
|
||||
factory WallConfig.sample() {
|
||||
return WallConfig(
|
||||
id: 'wall_1',
|
||||
name: 'Hlavní videostěna',
|
||||
sections: [
|
||||
// Vrchní část - 8 columns x 4 rows irregular grid
|
||||
WallSection(
|
||||
id: 'top',
|
||||
name: 'Vrchní část',
|
||||
columns: 8,
|
||||
rows: 4,
|
||||
monitors: [
|
||||
// Row 1-2: Three quad monitors
|
||||
PhysicalMonitor(
|
||||
id: 1,
|
||||
viewerIds: [210, 211, 212, 213],
|
||||
isQuadView: true,
|
||||
row: 1, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 2,
|
||||
viewerIds: [214, 215, 216, 217],
|
||||
isQuadView: true,
|
||||
row: 1, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 3,
|
||||
viewerIds: [1001, 1002, 1003, 1004],
|
||||
isQuadView: true,
|
||||
row: 1, col: 5, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
// Row 4: Single monitor
|
||||
PhysicalMonitor(
|
||||
id: 4,
|
||||
viewerIds: [222],
|
||||
row: 4, col: 2, rowSpan: 1, colSpan: 1,
|
||||
),
|
||||
// Row 3-4: Three quad monitors
|
||||
PhysicalMonitor(
|
||||
id: 5,
|
||||
viewerIds: [223, 224, 225, 226],
|
||||
isQuadView: true,
|
||||
row: 3, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 6,
|
||||
viewerIds: [227, 228, 229, 230],
|
||||
isQuadView: true,
|
||||
row: 3, col: 5, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 7,
|
||||
viewerIds: [231, 232, 233, 234],
|
||||
isQuadView: true,
|
||||
row: 3, col: 7, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Levá část - 7 columns x 6 rows
|
||||
WallSection(
|
||||
id: 'left',
|
||||
name: 'Levá část',
|
||||
columns: 7,
|
||||
rows: 6,
|
||||
monitors: [
|
||||
// Row 1-2: 3x2 monitor + two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 8,
|
||||
viewerIds: [88, 89, 90, 91, 92, 93],
|
||||
row: 1, col: 1, rowSpan: 2, colSpan: 3,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 9,
|
||||
viewerIds: [40, 41, 42, 43],
|
||||
isQuadView: true,
|
||||
row: 1, col: 4, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 10,
|
||||
viewerIds: [44, 45, 46, 47],
|
||||
isQuadView: true,
|
||||
row: 1, col: 6, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
// Row 3-4: Two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 11,
|
||||
viewerIds: [48, 49, 50, 51],
|
||||
isQuadView: true,
|
||||
row: 3, col: 4, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 12,
|
||||
viewerIds: [52, 53, 54, 55],
|
||||
isQuadView: true,
|
||||
row: 3, col: 6, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
// Row 5-6: Two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 13,
|
||||
viewerIds: [56, 57, 58, 59],
|
||||
isQuadView: true,
|
||||
row: 5, col: 4, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 14,
|
||||
viewerIds: [60, 61, 62, 63],
|
||||
isQuadView: true,
|
||||
row: 5, col: 6, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Střed stěny - 8 columns x 4 rows
|
||||
WallSection(
|
||||
id: 'center',
|
||||
name: 'Střed stěny',
|
||||
columns: 8,
|
||||
rows: 4,
|
||||
monitors: [
|
||||
// Row 1-2: Quad + 4 tall single monitors
|
||||
PhysicalMonitor(
|
||||
id: 15,
|
||||
viewerIds: [14, 15, 16, 17],
|
||||
isQuadView: true,
|
||||
row: 1, col: 2, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(id: 16, viewerIds: [18], row: 1, col: 4, rowSpan: 2, colSpan: 1),
|
||||
PhysicalMonitor(id: 17, viewerIds: [19], row: 1, col: 5, rowSpan: 2, colSpan: 1),
|
||||
PhysicalMonitor(id: 18, viewerIds: [20], row: 1, col: 6, rowSpan: 2, colSpan: 1),
|
||||
PhysicalMonitor(id: 19, viewerIds: [21], row: 1, col: 7, rowSpan: 2, colSpan: 1),
|
||||
// Row 3-4: Four 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 20,
|
||||
viewerIds: [24, 25, 32, 33],
|
||||
isQuadView: true,
|
||||
row: 3, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 21,
|
||||
viewerIds: [26, 27, 34, 35],
|
||||
isQuadView: true,
|
||||
row: 3, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 22,
|
||||
viewerIds: [28, 29, 36, 37],
|
||||
isQuadView: true,
|
||||
row: 3, col: 5, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 23,
|
||||
viewerIds: [30, 31, 38, 39],
|
||||
isQuadView: true,
|
||||
row: 3, col: 7, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Pravá část - 7 columns x 6 rows
|
||||
WallSection(
|
||||
id: 'right',
|
||||
name: 'Pravá část',
|
||||
columns: 7,
|
||||
rows: 6,
|
||||
monitors: [
|
||||
// Row 1-2: Two 2x2 quads + 3x2 monitor
|
||||
PhysicalMonitor(
|
||||
id: 24,
|
||||
viewerIds: [64, 65, 66, 67],
|
||||
isQuadView: true,
|
||||
row: 1, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 25,
|
||||
viewerIds: [68, 69, 70, 71],
|
||||
isQuadView: true,
|
||||
row: 1, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 26,
|
||||
viewerIds: [94, 95, 96, 97, 98, 99],
|
||||
row: 1, col: 5, rowSpan: 2, colSpan: 3,
|
||||
),
|
||||
// Row 3-4: Two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 27,
|
||||
viewerIds: [72, 73, 74, 75],
|
||||
isQuadView: true,
|
||||
row: 3, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 28,
|
||||
viewerIds: [76, 77, 78, 79],
|
||||
isQuadView: true,
|
||||
row: 3, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
// Row 5-6: Two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 29,
|
||||
viewerIds: [80, 81, 82, 83],
|
||||
isQuadView: true,
|
||||
row: 5, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 30,
|
||||
viewerIds: [84, 85, 86, 87],
|
||||
isQuadView: true,
|
||||
row: 5, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Stupínek - 8 columns x 4 rows
|
||||
WallSection(
|
||||
id: 'bottom',
|
||||
name: 'Stupínek',
|
||||
columns: 8,
|
||||
rows: 4,
|
||||
monitors: [
|
||||
// Row 1-2: Three 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 31,
|
||||
viewerIds: [183, 184, 185, 186],
|
||||
isQuadView: true,
|
||||
row: 1, col: 1, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 32,
|
||||
viewerIds: [187, 188, 189, 190],
|
||||
isQuadView: true,
|
||||
row: 1, col: 3, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 33,
|
||||
viewerIds: [191, 192, 193, 194],
|
||||
isQuadView: true,
|
||||
row: 1, col: 5, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
// Row 3-4: Two 2x2 quads
|
||||
PhysicalMonitor(
|
||||
id: 34,
|
||||
viewerIds: [195, 196, 197, 198],
|
||||
isQuadView: true,
|
||||
row: 3, col: 5, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
PhysicalMonitor(
|
||||
id: 35,
|
||||
viewerIds: [199, 200, 201, 202],
|
||||
isQuadView: true,
|
||||
row: 3, col: 7, rowSpan: 2, colSpan: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
alarmMonitorIds: [222, 223, 224], // Example alarm monitors
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, sections, alarmMonitorIds];
|
||||
}
|
||||
Reference in New Issue
Block a user