pumaguard 21.post27__py3-none-any.whl → 21.post83__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. pumaguard/presets.py +1 -0
  2. pumaguard/pumaguard-ui/.last_build_id +1 -1
  3. pumaguard/pumaguard-ui/assets/NOTICES +621 -71
  4. pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
  5. pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
  6. pumaguard/pumaguard-ui/main.dart.js +28869 -28787
  7. pumaguard/web_routes/dhcp.py +311 -54
  8. pumaguard/web_routes/diagnostics.py +6 -0
  9. pumaguard/web_routes/settings.py +13 -0
  10. pumaguard/web_ui.py +29 -0
  11. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
  12. pumaguard-21.post83.dist-info/RECORD +254 -0
  13. pumaguard-ui/.gitignore +48 -0
  14. pumaguard-ui/.metadata +45 -0
  15. pumaguard-ui/API_REFERENCE.md +717 -0
  16. pumaguard-ui/LICENSE +201 -0
  17. pumaguard-ui/Makefile +36 -0
  18. pumaguard-ui/README.md +371 -0
  19. pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
  20. pumaguard-ui/analysis_options.yaml +28 -0
  21. pumaguard-ui/android/.gitignore +14 -0
  22. pumaguard-ui/android/app/build.gradle.kts +44 -0
  23. pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
  24. pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
  25. pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
  26. pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
  27. pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  28. pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  29. pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  31. pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
  34. pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
  35. pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
  36. pumaguard-ui/android/build.gradle.kts +24 -0
  37. pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  38. pumaguard-ui/android/gradle.properties +2 -0
  39. pumaguard-ui/android/settings.gradle.kts +26 -0
  40. pumaguard-ui/fonts/README.md +38 -0
  41. pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
  42. pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
  43. pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
  44. pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
  45. pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
  46. pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
  47. pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
  48. pumaguard-ui/fonts/download_fonts.sh +76 -0
  49. pumaguard-ui/ios/.gitignore +34 -0
  50. pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
  51. pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
  52. pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
  53. pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
  54. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  55. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  56. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  57. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  58. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  59. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  60. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  61. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  62. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  63. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  64. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  65. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  66. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  67. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  68. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  69. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  70. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  71. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  72. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  73. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  74. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  75. pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  76. pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
  77. pumaguard-ui/ios/Runner/Info.plist +49 -0
  78. pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
  79. pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
  80. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  81. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  82. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  83. pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  84. pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  85. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  86. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  87. pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
  88. pumaguard-ui/lib/main.dart +56 -0
  89. pumaguard-ui/lib/models/camera.dart +45 -0
  90. pumaguard-ui/lib/models/plug.dart +45 -0
  91. pumaguard-ui/lib/models/settings.dart +112 -0
  92. pumaguard-ui/lib/models/status.dart +58 -0
  93. pumaguard-ui/lib/screens/directories_screen.dart +319 -0
  94. pumaguard-ui/lib/screens/home_screen.dart +545 -0
  95. pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
  96. pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
  97. pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
  98. pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
  99. pumaguard-ui/lib/services/api_service.dart +717 -0
  100. pumaguard-ui/lib/services/camera_events_service.dart +195 -0
  101. pumaguard-ui/lib/services/mdns_service.dart +4 -0
  102. pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
  103. pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
  104. pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
  105. pumaguard-ui/lib/utils/download_helper.dart +2 -0
  106. pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
  107. pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
  108. pumaguard-ui/lib/utils/platform_url.dart +10 -0
  109. pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
  110. pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
  111. pumaguard-ui/linux/.gitignore +1 -0
  112. pumaguard-ui/linux/CMakeLists.txt +128 -0
  113. pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
  114. pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
  115. pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
  116. pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
  117. pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
  118. pumaguard-ui/linux/runner/main.cc +6 -0
  119. pumaguard-ui/linux/runner/my_application.cc +148 -0
  120. pumaguard-ui/linux/runner/my_application.h +21 -0
  121. pumaguard-ui/macos/.gitignore +7 -0
  122. pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
  123. pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
  124. pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
  125. pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
  126. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  127. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  128. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  129. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  130. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  131. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  132. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  133. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  134. pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  135. pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  136. pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
  137. pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
  138. pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
  139. pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
  140. pumaguard-ui/macos/Runner/Info.plist +32 -0
  141. pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
  142. pumaguard-ui/macos/Runner/Release.entitlements +8 -0
  143. pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
  144. pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  145. pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  146. pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  147. pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  148. pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
  149. pumaguard-ui/pubspec.lock +882 -0
  150. pumaguard-ui/pubspec.yaml +125 -0
  151. pumaguard-ui/test/models/camera_test.dart +515 -0
  152. pumaguard-ui/test/models/plug_test.dart +499 -0
  153. pumaguard-ui/test/models/settings_test.dart +903 -0
  154. pumaguard-ui/test/models/status_test.dart +707 -0
  155. pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
  156. pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
  157. pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
  158. pumaguard-ui/test/widget_test.dart.skip +38 -0
  159. pumaguard-ui/web/favicon.png +0 -0
  160. pumaguard-ui/web/icons/Icon-192.png +0 -0
  161. pumaguard-ui/web/icons/Icon-512.png +0 -0
  162. pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
  163. pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
  164. pumaguard-ui/web/index.html +38 -0
  165. pumaguard-ui/web/manifest.json +35 -0
  166. pumaguard-ui/windows/.gitignore +17 -0
  167. pumaguard-ui/windows/CMakeLists.txt +108 -0
  168. pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
  169. pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
  170. pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
  171. pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
  172. pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
  173. pumaguard-ui/windows/runner/Runner.rc +121 -0
  174. pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
  175. pumaguard-ui/windows/runner/flutter_window.h +33 -0
  176. pumaguard-ui/windows/runner/main.cpp +43 -0
  177. pumaguard-ui/windows/runner/resource.h +16 -0
  178. pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
  179. pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
  180. pumaguard-ui/windows/runner/utils.cpp +65 -0
  181. pumaguard-ui/windows/runner/utils.h +19 -0
  182. pumaguard-ui/windows/runner/win32_window.cpp +288 -0
  183. pumaguard-ui/windows/runner/win32_window.h +102 -0
  184. pumaguard-21.post27.dist-info/RECORD +0 -83
  185. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,671 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../services/api_service.dart';
3
+
4
+ class WifiSettingsScreen extends StatefulWidget {
5
+ final ApiService apiService;
6
+
7
+ const WifiSettingsScreen({super.key, required this.apiService});
8
+
9
+ @override
10
+ State<WifiSettingsScreen> createState() => _WifiSettingsScreenState();
11
+ }
12
+
13
+ class _WifiSettingsScreenState extends State<WifiSettingsScreen> {
14
+ bool _isLoading = false;
15
+ bool _isScanning = false;
16
+ String? _errorMessage;
17
+ String? _successMessage;
18
+
19
+ String _currentMode = 'unknown';
20
+ String? _currentSsid;
21
+ bool _isConnected = false;
22
+
23
+ List<WifiNetwork> _networks = [];
24
+ final TextEditingController _passwordController = TextEditingController();
25
+ final TextEditingController _apSsidController = TextEditingController();
26
+ final TextEditingController _apPasswordController = TextEditingController();
27
+
28
+ bool _obscurePassword = true;
29
+
30
+ @override
31
+ void initState() {
32
+ super.initState();
33
+ _loadCurrentMode();
34
+ }
35
+
36
+ @override
37
+ void dispose() {
38
+ _passwordController.dispose();
39
+ _apSsidController.dispose();
40
+ _apPasswordController.dispose();
41
+ super.dispose();
42
+ }
43
+
44
+ Future<void> _loadCurrentMode() async {
45
+ setState(() {
46
+ _isLoading = true;
47
+ _errorMessage = null;
48
+ });
49
+
50
+ try {
51
+ final response = await widget.apiService.getWifiMode();
52
+ setState(() {
53
+ _currentMode = response['mode'] as String? ?? 'unknown';
54
+ _currentSsid = response['ssid'] as String?;
55
+ _isConnected = response['connected'] as bool? ?? false;
56
+ _isLoading = false;
57
+ });
58
+ } catch (e) {
59
+ setState(() {
60
+ _errorMessage = 'Failed to load WiFi mode: $e';
61
+ _isLoading = false;
62
+ });
63
+ }
64
+ }
65
+
66
+ Future<void> _scanNetworks() async {
67
+ setState(() {
68
+ _isScanning = true;
69
+ _errorMessage = null;
70
+ _successMessage = null;
71
+ });
72
+
73
+ try {
74
+ final response = await widget.apiService.scanWifiNetworks();
75
+ final networksJson = response['networks'] as List<dynamic>;
76
+
77
+ setState(() {
78
+ _networks = networksJson
79
+ .map((json) => WifiNetwork.fromJson(json as Map<String, dynamic>))
80
+ .toList();
81
+ _isScanning = false;
82
+ });
83
+ } catch (e) {
84
+ setState(() {
85
+ _errorMessage = 'Failed to scan networks: $e';
86
+ _isScanning = false;
87
+ });
88
+ }
89
+ }
90
+
91
+ Future<void> _connectToNetwork(
92
+ String ssid,
93
+ String password,
94
+ bool isSecured,
95
+ ) async {
96
+ setState(() {
97
+ _isLoading = true;
98
+ _errorMessage = null;
99
+ _successMessage = null;
100
+ });
101
+
102
+ try {
103
+ await widget.apiService.setWifiMode(
104
+ mode: 'client',
105
+ ssid: ssid,
106
+ password: isSecured ? password : null,
107
+ );
108
+
109
+ setState(() {
110
+ _successMessage = 'Successfully connected to $ssid';
111
+ _isLoading = false;
112
+ _passwordController.clear();
113
+ });
114
+
115
+ // Reload current mode after a short delay
116
+ await Future.delayed(const Duration(seconds: 2));
117
+ await _loadCurrentMode();
118
+ } catch (e) {
119
+ setState(() {
120
+ _errorMessage = 'Failed to connect: $e';
121
+ _isLoading = false;
122
+ });
123
+ }
124
+ }
125
+
126
+ Future<void> _enableApMode() async {
127
+ final ssid = _apSsidController.text.trim();
128
+ final password = _apPasswordController.text.trim();
129
+
130
+ if (ssid.isEmpty) {
131
+ setState(() {
132
+ _errorMessage = 'SSID cannot be empty';
133
+ });
134
+ return;
135
+ }
136
+
137
+ if (password.isNotEmpty && password.length < 8) {
138
+ setState(() {
139
+ _errorMessage = 'Password must be at least 8 characters';
140
+ });
141
+ return;
142
+ }
143
+
144
+ setState(() {
145
+ _isLoading = true;
146
+ _errorMessage = null;
147
+ _successMessage = null;
148
+ });
149
+
150
+ try {
151
+ await widget.apiService.setWifiMode(
152
+ mode: 'ap',
153
+ ssid: ssid,
154
+ password: password.isEmpty ? null : password,
155
+ );
156
+
157
+ setState(() {
158
+ _successMessage = 'Access Point enabled: $ssid';
159
+ _isLoading = false;
160
+ _apSsidController.clear();
161
+ _apPasswordController.clear();
162
+ });
163
+
164
+ // Reload current mode after a short delay
165
+ await Future.delayed(const Duration(seconds: 2));
166
+ await _loadCurrentMode();
167
+ } catch (e) {
168
+ setState(() {
169
+ _errorMessage = 'Failed to enable AP mode: $e';
170
+ _isLoading = false;
171
+ });
172
+ }
173
+ }
174
+
175
+ void _showPasswordDialog(WifiNetwork network) {
176
+ _passwordController.clear();
177
+
178
+ showDialog(
179
+ context: context,
180
+ builder: (BuildContext context) {
181
+ return StatefulBuilder(
182
+ builder: (context, setDialogState) {
183
+ return AlertDialog(
184
+ title: Text('Connect to ${network.ssid}'),
185
+ content: Column(
186
+ mainAxisSize: MainAxisSize.min,
187
+ crossAxisAlignment: CrossAxisAlignment.start,
188
+ children: [
189
+ Text(
190
+ 'Signal: ${network.signal}%',
191
+ style: Theme.of(context).textTheme.bodySmall,
192
+ ),
193
+ const SizedBox(height: 16),
194
+ if (network.secured) ...[
195
+ TextField(
196
+ controller: _passwordController,
197
+ obscureText: _obscurePassword,
198
+ decoration: InputDecoration(
199
+ labelText: 'Password',
200
+ border: const OutlineInputBorder(),
201
+ suffixIcon: IconButton(
202
+ icon: Icon(
203
+ _obscurePassword
204
+ ? Icons.visibility
205
+ : Icons.visibility_off,
206
+ ),
207
+ onPressed: () {
208
+ setDialogState(() {
209
+ _obscurePassword = !_obscurePassword;
210
+ });
211
+ },
212
+ ),
213
+ ),
214
+ ),
215
+ ] else ...[
216
+ Text(
217
+ 'This is an open network (no password required)',
218
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
219
+ color: Theme.of(context).colorScheme.secondary,
220
+ ),
221
+ ),
222
+ ],
223
+ ],
224
+ ),
225
+ actions: [
226
+ TextButton(
227
+ onPressed: () => Navigator.of(context).pop(),
228
+ child: const Text('Cancel'),
229
+ ),
230
+ FilledButton(
231
+ onPressed: () {
232
+ Navigator.of(context).pop();
233
+ _connectToNetwork(
234
+ network.ssid,
235
+ _passwordController.text,
236
+ network.secured,
237
+ );
238
+ },
239
+ child: const Text('Connect'),
240
+ ),
241
+ ],
242
+ );
243
+ },
244
+ );
245
+ },
246
+ );
247
+ }
248
+
249
+ @override
250
+ Widget build(BuildContext context) {
251
+ return Scaffold(
252
+ appBar: AppBar(title: const Text('WiFi Settings')),
253
+ body: _isLoading && _networks.isEmpty
254
+ ? const Center(child: CircularProgressIndicator())
255
+ : SingleChildScrollView(
256
+ padding: const EdgeInsets.all(16),
257
+ child: Column(
258
+ crossAxisAlignment: CrossAxisAlignment.stretch,
259
+ children: [
260
+ // Error/Success Messages
261
+ if (_errorMessage != null) ...[
262
+ Card(
263
+ color: Theme.of(context).colorScheme.errorContainer,
264
+ child: Padding(
265
+ padding: const EdgeInsets.all(16),
266
+ child: Row(
267
+ children: [
268
+ Icon(
269
+ Icons.error_outline,
270
+ color: Theme.of(context).colorScheme.error,
271
+ ),
272
+ const SizedBox(width: 8),
273
+ Expanded(
274
+ child: Text(
275
+ _errorMessage!,
276
+ style: TextStyle(
277
+ color: Theme.of(context).colorScheme.error,
278
+ ),
279
+ ),
280
+ ),
281
+ IconButton(
282
+ icon: const Icon(Icons.close),
283
+ onPressed: () {
284
+ setState(() => _errorMessage = null);
285
+ },
286
+ ),
287
+ ],
288
+ ),
289
+ ),
290
+ ),
291
+ const SizedBox(height: 16),
292
+ ],
293
+ if (_successMessage != null) ...[
294
+ Card(
295
+ color: Colors.green.shade50,
296
+ child: Padding(
297
+ padding: const EdgeInsets.all(16),
298
+ child: Row(
299
+ children: [
300
+ const Icon(Icons.check_circle, color: Colors.green),
301
+ const SizedBox(width: 8),
302
+ Expanded(
303
+ child: Text(
304
+ _successMessage!,
305
+ style: const TextStyle(color: Colors.green),
306
+ ),
307
+ ),
308
+ IconButton(
309
+ icon: const Icon(Icons.close),
310
+ onPressed: () {
311
+ setState(() => _successMessage = null);
312
+ },
313
+ ),
314
+ ],
315
+ ),
316
+ ),
317
+ ),
318
+ const SizedBox(height: 16),
319
+ ],
320
+
321
+ // Current Status
322
+ Card(
323
+ child: Padding(
324
+ padding: const EdgeInsets.all(16),
325
+ child: Column(
326
+ crossAxisAlignment: CrossAxisAlignment.start,
327
+ children: [
328
+ Row(
329
+ children: [
330
+ Icon(
331
+ _currentMode == 'ap'
332
+ ? Icons.wifi_tethering
333
+ : Icons.wifi,
334
+ color: Theme.of(context).colorScheme.primary,
335
+ ),
336
+ const SizedBox(width: 8),
337
+ Text(
338
+ 'Current Status',
339
+ style: Theme.of(context).textTheme.titleLarge,
340
+ ),
341
+ ],
342
+ ),
343
+ const SizedBox(height: 16),
344
+ _buildStatusRow(
345
+ 'Mode',
346
+ _getModeDisplay(_currentMode),
347
+ ),
348
+ const SizedBox(height: 8),
349
+ if (_currentSsid != null)
350
+ _buildStatusRow('Network', _currentSsid!),
351
+ const SizedBox(height: 8),
352
+ _buildStatusRow(
353
+ 'Status',
354
+ _isConnected ? 'Connected' : 'Disconnected',
355
+ statusColor: _isConnected
356
+ ? Colors.green
357
+ : Colors.grey,
358
+ ),
359
+ ],
360
+ ),
361
+ ),
362
+ ),
363
+ const SizedBox(height: 24),
364
+
365
+ // Mode Selection Tabs
366
+ DefaultTabController(
367
+ length: 2,
368
+ child: Column(
369
+ children: [
370
+ TabBar(
371
+ tabs: const [
372
+ Tab(
373
+ icon: Icon(Icons.wifi),
374
+ text: 'Connect to Network',
375
+ ),
376
+ Tab(
377
+ icon: Icon(Icons.wifi_tethering),
378
+ text: 'Create Hotspot',
379
+ ),
380
+ ],
381
+ ),
382
+ SizedBox(
383
+ height: 500,
384
+ child: TabBarView(
385
+ children: [
386
+ _buildClientModeTab(),
387
+ _buildApModeTab(),
388
+ ],
389
+ ),
390
+ ),
391
+ ],
392
+ ),
393
+ ),
394
+ ],
395
+ ),
396
+ ),
397
+ );
398
+ }
399
+
400
+ Widget _buildStatusRow(String label, String value, {Color? statusColor}) {
401
+ return Row(
402
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
403
+ children: [
404
+ Text(
405
+ label,
406
+ style: Theme.of(
407
+ context,
408
+ ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
409
+ ),
410
+ Text(
411
+ value,
412
+ style: Theme.of(
413
+ context,
414
+ ).textTheme.bodyMedium?.copyWith(color: statusColor),
415
+ ),
416
+ ],
417
+ );
418
+ }
419
+
420
+ String _getModeDisplay(String mode) {
421
+ switch (mode) {
422
+ case 'ap':
423
+ return 'Access Point (Hotspot)';
424
+ case 'client':
425
+ return 'Client (Connected)';
426
+ default:
427
+ return 'Unknown';
428
+ }
429
+ }
430
+
431
+ Widget _buildClientModeTab() {
432
+ return Padding(
433
+ padding: const EdgeInsets.all(16),
434
+ child: Column(
435
+ crossAxisAlignment: CrossAxisAlignment.stretch,
436
+ children: [
437
+ FilledButton.icon(
438
+ onPressed: _isScanning ? null : _scanNetworks,
439
+ icon: _isScanning
440
+ ? const SizedBox(
441
+ width: 16,
442
+ height: 16,
443
+ child: CircularProgressIndicator(strokeWidth: 2),
444
+ )
445
+ : const Icon(Icons.refresh),
446
+ label: Text(_isScanning ? 'Scanning...' : 'Scan for Networks'),
447
+ ),
448
+ const SizedBox(height: 16),
449
+ if (_networks.isEmpty && !_isScanning) ...[
450
+ Center(
451
+ child: Column(
452
+ children: [
453
+ Icon(
454
+ Icons.wifi_off,
455
+ size: 64,
456
+ color: Theme.of(context).colorScheme.outline,
457
+ ),
458
+ const SizedBox(height: 16),
459
+ Text(
460
+ 'No networks found',
461
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
462
+ color: Theme.of(context).colorScheme.outline,
463
+ ),
464
+ ),
465
+ const SizedBox(height: 8),
466
+ Text(
467
+ 'Tap the button above to scan',
468
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
469
+ color: Theme.of(context).colorScheme.outline,
470
+ ),
471
+ ),
472
+ ],
473
+ ),
474
+ ),
475
+ ] else ...[
476
+ Expanded(
477
+ child: ListView.builder(
478
+ itemCount: _networks.length,
479
+ itemBuilder: (context, index) {
480
+ final network = _networks[index];
481
+ return Card(
482
+ child: ListTile(
483
+ leading: Icon(
484
+ _getSignalIcon(network.signal),
485
+ color: network.connected
486
+ ? Colors.green
487
+ : Theme.of(context).colorScheme.primary,
488
+ ),
489
+ title: Row(
490
+ children: [
491
+ Expanded(child: Text(network.ssid)),
492
+ if (network.secured)
493
+ Icon(
494
+ Icons.lock,
495
+ size: 16,
496
+ color: Theme.of(context).colorScheme.outline,
497
+ ),
498
+ if (network.connected)
499
+ Padding(
500
+ padding: const EdgeInsets.only(left: 8),
501
+ child: Chip(
502
+ label: const Text('Connected'),
503
+ backgroundColor: Colors.green.shade100,
504
+ labelStyle: const TextStyle(
505
+ color: Colors.green,
506
+ fontSize: 12,
507
+ ),
508
+ ),
509
+ ),
510
+ ],
511
+ ),
512
+ subtitle: Text(
513
+ 'Signal: ${network.signal}% • ${network.security}',
514
+ style: Theme.of(context).textTheme.bodySmall,
515
+ ),
516
+ trailing: network.connected
517
+ ? null
518
+ : IconButton(
519
+ icon: const Icon(Icons.arrow_forward),
520
+ onPressed: () => _showPasswordDialog(network),
521
+ ),
522
+ ),
523
+ );
524
+ },
525
+ ),
526
+ ),
527
+ ],
528
+ ],
529
+ ),
530
+ );
531
+ }
532
+
533
+ Widget _buildApModeTab() {
534
+ return Padding(
535
+ padding: const EdgeInsets.all(16),
536
+ child: SingleChildScrollView(
537
+ child: Column(
538
+ crossAxisAlignment: CrossAxisAlignment.stretch,
539
+ children: [
540
+ Text(
541
+ 'Create your own WiFi hotspot',
542
+ style: Theme.of(context).textTheme.titleMedium,
543
+ ),
544
+ const SizedBox(height: 8),
545
+ Text(
546
+ 'Other devices will be able to connect to this network',
547
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
548
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
549
+ ),
550
+ ),
551
+ const SizedBox(height: 24),
552
+ TextField(
553
+ controller: _apSsidController,
554
+ decoration: const InputDecoration(
555
+ labelText: 'Network Name (SSID)',
556
+ hintText: 'pumaguard',
557
+ border: OutlineInputBorder(),
558
+ helperText: 'The name others will see',
559
+ ),
560
+ ),
561
+ const SizedBox(height: 16),
562
+ TextField(
563
+ controller: _apPasswordController,
564
+ obscureText: _obscurePassword,
565
+ decoration: InputDecoration(
566
+ labelText: 'Password (optional)',
567
+ hintText: 'Leave empty for open network',
568
+ border: const OutlineInputBorder(),
569
+ helperText: 'Minimum 8 characters if set',
570
+ suffixIcon: IconButton(
571
+ icon: Icon(
572
+ _obscurePassword ? Icons.visibility : Icons.visibility_off,
573
+ ),
574
+ onPressed: () {
575
+ setState(() {
576
+ _obscurePassword = !_obscurePassword;
577
+ });
578
+ },
579
+ ),
580
+ ),
581
+ ),
582
+ const SizedBox(height: 24),
583
+ FilledButton.icon(
584
+ onPressed: _isLoading ? null : _enableApMode,
585
+ icon: _isLoading
586
+ ? const SizedBox(
587
+ width: 16,
588
+ height: 16,
589
+ child: CircularProgressIndicator(strokeWidth: 2),
590
+ )
591
+ : const Icon(Icons.wifi_tethering),
592
+ label: const Text('Enable Hotspot'),
593
+ ),
594
+ const SizedBox(height: 16),
595
+ Card(
596
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
597
+ child: Padding(
598
+ padding: const EdgeInsets.all(16),
599
+ child: Column(
600
+ crossAxisAlignment: CrossAxisAlignment.start,
601
+ children: [
602
+ Row(
603
+ children: [
604
+ Icon(
605
+ Icons.info_outline,
606
+ size: 20,
607
+ color: Theme.of(context).colorScheme.primary,
608
+ ),
609
+ const SizedBox(width: 8),
610
+ Text(
611
+ 'Information',
612
+ style: Theme.of(context).textTheme.titleSmall,
613
+ ),
614
+ ],
615
+ ),
616
+ const SizedBox(height: 8),
617
+ Text(
618
+ '• Hotspot mode allows cameras and other devices to connect directly to this device\n'
619
+ '• You will not have internet access in hotspot mode\n'
620
+ '• The device IP will be 192.168.52.1\n'
621
+ '• To switch back, use the "Connect to Network" tab',
622
+ style: Theme.of(context).textTheme.bodySmall,
623
+ ),
624
+ ],
625
+ ),
626
+ ),
627
+ ),
628
+ ],
629
+ ),
630
+ ),
631
+ );
632
+ }
633
+
634
+ IconData _getSignalIcon(int signal) {
635
+ if (signal >= 75) {
636
+ return Icons.signal_wifi_4_bar;
637
+ } else if (signal >= 50) {
638
+ return Icons.network_wifi_3_bar;
639
+ } else if (signal >= 25) {
640
+ return Icons.network_wifi_2_bar;
641
+ } else {
642
+ return Icons.network_wifi_1_bar;
643
+ }
644
+ }
645
+ }
646
+
647
+ class WifiNetwork {
648
+ final String ssid;
649
+ final int signal;
650
+ final String security;
651
+ final bool secured;
652
+ final bool connected;
653
+
654
+ WifiNetwork({
655
+ required this.ssid,
656
+ required this.signal,
657
+ required this.security,
658
+ required this.secured,
659
+ required this.connected,
660
+ });
661
+
662
+ factory WifiNetwork.fromJson(Map<String, dynamic> json) {
663
+ return WifiNetwork(
664
+ ssid: json['ssid'] as String,
665
+ signal: json['signal'] as int,
666
+ security: json['security'] as String,
667
+ secured: json['secured'] as bool,
668
+ connected: json['connected'] as bool? ?? false,
669
+ );
670
+ }
671
+ }