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,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
|
+
}
|