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.
- 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.post27.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.post27.dist-info/RECORD +0 -83
- {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
- {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
- {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
- {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
import 'package:provider/provider.dart';
|
|
4
|
+
import '../models/settings.dart';
|
|
5
|
+
import '../services/api_service.dart';
|
|
6
|
+
import '../services/camera_events_service.dart';
|
|
7
|
+
import 'dart:developer' as developer;
|
|
8
|
+
import 'wifi_settings_screen.dart';
|
|
9
|
+
import 'package:file_picker/file_picker.dart';
|
|
10
|
+
|
|
11
|
+
class SettingsScreen extends StatefulWidget {
|
|
12
|
+
const SettingsScreen({super.key});
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class _SettingsScreenState extends State<SettingsScreen> {
|
|
19
|
+
Settings? _settings;
|
|
20
|
+
bool _isLoading = true;
|
|
21
|
+
bool _isSaving = false;
|
|
22
|
+
bool _isTestingSound = false;
|
|
23
|
+
String? _error;
|
|
24
|
+
|
|
25
|
+
// Controllers for text fields
|
|
26
|
+
late TextEditingController _yoloMinSizeController;
|
|
27
|
+
late TextEditingController _yoloConfThreshController;
|
|
28
|
+
late TextEditingController _yoloMaxDetsController;
|
|
29
|
+
late TextEditingController _yoloModelController;
|
|
30
|
+
late TextEditingController _classifierModelController;
|
|
31
|
+
late TextEditingController _soundFileController;
|
|
32
|
+
late TextEditingController _fileStabilizationController;
|
|
33
|
+
bool _playSound = false;
|
|
34
|
+
double _volume = 80.0;
|
|
35
|
+
List<Map<String, dynamic>> _availableModels = [];
|
|
36
|
+
List<Map<String, dynamic>> _availableSounds = [];
|
|
37
|
+
Timer? _debounceTimer;
|
|
38
|
+
Timer? _cameraRefreshTimer;
|
|
39
|
+
CameraEventsService? _cameraEventsService;
|
|
40
|
+
StreamSubscription<CameraEvent>? _eventsSubscription;
|
|
41
|
+
|
|
42
|
+
// Camera refresh interval (in seconds)
|
|
43
|
+
static const int _cameraRefreshInterval = 30;
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
void initState() {
|
|
47
|
+
super.initState();
|
|
48
|
+
_yoloMinSizeController = TextEditingController();
|
|
49
|
+
_yoloConfThreshController = TextEditingController();
|
|
50
|
+
_yoloMaxDetsController = TextEditingController();
|
|
51
|
+
_yoloModelController = TextEditingController();
|
|
52
|
+
_classifierModelController = TextEditingController();
|
|
53
|
+
_soundFileController = TextEditingController();
|
|
54
|
+
_fileStabilizationController = TextEditingController();
|
|
55
|
+
_loadSettings();
|
|
56
|
+
_initializeCameraEvents();
|
|
57
|
+
_startCameraRefresh();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
void dispose() {
|
|
62
|
+
_debounceTimer?.cancel();
|
|
63
|
+
_cameraRefreshTimer?.cancel();
|
|
64
|
+
_eventsSubscription?.cancel();
|
|
65
|
+
_cameraEventsService?.dispose();
|
|
66
|
+
_yoloMinSizeController.dispose();
|
|
67
|
+
_yoloConfThreshController.dispose();
|
|
68
|
+
_yoloMaxDetsController.dispose();
|
|
69
|
+
_yoloModelController.dispose();
|
|
70
|
+
_classifierModelController.dispose();
|
|
71
|
+
_soundFileController.dispose();
|
|
72
|
+
_fileStabilizationController.dispose();
|
|
73
|
+
super.dispose();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void _initializeCameraEvents() {
|
|
77
|
+
try {
|
|
78
|
+
final apiService = context.read<ApiService>();
|
|
79
|
+
final baseUrl = apiService.getApiUrl('').replaceAll('/api/', '');
|
|
80
|
+
|
|
81
|
+
_cameraEventsService = CameraEventsService(baseUrl);
|
|
82
|
+
|
|
83
|
+
// Subscribe to camera events
|
|
84
|
+
_eventsSubscription = _cameraEventsService!.events.listen(
|
|
85
|
+
(event) {
|
|
86
|
+
// Reload settings when camera status changes
|
|
87
|
+
if (event.type != CameraEventType.connected &&
|
|
88
|
+
event.type != CameraEventType.unknown) {
|
|
89
|
+
debugPrint(
|
|
90
|
+
'[SettingsScreen] Camera event: ${event.type}, reloading settings',
|
|
91
|
+
);
|
|
92
|
+
_loadSettings();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onError: (error) {
|
|
96
|
+
debugPrint('[SettingsScreen] Camera events error: $error');
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Start listening for events
|
|
101
|
+
_cameraEventsService!.startListening();
|
|
102
|
+
debugPrint('[SettingsScreen] Camera events service initialized');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
debugPrint('[SettingsScreen] Error initializing camera events: $e');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void _startCameraRefresh() {
|
|
109
|
+
// Set up periodic timer to refresh camera status
|
|
110
|
+
_cameraRefreshTimer = Timer.periodic(
|
|
111
|
+
const Duration(seconds: _cameraRefreshInterval),
|
|
112
|
+
(_) => _loadSettings(),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Future<void> _loadSettings() async {
|
|
117
|
+
setState(() {
|
|
118
|
+
_isLoading = true;
|
|
119
|
+
_error = null;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
final apiService = context.read<ApiService>();
|
|
124
|
+
final settings = await apiService.getSettings();
|
|
125
|
+
setState(() {
|
|
126
|
+
_settings = settings;
|
|
127
|
+
_yoloMinSizeController.text = settings.yoloMinSize.toString();
|
|
128
|
+
_yoloConfThreshController.text = settings.yoloConfThresh.toString();
|
|
129
|
+
_yoloMaxDetsController.text = settings.yoloMaxDets.toString();
|
|
130
|
+
_yoloModelController.text = settings.yoloModelFilename;
|
|
131
|
+
_classifierModelController.text = settings.classifierModelFilename;
|
|
132
|
+
_soundFileController.text = settings.deterrentSoundFile;
|
|
133
|
+
_fileStabilizationController.text = settings.fileStabilizationExtraWait
|
|
134
|
+
.toString();
|
|
135
|
+
_playSound = settings.playSound;
|
|
136
|
+
_volume = settings.volume.toDouble();
|
|
137
|
+
_isLoading = false;
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
setState(() {
|
|
141
|
+
_error = e.toString();
|
|
142
|
+
_isLoading = false;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void _onTextFieldChanged() {
|
|
148
|
+
// Cancel any existing timer
|
|
149
|
+
_debounceTimer?.cancel();
|
|
150
|
+
|
|
151
|
+
// Create a new timer that will save after 1 second of no changes
|
|
152
|
+
_debounceTimer = Timer(const Duration(seconds: 1), () {
|
|
153
|
+
_saveSettings();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Future<void> _saveSettings() async {
|
|
158
|
+
if (_settings == null) return;
|
|
159
|
+
|
|
160
|
+
setState(() {
|
|
161
|
+
_isSaving = true;
|
|
162
|
+
_error = null;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
final updatedSettings = Settings(
|
|
167
|
+
yoloMinSize: double.tryParse(_yoloMinSizeController.text) ?? 0.01,
|
|
168
|
+
yoloConfThresh: double.tryParse(_yoloConfThreshController.text) ?? 0.25,
|
|
169
|
+
yoloMaxDets: int.tryParse(_yoloMaxDetsController.text) ?? 10,
|
|
170
|
+
yoloModelFilename: _yoloModelController.text,
|
|
171
|
+
classifierModelFilename: _classifierModelController.text,
|
|
172
|
+
deterrentSoundFile: _soundFileController.text,
|
|
173
|
+
fileStabilizationExtraWait:
|
|
174
|
+
double.tryParse(_fileStabilizationController.text) ?? 2.0,
|
|
175
|
+
playSound: _playSound,
|
|
176
|
+
volume: _volume.round(),
|
|
177
|
+
cameras: _settings?.cameras ?? [],
|
|
178
|
+
plugs: _settings?.plugs ?? [],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
developer.log(
|
|
182
|
+
'[SettingsScreen._saveSettings] Settings JSON: ${updatedSettings.toJson()}',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
final apiService = context.read<ApiService>();
|
|
186
|
+
await apiService.updateSettings(updatedSettings);
|
|
187
|
+
|
|
188
|
+
developer.log(
|
|
189
|
+
'[SettingsScreen._saveSettings] Settings saved successfully',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
setState(() {
|
|
193
|
+
_settings = updatedSettings;
|
|
194
|
+
_isSaving = false;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (mounted) {
|
|
198
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
199
|
+
const SnackBar(
|
|
200
|
+
content: Text('Settings saved successfully'),
|
|
201
|
+
backgroundColor: Colors.green,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
setState(() {
|
|
207
|
+
_error = e.toString();
|
|
208
|
+
_isSaving = false;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (mounted) {
|
|
212
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
213
|
+
SnackBar(
|
|
214
|
+
content: Text('Failed to save settings: $e'),
|
|
215
|
+
backgroundColor: Colors.red,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Future<void> _testSound() async {
|
|
223
|
+
setState(() {
|
|
224
|
+
_isTestingSound = true;
|
|
225
|
+
_error = null;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
final apiService = context.read<ApiService>();
|
|
230
|
+
await apiService.testSound();
|
|
231
|
+
|
|
232
|
+
if (mounted) {
|
|
233
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
234
|
+
const SnackBar(
|
|
235
|
+
content: Text('Sound test started'),
|
|
236
|
+
backgroundColor: Colors.green,
|
|
237
|
+
duration: Duration(seconds: 2),
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Poll sound status to detect when it finishes
|
|
243
|
+
while (mounted && _isTestingSound) {
|
|
244
|
+
await Future.delayed(const Duration(milliseconds: 500));
|
|
245
|
+
final isPlaying = await apiService.getSoundStatus();
|
|
246
|
+
if (!isPlaying && mounted) {
|
|
247
|
+
setState(() {
|
|
248
|
+
_isTestingSound = false;
|
|
249
|
+
});
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (mounted) {
|
|
255
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
256
|
+
SnackBar(
|
|
257
|
+
content: Text('Failed to test sound: $e'),
|
|
258
|
+
backgroundColor: Colors.red,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
setState(() {
|
|
262
|
+
_isTestingSound = false;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
Future<void> _stopSound() async {
|
|
269
|
+
try {
|
|
270
|
+
final apiService = context.read<ApiService>();
|
|
271
|
+
await apiService.stopSound();
|
|
272
|
+
|
|
273
|
+
if (mounted) {
|
|
274
|
+
setState(() {
|
|
275
|
+
_isTestingSound = false;
|
|
276
|
+
});
|
|
277
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
278
|
+
const SnackBar(
|
|
279
|
+
content: Text('Sound stopped'),
|
|
280
|
+
backgroundColor: Colors.orange,
|
|
281
|
+
duration: Duration(seconds: 2),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
if (mounted) {
|
|
287
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
288
|
+
SnackBar(
|
|
289
|
+
content: Text('Failed to stop sound: $e'),
|
|
290
|
+
backgroundColor: Colors.red,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
Future<void> _uploadSoundFile() async {
|
|
298
|
+
try {
|
|
299
|
+
// Get API service before any async operations
|
|
300
|
+
final apiService = context.read<ApiService>();
|
|
301
|
+
|
|
302
|
+
// Pick a file
|
|
303
|
+
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
304
|
+
type: FileType.custom,
|
|
305
|
+
allowedExtensions: ['mp3'],
|
|
306
|
+
withData: true,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (result == null || result.files.isEmpty) {
|
|
310
|
+
// User canceled the picker
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
final file = result.files.first;
|
|
315
|
+
if (file.bytes == null) {
|
|
316
|
+
if (mounted) {
|
|
317
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
318
|
+
const SnackBar(
|
|
319
|
+
content: Text('Failed to read file'),
|
|
320
|
+
backgroundColor: Colors.red,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Show loading indicator
|
|
328
|
+
if (mounted) {
|
|
329
|
+
setState(() {
|
|
330
|
+
_isLoading = true;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
debugPrint(
|
|
335
|
+
'[SettingsScreen] Attempting to upload sound file: ${file.name}',
|
|
336
|
+
);
|
|
337
|
+
debugPrint('[SettingsScreen] File size: ${file.bytes!.length} bytes');
|
|
338
|
+
|
|
339
|
+
// Upload the file
|
|
340
|
+
// Note: file.path is not available on web, so we pass empty string
|
|
341
|
+
final response = await apiService.uploadSound(
|
|
342
|
+
'', // Path not available/needed on web
|
|
343
|
+
file.bytes!,
|
|
344
|
+
file.name,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
debugPrint('[SettingsScreen] Upload response: $response');
|
|
348
|
+
|
|
349
|
+
if (mounted) {
|
|
350
|
+
setState(() {
|
|
351
|
+
_isLoading = false;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Show success message
|
|
355
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
356
|
+
SnackBar(
|
|
357
|
+
content: Text(
|
|
358
|
+
response['message'] ?? 'Sound file uploaded successfully',
|
|
359
|
+
),
|
|
360
|
+
backgroundColor: Colors.green,
|
|
361
|
+
duration: const Duration(seconds: 3),
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Set the uploaded file as the current sound
|
|
366
|
+
final uploadedFilename = response['filename'] as String?;
|
|
367
|
+
if (uploadedFilename != null) {
|
|
368
|
+
setState(() {
|
|
369
|
+
_soundFileController.text = uploadedFilename;
|
|
370
|
+
});
|
|
371
|
+
await _saveSettings();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Refresh the sound picker dialog by calling it again
|
|
375
|
+
await _showSoundPicker();
|
|
376
|
+
}
|
|
377
|
+
} catch (e, stackTrace) {
|
|
378
|
+
developer.log('Error uploading sound', error: e, stackTrace: stackTrace);
|
|
379
|
+
debugPrint('[SettingsScreen] Upload error: $e');
|
|
380
|
+
debugPrint('[SettingsScreen] Stack trace: $stackTrace');
|
|
381
|
+
if (mounted) {
|
|
382
|
+
setState(() {
|
|
383
|
+
_isLoading = false;
|
|
384
|
+
});
|
|
385
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
386
|
+
SnackBar(
|
|
387
|
+
content: Text('Failed to upload sound: $e'),
|
|
388
|
+
backgroundColor: Colors.red,
|
|
389
|
+
duration: const Duration(seconds: 5),
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
Future<void> _showModelPicker({String modelType = 'classifier'}) async {
|
|
397
|
+
setState(() {
|
|
398
|
+
_isLoading = true;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
final apiService = context.read<ApiService>();
|
|
403
|
+
final models = await apiService.getAvailableModels(modelType: modelType);
|
|
404
|
+
|
|
405
|
+
setState(() {
|
|
406
|
+
_availableModels = models;
|
|
407
|
+
_isLoading = false;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!mounted) return;
|
|
411
|
+
|
|
412
|
+
final selectedModel = await showDialog<String>(
|
|
413
|
+
context: context,
|
|
414
|
+
builder: (BuildContext context) {
|
|
415
|
+
return AlertDialog(
|
|
416
|
+
title: Text(
|
|
417
|
+
modelType == 'yolo'
|
|
418
|
+
? 'Select YOLO Model'
|
|
419
|
+
: 'Select Classifier Model',
|
|
420
|
+
),
|
|
421
|
+
content: SizedBox(
|
|
422
|
+
width: double.maxFinite,
|
|
423
|
+
child: ListView.builder(
|
|
424
|
+
shrinkWrap: true,
|
|
425
|
+
itemCount: _availableModels.length,
|
|
426
|
+
itemBuilder: (context, index) {
|
|
427
|
+
final model = _availableModels[index];
|
|
428
|
+
final isCached = model['cached'] as bool;
|
|
429
|
+
final modelName = model['name'] as String;
|
|
430
|
+
final sizeMb = model['size_mb'] as double?;
|
|
431
|
+
|
|
432
|
+
return ListTile(
|
|
433
|
+
title: Text(modelName),
|
|
434
|
+
subtitle: Text(
|
|
435
|
+
isCached
|
|
436
|
+
? 'Cached${sizeMb != null ? " (${sizeMb.toStringAsFixed(1)} MB)" : ""}'
|
|
437
|
+
: 'Requires download',
|
|
438
|
+
style: TextStyle(
|
|
439
|
+
color: isCached ? Colors.green : Colors.orange,
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
leading: Icon(
|
|
443
|
+
isCached ? Icons.check_circle : Icons.cloud_download,
|
|
444
|
+
color: isCached ? Colors.green : Colors.orange,
|
|
445
|
+
),
|
|
446
|
+
trailing: modelName == _classifierModelController.text
|
|
447
|
+
? const Icon(Icons.check, color: Colors.blue)
|
|
448
|
+
: null,
|
|
449
|
+
onTap: () {
|
|
450
|
+
Navigator.of(context).pop(modelName);
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
actions: [
|
|
457
|
+
TextButton(
|
|
458
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
459
|
+
child: const Text('Cancel'),
|
|
460
|
+
),
|
|
461
|
+
],
|
|
462
|
+
);
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (selectedModel != null) {
|
|
467
|
+
setState(() {
|
|
468
|
+
if (modelType == 'yolo') {
|
|
469
|
+
_yoloModelController.text = selectedModel;
|
|
470
|
+
} else {
|
|
471
|
+
_classifierModelController.text = selectedModel;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
// Persist the change immediately
|
|
475
|
+
await _saveSettings();
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
developer.log('Error loading models: $e');
|
|
479
|
+
setState(() {
|
|
480
|
+
_isLoading = false;
|
|
481
|
+
});
|
|
482
|
+
if (mounted) {
|
|
483
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
484
|
+
SnackBar(
|
|
485
|
+
content: Text('Failed to load models: $e'),
|
|
486
|
+
backgroundColor: Colors.red,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
Future<void> _showSoundPicker() async {
|
|
494
|
+
setState(() {
|
|
495
|
+
_isLoading = true;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
final apiService = context.read<ApiService>();
|
|
500
|
+
final sounds = await apiService.getAvailableSounds();
|
|
501
|
+
|
|
502
|
+
setState(() {
|
|
503
|
+
_availableSounds = sounds;
|
|
504
|
+
_isLoading = false;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (!mounted) return;
|
|
508
|
+
|
|
509
|
+
final selectedSound = await showDialog<String>(
|
|
510
|
+
context: context,
|
|
511
|
+
builder: (BuildContext context) {
|
|
512
|
+
return AlertDialog(
|
|
513
|
+
title: const Text('Select Sound File'),
|
|
514
|
+
content: SizedBox(
|
|
515
|
+
width: double.maxFinite,
|
|
516
|
+
child: Column(
|
|
517
|
+
mainAxisSize: MainAxisSize.min,
|
|
518
|
+
children: [
|
|
519
|
+
ElevatedButton.icon(
|
|
520
|
+
onPressed: () async {
|
|
521
|
+
Navigator.of(context).pop('__upload__');
|
|
522
|
+
},
|
|
523
|
+
icon: const Icon(Icons.upload_file),
|
|
524
|
+
label: const Text('Upload New Sound File'),
|
|
525
|
+
style: ElevatedButton.styleFrom(
|
|
526
|
+
minimumSize: const Size(double.infinity, 48),
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
const SizedBox(height: 16),
|
|
530
|
+
const Divider(),
|
|
531
|
+
const SizedBox(height: 8),
|
|
532
|
+
Flexible(
|
|
533
|
+
child: ListView.builder(
|
|
534
|
+
shrinkWrap: true,
|
|
535
|
+
itemCount: _availableSounds.length,
|
|
536
|
+
itemBuilder: (context, index) {
|
|
537
|
+
final sound = _availableSounds[index];
|
|
538
|
+
final soundName = sound['name'] as String;
|
|
539
|
+
final sizeMb = sound['size_mb'] as double?;
|
|
540
|
+
|
|
541
|
+
return ListTile(
|
|
542
|
+
title: Text(soundName),
|
|
543
|
+
subtitle: Text(
|
|
544
|
+
sizeMb != null
|
|
545
|
+
? '${sizeMb.toStringAsFixed(2)} MB'
|
|
546
|
+
: '',
|
|
547
|
+
style: const TextStyle(color: Colors.blue),
|
|
548
|
+
),
|
|
549
|
+
leading: const Icon(
|
|
550
|
+
Icons.music_note,
|
|
551
|
+
color: Colors.blue,
|
|
552
|
+
),
|
|
553
|
+
trailing: soundName == _soundFileController.text
|
|
554
|
+
? const Icon(Icons.check, color: Colors.blue)
|
|
555
|
+
: null,
|
|
556
|
+
onTap: () {
|
|
557
|
+
Navigator.of(context).pop(soundName);
|
|
558
|
+
},
|
|
559
|
+
);
|
|
560
|
+
},
|
|
561
|
+
),
|
|
562
|
+
),
|
|
563
|
+
],
|
|
564
|
+
),
|
|
565
|
+
),
|
|
566
|
+
actions: [
|
|
567
|
+
TextButton(
|
|
568
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
569
|
+
child: const Text('Cancel'),
|
|
570
|
+
),
|
|
571
|
+
],
|
|
572
|
+
);
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (selectedSound == '__upload__') {
|
|
577
|
+
// User selected upload option
|
|
578
|
+
await _uploadSoundFile();
|
|
579
|
+
} else if (selectedSound != null) {
|
|
580
|
+
setState(() {
|
|
581
|
+
_soundFileController.text = selectedSound;
|
|
582
|
+
});
|
|
583
|
+
// Persist the change immediately
|
|
584
|
+
await _saveSettings();
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
developer.log('Error loading sounds: $e');
|
|
588
|
+
setState(() {
|
|
589
|
+
_isLoading = false;
|
|
590
|
+
});
|
|
591
|
+
if (mounted) {
|
|
592
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
593
|
+
SnackBar(
|
|
594
|
+
content: Text('Failed to load sounds: $e'),
|
|
595
|
+
backgroundColor: Colors.red,
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@override
|
|
603
|
+
Widget build(BuildContext context) {
|
|
604
|
+
return Scaffold(
|
|
605
|
+
appBar: AppBar(
|
|
606
|
+
title: const Text('Settings'),
|
|
607
|
+
actions: [
|
|
608
|
+
if (_isSaving)
|
|
609
|
+
const Padding(
|
|
610
|
+
padding: EdgeInsets.all(16.0),
|
|
611
|
+
child: SizedBox(
|
|
612
|
+
width: 20,
|
|
613
|
+
height: 20,
|
|
614
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
],
|
|
618
|
+
),
|
|
619
|
+
body: _buildBody(),
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
Widget _buildBody() {
|
|
624
|
+
if (_isLoading) {
|
|
625
|
+
return const Center(child: CircularProgressIndicator());
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (_error != null && _settings == null) {
|
|
629
|
+
return Center(
|
|
630
|
+
child: Column(
|
|
631
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
632
|
+
children: [
|
|
633
|
+
Icon(
|
|
634
|
+
Icons.error_outline,
|
|
635
|
+
size: 64,
|
|
636
|
+
color: Theme.of(context).colorScheme.error,
|
|
637
|
+
),
|
|
638
|
+
const SizedBox(height: 16),
|
|
639
|
+
Text(
|
|
640
|
+
'Failed to Load Settings',
|
|
641
|
+
style: Theme.of(context).textTheme.headlineSmall,
|
|
642
|
+
),
|
|
643
|
+
const SizedBox(height: 8),
|
|
644
|
+
Padding(
|
|
645
|
+
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
646
|
+
child: Text(
|
|
647
|
+
_error!,
|
|
648
|
+
textAlign: TextAlign.center,
|
|
649
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
650
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
651
|
+
),
|
|
652
|
+
),
|
|
653
|
+
),
|
|
654
|
+
const SizedBox(height: 24),
|
|
655
|
+
ElevatedButton.icon(
|
|
656
|
+
onPressed: _loadSettings,
|
|
657
|
+
icon: const Icon(Icons.refresh),
|
|
658
|
+
label: const Text('Retry'),
|
|
659
|
+
),
|
|
660
|
+
],
|
|
661
|
+
),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return SingleChildScrollView(
|
|
666
|
+
padding: const EdgeInsets.all(16),
|
|
667
|
+
child: Column(
|
|
668
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
669
|
+
children: [
|
|
670
|
+
_buildYoloSection(),
|
|
671
|
+
const SizedBox(height: 16),
|
|
672
|
+
_buildClassifierSection(),
|
|
673
|
+
const SizedBox(height: 16),
|
|
674
|
+
_buildSoundSection(),
|
|
675
|
+
const SizedBox(height: 16),
|
|
676
|
+
_buildSystemSection(),
|
|
677
|
+
const SizedBox(height: 24),
|
|
678
|
+
ElevatedButton.icon(
|
|
679
|
+
onPressed: _isSaving ? null : _saveSettings,
|
|
680
|
+
icon: _isSaving
|
|
681
|
+
? const SizedBox(
|
|
682
|
+
width: 20,
|
|
683
|
+
height: 20,
|
|
684
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
685
|
+
)
|
|
686
|
+
: const Icon(Icons.save),
|
|
687
|
+
label: const Text('Save Settings'),
|
|
688
|
+
style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16)),
|
|
689
|
+
),
|
|
690
|
+
],
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
Widget _buildYoloSection() {
|
|
696
|
+
return Card(
|
|
697
|
+
child: Padding(
|
|
698
|
+
padding: const EdgeInsets.all(16),
|
|
699
|
+
child: Column(
|
|
700
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
701
|
+
children: [
|
|
702
|
+
Row(
|
|
703
|
+
children: [
|
|
704
|
+
Icon(
|
|
705
|
+
Icons.track_changes,
|
|
706
|
+
color: Theme.of(context).colorScheme.primary,
|
|
707
|
+
),
|
|
708
|
+
const SizedBox(width: 8),
|
|
709
|
+
Text(
|
|
710
|
+
'YOLO Detection Settings',
|
|
711
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
712
|
+
),
|
|
713
|
+
],
|
|
714
|
+
),
|
|
715
|
+
const SizedBox(height: 8),
|
|
716
|
+
Text(
|
|
717
|
+
'Configure object detection parameters',
|
|
718
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
719
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
720
|
+
),
|
|
721
|
+
),
|
|
722
|
+
const SizedBox(height: 16),
|
|
723
|
+
TextField(
|
|
724
|
+
controller: _yoloMinSizeController,
|
|
725
|
+
onChanged: (_) => _onTextFieldChanged(),
|
|
726
|
+
decoration: const InputDecoration(
|
|
727
|
+
labelText: 'Minimum Size',
|
|
728
|
+
hintText: '0.01',
|
|
729
|
+
helperText: 'Minimum object size as fraction of image',
|
|
730
|
+
border: OutlineInputBorder(),
|
|
731
|
+
),
|
|
732
|
+
keyboardType: TextInputType.number,
|
|
733
|
+
),
|
|
734
|
+
const SizedBox(height: 16),
|
|
735
|
+
TextField(
|
|
736
|
+
controller: _yoloConfThreshController,
|
|
737
|
+
onChanged: (_) => _onTextFieldChanged(),
|
|
738
|
+
decoration: const InputDecoration(
|
|
739
|
+
labelText: 'Confidence Threshold',
|
|
740
|
+
hintText: '0.25',
|
|
741
|
+
helperText: 'Detection confidence threshold (0.0 - 1.0)',
|
|
742
|
+
border: OutlineInputBorder(),
|
|
743
|
+
),
|
|
744
|
+
keyboardType: TextInputType.number,
|
|
745
|
+
),
|
|
746
|
+
const SizedBox(height: 16),
|
|
747
|
+
TextField(
|
|
748
|
+
controller: _yoloMaxDetsController,
|
|
749
|
+
onChanged: (_) => _onTextFieldChanged(),
|
|
750
|
+
decoration: const InputDecoration(
|
|
751
|
+
labelText: 'Maximum Detections',
|
|
752
|
+
hintText: '10',
|
|
753
|
+
helperText: 'Maximum number of objects to detect',
|
|
754
|
+
border: OutlineInputBorder(),
|
|
755
|
+
),
|
|
756
|
+
keyboardType: TextInputType.number,
|
|
757
|
+
),
|
|
758
|
+
const SizedBox(height: 16),
|
|
759
|
+
TextField(
|
|
760
|
+
controller: _yoloModelController,
|
|
761
|
+
decoration: InputDecoration(
|
|
762
|
+
labelText: 'YOLO Model Filename',
|
|
763
|
+
hintText: 'yolo_model.pt',
|
|
764
|
+
helperText: 'Path to YOLO model file',
|
|
765
|
+
border: const OutlineInputBorder(),
|
|
766
|
+
suffixIcon: IconButton(
|
|
767
|
+
icon: const Icon(Icons.folder_open),
|
|
768
|
+
onPressed: () => _showModelPicker(modelType: 'yolo'),
|
|
769
|
+
tooltip: 'Browse available models',
|
|
770
|
+
),
|
|
771
|
+
),
|
|
772
|
+
readOnly: false,
|
|
773
|
+
),
|
|
774
|
+
const SizedBox(height: 8),
|
|
775
|
+
OutlinedButton.icon(
|
|
776
|
+
onPressed: () => _showModelPicker(modelType: 'yolo'),
|
|
777
|
+
icon: const Icon(Icons.list),
|
|
778
|
+
label: const Text('Choose from Available Models'),
|
|
779
|
+
style: OutlinedButton.styleFrom(
|
|
780
|
+
padding: const EdgeInsets.all(12),
|
|
781
|
+
),
|
|
782
|
+
),
|
|
783
|
+
],
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
Widget _buildClassifierSection() {
|
|
790
|
+
return Card(
|
|
791
|
+
child: Padding(
|
|
792
|
+
padding: const EdgeInsets.all(16),
|
|
793
|
+
child: Column(
|
|
794
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
795
|
+
children: [
|
|
796
|
+
Row(
|
|
797
|
+
children: [
|
|
798
|
+
Icon(
|
|
799
|
+
Icons.category,
|
|
800
|
+
color: Theme.of(context).colorScheme.primary,
|
|
801
|
+
),
|
|
802
|
+
const SizedBox(width: 8),
|
|
803
|
+
Text(
|
|
804
|
+
'Classifier Settings',
|
|
805
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
806
|
+
),
|
|
807
|
+
],
|
|
808
|
+
),
|
|
809
|
+
const SizedBox(height: 8),
|
|
810
|
+
Text(
|
|
811
|
+
'Configure EfficientNet classifier',
|
|
812
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
813
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
814
|
+
),
|
|
815
|
+
),
|
|
816
|
+
const SizedBox(height: 16),
|
|
817
|
+
TextField(
|
|
818
|
+
controller: _classifierModelController,
|
|
819
|
+
decoration: InputDecoration(
|
|
820
|
+
labelText: 'Classifier Model Filename',
|
|
821
|
+
hintText: 'classifier_model.h5',
|
|
822
|
+
helperText: 'Path to classifier model file',
|
|
823
|
+
border: const OutlineInputBorder(),
|
|
824
|
+
suffixIcon: IconButton(
|
|
825
|
+
icon: const Icon(Icons.folder_open),
|
|
826
|
+
onPressed: _showModelPicker,
|
|
827
|
+
tooltip: 'Browse available models',
|
|
828
|
+
),
|
|
829
|
+
),
|
|
830
|
+
readOnly: false,
|
|
831
|
+
),
|
|
832
|
+
const SizedBox(height: 8),
|
|
833
|
+
OutlinedButton.icon(
|
|
834
|
+
onPressed: _showModelPicker,
|
|
835
|
+
icon: const Icon(Icons.list),
|
|
836
|
+
label: const Text('Choose from Available Models'),
|
|
837
|
+
style: OutlinedButton.styleFrom(
|
|
838
|
+
padding: const EdgeInsets.all(12),
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
],
|
|
842
|
+
),
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
Widget _buildSoundSection() {
|
|
848
|
+
return Card(
|
|
849
|
+
child: Padding(
|
|
850
|
+
padding: const EdgeInsets.all(16),
|
|
851
|
+
child: Column(
|
|
852
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
853
|
+
children: [
|
|
854
|
+
Row(
|
|
855
|
+
children: [
|
|
856
|
+
Icon(
|
|
857
|
+
Icons.volume_up,
|
|
858
|
+
color: Theme.of(context).colorScheme.primary,
|
|
859
|
+
),
|
|
860
|
+
const SizedBox(width: 8),
|
|
861
|
+
Text(
|
|
862
|
+
'Sound Settings',
|
|
863
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
864
|
+
),
|
|
865
|
+
],
|
|
866
|
+
),
|
|
867
|
+
const SizedBox(height: 8),
|
|
868
|
+
Text(
|
|
869
|
+
'Configure deterrent sound playback',
|
|
870
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
871
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
872
|
+
),
|
|
873
|
+
),
|
|
874
|
+
const SizedBox(height: 16),
|
|
875
|
+
TextField(
|
|
876
|
+
controller: _soundFileController,
|
|
877
|
+
decoration: InputDecoration(
|
|
878
|
+
labelText: 'Deterrent Sound File',
|
|
879
|
+
hintText: 'deterrent.wav',
|
|
880
|
+
helperText: 'Path to sound file to play when puma detected',
|
|
881
|
+
border: const OutlineInputBorder(),
|
|
882
|
+
suffixIcon: IconButton(
|
|
883
|
+
icon: const Icon(Icons.folder_open),
|
|
884
|
+
onPressed: _showSoundPicker,
|
|
885
|
+
tooltip: 'Browse available sounds',
|
|
886
|
+
),
|
|
887
|
+
),
|
|
888
|
+
readOnly: false,
|
|
889
|
+
),
|
|
890
|
+
const SizedBox(height: 8),
|
|
891
|
+
Row(
|
|
892
|
+
children: [
|
|
893
|
+
Expanded(
|
|
894
|
+
child: OutlinedButton.icon(
|
|
895
|
+
onPressed: _showSoundPicker,
|
|
896
|
+
icon: const Icon(Icons.list),
|
|
897
|
+
label: const Text('Choose from Available Sounds'),
|
|
898
|
+
style: OutlinedButton.styleFrom(
|
|
899
|
+
padding: const EdgeInsets.all(12),
|
|
900
|
+
),
|
|
901
|
+
),
|
|
902
|
+
),
|
|
903
|
+
const SizedBox(width: 8),
|
|
904
|
+
Expanded(
|
|
905
|
+
child: ElevatedButton.icon(
|
|
906
|
+
onPressed: _isLoading ? null : _uploadSoundFile,
|
|
907
|
+
icon: const Icon(Icons.upload_file),
|
|
908
|
+
label: const Text('Upload New Sound File'),
|
|
909
|
+
style: ElevatedButton.styleFrom(
|
|
910
|
+
padding: const EdgeInsets.all(12),
|
|
911
|
+
),
|
|
912
|
+
),
|
|
913
|
+
),
|
|
914
|
+
],
|
|
915
|
+
),
|
|
916
|
+
const SizedBox(height: 16),
|
|
917
|
+
SwitchListTile(
|
|
918
|
+
title: const Text('Play Sound'),
|
|
919
|
+
subtitle: const Text('Enable deterrent sound playback'),
|
|
920
|
+
value: _playSound,
|
|
921
|
+
onChanged: (value) {
|
|
922
|
+
setState(() {
|
|
923
|
+
_playSound = value;
|
|
924
|
+
});
|
|
925
|
+
// Persist the change immediately
|
|
926
|
+
_saveSettings();
|
|
927
|
+
},
|
|
928
|
+
),
|
|
929
|
+
const SizedBox(height: 16),
|
|
930
|
+
Row(
|
|
931
|
+
children: [
|
|
932
|
+
Icon(
|
|
933
|
+
Icons.volume_down,
|
|
934
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
935
|
+
),
|
|
936
|
+
Expanded(
|
|
937
|
+
child: Slider(
|
|
938
|
+
value: _volume,
|
|
939
|
+
min: 0,
|
|
940
|
+
max: 100,
|
|
941
|
+
divisions: 20,
|
|
942
|
+
label: _volume.round().toString(),
|
|
943
|
+
onChanged: (value) {
|
|
944
|
+
setState(() {
|
|
945
|
+
_volume = value;
|
|
946
|
+
});
|
|
947
|
+
},
|
|
948
|
+
onChangeEnd: (value) {
|
|
949
|
+
// Save when user releases the slider
|
|
950
|
+
_saveSettings();
|
|
951
|
+
},
|
|
952
|
+
),
|
|
953
|
+
),
|
|
954
|
+
Icon(
|
|
955
|
+
Icons.volume_up,
|
|
956
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
957
|
+
),
|
|
958
|
+
const SizedBox(width: 8),
|
|
959
|
+
SizedBox(
|
|
960
|
+
width: 45,
|
|
961
|
+
child: Text(
|
|
962
|
+
'${_volume.round()}%',
|
|
963
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
964
|
+
fontFamily: 'RobotoMono',
|
|
965
|
+
fontWeight: FontWeight.bold,
|
|
966
|
+
),
|
|
967
|
+
),
|
|
968
|
+
),
|
|
969
|
+
],
|
|
970
|
+
),
|
|
971
|
+
const SizedBox(height: 16),
|
|
972
|
+
Row(
|
|
973
|
+
children: [
|
|
974
|
+
Expanded(
|
|
975
|
+
child: OutlinedButton.icon(
|
|
976
|
+
onPressed: _isTestingSound ? null : _testSound,
|
|
977
|
+
icon: _isTestingSound
|
|
978
|
+
? const SizedBox(
|
|
979
|
+
width: 20,
|
|
980
|
+
height: 20,
|
|
981
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
982
|
+
)
|
|
983
|
+
: const Icon(Icons.play_arrow),
|
|
984
|
+
label: const Text('Test Sound'),
|
|
985
|
+
style: OutlinedButton.styleFrom(
|
|
986
|
+
padding: const EdgeInsets.all(12),
|
|
987
|
+
),
|
|
988
|
+
),
|
|
989
|
+
),
|
|
990
|
+
const SizedBox(width: 8),
|
|
991
|
+
Expanded(
|
|
992
|
+
child: OutlinedButton.icon(
|
|
993
|
+
onPressed: _isTestingSound ? _stopSound : null,
|
|
994
|
+
icon: const Icon(Icons.stop),
|
|
995
|
+
label: const Text('Stop Sound'),
|
|
996
|
+
style: OutlinedButton.styleFrom(
|
|
997
|
+
padding: const EdgeInsets.all(12),
|
|
998
|
+
foregroundColor: Colors.orange,
|
|
999
|
+
),
|
|
1000
|
+
),
|
|
1001
|
+
),
|
|
1002
|
+
],
|
|
1003
|
+
),
|
|
1004
|
+
],
|
|
1005
|
+
),
|
|
1006
|
+
),
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
Widget _buildSystemSection() {
|
|
1011
|
+
return Card(
|
|
1012
|
+
child: Padding(
|
|
1013
|
+
padding: const EdgeInsets.all(16),
|
|
1014
|
+
child: Column(
|
|
1015
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1016
|
+
children: [
|
|
1017
|
+
Row(
|
|
1018
|
+
children: [
|
|
1019
|
+
Icon(
|
|
1020
|
+
Icons.settings_system_daydream,
|
|
1021
|
+
color: Theme.of(context).colorScheme.primary,
|
|
1022
|
+
),
|
|
1023
|
+
const SizedBox(width: 8),
|
|
1024
|
+
Text(
|
|
1025
|
+
'System Settings',
|
|
1026
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
1027
|
+
),
|
|
1028
|
+
],
|
|
1029
|
+
),
|
|
1030
|
+
const SizedBox(height: 8),
|
|
1031
|
+
Text(
|
|
1032
|
+
'Configure system behavior',
|
|
1033
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
1034
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
1035
|
+
),
|
|
1036
|
+
),
|
|
1037
|
+
const SizedBox(height: 16),
|
|
1038
|
+
TextField(
|
|
1039
|
+
controller: _fileStabilizationController,
|
|
1040
|
+
onChanged: (_) => _onTextFieldChanged(),
|
|
1041
|
+
decoration: const InputDecoration(
|
|
1042
|
+
labelText: 'File Stabilization Wait (seconds)',
|
|
1043
|
+
hintText: '2.0',
|
|
1044
|
+
helperText: 'Extra wait time for file operations to complete',
|
|
1045
|
+
border: OutlineInputBorder(),
|
|
1046
|
+
),
|
|
1047
|
+
keyboardType: TextInputType.number,
|
|
1048
|
+
),
|
|
1049
|
+
const SizedBox(height: 24),
|
|
1050
|
+
// WiFi Settings Button
|
|
1051
|
+
FilledButton.icon(
|
|
1052
|
+
onPressed: () {
|
|
1053
|
+
Navigator.of(context).push(
|
|
1054
|
+
MaterialPageRoute(
|
|
1055
|
+
builder: (context) => WifiSettingsScreen(
|
|
1056
|
+
apiService: context.read<ApiService>(),
|
|
1057
|
+
),
|
|
1058
|
+
),
|
|
1059
|
+
);
|
|
1060
|
+
},
|
|
1061
|
+
icon: const Icon(Icons.wifi),
|
|
1062
|
+
label: const Text('WiFi Settings'),
|
|
1063
|
+
style: FilledButton.styleFrom(
|
|
1064
|
+
minimumSize: const Size(double.infinity, 48),
|
|
1065
|
+
),
|
|
1066
|
+
),
|
|
1067
|
+
const SizedBox(height: 24),
|
|
1068
|
+
// Detected Cameras Section
|
|
1069
|
+
if (_settings != null && _settings!.cameras.isNotEmpty) ...[
|
|
1070
|
+
Text(
|
|
1071
|
+
'Detected Cameras',
|
|
1072
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
1073
|
+
),
|
|
1074
|
+
const SizedBox(height: 8),
|
|
1075
|
+
Text(
|
|
1076
|
+
'Cameras detected via DHCP',
|
|
1077
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
1078
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
1079
|
+
),
|
|
1080
|
+
),
|
|
1081
|
+
const SizedBox(height: 16),
|
|
1082
|
+
..._settings!.cameras.map((camera) {
|
|
1083
|
+
return Card(
|
|
1084
|
+
child: ListTile(
|
|
1085
|
+
leading: Icon(
|
|
1086
|
+
Icons.videocam,
|
|
1087
|
+
color: camera.isConnected ? Colors.green : Colors.grey,
|
|
1088
|
+
),
|
|
1089
|
+
title: Text(camera.displayName),
|
|
1090
|
+
subtitle: Column(
|
|
1091
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1092
|
+
children: [
|
|
1093
|
+
Text('IP: ${camera.ipAddress}'),
|
|
1094
|
+
Text(
|
|
1095
|
+
'Status: ${camera.status}',
|
|
1096
|
+
style: TextStyle(
|
|
1097
|
+
color: camera.isConnected
|
|
1098
|
+
? Colors.green
|
|
1099
|
+
: Colors.grey,
|
|
1100
|
+
),
|
|
1101
|
+
),
|
|
1102
|
+
],
|
|
1103
|
+
),
|
|
1104
|
+
trailing: Icon(
|
|
1105
|
+
camera.isConnected ? Icons.check_circle : Icons.cancel,
|
|
1106
|
+
color: camera.isConnected ? Colors.green : Colors.grey,
|
|
1107
|
+
),
|
|
1108
|
+
),
|
|
1109
|
+
);
|
|
1110
|
+
}),
|
|
1111
|
+
const SizedBox(height: 24),
|
|
1112
|
+
],
|
|
1113
|
+
// Detected Plugs Section
|
|
1114
|
+
if (_settings != null && _settings!.plugs.isNotEmpty) ...[
|
|
1115
|
+
Text(
|
|
1116
|
+
'Detected Plugs',
|
|
1117
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
1118
|
+
),
|
|
1119
|
+
const SizedBox(height: 8),
|
|
1120
|
+
Text(
|
|
1121
|
+
'Smart plugs detected via DHCP (for deterrents)',
|
|
1122
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
1123
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
1124
|
+
),
|
|
1125
|
+
),
|
|
1126
|
+
const SizedBox(height: 16),
|
|
1127
|
+
..._settings!.plugs.map((plug) {
|
|
1128
|
+
return Card(
|
|
1129
|
+
child: ListTile(
|
|
1130
|
+
leading: Icon(
|
|
1131
|
+
Icons.power,
|
|
1132
|
+
color: plug.isConnected ? Colors.green : Colors.grey,
|
|
1133
|
+
),
|
|
1134
|
+
title: Text(plug.displayName),
|
|
1135
|
+
subtitle: Column(
|
|
1136
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1137
|
+
children: [
|
|
1138
|
+
Text('IP: ${plug.ipAddress}'),
|
|
1139
|
+
Text(
|
|
1140
|
+
'Status: ${plug.status}',
|
|
1141
|
+
style: TextStyle(
|
|
1142
|
+
color: plug.isConnected
|
|
1143
|
+
? Colors.green
|
|
1144
|
+
: Colors.grey,
|
|
1145
|
+
),
|
|
1146
|
+
),
|
|
1147
|
+
],
|
|
1148
|
+
),
|
|
1149
|
+
trailing: Icon(
|
|
1150
|
+
plug.isConnected ? Icons.check_circle : Icons.cancel,
|
|
1151
|
+
color: plug.isConnected ? Colors.green : Colors.grey,
|
|
1152
|
+
),
|
|
1153
|
+
),
|
|
1154
|
+
);
|
|
1155
|
+
}),
|
|
1156
|
+
],
|
|
1157
|
+
],
|
|
1158
|
+
),
|
|
1159
|
+
),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
}
|