pumaguard 21.post29__py3-none-any.whl → 21.post83__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pumaguard/presets.py +1 -0
- pumaguard/pumaguard-ui/.last_build_id +1 -1
- pumaguard/pumaguard-ui/assets/NOTICES +621 -71
- pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
- pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
- pumaguard/pumaguard-ui/main.dart.js +28869 -28787
- pumaguard/web_routes/dhcp.py +311 -54
- pumaguard/web_routes/diagnostics.py +6 -0
- pumaguard/web_routes/settings.py +13 -0
- pumaguard/web_ui.py +29 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
- pumaguard-21.post83.dist-info/RECORD +254 -0
- pumaguard-ui/.gitignore +48 -0
- pumaguard-ui/.metadata +45 -0
- pumaguard-ui/API_REFERENCE.md +717 -0
- pumaguard-ui/LICENSE +201 -0
- pumaguard-ui/Makefile +36 -0
- pumaguard-ui/README.md +371 -0
- pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
- pumaguard-ui/analysis_options.yaml +28 -0
- pumaguard-ui/android/.gitignore +14 -0
- pumaguard-ui/android/app/build.gradle.kts +44 -0
- pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
- pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
- pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
- pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
- pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
- pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
- pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
- pumaguard-ui/android/build.gradle.kts +24 -0
- pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- pumaguard-ui/android/gradle.properties +2 -0
- pumaguard-ui/android/settings.gradle.kts +26 -0
- pumaguard-ui/fonts/README.md +38 -0
- pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
- pumaguard-ui/fonts/download_fonts.sh +76 -0
- pumaguard-ui/ios/.gitignore +34 -0
- pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
- pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
- pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
- pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
- pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
- pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
- pumaguard-ui/ios/Runner/Info.plist +49 -0
- pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
- pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
- pumaguard-ui/lib/main.dart +56 -0
- pumaguard-ui/lib/models/camera.dart +45 -0
- pumaguard-ui/lib/models/plug.dart +45 -0
- pumaguard-ui/lib/models/settings.dart +112 -0
- pumaguard-ui/lib/models/status.dart +58 -0
- pumaguard-ui/lib/screens/directories_screen.dart +319 -0
- pumaguard-ui/lib/screens/home_screen.dart +545 -0
- pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
- pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
- pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
- pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
- pumaguard-ui/lib/services/api_service.dart +717 -0
- pumaguard-ui/lib/services/camera_events_service.dart +195 -0
- pumaguard-ui/lib/services/mdns_service.dart +4 -0
- pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
- pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
- pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
- pumaguard-ui/lib/utils/download_helper.dart +2 -0
- pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
- pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
- pumaguard-ui/lib/utils/platform_url.dart +10 -0
- pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
- pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
- pumaguard-ui/linux/.gitignore +1 -0
- pumaguard-ui/linux/CMakeLists.txt +128 -0
- pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
- pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
- pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
- pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
- pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
- pumaguard-ui/linux/runner/main.cc +6 -0
- pumaguard-ui/linux/runner/my_application.cc +148 -0
- pumaguard-ui/linux/runner/my_application.h +21 -0
- pumaguard-ui/macos/.gitignore +7 -0
- pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
- pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
- pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
- pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
- pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
- pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
- pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
- pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
- pumaguard-ui/macos/Runner/Info.plist +32 -0
- pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
- pumaguard-ui/macos/Runner/Release.entitlements +8 -0
- pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
- pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
- pumaguard-ui/pubspec.lock +882 -0
- pumaguard-ui/pubspec.yaml +125 -0
- pumaguard-ui/test/models/camera_test.dart +515 -0
- pumaguard-ui/test/models/plug_test.dart +499 -0
- pumaguard-ui/test/models/settings_test.dart +903 -0
- pumaguard-ui/test/models/status_test.dart +707 -0
- pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
- pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
- pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
- pumaguard-ui/test/widget_test.dart.skip +38 -0
- pumaguard-ui/web/favicon.png +0 -0
- pumaguard-ui/web/icons/Icon-192.png +0 -0
- pumaguard-ui/web/icons/Icon-512.png +0 -0
- pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
- pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
- pumaguard-ui/web/index.html +38 -0
- pumaguard-ui/web/manifest.json +35 -0
- pumaguard-ui/windows/.gitignore +17 -0
- pumaguard-ui/windows/CMakeLists.txt +108 -0
- pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
- pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
- pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
- pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
- pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
- pumaguard-ui/windows/runner/Runner.rc +121 -0
- pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
- pumaguard-ui/windows/runner/flutter_window.h +33 -0
- pumaguard-ui/windows/runner/main.cpp +43 -0
- pumaguard-ui/windows/runner/resource.h +16 -0
- pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
- pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
- pumaguard-ui/windows/runner/utils.cpp +65 -0
- pumaguard-ui/windows/runner/utils.h +19 -0
- pumaguard-ui/windows/runner/win32_window.cpp +288 -0
- pumaguard-ui/windows/runner/win32_window.h +102 -0
- pumaguard-21.post29.dist-info/RECORD +0 -83
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:convert';
|
|
3
|
+
import 'package:flutter/foundation.dart';
|
|
4
|
+
import 'package:http/http.dart' as http;
|
|
5
|
+
import '../models/camera.dart';
|
|
6
|
+
|
|
7
|
+
/// Event types for camera status changes
|
|
8
|
+
enum CameraEventType {
|
|
9
|
+
connected,
|
|
10
|
+
cameraConnected,
|
|
11
|
+
cameraDisconnected,
|
|
12
|
+
cameraAdded,
|
|
13
|
+
cameraStatusChangedOnline,
|
|
14
|
+
cameraStatusChangedOffline,
|
|
15
|
+
unknown,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Camera event data
|
|
19
|
+
class CameraEvent {
|
|
20
|
+
final CameraEventType type;
|
|
21
|
+
final Camera? camera;
|
|
22
|
+
final String timestamp;
|
|
23
|
+
|
|
24
|
+
CameraEvent({required this.type, required this.timestamp, this.camera});
|
|
25
|
+
|
|
26
|
+
factory CameraEvent.fromJson(Map<String, dynamic> json) {
|
|
27
|
+
final typeStr = json['type'] as String? ?? 'unknown';
|
|
28
|
+
final type = _parseEventType(typeStr);
|
|
29
|
+
|
|
30
|
+
Camera? camera;
|
|
31
|
+
if (json['data'] != null && json['data'] is Map<String, dynamic>) {
|
|
32
|
+
try {
|
|
33
|
+
camera = Camera.fromJson(json['data'] as Map<String, dynamic>);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
debugPrint('[CameraEvent] Error parsing camera data: $e');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return CameraEvent(
|
|
40
|
+
type: type,
|
|
41
|
+
timestamp: json['timestamp'] as String? ?? '',
|
|
42
|
+
camera: camera,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static CameraEventType _parseEventType(String typeStr) {
|
|
47
|
+
switch (typeStr) {
|
|
48
|
+
case 'connected':
|
|
49
|
+
return CameraEventType.connected;
|
|
50
|
+
case 'camera_connected':
|
|
51
|
+
return CameraEventType.cameraConnected;
|
|
52
|
+
case 'camera_disconnected':
|
|
53
|
+
return CameraEventType.cameraDisconnected;
|
|
54
|
+
case 'camera_added':
|
|
55
|
+
return CameraEventType.cameraAdded;
|
|
56
|
+
case 'camera_status_changed_online':
|
|
57
|
+
return CameraEventType.cameraStatusChangedOnline;
|
|
58
|
+
case 'camera_status_changed_offline':
|
|
59
|
+
return CameraEventType.cameraStatusChangedOffline;
|
|
60
|
+
default:
|
|
61
|
+
return CameraEventType.unknown;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Service for receiving real-time camera status updates via Server-Sent Events
|
|
67
|
+
class CameraEventsService {
|
|
68
|
+
final String baseUrl;
|
|
69
|
+
final _eventController = StreamController<CameraEvent>.broadcast();
|
|
70
|
+
http.Client? _client;
|
|
71
|
+
bool _isListening = false;
|
|
72
|
+
|
|
73
|
+
CameraEventsService(this.baseUrl);
|
|
74
|
+
|
|
75
|
+
/// Stream of camera events
|
|
76
|
+
Stream<CameraEvent> get events => _eventController.stream;
|
|
77
|
+
|
|
78
|
+
/// Whether the service is currently listening for events
|
|
79
|
+
bool get isListening => _isListening;
|
|
80
|
+
|
|
81
|
+
/// Start listening for camera events
|
|
82
|
+
Future<void> startListening() async {
|
|
83
|
+
if (_isListening) {
|
|
84
|
+
debugPrint('[CameraEventsService] Already listening');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_isListening = true;
|
|
89
|
+
_client = http.Client();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
final url = Uri.parse('$baseUrl/api/dhcp/cameras/events');
|
|
93
|
+
debugPrint('[CameraEventsService] Connecting to SSE: $url');
|
|
94
|
+
|
|
95
|
+
final request = http.Request('GET', url);
|
|
96
|
+
request.headers['Accept'] = 'text/event-stream';
|
|
97
|
+
request.headers['Cache-Control'] = 'no-cache';
|
|
98
|
+
|
|
99
|
+
final response = await _client!.send(request);
|
|
100
|
+
|
|
101
|
+
if (response.statusCode != 200) {
|
|
102
|
+
debugPrint(
|
|
103
|
+
'[CameraEventsService] Failed to connect: ${response.statusCode}',
|
|
104
|
+
);
|
|
105
|
+
_isListening = false;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
debugPrint('[CameraEventsService] Connected successfully');
|
|
110
|
+
|
|
111
|
+
// Listen to the stream
|
|
112
|
+
response.stream
|
|
113
|
+
.transform(utf8.decoder)
|
|
114
|
+
.transform(const LineSplitter())
|
|
115
|
+
.listen(
|
|
116
|
+
_handleLine,
|
|
117
|
+
onError: (error) {
|
|
118
|
+
debugPrint('[CameraEventsService] Stream error: $error');
|
|
119
|
+
_handleDisconnect();
|
|
120
|
+
},
|
|
121
|
+
onDone: () {
|
|
122
|
+
debugPrint('[CameraEventsService] Stream closed');
|
|
123
|
+
_handleDisconnect();
|
|
124
|
+
},
|
|
125
|
+
cancelOnError: false,
|
|
126
|
+
);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
debugPrint('[CameraEventsService] Connection error: $e');
|
|
129
|
+
_isListening = false;
|
|
130
|
+
_client?.close();
|
|
131
|
+
_client = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Stop listening for camera events
|
|
136
|
+
void stopListening() {
|
|
137
|
+
if (!_isListening) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
debugPrint('[CameraEventsService] Stopping event listener');
|
|
142
|
+
_isListening = false;
|
|
143
|
+
_client?.close();
|
|
144
|
+
_client = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Handle incoming SSE line
|
|
148
|
+
void _handleLine(String line) {
|
|
149
|
+
if (line.isEmpty || line.startsWith(':')) {
|
|
150
|
+
// Skip empty lines and comments (keepalive)
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (line.startsWith('data: ')) {
|
|
155
|
+
final data = line.substring(6); // Remove 'data: ' prefix
|
|
156
|
+
try {
|
|
157
|
+
final json = jsonDecode(data) as Map<String, dynamic>;
|
|
158
|
+
final event = CameraEvent.fromJson(json);
|
|
159
|
+
|
|
160
|
+
debugPrint(
|
|
161
|
+
'[CameraEventsService] Received event: ${event.type} - ${event.camera?.hostname ?? "N/A"}',
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
_eventController.add(event);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
debugPrint('[CameraEventsService] Error parsing event: $e');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Handle disconnection
|
|
172
|
+
void _handleDisconnect() {
|
|
173
|
+
if (!_isListening) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_isListening = false;
|
|
178
|
+
_client?.close();
|
|
179
|
+
_client = null;
|
|
180
|
+
|
|
181
|
+
// Attempt to reconnect after a delay
|
|
182
|
+
debugPrint('[CameraEventsService] Attempting to reconnect in 5 seconds');
|
|
183
|
+
Future.delayed(const Duration(seconds: 5), () {
|
|
184
|
+
if (!_isListening) {
|
|
185
|
+
startListening();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Dispose of resources
|
|
191
|
+
void dispose() {
|
|
192
|
+
stopListening();
|
|
193
|
+
_eventController.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
|
|
4
|
+
import 'package:flutter/foundation.dart';
|
|
5
|
+
import 'package:multicast_dns/multicast_dns.dart';
|
|
6
|
+
|
|
7
|
+
/// Represents a discovered Pumaguard server via mDNS
|
|
8
|
+
class PumaguardServer {
|
|
9
|
+
final String name;
|
|
10
|
+
final String hostname;
|
|
11
|
+
final String ip;
|
|
12
|
+
final int port;
|
|
13
|
+
final Map<String, String> properties;
|
|
14
|
+
|
|
15
|
+
PumaguardServer({
|
|
16
|
+
required this.name,
|
|
17
|
+
required this.hostname,
|
|
18
|
+
required this.ip,
|
|
19
|
+
required this.port,
|
|
20
|
+
required this.properties,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/// Get the base URL for this server
|
|
24
|
+
String get baseUrl => 'http://$ip:$port';
|
|
25
|
+
|
|
26
|
+
/// Get the mDNS URL (hostname.local)
|
|
27
|
+
String get mdnsUrl => 'http://$hostname:$port';
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
String toString() {
|
|
31
|
+
return 'PumaguardServer(name: $name, ip: $ip, port: $port, hostname: $hostname)';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
bool operator ==(Object other) {
|
|
36
|
+
if (identical(this, other)) return true;
|
|
37
|
+
return other is PumaguardServer &&
|
|
38
|
+
other.name == name &&
|
|
39
|
+
other.ip == ip &&
|
|
40
|
+
other.port == port;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
int get hashCode => Object.hash(name, ip, port);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Service for discovering Pumaguard servers via mDNS/Zeroconf
|
|
48
|
+
class MdnsService {
|
|
49
|
+
static const String serviceType = '_http._tcp';
|
|
50
|
+
static const Duration discoveryTimeout = Duration(seconds: 5);
|
|
51
|
+
static const Duration discoveryInterval = Duration(seconds: 30);
|
|
52
|
+
|
|
53
|
+
final List<PumaguardServer> _discoveredServers = [];
|
|
54
|
+
final StreamController<List<PumaguardServer>> _serversController =
|
|
55
|
+
StreamController<List<PumaguardServer>>.broadcast();
|
|
56
|
+
|
|
57
|
+
MDnsClient? _mdns;
|
|
58
|
+
Timer? _discoveryTimer;
|
|
59
|
+
bool _isDiscovering = false;
|
|
60
|
+
|
|
61
|
+
/// Stream of discovered servers
|
|
62
|
+
Stream<List<PumaguardServer>> get serversStream => _serversController.stream;
|
|
63
|
+
|
|
64
|
+
/// List of currently discovered servers
|
|
65
|
+
List<PumaguardServer> get servers => List.unmodifiable(_discoveredServers);
|
|
66
|
+
|
|
67
|
+
/// Start continuous discovery of Pumaguard servers
|
|
68
|
+
Future<void> startDiscovery() async {
|
|
69
|
+
if (_isDiscovering) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_isDiscovering = true;
|
|
74
|
+
|
|
75
|
+
// Do initial discovery
|
|
76
|
+
await discoverServers();
|
|
77
|
+
|
|
78
|
+
// Set up periodic discovery
|
|
79
|
+
_discoveryTimer = Timer.periodic(discoveryInterval, (timer) async {
|
|
80
|
+
await discoverServers();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Stop continuous discovery
|
|
85
|
+
void stopDiscovery() {
|
|
86
|
+
_discoveryTimer?.cancel();
|
|
87
|
+
_discoveryTimer = null;
|
|
88
|
+
_isDiscovering = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Discover Pumaguard servers on the network
|
|
92
|
+
Future<List<PumaguardServer>> discoverServers() async {
|
|
93
|
+
try {
|
|
94
|
+
// Create mDNS client if needed
|
|
95
|
+
if (_mdns == null) {
|
|
96
|
+
_mdns = MDnsClient(
|
|
97
|
+
rawDatagramSocketFactory:
|
|
98
|
+
(
|
|
99
|
+
dynamic host,
|
|
100
|
+
int port, {
|
|
101
|
+
bool? reuseAddress,
|
|
102
|
+
bool? reusePort,
|
|
103
|
+
int? ttl,
|
|
104
|
+
}) async {
|
|
105
|
+
return RawDatagramSocket.bind(
|
|
106
|
+
host,
|
|
107
|
+
port,
|
|
108
|
+
reuseAddress: reuseAddress ?? true,
|
|
109
|
+
reusePort: reusePort ?? false,
|
|
110
|
+
ttl: ttl ?? 255,
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
await _mdns!.start();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
final Set<PumaguardServer> foundServers = {};
|
|
118
|
+
|
|
119
|
+
// Query for HTTP services
|
|
120
|
+
await for (final PtrResourceRecord ptr
|
|
121
|
+
in _mdns!
|
|
122
|
+
.lookup<PtrResourceRecord>(
|
|
123
|
+
ResourceRecordQuery.serverPointer(serviceType),
|
|
124
|
+
)
|
|
125
|
+
.timeout(discoveryTimeout)) {
|
|
126
|
+
// Get service details
|
|
127
|
+
final String serviceName = ptr.domainName;
|
|
128
|
+
|
|
129
|
+
// Query for service details (SRV and TXT records)
|
|
130
|
+
final srvFuture = _mdns!
|
|
131
|
+
.lookup<SrvResourceRecord>(ResourceRecordQuery.service(serviceName))
|
|
132
|
+
.timeout(discoveryTimeout)
|
|
133
|
+
.toList();
|
|
134
|
+
|
|
135
|
+
final txtFuture = _mdns!
|
|
136
|
+
.lookup<TxtResourceRecord>(ResourceRecordQuery.text(serviceName))
|
|
137
|
+
.timeout(discoveryTimeout)
|
|
138
|
+
.toList();
|
|
139
|
+
|
|
140
|
+
final ipFuture = _mdns!
|
|
141
|
+
.lookup<IPAddressResourceRecord>(
|
|
142
|
+
ResourceRecordQuery.addressIPv4(serviceName),
|
|
143
|
+
)
|
|
144
|
+
.timeout(discoveryTimeout)
|
|
145
|
+
.toList();
|
|
146
|
+
|
|
147
|
+
final results = await Future.wait([srvFuture, txtFuture, ipFuture]);
|
|
148
|
+
final srvRecords = results[0] as List<SrvResourceRecord>;
|
|
149
|
+
final txtRecords = results[1] as List<TxtResourceRecord>;
|
|
150
|
+
final ipRecords = results[2] as List<IPAddressResourceRecord>;
|
|
151
|
+
|
|
152
|
+
// Process SRV records to get port and hostname
|
|
153
|
+
for (final srv in srvRecords) {
|
|
154
|
+
final int port = srv.port;
|
|
155
|
+
final String target = srv.target;
|
|
156
|
+
|
|
157
|
+
// Parse TXT records for properties
|
|
158
|
+
final Map<String, String> properties = {};
|
|
159
|
+
for (final txt in txtRecords) {
|
|
160
|
+
for (final String data in txt.text.split('\n')) {
|
|
161
|
+
final parts = data.split('=');
|
|
162
|
+
if (parts.length == 2) {
|
|
163
|
+
properties[parts[0]] = parts[1];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if this is a Pumaguard server
|
|
169
|
+
final ispumaguard =
|
|
170
|
+
properties['app'] == 'pumaguard' ||
|
|
171
|
+
serviceName.toLowerCase().contains('pumaguard');
|
|
172
|
+
|
|
173
|
+
if (!ispumaguard) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Get IP addresses
|
|
178
|
+
for (final ip in ipRecords) {
|
|
179
|
+
final server = PumaguardServer(
|
|
180
|
+
name: serviceName.split('.').first,
|
|
181
|
+
hostname: target.replaceAll(RegExp(r'\.$'), ''),
|
|
182
|
+
ip: ip.address.address,
|
|
183
|
+
port: port,
|
|
184
|
+
properties: properties,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
foundServers.add(server);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Update discovered servers list
|
|
193
|
+
_discoveredServers.clear();
|
|
194
|
+
_discoveredServers.addAll(foundServers);
|
|
195
|
+
|
|
196
|
+
// Notify listeners
|
|
197
|
+
if (!_serversController.isClosed) {
|
|
198
|
+
_serversController.add(_discoveredServers);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return _discoveredServers;
|
|
202
|
+
} catch (e) {
|
|
203
|
+
// Discovery failed, return empty list
|
|
204
|
+
debugPrint('mDNS discovery error: $e');
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Find a specific server by name
|
|
210
|
+
Future<PumaguardServer?> findServerByName(String name) async {
|
|
211
|
+
// First check already discovered servers
|
|
212
|
+
for (final server in _discoveredServers) {
|
|
213
|
+
if (server.name.toLowerCase() == name.toLowerCase()) {
|
|
214
|
+
return server;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Do a fresh discovery
|
|
219
|
+
final servers = await discoverServers();
|
|
220
|
+
for (final server in servers) {
|
|
221
|
+
if (server.name.toLowerCase() == name.toLowerCase()) {
|
|
222
|
+
return server;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Resolve a .local hostname to an IP address
|
|
230
|
+
Future<String?> resolveLocalHostname(String hostname) async {
|
|
231
|
+
try {
|
|
232
|
+
if (_mdns == null) {
|
|
233
|
+
_mdns = MDnsClient(
|
|
234
|
+
rawDatagramSocketFactory:
|
|
235
|
+
(
|
|
236
|
+
dynamic host,
|
|
237
|
+
int port, {
|
|
238
|
+
bool? reuseAddress,
|
|
239
|
+
bool? reusePort,
|
|
240
|
+
int? ttl,
|
|
241
|
+
}) async {
|
|
242
|
+
return RawDatagramSocket.bind(
|
|
243
|
+
host,
|
|
244
|
+
port,
|
|
245
|
+
reuseAddress: reuseAddress ?? true,
|
|
246
|
+
reusePort: reusePort ?? false,
|
|
247
|
+
ttl: ttl ?? 255,
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
await _mdns!.start();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Ensure hostname ends with .local
|
|
255
|
+
if (!hostname.endsWith('.local')) {
|
|
256
|
+
hostname = '$hostname.local';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Query for IPv4 address
|
|
260
|
+
await for (final IPAddressResourceRecord record
|
|
261
|
+
in _mdns!
|
|
262
|
+
.lookup<IPAddressResourceRecord>(
|
|
263
|
+
ResourceRecordQuery.addressIPv4(hostname),
|
|
264
|
+
)
|
|
265
|
+
.timeout(discoveryTimeout)) {
|
|
266
|
+
return record.address.address;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
debugPrint('Hostname resolution error: $e');
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Clean up resources
|
|
277
|
+
void dispose() {
|
|
278
|
+
stopDiscovery();
|
|
279
|
+
_mdns?.stop();
|
|
280
|
+
_serversController.close();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export 'mdns_service_impl.dart';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter/foundation.dart';
|
|
4
|
+
|
|
5
|
+
/// Represents a discovered Pumaguard server via mDNS
|
|
6
|
+
class PumaguardServer {
|
|
7
|
+
final String name;
|
|
8
|
+
final String hostname;
|
|
9
|
+
final String ip;
|
|
10
|
+
final int port;
|
|
11
|
+
final Map<String, String> properties;
|
|
12
|
+
|
|
13
|
+
PumaguardServer({
|
|
14
|
+
required this.name,
|
|
15
|
+
required this.hostname,
|
|
16
|
+
required this.ip,
|
|
17
|
+
required this.port,
|
|
18
|
+
required this.properties,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/// Get the base URL for this server
|
|
22
|
+
String get baseUrl => 'http://$ip:$port';
|
|
23
|
+
|
|
24
|
+
/// Get the mDNS URL (hostname.local)
|
|
25
|
+
String get mdnsUrl => 'http://$hostname:$port';
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
String toString() {
|
|
29
|
+
return 'PumaguardServer(name: $name, ip: $ip, port: $port, hostname: $hostname)';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
bool operator ==(Object other) {
|
|
34
|
+
if (identical(this, other)) return true;
|
|
35
|
+
return other is PumaguardServer &&
|
|
36
|
+
other.name == name &&
|
|
37
|
+
other.ip == ip &&
|
|
38
|
+
other.port == port;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
int get hashCode => Object.hash(name, ip, port);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Web stub for mDNS service (mDNS discovery not supported in web browsers)
|
|
46
|
+
///
|
|
47
|
+
/// Note: mDNS/Zeroconf discovery requires native socket access which is not
|
|
48
|
+
/// available in web browsers. This is a no-op implementation for web platforms.
|
|
49
|
+
/// Users should use manual URL entry or rely on the dynamic URL detection.
|
|
50
|
+
class MdnsService {
|
|
51
|
+
static const String serviceType = '_http._tcp';
|
|
52
|
+
static const Duration discoveryTimeout = Duration(seconds: 5);
|
|
53
|
+
static const Duration discoveryInterval = Duration(seconds: 30);
|
|
54
|
+
|
|
55
|
+
final List<PumaguardServer> _discoveredServers = [];
|
|
56
|
+
final StreamController<List<PumaguardServer>> _serversController =
|
|
57
|
+
StreamController<List<PumaguardServer>>.broadcast();
|
|
58
|
+
|
|
59
|
+
/// Stream of discovered servers (always empty on web)
|
|
60
|
+
Stream<List<PumaguardServer>> get serversStream => _serversController.stream;
|
|
61
|
+
|
|
62
|
+
/// List of currently discovered servers (always empty on web)
|
|
63
|
+
List<PumaguardServer> get servers => List.unmodifiable(_discoveredServers);
|
|
64
|
+
|
|
65
|
+
/// Start continuous discovery (no-op on web)
|
|
66
|
+
Future<void> startDiscovery() async {
|
|
67
|
+
// mDNS discovery is not supported on web platforms
|
|
68
|
+
debugPrint('mDNS discovery is not available in web browsers');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Stop continuous discovery (no-op on web)
|
|
73
|
+
void stopDiscovery() {
|
|
74
|
+
// No-op on web
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Discover servers (always returns empty list on web)
|
|
78
|
+
Future<List<PumaguardServer>> discoverServers() async {
|
|
79
|
+
// mDNS discovery requires native socket access not available in browsers
|
|
80
|
+
debugPrint('mDNS discovery is not supported on web. Use manual URL entry.');
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Find a specific server by name (always returns null on web)
|
|
85
|
+
Future<PumaguardServer?> findServerByName(String name) async {
|
|
86
|
+
// Not supported on web
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Resolve a .local hostname (not supported on web)
|
|
91
|
+
///
|
|
92
|
+
/// Note: Browsers may be able to resolve .local hostnames through the OS
|
|
93
|
+
/// if mDNS is properly configured, but programmatic resolution is not available.
|
|
94
|
+
Future<String?> resolveLocalHostname(String hostname) async {
|
|
95
|
+
debugPrint('Hostname resolution is not available in web browsers');
|
|
96
|
+
debugPrint(
|
|
97
|
+
'Browsers may resolve .local hostnames through the OS automatically',
|
|
98
|
+
);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Clean up resources
|
|
103
|
+
void dispose() {
|
|
104
|
+
_serversController.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'dart:js_interop';
|
|
2
|
+
import 'package:web/web.dart' as web;
|
|
3
|
+
import 'dart:typed_data';
|
|
4
|
+
|
|
5
|
+
/// Download files using web browser APIs
|
|
6
|
+
void downloadFilesWeb(Uint8List fileBytes, String filename) {
|
|
7
|
+
final anchor = web.document.createElement('a') as web.HTMLAnchorElement;
|
|
8
|
+
final blob = web.Blob([fileBytes.toJS].toJS);
|
|
9
|
+
final url = web.URL.createObjectURL(blob);
|
|
10
|
+
anchor.href = url;
|
|
11
|
+
anchor.download = filename;
|
|
12
|
+
anchor.click();
|
|
13
|
+
web.URL.revokeObjectURL(url);
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// Platform-agnostic URL helper that provides the API base URL.
|
|
2
|
+
///
|
|
3
|
+
/// This file uses conditional exports to provide different implementations
|
|
4
|
+
/// for web and non-web platforms, avoiding the use of web-only APIs
|
|
5
|
+
/// in test environments.
|
|
6
|
+
library;
|
|
7
|
+
|
|
8
|
+
export 'platform_url_stub.dart'
|
|
9
|
+
if (dart.library.html) 'platform_url_web.dart'
|
|
10
|
+
if (dart.library.io) 'platform_url_stub.dart';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// Stub implementation for non-web platforms (VM, tests, etc.).
|
|
2
|
+
///
|
|
3
|
+
/// This provides a default localhost URL for testing and non-web environments
|
|
4
|
+
/// where browser APIs are not available.
|
|
5
|
+
library;
|
|
6
|
+
|
|
7
|
+
String getApiBaseUrl() {
|
|
8
|
+
// Default to localhost for test environments
|
|
9
|
+
// In a real production non-web build, this would need proper configuration
|
|
10
|
+
return 'http://localhost:5000';
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// Web-specific implementation for getting the API base URL.
|
|
2
|
+
///
|
|
3
|
+
/// This implementation uses package:web to access the browser's window.location
|
|
4
|
+
/// and construct the API base URL dynamically based on the current page URL.
|
|
5
|
+
library;
|
|
6
|
+
|
|
7
|
+
import 'package:web/web.dart' as web;
|
|
8
|
+
|
|
9
|
+
String getApiBaseUrl() {
|
|
10
|
+
// Get the current window location to construct the API base URL
|
|
11
|
+
// This allows the app to work when accessed from any host/port
|
|
12
|
+
final window = web.window;
|
|
13
|
+
final protocol = window.location.protocol; // 'http:' or 'https:'
|
|
14
|
+
final host = window.location.host; // 'hostname:port' or just 'hostname'
|
|
15
|
+
return '$protocol//$host';
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flutter/ephemeral
|