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>
180 lines
5.2 KiB
Dart
180 lines
5.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:logger/logger.dart';
|
|
|
|
import '../domain/entities/function_button_config.dart';
|
|
import '../domain/entities/server_config.dart';
|
|
|
|
/// Application configuration loaded from servers.json
|
|
class AppConfig {
|
|
final List<ServerConfig> servers;
|
|
final String coordinatorUrl;
|
|
final String keyboardId;
|
|
final FunctionButtonConfig functionButtons;
|
|
final int alarmSyncIntervalSeconds;
|
|
final int connectionRetrySeconds;
|
|
final int commandTimeoutSeconds;
|
|
|
|
const AppConfig({
|
|
required this.servers,
|
|
this.coordinatorUrl = 'http://localhost:8090',
|
|
this.keyboardId = 'keyboard-1',
|
|
this.functionButtons = const FunctionButtonConfig(),
|
|
this.alarmSyncIntervalSeconds = 30,
|
|
this.connectionRetrySeconds = 5,
|
|
this.commandTimeoutSeconds = 10,
|
|
});
|
|
|
|
/// Load configuration from file or assets
|
|
static Future<AppConfig> load({String? configPath}) async {
|
|
final logger = Logger();
|
|
Map<String, dynamic> configJson;
|
|
|
|
// Try loading from file path first
|
|
if (configPath != null) {
|
|
try {
|
|
final file = File(configPath);
|
|
if (await file.exists()) {
|
|
final contents = await file.readAsString();
|
|
configJson = jsonDecode(contents) as Map<String, dynamic>;
|
|
logger.i('Loaded config from: $configPath');
|
|
return _parseConfig(configJson);
|
|
}
|
|
} catch (e) {
|
|
logger.w('Failed to load config from file: $e');
|
|
}
|
|
}
|
|
|
|
// Try common file locations
|
|
final commonPaths = [
|
|
'servers.json',
|
|
'../servers.json',
|
|
'config/servers.json',
|
|
r'C:\DEV\COPILOT_D6\servers.json',
|
|
];
|
|
|
|
for (final path in commonPaths) {
|
|
try {
|
|
final file = File(path);
|
|
if (await file.exists()) {
|
|
final contents = await file.readAsString();
|
|
configJson = jsonDecode(contents) as Map<String, dynamic>;
|
|
logger.i('Loaded config from: $path');
|
|
return _parseConfig(configJson);
|
|
}
|
|
} catch (e) {
|
|
// Continue to next path
|
|
}
|
|
}
|
|
|
|
// Try loading from assets
|
|
try {
|
|
final contents = await rootBundle.loadString('assets/config/servers.json');
|
|
configJson = jsonDecode(contents) as Map<String, dynamic>;
|
|
logger.i('Loaded config from assets');
|
|
return _parseConfig(configJson);
|
|
} catch (e) {
|
|
logger.w('Failed to load config from assets: $e');
|
|
}
|
|
|
|
// Return default empty config
|
|
logger.w('No config file found, using defaults');
|
|
return const AppConfig(servers: []);
|
|
}
|
|
|
|
static AppConfig _parseConfig(Map<String, dynamic> json) {
|
|
final serversJson = json['servers'] as List<dynamic>? ?? [];
|
|
final servers = serversJson
|
|
.map((s) => ServerConfig.fromJson(s as Map<String, dynamic>))
|
|
.where((s) => s.enabled)
|
|
.toList();
|
|
|
|
final settings = json['settings'] as Map<String, dynamic>? ?? {};
|
|
|
|
// Parse function button config
|
|
final fbJson = json['functionButtons'] as Map<String, dynamic>?;
|
|
final functionButtons = fbJson != null
|
|
? FunctionButtonConfig.fromJson(fbJson)
|
|
: const FunctionButtonConfig();
|
|
|
|
return AppConfig(
|
|
servers: servers,
|
|
coordinatorUrl: settings['coordinatorUrl'] as String? ?? 'http://localhost:8090',
|
|
keyboardId: settings['keyboardId'] as String? ?? 'keyboard-1',
|
|
functionButtons: functionButtons,
|
|
alarmSyncIntervalSeconds: settings['alarmSyncIntervalSeconds'] as int? ?? 30,
|
|
connectionRetrySeconds: settings['connectionRetrySeconds'] as int? ?? 5,
|
|
commandTimeoutSeconds: settings['commandTimeoutSeconds'] as int? ?? 10,
|
|
);
|
|
}
|
|
|
|
/// Get servers by type
|
|
List<ServerConfig> getServersByType(ServerType type) {
|
|
return servers.where((s) => s.type == type).toList();
|
|
}
|
|
|
|
/// Get server that owns a camera ID
|
|
ServerConfig? getServerForCamera(int cameraId) {
|
|
for (final server in servers) {
|
|
if (server.ownsCamera(cameraId)) {
|
|
return server;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get server that owns a monitor ID
|
|
ServerConfig? getServerForMonitor(int monitorId) {
|
|
for (final server in servers) {
|
|
if (server.ownsMonitor(monitorId)) {
|
|
return server;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get total camera count across all servers
|
|
int get totalCameras {
|
|
int count = 0;
|
|
for (final server in servers) {
|
|
count += (server.cameraRangeEnd - server.cameraRangeStart + 1);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/// Get total monitor count across all servers
|
|
int get totalMonitors {
|
|
int count = 0;
|
|
for (final server in servers) {
|
|
count += (server.monitorRangeEnd - server.monitorRangeStart + 1);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/// Get all camera IDs
|
|
List<int> get allCameraIds {
|
|
final ids = <int>[];
|
|
for (final server in servers) {
|
|
for (int i = server.cameraRangeStart; i <= server.cameraRangeEnd; i++) {
|
|
ids.add(i);
|
|
}
|
|
}
|
|
ids.sort();
|
|
return ids;
|
|
}
|
|
|
|
/// Get all monitor IDs
|
|
List<int> get allMonitorIds {
|
|
final ids = <int>[];
|
|
for (final server in servers) {
|
|
for (int i = server.monitorRangeStart; i <= server.monitorRangeEnd; i++) {
|
|
ids.add(i);
|
|
}
|
|
}
|
|
ids.sort();
|
|
return ids;
|
|
}
|
|
}
|