pumaguard 21.post27__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.
Files changed (188) hide show
  1. pumaguard/presets.py +1 -0
  2. pumaguard/pumaguard-ui/.last_build_id +1 -1
  3. pumaguard/pumaguard-ui/assets/NOTICES +621 -71
  4. pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
  5. pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
  6. pumaguard/pumaguard-ui/main.dart.js +28869 -28787
  7. pumaguard/web_routes/dhcp.py +311 -54
  8. pumaguard/web_routes/diagnostics.py +6 -0
  9. pumaguard/web_routes/settings.py +13 -0
  10. pumaguard/web_ui.py +29 -0
  11. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
  12. pumaguard-21.post83.dist-info/RECORD +254 -0
  13. pumaguard-ui/.gitignore +48 -0
  14. pumaguard-ui/.metadata +45 -0
  15. pumaguard-ui/API_REFERENCE.md +717 -0
  16. pumaguard-ui/LICENSE +201 -0
  17. pumaguard-ui/Makefile +36 -0
  18. pumaguard-ui/README.md +371 -0
  19. pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
  20. pumaguard-ui/analysis_options.yaml +28 -0
  21. pumaguard-ui/android/.gitignore +14 -0
  22. pumaguard-ui/android/app/build.gradle.kts +44 -0
  23. pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
  24. pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
  25. pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
  26. pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
  27. pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  28. pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  29. pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  31. pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
  34. pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
  35. pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
  36. pumaguard-ui/android/build.gradle.kts +24 -0
  37. pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  38. pumaguard-ui/android/gradle.properties +2 -0
  39. pumaguard-ui/android/settings.gradle.kts +26 -0
  40. pumaguard-ui/fonts/README.md +38 -0
  41. pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
  42. pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
  43. pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
  44. pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
  45. pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
  46. pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
  47. pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
  48. pumaguard-ui/fonts/download_fonts.sh +76 -0
  49. pumaguard-ui/ios/.gitignore +34 -0
  50. pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
  51. pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
  52. pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
  53. pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
  54. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  55. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  56. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  57. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  58. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  59. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  60. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  61. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  62. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  63. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  64. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  65. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  66. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  67. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  68. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  69. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  70. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  71. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  72. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  73. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  74. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  75. pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  76. pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
  77. pumaguard-ui/ios/Runner/Info.plist +49 -0
  78. pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
  79. pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
  80. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  81. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  82. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  83. pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  84. pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  85. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  86. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  87. pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
  88. pumaguard-ui/lib/main.dart +56 -0
  89. pumaguard-ui/lib/models/camera.dart +45 -0
  90. pumaguard-ui/lib/models/plug.dart +45 -0
  91. pumaguard-ui/lib/models/settings.dart +112 -0
  92. pumaguard-ui/lib/models/status.dart +58 -0
  93. pumaguard-ui/lib/screens/directories_screen.dart +319 -0
  94. pumaguard-ui/lib/screens/home_screen.dart +545 -0
  95. pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
  96. pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
  97. pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
  98. pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
  99. pumaguard-ui/lib/services/api_service.dart +717 -0
  100. pumaguard-ui/lib/services/camera_events_service.dart +195 -0
  101. pumaguard-ui/lib/services/mdns_service.dart +4 -0
  102. pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
  103. pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
  104. pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
  105. pumaguard-ui/lib/utils/download_helper.dart +2 -0
  106. pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
  107. pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
  108. pumaguard-ui/lib/utils/platform_url.dart +10 -0
  109. pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
  110. pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
  111. pumaguard-ui/linux/.gitignore +1 -0
  112. pumaguard-ui/linux/CMakeLists.txt +128 -0
  113. pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
  114. pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
  115. pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
  116. pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
  117. pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
  118. pumaguard-ui/linux/runner/main.cc +6 -0
  119. pumaguard-ui/linux/runner/my_application.cc +148 -0
  120. pumaguard-ui/linux/runner/my_application.h +21 -0
  121. pumaguard-ui/macos/.gitignore +7 -0
  122. pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
  123. pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
  124. pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
  125. pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
  126. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  127. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  128. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  129. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  130. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  131. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  132. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  133. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  134. pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  135. pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  136. pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
  137. pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
  138. pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
  139. pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
  140. pumaguard-ui/macos/Runner/Info.plist +32 -0
  141. pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
  142. pumaguard-ui/macos/Runner/Release.entitlements +8 -0
  143. pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
  144. pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  145. pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  146. pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  147. pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  148. pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
  149. pumaguard-ui/pubspec.lock +882 -0
  150. pumaguard-ui/pubspec.yaml +125 -0
  151. pumaguard-ui/test/models/camera_test.dart +515 -0
  152. pumaguard-ui/test/models/plug_test.dart +499 -0
  153. pumaguard-ui/test/models/settings_test.dart +903 -0
  154. pumaguard-ui/test/models/status_test.dart +707 -0
  155. pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
  156. pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
  157. pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
  158. pumaguard-ui/test/widget_test.dart.skip +38 -0
  159. pumaguard-ui/web/favicon.png +0 -0
  160. pumaguard-ui/web/icons/Icon-192.png +0 -0
  161. pumaguard-ui/web/icons/Icon-512.png +0 -0
  162. pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
  163. pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
  164. pumaguard-ui/web/index.html +38 -0
  165. pumaguard-ui/web/manifest.json +35 -0
  166. pumaguard-ui/windows/.gitignore +17 -0
  167. pumaguard-ui/windows/CMakeLists.txt +108 -0
  168. pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
  169. pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
  170. pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
  171. pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
  172. pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
  173. pumaguard-ui/windows/runner/Runner.rc +121 -0
  174. pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
  175. pumaguard-ui/windows/runner/flutter_window.h +33 -0
  176. pumaguard-ui/windows/runner/main.cpp +43 -0
  177. pumaguard-ui/windows/runner/resource.h +16 -0
  178. pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
  179. pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
  180. pumaguard-ui/windows/runner/utils.cpp +65 -0
  181. pumaguard-ui/windows/runner/utils.h +19 -0
  182. pumaguard-ui/windows/runner/win32_window.cpp +288 -0
  183. pumaguard-ui/windows/runner/win32_window.h +102 -0
  184. pumaguard-21.post27.dist-info/RECORD +0 -83
  185. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post27.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,4 @@
1
+ // Conditional export for mDNS service based on platform
2
+ // Uses native implementation on mobile/desktop, stub on web
3
+
4
+ export 'mdns_service_web.dart' if (dart.library.io) 'mdns_service_io.dart';
@@ -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,2 @@
1
+ export 'download_helper_stub.dart'
2
+ if (dart.library.js_interop) 'download_helper_web.dart';
@@ -0,0 +1,6 @@
1
+ import 'dart:typed_data';
2
+
3
+ /// Stub implementation for non-web platforms
4
+ void downloadFilesWeb(Uint8List fileBytes, String filename) {
5
+ throw UnsupportedError('downloadFilesWeb is only supported on web platforms');
6
+ }
@@ -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