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.
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.post29.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.post29.dist-info/RECORD +0 -83
  185. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,717 @@
1
+ import 'dart:convert';
2
+ import 'dart:typed_data';
3
+ import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
4
+ import 'package:http/http.dart' as http;
5
+ import 'package:crypto/crypto.dart';
6
+ import '../models/status.dart';
7
+ import '../models/settings.dart';
8
+ import '../models/camera.dart';
9
+
10
+ class ApiService {
11
+ String? _baseUrl;
12
+
13
+ ApiService({String? baseUrl})
14
+ : _baseUrl = baseUrl?.replaceAll(RegExp(r'/$'), '');
15
+
16
+ /// Update the base URL (useful when connecting to a discovered server)
17
+ void setBaseUrl(String url) {
18
+ _baseUrl = url.replaceAll(RegExp(r'/$'), ''); // Remove trailing slash
19
+ }
20
+
21
+ /// Get the appropriate API URL for the given endpoint
22
+ /// On web, uses the browser's current origin (e.g., http://192.168.1.100:5000)
23
+ /// On mobile/desktop, uses the configured baseUrl or localhost:5000 as fallback
24
+ String getApiUrl(String endpoint) {
25
+ // 1. If we are on the Web, use the browser's current location
26
+ if (kIsWeb) {
27
+ // Uri.base.origin gives you "http://192.168.1.55:5000" or whatever the current IP is
28
+ // It includes the scheme (http/https) and the port if it's not 80/443.
29
+ return '${Uri.base.origin}$endpoint';
30
+ }
31
+ // 2. If we are on Mobile/Desktop, use configured baseUrl or default to localhost
32
+ else {
33
+ final base = _baseUrl ?? 'http://localhost:5000';
34
+ return '$base$endpoint';
35
+ }
36
+ }
37
+
38
+ /// Get system status
39
+ Future<Status> getStatus() async {
40
+ try {
41
+ final response = await http.get(
42
+ Uri.parse(getApiUrl('/api/status')),
43
+ headers: {'Content-Type': 'application/json'},
44
+ );
45
+
46
+ if (response.statusCode == 200) {
47
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
48
+ return Status.fromJson(json);
49
+ } else {
50
+ throw Exception('Failed to load status: ${response.statusCode}');
51
+ }
52
+ } catch (e) {
53
+ throw Exception('Failed to connect to PumaGuard server: $e');
54
+ }
55
+ }
56
+
57
+ /// Get current settings
58
+ Future<Settings> getSettings() async {
59
+ try {
60
+ final response = await http.get(
61
+ Uri.parse(getApiUrl('/api/settings')),
62
+ headers: {'Content-Type': 'application/json'},
63
+ );
64
+
65
+ if (response.statusCode == 200) {
66
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
67
+ return Settings.fromJson(json);
68
+ } else {
69
+ throw Exception('Failed to load settings: ${response.statusCode}');
70
+ }
71
+ } catch (e) {
72
+ throw Exception('Failed to load settings: $e');
73
+ }
74
+ }
75
+
76
+ /// Update settings
77
+ Future<bool> updateSettings(Settings settings) async {
78
+ try {
79
+ final url = getApiUrl('/api/settings');
80
+ final settingsJson = settings.toJson();
81
+ final body = jsonEncode(settingsJson);
82
+
83
+ debugPrint('[ApiService.updateSettings] URL: $url');
84
+ debugPrint('[ApiService.updateSettings] Settings JSON: $settingsJson');
85
+ debugPrint('[ApiService.updateSettings] Body length: ${body.length}');
86
+
87
+ final response = await http.put(
88
+ Uri.parse(url),
89
+ headers: {'Content-Type': 'application/json'},
90
+ body: body,
91
+ );
92
+
93
+ debugPrint(
94
+ '[ApiService.updateSettings] Response status: ${response.statusCode}',
95
+ );
96
+ debugPrint('[ApiService.updateSettings] Response body: ${response.body}');
97
+
98
+ if (response.statusCode == 200) {
99
+ debugPrint('[ApiService.updateSettings] Settings updated successfully');
100
+ return true;
101
+ } else {
102
+ final error = jsonDecode(response.body);
103
+ debugPrint('[ApiService.updateSettings] Error response: $error');
104
+ throw Exception(error['error'] ?? 'Failed to update settings');
105
+ }
106
+ } catch (e) {
107
+ debugPrint('[ApiService.updateSettings] Exception: $e');
108
+ throw Exception('Failed to update settings: $e');
109
+ }
110
+ }
111
+
112
+ /// Save settings to file
113
+ Future<String> saveSettings({String? filepath}) async {
114
+ try {
115
+ final body = filepath != null ? jsonEncode({'filepath': filepath}) : '{}';
116
+ final response = await http.post(
117
+ Uri.parse(getApiUrl('/api/settings/save')),
118
+ headers: {'Content-Type': 'application/json'},
119
+ body: body,
120
+ );
121
+
122
+ if (response.statusCode == 200) {
123
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
124
+ return json['filepath'] as String;
125
+ } else {
126
+ throw Exception('Failed to save settings: ${response.statusCode}');
127
+ }
128
+ } catch (e) {
129
+ throw Exception('Failed to save settings: $e');
130
+ }
131
+ }
132
+
133
+ /// Load settings from file
134
+ Future<bool> loadSettings(String filepath) async {
135
+ try {
136
+ final response = await http.post(
137
+ Uri.parse(getApiUrl('/api/settings/load')),
138
+ headers: {'Content-Type': 'application/json'},
139
+ body: jsonEncode({'filepath': filepath}),
140
+ );
141
+
142
+ if (response.statusCode == 200) {
143
+ return true;
144
+ } else {
145
+ final error = jsonDecode(response.body);
146
+ throw Exception(error['error'] ?? 'Failed to load settings');
147
+ }
148
+ } catch (e) {
149
+ throw Exception('Failed to load settings: $e');
150
+ }
151
+ }
152
+
153
+ /// Get list of monitored directories
154
+ Future<List<String>> getDirectories() async {
155
+ try {
156
+ final response = await http.get(
157
+ Uri.parse(getApiUrl('/api/directories')),
158
+ headers: {'Content-Type': 'application/json'},
159
+ );
160
+
161
+ if (response.statusCode == 200) {
162
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
163
+ final dirs = json['directories'] as List<dynamic>;
164
+ return dirs.map((d) => d.toString()).toList();
165
+ } else {
166
+ throw Exception('Failed to load directories: ${response.statusCode}');
167
+ }
168
+ } catch (e) {
169
+ throw Exception('Failed to load directories: $e');
170
+ }
171
+ }
172
+
173
+ /// Get list of classification output directories
174
+ Future<List<String>> getClassificationDirectories() async {
175
+ try {
176
+ final response = await http.get(
177
+ Uri.parse(getApiUrl('/api/directories/classification')),
178
+ headers: {'Content-Type': 'application/json'},
179
+ );
180
+
181
+ if (response.statusCode == 200) {
182
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
183
+ final dirs = json['directories'] as List<dynamic>;
184
+ return dirs.map((d) => d.toString()).toList();
185
+ } else {
186
+ throw Exception(
187
+ 'Failed to load classification directories: ${response.statusCode}',
188
+ );
189
+ }
190
+ } catch (e) {
191
+ throw Exception('Failed to load classification directories: $e');
192
+ }
193
+ }
194
+
195
+ /// Add a directory to monitor
196
+ Future<List<String>> addDirectory(String directory) async {
197
+ try {
198
+ final response = await http.post(
199
+ Uri.parse(getApiUrl('/api/directories')),
200
+ headers: {'Content-Type': 'application/json'},
201
+ body: jsonEncode({'directory': directory}),
202
+ );
203
+
204
+ if (response.statusCode == 200) {
205
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
206
+ final dirs = json['directories'] as List<dynamic>;
207
+ return dirs.map((d) => d.toString()).toList();
208
+ } else {
209
+ final error = jsonDecode(response.body);
210
+ throw Exception(error['error'] ?? 'Failed to add directory');
211
+ }
212
+ } catch (e) {
213
+ throw Exception('Failed to add directory: $e');
214
+ }
215
+ }
216
+
217
+ /// Remove a directory from monitoring
218
+ Future<List<String>> removeDirectory(int index) async {
219
+ try {
220
+ final response = await http.delete(
221
+ Uri.parse(getApiUrl('/api/directories/$index')),
222
+ headers: {'Content-Type': 'application/json'},
223
+ );
224
+
225
+ if (response.statusCode == 200) {
226
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
227
+ final dirs = json['directories'] as List<dynamic>;
228
+ return dirs.map((d) => d.toString()).toList();
229
+ } else {
230
+ final error = jsonDecode(response.body);
231
+ throw Exception(error['error'] ?? 'Failed to remove directory');
232
+ }
233
+ } catch (e) {
234
+ throw Exception('Failed to remove directory: $e');
235
+ }
236
+ }
237
+
238
+ /// Get list of watched folders with image counts
239
+ Future<List<Map<String, dynamic>>> getFolders() async {
240
+ try {
241
+ final response = await http.get(
242
+ Uri.parse(getApiUrl('/api/folders')),
243
+ headers: {'Content-Type': 'application/json'},
244
+ );
245
+
246
+ if (response.statusCode == 200) {
247
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
248
+ final folders = json['folders'] as List<dynamic>;
249
+ return folders.map((f) => f as Map<String, dynamic>).toList();
250
+ } else {
251
+ throw Exception('Failed to load folders: ${response.statusCode}');
252
+ }
253
+ } catch (e) {
254
+ throw Exception('Failed to load folders: $e');
255
+ }
256
+ }
257
+
258
+ /// Get list of images in a specific folder
259
+ Future<Map<String, dynamic>> getFolderImages(String folderPath) async {
260
+ debugPrint('[ApiService.getFolderImages] START');
261
+ debugPrint('[ApiService.getFolderImages] Input folderPath: $folderPath');
262
+
263
+ try {
264
+ // Don't encode absolute paths - Flask's <path:> converter handles them directly
265
+ // Only encode individual path components if needed
266
+ String pathForUrl;
267
+ if (folderPath.startsWith('/') || folderPath.startsWith('\\')) {
268
+ // Absolute path - remove leading slash since Flask's <path:> adds it back
269
+ pathForUrl = folderPath.substring(1);
270
+ } else {
271
+ // Relative path - use as-is
272
+ pathForUrl = folderPath;
273
+ }
274
+ debugPrint('[ApiService.getFolderImages] Path for URL: $pathForUrl');
275
+
276
+ final url = getApiUrl('/api/folders/$pathForUrl/images');
277
+ debugPrint('[ApiService.getFolderImages] Full URL: $url');
278
+
279
+ final response = await http.get(
280
+ Uri.parse(url),
281
+ headers: {'Content-Type': 'application/json'},
282
+ );
283
+
284
+ debugPrint(
285
+ '[ApiService.getFolderImages] Response status: ${response.statusCode}',
286
+ );
287
+ debugPrint(
288
+ '[ApiService.getFolderImages] Response body length: ${response.body.length} chars',
289
+ );
290
+
291
+ if (response.statusCode == 200) {
292
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
293
+ debugPrint(
294
+ '[ApiService.getFolderImages] SUCCESS - Images count: ${(json['images'] as List?)?.length ?? 0}',
295
+ );
296
+ return json;
297
+ } else {
298
+ debugPrint(
299
+ '[ApiService.getFolderImages] ERROR - Status ${response.statusCode}',
300
+ );
301
+ debugPrint(
302
+ '[ApiService.getFolderImages] ERROR - Body: ${response.body}',
303
+ );
304
+ throw Exception('Failed to load folder images: ${response.statusCode}');
305
+ }
306
+ } catch (e) {
307
+ debugPrint('[ApiService.getFolderImages] EXCEPTION: $e');
308
+ throw Exception('Failed to load folder images: $e');
309
+ }
310
+ }
311
+
312
+ /// Calculate checksum for a file (client-side)
313
+ String calculateChecksum(Uint8List bytes) {
314
+ return sha256.convert(bytes).toString();
315
+ }
316
+
317
+ /// Compare local files with server and get list of files to download
318
+ Future<List<Map<String, dynamic>>> getFilesToSync(
319
+ Map<String, String> localFilesWithChecksums,
320
+ ) async {
321
+ try {
322
+ final response = await http.post(
323
+ Uri.parse(getApiUrl('/api/sync/checksums')),
324
+ headers: {'Content-Type': 'application/json'},
325
+ body: jsonEncode({'files': localFilesWithChecksums}),
326
+ );
327
+
328
+ if (response.statusCode == 200) {
329
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
330
+ final files = json['files_to_download'] as List<dynamic>;
331
+ return files.map((f) => f as Map<String, dynamic>).toList();
332
+ } else {
333
+ throw Exception('Failed to get sync info: ${response.statusCode}');
334
+ }
335
+ } catch (e) {
336
+ throw Exception('Failed to get sync info: $e');
337
+ }
338
+ }
339
+
340
+ /// Download multiple files as ZIP or single file
341
+ Future<Uint8List> downloadFiles(List<String> filePaths) async {
342
+ try {
343
+ final response = await http.post(
344
+ Uri.parse(getApiUrl('/api/sync/download')),
345
+ headers: {'Content-Type': 'application/json'},
346
+ body: jsonEncode({'files': filePaths}),
347
+ );
348
+
349
+ if (response.statusCode == 200) {
350
+ return response.bodyBytes;
351
+ } else {
352
+ throw Exception('Failed to download files: ${response.statusCode}');
353
+ }
354
+ } catch (e) {
355
+ throw Exception('Failed to download files: $e');
356
+ }
357
+ }
358
+
359
+ /// Get URL for a specific photo/image
360
+ /// If [thumbnail] is true, request a thumbnail version (if supported by backend)
361
+ /// [maxWidth] and [maxHeight] can be used to request specific thumbnail dimensions
362
+ String getPhotoUrl(
363
+ String filepath, {
364
+ bool thumbnail = false,
365
+ int? maxWidth,
366
+ int? maxHeight,
367
+ }) {
368
+ final encodedPath = Uri.encodeComponent(filepath);
369
+ var url = getApiUrl('/api/photos/$encodedPath');
370
+
371
+ // Add query parameters for thumbnail if requested
372
+ // Note: Backend may not support these yet - they will be gracefully ignored
373
+ if (thumbnail || maxWidth != null || maxHeight != null) {
374
+ final queryParams = <String, String>{};
375
+ if (thumbnail) queryParams['thumbnail'] = 'true';
376
+ if (maxWidth != null) queryParams['width'] = maxWidth.toString();
377
+ if (maxHeight != null) queryParams['height'] = maxHeight.toString();
378
+
379
+ if (queryParams.isNotEmpty) {
380
+ url +=
381
+ '?${queryParams.entries.map((e) => '${e.key}=${e.value}').join('&')}';
382
+ }
383
+ }
384
+
385
+ return url;
386
+ }
387
+
388
+ /// Delete a photo/image file
389
+ Future<bool> deletePhoto(String filepath) async {
390
+ try {
391
+ final encodedPath = Uri.encodeComponent(filepath);
392
+ final response = await http.delete(
393
+ Uri.parse(getApiUrl('/api/photos/$encodedPath')),
394
+ headers: {'Content-Type': 'application/json'},
395
+ );
396
+
397
+ if (response.statusCode == 200) {
398
+ return true;
399
+ } else {
400
+ final error = jsonDecode(response.body);
401
+ throw Exception(error['error'] ?? 'Failed to delete photo');
402
+ }
403
+ } catch (e) {
404
+ throw Exception('Failed to delete photo: $e');
405
+ }
406
+ }
407
+
408
+ /// Test deterrent sound playback
409
+ Future<bool> testSound() async {
410
+ try {
411
+ final response = await http.post(
412
+ Uri.parse(getApiUrl('/api/settings/test-sound')),
413
+ headers: {'Content-Type': 'application/json'},
414
+ );
415
+
416
+ if (response.statusCode == 200) {
417
+ return true;
418
+ } else {
419
+ final error = jsonDecode(response.body);
420
+ throw Exception(error['error'] ?? 'Failed to test sound');
421
+ }
422
+ } catch (e) {
423
+ throw Exception('Failed to test sound: $e');
424
+ }
425
+ }
426
+
427
+ /// Stop currently playing sound
428
+ Future<bool> stopSound() async {
429
+ try {
430
+ final response = await http.post(
431
+ Uri.parse(getApiUrl('/api/settings/stop-sound')),
432
+ headers: {'Content-Type': 'application/json'},
433
+ );
434
+
435
+ if (response.statusCode == 200) {
436
+ return true;
437
+ } else {
438
+ final error = jsonDecode(response.body);
439
+ throw Exception(error['error'] ?? 'Failed to stop sound');
440
+ }
441
+ } catch (e) {
442
+ throw Exception('Failed to stop sound: $e');
443
+ }
444
+ }
445
+
446
+ /// Check if sound is currently playing
447
+ Future<bool> getSoundStatus() async {
448
+ try {
449
+ final response = await http.get(
450
+ Uri.parse(getApiUrl('/api/settings/sound-status')),
451
+ );
452
+
453
+ if (response.statusCode == 200) {
454
+ final data = jsonDecode(response.body);
455
+ return data['playing'] ?? false;
456
+ } else {
457
+ return false;
458
+ }
459
+ } catch (e) {
460
+ return false;
461
+ }
462
+ }
463
+
464
+ /// Get list of available models with cache status
465
+ /// [modelType] can be 'classifier' (*.h5 files) or 'yolo' (*.pt files)
466
+ Future<List<Map<String, dynamic>>> getAvailableModels({
467
+ String modelType = 'classifier',
468
+ }) async {
469
+ try {
470
+ final response = await http.get(
471
+ Uri.parse(getApiUrl('/api/models/available?type=$modelType')),
472
+ headers: {'Content-Type': 'application/json'},
473
+ );
474
+
475
+ if (response.statusCode == 200) {
476
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
477
+ final models = json['models'] as List<dynamic>;
478
+ return models.map((m) => m as Map<String, dynamic>).toList();
479
+ } else {
480
+ final error = jsonDecode(response.body);
481
+ throw Exception(error['error'] ?? 'Failed to get available models');
482
+ }
483
+ } catch (e) {
484
+ throw Exception('Failed to get available models: $e');
485
+ }
486
+ }
487
+
488
+ /// Get list of available sound files
489
+ Future<List<Map<String, dynamic>>> getAvailableSounds() async {
490
+ try {
491
+ final response = await http.get(
492
+ Uri.parse(getApiUrl('/api/sounds/available')),
493
+ headers: {'Content-Type': 'application/json'},
494
+ );
495
+
496
+ if (response.statusCode == 200) {
497
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
498
+ final sounds = json['sounds'] as List<dynamic>;
499
+ return sounds.map((s) => s as Map<String, dynamic>).toList();
500
+ } else {
501
+ final error = jsonDecode(response.body);
502
+ throw Exception(error['error'] ?? 'Failed to get available sounds');
503
+ }
504
+ } catch (e) {
505
+ throw Exception('Failed to get available sounds: $e');
506
+ }
507
+ }
508
+
509
+ /// Upload a sound file
510
+ Future<Map<String, dynamic>> uploadSound(
511
+ String filePath,
512
+ Uint8List fileBytes,
513
+ String fileName,
514
+ ) async {
515
+ try {
516
+ final uri = Uri.parse(getApiUrl('/api/sounds/upload'));
517
+ debugPrint('[ApiService.uploadSound] URI: $uri');
518
+ debugPrint('[ApiService.uploadSound] File name: $fileName');
519
+ debugPrint(
520
+ '[ApiService.uploadSound] File size: ${fileBytes.length} bytes',
521
+ );
522
+
523
+ final request = http.MultipartRequest('POST', uri);
524
+
525
+ // Add the file to the request
526
+ request.files.add(
527
+ http.MultipartFile.fromBytes('file', fileBytes, filename: fileName),
528
+ );
529
+
530
+ debugPrint('[ApiService.uploadSound] Sending request...');
531
+ final streamedResponse = await request.send();
532
+ debugPrint(
533
+ '[ApiService.uploadSound] Response status: ${streamedResponse.statusCode}',
534
+ );
535
+
536
+ final response = await http.Response.fromStream(streamedResponse);
537
+ debugPrint('[ApiService.uploadSound] Response body: ${response.body}');
538
+
539
+ if (response.statusCode == 200) {
540
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
541
+ return json;
542
+ } else {
543
+ final error = jsonDecode(response.body);
544
+ throw Exception(error['error'] ?? 'Failed to upload sound');
545
+ }
546
+ } catch (e) {
547
+ debugPrint('[ApiService.uploadSound] Exception: $e');
548
+ throw Exception('Failed to upload sound: $e');
549
+ }
550
+ }
551
+
552
+ /// Get list of detected cameras
553
+ Future<List<Camera>> getCameras() async {
554
+ try {
555
+ final url = getApiUrl('/api/dhcp/cameras');
556
+ debugPrint('[ApiService.getCameras] Requesting URL: $url');
557
+
558
+ final response = await http.get(
559
+ Uri.parse(url),
560
+ headers: {'Content-Type': 'application/json'},
561
+ );
562
+
563
+ debugPrint(
564
+ '[ApiService.getCameras] Response status: ${response.statusCode}',
565
+ );
566
+ debugPrint('[ApiService.getCameras] Response body: ${response.body}');
567
+
568
+ if (response.statusCode == 200) {
569
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
570
+ final camerasList = json['cameras'] as List? ?? [];
571
+ final cameras = camerasList
572
+ .map(
573
+ (cameraJson) =>
574
+ Camera.fromJson(cameraJson as Map<String, dynamic>),
575
+ )
576
+ .toList();
577
+ debugPrint('[ApiService.getCameras] Parsed ${cameras.length} cameras');
578
+ return cameras;
579
+ } else {
580
+ final error = jsonDecode(response.body);
581
+ debugPrint('[ApiService.getCameras] Error response: $error');
582
+ throw Exception(error['error'] ?? 'Failed to get cameras');
583
+ }
584
+ } catch (e) {
585
+ debugPrint('[ApiService.getCameras] Exception: $e');
586
+ throw Exception('Failed to get cameras: $e');
587
+ }
588
+ }
589
+
590
+ /// Scan for available WiFi networks
591
+ Future<Map<String, dynamic>> scanWifiNetworks() async {
592
+ try {
593
+ final url = getApiUrl('/api/wifi/scan');
594
+ debugPrint('[ApiService.scanWifiNetworks] Requesting URL: $url');
595
+
596
+ final response = await http.get(
597
+ Uri.parse(url),
598
+ headers: {'Content-Type': 'application/json'},
599
+ );
600
+
601
+ debugPrint(
602
+ '[ApiService.scanWifiNetworks] Response status: ${response.statusCode}',
603
+ );
604
+
605
+ if (response.statusCode == 200) {
606
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
607
+ return json;
608
+ } else {
609
+ final error = jsonDecode(response.body);
610
+ throw Exception(error['error'] ?? 'Failed to scan WiFi networks');
611
+ }
612
+ } catch (e) {
613
+ debugPrint('[ApiService.scanWifiNetworks] Exception: $e');
614
+ throw Exception('Failed to scan WiFi networks: $e');
615
+ }
616
+ }
617
+
618
+ /// Get current WiFi mode (ap or client)
619
+ Future<Map<String, dynamic>> getWifiMode() async {
620
+ try {
621
+ final url = getApiUrl('/api/wifi/mode');
622
+ debugPrint('[ApiService.getWifiMode] Requesting URL: $url');
623
+
624
+ final response = await http.get(
625
+ Uri.parse(url),
626
+ headers: {'Content-Type': 'application/json'},
627
+ );
628
+
629
+ debugPrint(
630
+ '[ApiService.getWifiMode] Response status: ${response.statusCode}',
631
+ );
632
+
633
+ if (response.statusCode == 200) {
634
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
635
+ return json;
636
+ } else {
637
+ final error = jsonDecode(response.body);
638
+ throw Exception(error['error'] ?? 'Failed to get WiFi mode');
639
+ }
640
+ } catch (e) {
641
+ debugPrint('[ApiService.getWifiMode] Exception: $e');
642
+ throw Exception('Failed to get WiFi mode: $e');
643
+ }
644
+ }
645
+
646
+ /// Set WiFi mode (ap or client)
647
+ Future<Map<String, dynamic>> setWifiMode({
648
+ required String mode,
649
+ required String ssid,
650
+ String? password,
651
+ }) async {
652
+ try {
653
+ final url = getApiUrl('/api/wifi/mode');
654
+ debugPrint('[ApiService.setWifiMode] Requesting URL: $url');
655
+ debugPrint('[ApiService.setWifiMode] Mode: $mode, SSID: $ssid');
656
+
657
+ final body = jsonEncode({
658
+ 'mode': mode,
659
+ 'ssid': ssid,
660
+ if (password != null && password.isNotEmpty) 'password': password,
661
+ });
662
+
663
+ final response = await http.put(
664
+ Uri.parse(url),
665
+ headers: {'Content-Type': 'application/json'},
666
+ body: body,
667
+ );
668
+
669
+ debugPrint(
670
+ '[ApiService.setWifiMode] Response status: ${response.statusCode}',
671
+ );
672
+ debugPrint('[ApiService.setWifiMode] Response body: ${response.body}');
673
+
674
+ if (response.statusCode == 200) {
675
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
676
+ return json;
677
+ } else {
678
+ final error = jsonDecode(response.body);
679
+ throw Exception(error['error'] ?? 'Failed to set WiFi mode');
680
+ }
681
+ } catch (e) {
682
+ debugPrint('[ApiService.setWifiMode] Exception: $e');
683
+ throw Exception('Failed to set WiFi mode: $e');
684
+ }
685
+ }
686
+
687
+ /// Forget a saved WiFi network
688
+ Future<Map<String, dynamic>> forgetWifiNetwork(String ssid) async {
689
+ try {
690
+ final url = getApiUrl('/api/wifi/forget');
691
+ debugPrint('[ApiService.forgetWifiNetwork] Requesting URL: $url');
692
+
693
+ final body = jsonEncode({'ssid': ssid});
694
+
695
+ final response = await http.post(
696
+ Uri.parse(url),
697
+ headers: {'Content-Type': 'application/json'},
698
+ body: body,
699
+ );
700
+
701
+ debugPrint(
702
+ '[ApiService.forgetWifiNetwork] Response status: ${response.statusCode}',
703
+ );
704
+
705
+ if (response.statusCode == 200) {
706
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
707
+ return json;
708
+ } else {
709
+ final error = jsonDecode(response.body);
710
+ throw Exception(error['error'] ?? 'Failed to forget WiFi network');
711
+ }
712
+ } catch (e) {
713
+ debugPrint('[ApiService.forgetWifiNetwork] Exception: $e');
714
+ throw Exception('Failed to forget WiFi network: $e');
715
+ }
716
+ }
717
+ }