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,545 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
4
|
+
import 'package:provider/provider.dart';
|
|
5
|
+
import 'package:web/web.dart' as web;
|
|
6
|
+
import '../models/status.dart';
|
|
7
|
+
import '../models/camera.dart';
|
|
8
|
+
import '../services/api_service.dart';
|
|
9
|
+
import '../services/camera_events_service.dart';
|
|
10
|
+
import '../version.dart';
|
|
11
|
+
import 'settings_screen.dart';
|
|
12
|
+
import 'directories_screen.dart';
|
|
13
|
+
import 'image_browser_screen.dart';
|
|
14
|
+
|
|
15
|
+
class HomeScreen extends StatefulWidget {
|
|
16
|
+
const HomeScreen({super.key});
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
State<HomeScreen> createState() => _HomeScreenState();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class _HomeScreenState extends State<HomeScreen> {
|
|
23
|
+
Status? _status;
|
|
24
|
+
bool _isLoading = true;
|
|
25
|
+
String? _error;
|
|
26
|
+
List<Camera> _cameras = [];
|
|
27
|
+
Timer? _cameraPollingTimer;
|
|
28
|
+
CameraEventsService? _cameraEventsService;
|
|
29
|
+
StreamSubscription<CameraEvent>? _eventsSubscription;
|
|
30
|
+
|
|
31
|
+
// Polling interval for camera status updates (in seconds)
|
|
32
|
+
// This serves as a fallback if SSE connection fails
|
|
33
|
+
static const int _cameraPollingInterval = 30;
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
void initState() {
|
|
37
|
+
super.initState();
|
|
38
|
+
_loadStatus();
|
|
39
|
+
_loadCameras();
|
|
40
|
+
_startCameraPolling();
|
|
41
|
+
_initializeCameraEvents();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
void dispose() {
|
|
46
|
+
_cameraPollingTimer?.cancel();
|
|
47
|
+
_eventsSubscription?.cancel();
|
|
48
|
+
_cameraEventsService?.dispose();
|
|
49
|
+
super.dispose();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void _initializeCameraEvents() {
|
|
53
|
+
try {
|
|
54
|
+
final apiService = context.read<ApiService>();
|
|
55
|
+
final baseUrl = apiService.getApiUrl('').replaceAll('/api/', '');
|
|
56
|
+
|
|
57
|
+
_cameraEventsService = CameraEventsService(baseUrl);
|
|
58
|
+
|
|
59
|
+
// Subscribe to camera events
|
|
60
|
+
_eventsSubscription = _cameraEventsService!.events.listen(
|
|
61
|
+
_handleCameraEvent,
|
|
62
|
+
onError: (error) {
|
|
63
|
+
debugPrint('[HomeScreen] Camera events error: $error');
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Start listening for events
|
|
68
|
+
_cameraEventsService!.startListening();
|
|
69
|
+
debugPrint('[HomeScreen] Camera events service initialized');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
debugPrint('[HomeScreen] Error initializing camera events: $e');
|
|
72
|
+
// Fallback to polling only if SSE fails
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void _handleCameraEvent(CameraEvent event) {
|
|
77
|
+
debugPrint('[HomeScreen] Camera event received: ${event.type}');
|
|
78
|
+
|
|
79
|
+
// Handle different event types
|
|
80
|
+
switch (event.type) {
|
|
81
|
+
case CameraEventType.connected:
|
|
82
|
+
// SSE connection established
|
|
83
|
+
debugPrint('[HomeScreen] Connected to camera events stream');
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case CameraEventType.cameraConnected:
|
|
87
|
+
case CameraEventType.cameraDisconnected:
|
|
88
|
+
case CameraEventType.cameraAdded:
|
|
89
|
+
case CameraEventType.cameraStatusChangedOnline:
|
|
90
|
+
case CameraEventType.cameraStatusChangedOffline:
|
|
91
|
+
// Camera status changed - reload camera list
|
|
92
|
+
_loadCameras();
|
|
93
|
+
|
|
94
|
+
// Show a subtle notification
|
|
95
|
+
if (mounted && event.camera != null) {
|
|
96
|
+
final isOnline =
|
|
97
|
+
event.type == CameraEventType.cameraConnected ||
|
|
98
|
+
event.type == CameraEventType.cameraStatusChangedOnline;
|
|
99
|
+
|
|
100
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
101
|
+
SnackBar(
|
|
102
|
+
content: Text(
|
|
103
|
+
'${event.camera!.hostname} is now ${isOnline ? "online" : "offline"}',
|
|
104
|
+
),
|
|
105
|
+
duration: const Duration(seconds: 2),
|
|
106
|
+
backgroundColor: isOnline ? Colors.green : Colors.orange,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case CameraEventType.unknown:
|
|
113
|
+
debugPrint('[HomeScreen] Unknown camera event type');
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void _startCameraPolling() {
|
|
119
|
+
// Set up periodic timer to check for camera status changes
|
|
120
|
+
_cameraPollingTimer = Timer.periodic(
|
|
121
|
+
const Duration(seconds: _cameraPollingInterval),
|
|
122
|
+
(_) => _loadCameras(),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Future<void> _refresh() async {
|
|
127
|
+
await Future.wait([_loadStatus(), _loadCameras()]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Future<void> _loadStatus() async {
|
|
131
|
+
setState(() {
|
|
132
|
+
_isLoading = true;
|
|
133
|
+
_error = null;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
final apiService = context.read<ApiService>();
|
|
138
|
+
final status = await apiService.getStatus();
|
|
139
|
+
setState(() {
|
|
140
|
+
_status = status;
|
|
141
|
+
_isLoading = false;
|
|
142
|
+
});
|
|
143
|
+
} catch (e) {
|
|
144
|
+
setState(() {
|
|
145
|
+
_error = e.toString();
|
|
146
|
+
_isLoading = false;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Future<void> _loadCameras() async {
|
|
152
|
+
try {
|
|
153
|
+
final apiService = context.read<ApiService>();
|
|
154
|
+
final cameras = await apiService.getCameras();
|
|
155
|
+
|
|
156
|
+
// Only update state if cameras have changed to avoid unnecessary rebuilds
|
|
157
|
+
if (_camerasChanged(cameras)) {
|
|
158
|
+
setState(() {
|
|
159
|
+
_cameras = cameras;
|
|
160
|
+
});
|
|
161
|
+
debugPrint(
|
|
162
|
+
'[HomeScreen._loadCameras] Camera list updated: ${cameras.length} cameras',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
debugPrint('[HomeScreen._loadCameras] Error loading cameras: $e');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
bool _camerasChanged(List<Camera> newCameras) {
|
|
171
|
+
if (_cameras.length != newCameras.length) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if any camera status has changed
|
|
176
|
+
for (int i = 0; i < _cameras.length; i++) {
|
|
177
|
+
final oldCamera = _cameras[i];
|
|
178
|
+
final newCamera = newCameras.firstWhere(
|
|
179
|
+
(c) => c.macAddress == oldCamera.macAddress,
|
|
180
|
+
orElse: () => newCameras[i],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (oldCamera.status != newCamera.status ||
|
|
184
|
+
oldCamera.ipAddress != newCamera.ipAddress ||
|
|
185
|
+
oldCamera.hostname != newCamera.hostname) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
Widget build(BuildContext context) {
|
|
195
|
+
return Scaffold(
|
|
196
|
+
appBar: AppBar(
|
|
197
|
+
title: Row(
|
|
198
|
+
mainAxisSize: MainAxisSize.min,
|
|
199
|
+
children: [
|
|
200
|
+
Icon(Icons.pets, color: Theme.of(context).colorScheme.primary),
|
|
201
|
+
const SizedBox(width: 8),
|
|
202
|
+
const Text('PumaGuard'),
|
|
203
|
+
],
|
|
204
|
+
),
|
|
205
|
+
actions: [
|
|
206
|
+
IconButton(
|
|
207
|
+
icon: const Icon(Icons.refresh),
|
|
208
|
+
onPressed: _refresh,
|
|
209
|
+
tooltip: 'Refresh Status',
|
|
210
|
+
),
|
|
211
|
+
],
|
|
212
|
+
),
|
|
213
|
+
body: RefreshIndicator(onRefresh: _refresh, child: _buildBody()),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Widget _buildBody() {
|
|
218
|
+
if (_isLoading) {
|
|
219
|
+
return const Center(child: CircularProgressIndicator());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (_error != null) {
|
|
223
|
+
return Center(
|
|
224
|
+
child: Column(
|
|
225
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
226
|
+
children: [
|
|
227
|
+
Icon(
|
|
228
|
+
Icons.error_outline,
|
|
229
|
+
size: 64,
|
|
230
|
+
color: Theme.of(context).colorScheme.error,
|
|
231
|
+
),
|
|
232
|
+
const SizedBox(height: 16),
|
|
233
|
+
Text(
|
|
234
|
+
'Connection Error',
|
|
235
|
+
style: Theme.of(context).textTheme.headlineSmall,
|
|
236
|
+
),
|
|
237
|
+
const SizedBox(height: 8),
|
|
238
|
+
Padding(
|
|
239
|
+
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
240
|
+
child: Text(
|
|
241
|
+
_error!,
|
|
242
|
+
textAlign: TextAlign.center,
|
|
243
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
244
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
const SizedBox(height: 24),
|
|
249
|
+
ElevatedButton.icon(
|
|
250
|
+
onPressed: _loadStatus,
|
|
251
|
+
icon: const Icon(Icons.refresh),
|
|
252
|
+
label: const Text('Retry'),
|
|
253
|
+
),
|
|
254
|
+
],
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return SingleChildScrollView(
|
|
260
|
+
physics: const AlwaysScrollableScrollPhysics(),
|
|
261
|
+
padding: const EdgeInsets.all(16),
|
|
262
|
+
child: Column(
|
|
263
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
264
|
+
children: [
|
|
265
|
+
_buildStatusCard(),
|
|
266
|
+
const SizedBox(height: 16),
|
|
267
|
+
_buildQuickActionsCard(),
|
|
268
|
+
const SizedBox(height: 16),
|
|
269
|
+
_buildInfoCard(),
|
|
270
|
+
],
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
Widget _buildStatusCard() {
|
|
276
|
+
final isRunning = _status?.isRunning ?? false;
|
|
277
|
+
|
|
278
|
+
return Card(
|
|
279
|
+
child: Padding(
|
|
280
|
+
padding: const EdgeInsets.all(24),
|
|
281
|
+
child: Column(
|
|
282
|
+
children: [
|
|
283
|
+
Icon(
|
|
284
|
+
isRunning ? Icons.check_circle : Icons.warning,
|
|
285
|
+
size: 64,
|
|
286
|
+
color: isRunning ? Colors.green : Colors.orange,
|
|
287
|
+
),
|
|
288
|
+
const SizedBox(height: 16),
|
|
289
|
+
Text(
|
|
290
|
+
isRunning ? 'System Running' : 'System Status Unknown',
|
|
291
|
+
style: Theme.of(context).textTheme.headlineSmall,
|
|
292
|
+
),
|
|
293
|
+
const SizedBox(height: 8),
|
|
294
|
+
Text(
|
|
295
|
+
isRunning
|
|
296
|
+
? 'PumaGuard is actively monitoring'
|
|
297
|
+
: 'Check system configuration',
|
|
298
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
299
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
Widget _buildQuickActionsCard() {
|
|
309
|
+
return Card(
|
|
310
|
+
child: Padding(
|
|
311
|
+
padding: const EdgeInsets.all(16),
|
|
312
|
+
child: Column(
|
|
313
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
314
|
+
children: [
|
|
315
|
+
Text(
|
|
316
|
+
'Quick Actions',
|
|
317
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
318
|
+
),
|
|
319
|
+
const SizedBox(height: 16),
|
|
320
|
+
_buildActionButton(
|
|
321
|
+
icon: Icons.settings,
|
|
322
|
+
label: 'Settings',
|
|
323
|
+
description: 'Configure YOLO and classifier parameters',
|
|
324
|
+
onTap: () {
|
|
325
|
+
Navigator.push(
|
|
326
|
+
context,
|
|
327
|
+
MaterialPageRoute(
|
|
328
|
+
builder: (context) => const SettingsScreen(),
|
|
329
|
+
),
|
|
330
|
+
).then((_) => _refresh());
|
|
331
|
+
},
|
|
332
|
+
),
|
|
333
|
+
const Divider(),
|
|
334
|
+
_buildActionButton(
|
|
335
|
+
icon: Icons.folder,
|
|
336
|
+
label: 'Directories',
|
|
337
|
+
description: 'Manage monitored image directories',
|
|
338
|
+
onTap: () {
|
|
339
|
+
Navigator.push(
|
|
340
|
+
context,
|
|
341
|
+
MaterialPageRoute(
|
|
342
|
+
builder: (context) => const DirectoriesScreen(),
|
|
343
|
+
),
|
|
344
|
+
).then((_) => _refresh());
|
|
345
|
+
},
|
|
346
|
+
),
|
|
347
|
+
const Divider(),
|
|
348
|
+
_buildActionButton(
|
|
349
|
+
icon: Icons.photo_library,
|
|
350
|
+
label: 'Image Browser',
|
|
351
|
+
description: 'Browse and download images from watched folders',
|
|
352
|
+
onTap: () {
|
|
353
|
+
Navigator.push(
|
|
354
|
+
context,
|
|
355
|
+
MaterialPageRoute(
|
|
356
|
+
builder: (context) => const ImageBrowserScreen(),
|
|
357
|
+
),
|
|
358
|
+
).then((_) => _refresh());
|
|
359
|
+
},
|
|
360
|
+
),
|
|
361
|
+
// Show detected cameras if any
|
|
362
|
+
if (_cameras.isNotEmpty) ...[
|
|
363
|
+
const Divider(),
|
|
364
|
+
const SizedBox(height: 16),
|
|
365
|
+
const Divider(thickness: 2),
|
|
366
|
+
const SizedBox(height: 8),
|
|
367
|
+
Text(
|
|
368
|
+
'Detected Cameras',
|
|
369
|
+
style: Theme.of(context).textTheme.titleMedium,
|
|
370
|
+
),
|
|
371
|
+
const SizedBox(height: 8),
|
|
372
|
+
..._cameras.map((camera) {
|
|
373
|
+
return Column(
|
|
374
|
+
children: [
|
|
375
|
+
_buildActionButton(
|
|
376
|
+
icon: Icons.videocam,
|
|
377
|
+
label: camera.displayName,
|
|
378
|
+
description: 'IP: ${camera.ipAddress} • ${camera.status}',
|
|
379
|
+
onTap: () => _openCameraByIp(camera.ipAddress),
|
|
380
|
+
),
|
|
381
|
+
if (camera != _cameras.last) const Divider(),
|
|
382
|
+
],
|
|
383
|
+
);
|
|
384
|
+
}),
|
|
385
|
+
],
|
|
386
|
+
],
|
|
387
|
+
),
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
Future<void> _openCameraByIp(String ipAddress) async {
|
|
393
|
+
try {
|
|
394
|
+
debugPrint('[HomeScreen._openCameraByIp] Opening camera at: $ipAddress');
|
|
395
|
+
|
|
396
|
+
if (ipAddress.isEmpty) {
|
|
397
|
+
if (!mounted) return;
|
|
398
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
399
|
+
const SnackBar(
|
|
400
|
+
content: Text('Invalid camera IP address.'),
|
|
401
|
+
duration: Duration(seconds: 3),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Add http:// if no scheme is present
|
|
408
|
+
String urlToOpen = ipAddress;
|
|
409
|
+
if (!ipAddress.startsWith('http://') &&
|
|
410
|
+
!ipAddress.startsWith('https://')) {
|
|
411
|
+
urlToOpen = 'http://$ipAddress';
|
|
412
|
+
}
|
|
413
|
+
debugPrint('[HomeScreen._openCameraByIp] URL to open: "$urlToOpen"');
|
|
414
|
+
|
|
415
|
+
// On web, use native JavaScript window.open()
|
|
416
|
+
if (kIsWeb) {
|
|
417
|
+
web.window.open(urlToOpen, '_blank');
|
|
418
|
+
debugPrint('[HomeScreen._openCameraByIp] URL opened successfully');
|
|
419
|
+
} else {
|
|
420
|
+
if (!mounted) return;
|
|
421
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
422
|
+
const SnackBar(
|
|
423
|
+
content: Text(
|
|
424
|
+
'Opening camera URL is only supported on web currently.',
|
|
425
|
+
),
|
|
426
|
+
duration: Duration(seconds: 3),
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
} catch (e, stackTrace) {
|
|
431
|
+
debugPrint('[HomeScreen._openCameraByIp] Error: $e');
|
|
432
|
+
debugPrint('[HomeScreen._openCameraByIp] Stack trace: $stackTrace');
|
|
433
|
+
if (!mounted) return;
|
|
434
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
435
|
+
SnackBar(
|
|
436
|
+
content: Text('Error opening camera: $e'),
|
|
437
|
+
duration: const Duration(seconds: 3),
|
|
438
|
+
backgroundColor: Colors.red,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
Widget _buildActionButton({
|
|
445
|
+
required IconData icon,
|
|
446
|
+
required String label,
|
|
447
|
+
required String description,
|
|
448
|
+
required VoidCallback onTap,
|
|
449
|
+
}) {
|
|
450
|
+
return InkWell(
|
|
451
|
+
onTap: onTap,
|
|
452
|
+
borderRadius: BorderRadius.circular(8),
|
|
453
|
+
child: Padding(
|
|
454
|
+
padding: const EdgeInsets.all(12),
|
|
455
|
+
child: Row(
|
|
456
|
+
children: [
|
|
457
|
+
Container(
|
|
458
|
+
padding: const EdgeInsets.all(12),
|
|
459
|
+
decoration: BoxDecoration(
|
|
460
|
+
color: Theme.of(context).colorScheme.primaryContainer,
|
|
461
|
+
borderRadius: BorderRadius.circular(8),
|
|
462
|
+
),
|
|
463
|
+
child: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
|
464
|
+
),
|
|
465
|
+
const SizedBox(width: 16),
|
|
466
|
+
Expanded(
|
|
467
|
+
child: Column(
|
|
468
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
469
|
+
children: [
|
|
470
|
+
Text(label, style: Theme.of(context).textTheme.titleMedium),
|
|
471
|
+
const SizedBox(height: 4),
|
|
472
|
+
Text(
|
|
473
|
+
description,
|
|
474
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
475
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
],
|
|
479
|
+
),
|
|
480
|
+
),
|
|
481
|
+
Icon(
|
|
482
|
+
Icons.chevron_right,
|
|
483
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
484
|
+
),
|
|
485
|
+
],
|
|
486
|
+
),
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
Widget _buildInfoCard() {
|
|
492
|
+
return Card(
|
|
493
|
+
child: Padding(
|
|
494
|
+
padding: const EdgeInsets.all(16),
|
|
495
|
+
child: Column(
|
|
496
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
497
|
+
children: [
|
|
498
|
+
Text(
|
|
499
|
+
'System Information',
|
|
500
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
501
|
+
),
|
|
502
|
+
const SizedBox(height: 16),
|
|
503
|
+
_buildInfoRow('Backend Version', _status?.version ?? 'Unknown'),
|
|
504
|
+
const SizedBox(height: 12),
|
|
505
|
+
_buildInfoRow('UI Version', appVersion),
|
|
506
|
+
const SizedBox(height: 12),
|
|
507
|
+
_buildInfoRow(
|
|
508
|
+
'Host',
|
|
509
|
+
'${_status?.host ?? 'Unknown'}:${_status?.port ?? 0}',
|
|
510
|
+
),
|
|
511
|
+
const SizedBox(height: 12),
|
|
512
|
+
_buildInfoRow(
|
|
513
|
+
'Monitored Directories',
|
|
514
|
+
'${_status?.directoriesCount ?? 0}',
|
|
515
|
+
),
|
|
516
|
+
const SizedBox(height: 12),
|
|
517
|
+
_buildInfoRow('Status', _status?.status.toUpperCase() ?? 'UNKNOWN'),
|
|
518
|
+
const SizedBox(height: 12),
|
|
519
|
+
_buildInfoRow('Uptime', _status?.uptimeFormatted ?? 'Unknown'),
|
|
520
|
+
],
|
|
521
|
+
),
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
Widget _buildInfoRow(String label, String value) {
|
|
527
|
+
return Row(
|
|
528
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
529
|
+
children: [
|
|
530
|
+
Text(
|
|
531
|
+
label,
|
|
532
|
+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
533
|
+
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
Text(
|
|
537
|
+
value,
|
|
538
|
+
style: Theme.of(
|
|
539
|
+
context,
|
|
540
|
+
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
541
|
+
),
|
|
542
|
+
],
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|