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:
klas
2026-02-12 14:57:38 +01:00
commit 40143734fc
125 changed files with 65073 additions and 0 deletions

View 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? ??
'',
);
}
}