import 'dart:convert'; enum ConnectionMode { automatic, direct, relay } enum HostPlatform { linux, windows } class RemoteHost { const RemoteHost({ required this.id, required this.name, required this.address, required this.user, required this.certificateSha256, this.platform = HostPlatform.linux, this.connectionMode = ConnectionMode.direct, this.edgeApiUrl, this.agentPublicKey, }); final String id; final String name; final String address; final String user; final String certificateSha256; final HostPlatform platform; final ConnectionMode connectionMode; final String? edgeApiUrl; final String? agentPublicKey; Uri get uri => normalizeAgentUri(address); Uri get windowsUri => normalizeWindowsAgentUri(address); RemoteHost copyWith({ String? id, String? name, String? address, String? user, String? certificateSha256, HostPlatform? platform, ConnectionMode? connectionMode, String? edgeApiUrl, String? agentPublicKey, }) => RemoteHost( id: id ?? this.id, name: name ?? this.name, address: address ?? this.address, user: user ?? this.user, certificateSha256: certificateSha256 ?? this.certificateSha256, platform: platform ?? this.platform, connectionMode: connectionMode ?? this.connectionMode, edgeApiUrl: edgeApiUrl ?? this.edgeApiUrl, agentPublicKey: agentPublicKey ?? this.agentPublicKey, ); Map toJson() => { 'id': id, 'name': name, 'address': address, 'user': user, 'certificate_sha256': certificateSha256, 'platform': platform.name, 'connection_mode': connectionMode.name, 'use_relay': connectionMode != ConnectionMode.direct, 'edge_api_url': edgeApiUrl, 'agent_public_key': agentPublicKey, }; factory RemoteHost.fromJson(Map json) => RemoteHost( id: json['id'] as String, name: json['name'] as String, address: json['address'] as String, user: json['user'] as String? ?? '', certificateSha256: json['certificate_sha256'] as String? ?? '', platform: _hostPlatformFromJson(json['platform']), connectionMode: _connectionModeFromJson(json), edgeApiUrl: json['edge_api_url'] as String?, agentPublicKey: json['agent_public_key'] as String?, ); static List decodeList(String value) { final decoded = jsonDecode(value) as List; return decoded .map((item) => RemoteHost.fromJson(item as Map)) .toList(growable: false); } static String encodeList(List hosts) => jsonEncode(hosts.map((host) => host.toJson()).toList()); } HostPlatform _hostPlatformFromJson(Object? value) { if (value is String) { for (final platform in HostPlatform.values) { if (platform.name == value) return platform; } } return HostPlatform.linux; } ConnectionMode _connectionModeFromJson(Map json) { final value = json['connection_mode']; if (value is String) { for (final mode in ConnectionMode.values) { if (mode.name == value) return mode; } } return json['use_relay'] == true ? ConnectionMode.relay : ConnectionMode.direct; } class AppSettings { const AppSettings({ this.languageCode, this.maxWidth = 1280, this.maxHeight = 720, this.framesPerSecond = 15, }); final String? languageCode; final int maxWidth; final int maxHeight; final int framesPerSecond; AppSettings copyWith({ String? languageCode, int? maxWidth, int? maxHeight, int? framesPerSecond, }) => AppSettings( languageCode: languageCode ?? this.languageCode, maxWidth: maxWidth ?? this.maxWidth, maxHeight: maxHeight ?? this.maxHeight, framesPerSecond: framesPerSecond ?? this.framesPerSecond, ); } Uri normalizeAgentUri(String input) { final value = input.trim(); if (value.isEmpty) { throw const FormatException('empty address'); } final withScheme = value.contains('://') ? value : 'wss://$value'; final parsed = Uri.parse(withScheme); if (parsed.scheme != 'wss' || parsed.host.isEmpty) { throw const FormatException('invalid WSS address'); } if (parsed.path.isNotEmpty && parsed.path != '/') { throw const FormatException('agent URL must not contain a path'); } return Uri( scheme: 'wss', host: parsed.host, port: parsed.hasPort ? parsed.port : 39500, path: '/', ); } Uri normalizeWindowsAgentUri(String input) { final value = input.trim(); if (value.isEmpty) throw const FormatException('empty address'); final withScheme = value.contains('://') ? value : 'tcp://$value'; final parsed = Uri.parse(withScheme); if (parsed.scheme != 'tcp' || parsed.host.isEmpty || parsed.userInfo.isNotEmpty || parsed.query.isNotEmpty || parsed.fragment.isNotEmpty || (parsed.path.isNotEmpty && parsed.path != '/')) { throw const FormatException('invalid Windows Agent address'); } return Uri( scheme: 'tcp', host: parsed.host, port: parsed.hasPort ? parsed.port : 39501, ); } String normalizeFingerprint(String input) => input.trim().replaceAll(':', '').toLowerCase(); bool isValidFingerprint(String input) => RegExp(r'^[0-9a-f]{64}$').hasMatch(normalizeFingerprint(input)); bool isValidAgentPublicKey(String input) => RegExp(r'^[A-Za-z0-9+/]{43}$').hasMatch(input.trim());