pumaguard 21.post29__py3-none-any.whl → 21.post83__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pumaguard/presets.py +1 -0
- pumaguard/pumaguard-ui/.last_build_id +1 -1
- pumaguard/pumaguard-ui/assets/NOTICES +621 -71
- pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
- pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
- pumaguard/pumaguard-ui/main.dart.js +28869 -28787
- pumaguard/web_routes/dhcp.py +311 -54
- pumaguard/web_routes/diagnostics.py +6 -0
- pumaguard/web_routes/settings.py +13 -0
- pumaguard/web_ui.py +29 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
- pumaguard-21.post83.dist-info/RECORD +254 -0
- pumaguard-ui/.gitignore +48 -0
- pumaguard-ui/.metadata +45 -0
- pumaguard-ui/API_REFERENCE.md +717 -0
- pumaguard-ui/LICENSE +201 -0
- pumaguard-ui/Makefile +36 -0
- pumaguard-ui/README.md +371 -0
- pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
- pumaguard-ui/analysis_options.yaml +28 -0
- pumaguard-ui/android/.gitignore +14 -0
- pumaguard-ui/android/app/build.gradle.kts +44 -0
- pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
- pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
- pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
- pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
- pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
- pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
- pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
- pumaguard-ui/android/build.gradle.kts +24 -0
- pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- pumaguard-ui/android/gradle.properties +2 -0
- pumaguard-ui/android/settings.gradle.kts +26 -0
- pumaguard-ui/fonts/README.md +38 -0
- pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
- pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
- pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
- pumaguard-ui/fonts/download_fonts.sh +76 -0
- pumaguard-ui/ios/.gitignore +34 -0
- pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
- pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
- pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
- pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
- pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
- pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
- pumaguard-ui/ios/Runner/Info.plist +49 -0
- pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
- pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
- pumaguard-ui/lib/main.dart +56 -0
- pumaguard-ui/lib/models/camera.dart +45 -0
- pumaguard-ui/lib/models/plug.dart +45 -0
- pumaguard-ui/lib/models/settings.dart +112 -0
- pumaguard-ui/lib/models/status.dart +58 -0
- pumaguard-ui/lib/screens/directories_screen.dart +319 -0
- pumaguard-ui/lib/screens/home_screen.dart +545 -0
- pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
- pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
- pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
- pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
- pumaguard-ui/lib/services/api_service.dart +717 -0
- pumaguard-ui/lib/services/camera_events_service.dart +195 -0
- pumaguard-ui/lib/services/mdns_service.dart +4 -0
- pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
- pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
- pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
- pumaguard-ui/lib/utils/download_helper.dart +2 -0
- pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
- pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
- pumaguard-ui/lib/utils/platform_url.dart +10 -0
- pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
- pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
- pumaguard-ui/linux/.gitignore +1 -0
- pumaguard-ui/linux/CMakeLists.txt +128 -0
- pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
- pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
- pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
- pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
- pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
- pumaguard-ui/linux/runner/main.cc +6 -0
- pumaguard-ui/linux/runner/my_application.cc +148 -0
- pumaguard-ui/linux/runner/my_application.h +21 -0
- pumaguard-ui/macos/.gitignore +7 -0
- pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
- pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
- pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
- pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
- pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
- pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
- pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
- pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
- pumaguard-ui/macos/Runner/Info.plist +32 -0
- pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
- pumaguard-ui/macos/Runner/Release.entitlements +8 -0
- pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
- pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
- pumaguard-ui/pubspec.lock +882 -0
- pumaguard-ui/pubspec.yaml +125 -0
- pumaguard-ui/test/models/camera_test.dart +515 -0
- pumaguard-ui/test/models/plug_test.dart +499 -0
- pumaguard-ui/test/models/settings_test.dart +903 -0
- pumaguard-ui/test/models/status_test.dart +707 -0
- pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
- pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
- pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
- pumaguard-ui/test/widget_test.dart.skip +38 -0
- pumaguard-ui/web/favicon.png +0 -0
- pumaguard-ui/web/icons/Icon-192.png +0 -0
- pumaguard-ui/web/icons/Icon-512.png +0 -0
- pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
- pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
- pumaguard-ui/web/index.html +38 -0
- pumaguard-ui/web/manifest.json +35 -0
- pumaguard-ui/windows/.gitignore +17 -0
- pumaguard-ui/windows/CMakeLists.txt +108 -0
- pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
- pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
- pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
- pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
- pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
- pumaguard-ui/windows/runner/Runner.rc +121 -0
- pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
- pumaguard-ui/windows/runner/flutter_window.h +33 -0
- pumaguard-ui/windows/runner/main.cpp +43 -0
- pumaguard-ui/windows/runner/resource.h +16 -0
- pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
- pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
- pumaguard-ui/windows/runner/utils.cpp +65 -0
- pumaguard-ui/windows/runner/utils.h +19 -0
- pumaguard-ui/windows/runner/win32_window.cpp +288 -0
- pumaguard-ui/windows/runner/win32_window.h +102 -0
- pumaguard-21.post29.dist-info/RECORD +0 -83
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
- {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
2
|
+
import 'package:intl/intl.dart';
|
|
3
|
+
|
|
4
|
+
// Test helper to simulate the grouping logic
|
|
5
|
+
enum ImageGrouping { none, day, week }
|
|
6
|
+
|
|
7
|
+
List<Map<String, dynamic>> groupImages(
|
|
8
|
+
List<Map<String, dynamic>> images,
|
|
9
|
+
ImageGrouping grouping,
|
|
10
|
+
) {
|
|
11
|
+
if (grouping == ImageGrouping.none) {
|
|
12
|
+
return images;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Group images by date
|
|
16
|
+
final Map<String, List<Map<String, dynamic>>> grouped = {};
|
|
17
|
+
|
|
18
|
+
for (final image in images) {
|
|
19
|
+
final timestamp = image['modified'];
|
|
20
|
+
if (timestamp == null) continue;
|
|
21
|
+
|
|
22
|
+
// Handle both int and double (st_mtime is a float)
|
|
23
|
+
final timestampInt = (timestamp is int)
|
|
24
|
+
? timestamp
|
|
25
|
+
: (timestamp as num).round();
|
|
26
|
+
final date = DateTime.fromMillisecondsSinceEpoch(timestampInt * 1000);
|
|
27
|
+
String groupKey;
|
|
28
|
+
|
|
29
|
+
if (grouping == ImageGrouping.day) {
|
|
30
|
+
// Group by day
|
|
31
|
+
groupKey = DateFormat('yyyy-MM-dd EEEE').format(date);
|
|
32
|
+
} else {
|
|
33
|
+
// Group by week
|
|
34
|
+
final weekStart = date.subtract(Duration(days: date.weekday - 1));
|
|
35
|
+
final weekEnd = weekStart.add(const Duration(days: 6));
|
|
36
|
+
groupKey =
|
|
37
|
+
'${DateFormat('MMM d').format(weekStart)} - ${DateFormat('MMM d, yyyy').format(weekEnd)}';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!grouped.containsKey(groupKey)) {
|
|
41
|
+
grouped[groupKey] = [];
|
|
42
|
+
}
|
|
43
|
+
grouped[groupKey]!.add(image);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Sort groups by date (most recent first)
|
|
47
|
+
final sortedKeys = grouped.keys.toList()
|
|
48
|
+
..sort((a, b) {
|
|
49
|
+
// Get the first image from each group to compare dates
|
|
50
|
+
final aTimestamp = grouped[a]!.first['modified'];
|
|
51
|
+
final aTimestampInt = (aTimestamp is int)
|
|
52
|
+
? aTimestamp
|
|
53
|
+
: (aTimestamp as num).round();
|
|
54
|
+
final aDate = DateTime.fromMillisecondsSinceEpoch(aTimestampInt * 1000);
|
|
55
|
+
|
|
56
|
+
final bTimestamp = grouped[b]!.first['modified'];
|
|
57
|
+
final bTimestampInt = (bTimestamp is int)
|
|
58
|
+
? bTimestamp
|
|
59
|
+
: (bTimestamp as num).round();
|
|
60
|
+
final bDate = DateTime.fromMillisecondsSinceEpoch(bTimestampInt * 1000);
|
|
61
|
+
|
|
62
|
+
return bDate.compareTo(aDate); // Descending order
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Flatten the grouped images back into a list with headers
|
|
66
|
+
final List<Map<String, dynamic>> result = [];
|
|
67
|
+
for (final key in sortedKeys) {
|
|
68
|
+
// Add a header item with reference to group images
|
|
69
|
+
result.add({
|
|
70
|
+
'is_header': true,
|
|
71
|
+
'header_text': key,
|
|
72
|
+
'image_count': grouped[key]!.length,
|
|
73
|
+
'group_images': grouped[key]!, // Include images for group operations
|
|
74
|
+
});
|
|
75
|
+
// Add all images in this group
|
|
76
|
+
result.addAll(grouped[key]!);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void main() {
|
|
83
|
+
group('Image Grouping Tests', () {
|
|
84
|
+
late List<Map<String, dynamic>> sampleImages;
|
|
85
|
+
|
|
86
|
+
setUp(() {
|
|
87
|
+
// Create sample images with timestamps
|
|
88
|
+
// Using specific dates for predictable testing
|
|
89
|
+
sampleImages = [
|
|
90
|
+
{
|
|
91
|
+
'path': 'img1.jpg',
|
|
92
|
+
'filename': 'img1.jpg',
|
|
93
|
+
'modified':
|
|
94
|
+
DateTime(2024, 1, 15, 10, 30).millisecondsSinceEpoch ~/
|
|
95
|
+
1000, // Monday
|
|
96
|
+
'size': 1024,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
'path': 'img2.jpg',
|
|
100
|
+
'filename': 'img2.jpg',
|
|
101
|
+
'modified':
|
|
102
|
+
DateTime(2024, 1, 15, 14, 20).millisecondsSinceEpoch ~/
|
|
103
|
+
1000, // Same day
|
|
104
|
+
'size': 2048,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
'path': 'img3.jpg',
|
|
108
|
+
'filename': 'img3.jpg',
|
|
109
|
+
'modified':
|
|
110
|
+
DateTime(2024, 1, 16, 9, 15).millisecondsSinceEpoch ~/
|
|
111
|
+
1000, // Tuesday
|
|
112
|
+
'size': 1536,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
'path': 'img4.jpg',
|
|
116
|
+
'filename': 'img4.jpg',
|
|
117
|
+
'modified':
|
|
118
|
+
DateTime(2024, 1, 22, 11, 45).millisecondsSinceEpoch ~/
|
|
119
|
+
1000, // Next week Monday
|
|
120
|
+
'size': 3072,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
'path': 'img5.jpg',
|
|
124
|
+
'filename': 'img5.jpg',
|
|
125
|
+
'modified':
|
|
126
|
+
DateTime(2024, 1, 23, 16, 0).millisecondsSinceEpoch ~/
|
|
127
|
+
1000, // Next week Tuesday
|
|
128
|
+
'size': 2560,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('No grouping returns images as-is', () {
|
|
134
|
+
final result = groupImages(sampleImages, ImageGrouping.none);
|
|
135
|
+
|
|
136
|
+
expect(result.length, equals(5));
|
|
137
|
+
expect(result, equals(sampleImages));
|
|
138
|
+
expect(result.where((img) => img['is_header'] == true).isEmpty, isTrue);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('Group by day creates correct day headers', () {
|
|
142
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
143
|
+
|
|
144
|
+
// Should have 4 groups (Jan 15, Jan 16, Jan 22, Jan 23) = 4 headers + 5 images = 9 items
|
|
145
|
+
expect(result.length, equals(9));
|
|
146
|
+
|
|
147
|
+
// Count headers
|
|
148
|
+
final headers = result
|
|
149
|
+
.where((item) => item['is_header'] == true)
|
|
150
|
+
.toList();
|
|
151
|
+
expect(headers.length, equals(4));
|
|
152
|
+
|
|
153
|
+
// Verify first header (most recent date - Jan 23)
|
|
154
|
+
expect(headers[0]['header_text'], contains('2024-01-23'));
|
|
155
|
+
expect(headers[0]['image_count'], equals(1));
|
|
156
|
+
|
|
157
|
+
// Verify second header (Jan 22)
|
|
158
|
+
expect(headers[1]['header_text'], contains('2024-01-22'));
|
|
159
|
+
expect(headers[1]['image_count'], equals(1));
|
|
160
|
+
|
|
161
|
+
// Verify third header (Jan 16)
|
|
162
|
+
expect(headers[2]['header_text'], contains('2024-01-16'));
|
|
163
|
+
expect(headers[2]['image_count'], equals(1));
|
|
164
|
+
|
|
165
|
+
// Verify fourth header (Jan 15 - has 2 images)
|
|
166
|
+
expect(headers[3]['header_text'], contains('2024-01-15'));
|
|
167
|
+
expect(headers[3]['image_count'], equals(2));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Group by day sorts most recent first', () {
|
|
171
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
172
|
+
|
|
173
|
+
// Find all headers
|
|
174
|
+
final headerIndices = <int>[];
|
|
175
|
+
for (int i = 0; i < result.length; i++) {
|
|
176
|
+
if (result[i]['is_header'] == true) {
|
|
177
|
+
headerIndices.add(i);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Headers should be in descending order by date
|
|
182
|
+
expect(headerIndices.length, equals(4));
|
|
183
|
+
|
|
184
|
+
// Most recent should be first
|
|
185
|
+
expect(result[headerIndices[0]]['header_text'], contains('2024-01-23'));
|
|
186
|
+
expect(result[headerIndices[1]]['header_text'], contains('2024-01-22'));
|
|
187
|
+
expect(result[headerIndices[2]]['header_text'], contains('2024-01-16'));
|
|
188
|
+
expect(result[headerIndices[3]]['header_text'], contains('2024-01-15'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('Group by week creates correct week headers', () {
|
|
192
|
+
final result = groupImages(sampleImages, ImageGrouping.week);
|
|
193
|
+
|
|
194
|
+
// Should have 2 weeks = 2 headers + 5 images = 7 items
|
|
195
|
+
expect(result.length, equals(7));
|
|
196
|
+
|
|
197
|
+
// Count headers
|
|
198
|
+
final headers = result
|
|
199
|
+
.where((item) => item['is_header'] == true)
|
|
200
|
+
.toList();
|
|
201
|
+
expect(headers.length, equals(2));
|
|
202
|
+
|
|
203
|
+
// Week 1: Jan 15-21 (Monday to Sunday) - contains Jan 15 and Jan 16
|
|
204
|
+
// Week 2: Jan 22-28 (Monday to Sunday) - contains Jan 22 and Jan 23
|
|
205
|
+
|
|
206
|
+
// First header should be the most recent week
|
|
207
|
+
expect(headers[0]['header_text'], contains('Jan 22'));
|
|
208
|
+
expect(headers[0]['header_text'], contains('Jan 28'));
|
|
209
|
+
expect(headers[0]['image_count'], equals(2));
|
|
210
|
+
|
|
211
|
+
// Second header should be the earlier week
|
|
212
|
+
expect(headers[1]['header_text'], contains('Jan 15'));
|
|
213
|
+
expect(headers[1]['header_text'], contains('Jan 21'));
|
|
214
|
+
expect(headers[1]['image_count'], equals(3));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('Group by week handles images in same week correctly', () {
|
|
218
|
+
// Create images all in the same week
|
|
219
|
+
final sameWeekImages = [
|
|
220
|
+
{
|
|
221
|
+
'path': 'mon.jpg',
|
|
222
|
+
'filename': 'mon.jpg',
|
|
223
|
+
'modified':
|
|
224
|
+
DateTime(2024, 1, 15).millisecondsSinceEpoch ~/ 1000, // Monday
|
|
225
|
+
'size': 1024,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
'path': 'wed.jpg',
|
|
229
|
+
'filename': 'wed.jpg',
|
|
230
|
+
'modified':
|
|
231
|
+
DateTime(2024, 1, 17).millisecondsSinceEpoch ~/ 1000, // Wednesday
|
|
232
|
+
'size': 1024,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
'path': 'sun.jpg',
|
|
236
|
+
'filename': 'sun.jpg',
|
|
237
|
+
'modified':
|
|
238
|
+
DateTime(2024, 1, 21).millisecondsSinceEpoch ~/ 1000, // Sunday
|
|
239
|
+
'size': 1024,
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
final result = groupImages(sameWeekImages, ImageGrouping.week);
|
|
244
|
+
|
|
245
|
+
// Should have 1 header + 3 images = 4 items
|
|
246
|
+
expect(result.length, equals(4));
|
|
247
|
+
|
|
248
|
+
final headers = result
|
|
249
|
+
.where((item) => item['is_header'] == true)
|
|
250
|
+
.toList();
|
|
251
|
+
expect(headers.length, equals(1));
|
|
252
|
+
expect(headers[0]['image_count'], equals(3));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('Images within group maintain original order', () {
|
|
256
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
257
|
+
|
|
258
|
+
// Find the Jan 15 group (should have 2 images)
|
|
259
|
+
int headerIndex = -1;
|
|
260
|
+
for (int i = 0; i < result.length; i++) {
|
|
261
|
+
if (result[i]['is_header'] == true &&
|
|
262
|
+
result[i]['header_text'].toString().contains('2024-01-15')) {
|
|
263
|
+
headerIndex = i;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
expect(headerIndex, greaterThanOrEqualTo(0));
|
|
269
|
+
|
|
270
|
+
// The two images after this header should be img1 and img2
|
|
271
|
+
final firstImage = result[headerIndex + 1];
|
|
272
|
+
final secondImage = result[headerIndex + 2];
|
|
273
|
+
|
|
274
|
+
expect(firstImage['filename'], equals('img1.jpg'));
|
|
275
|
+
expect(secondImage['filename'], equals('img2.jpg'));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('Handles images without timestamps gracefully', () {
|
|
279
|
+
final imagesWithNull = [
|
|
280
|
+
{
|
|
281
|
+
'path': 'img1.jpg',
|
|
282
|
+
'filename': 'img1.jpg',
|
|
283
|
+
'modified': DateTime(2024, 1, 15).millisecondsSinceEpoch ~/ 1000,
|
|
284
|
+
'size': 1024,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
'path': 'img2.jpg',
|
|
288
|
+
'filename': 'img2.jpg',
|
|
289
|
+
'modified': null, // No timestamp
|
|
290
|
+
'size': 2048,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
'path': 'img3.jpg',
|
|
294
|
+
'filename': 'img3.jpg',
|
|
295
|
+
'modified': DateTime(2024, 1, 16).millisecondsSinceEpoch ~/ 1000,
|
|
296
|
+
'size': 1536,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
final result = groupImages(imagesWithNull, ImageGrouping.day);
|
|
301
|
+
|
|
302
|
+
// Should only group the images with valid timestamps (2 headers + 2 images)
|
|
303
|
+
expect(result.length, equals(4));
|
|
304
|
+
|
|
305
|
+
final headers = result
|
|
306
|
+
.where((item) => item['is_header'] == true)
|
|
307
|
+
.toList();
|
|
308
|
+
expect(headers.length, equals(2));
|
|
309
|
+
|
|
310
|
+
// Verify null timestamp image is not included
|
|
311
|
+
final allImagePaths = result
|
|
312
|
+
.where((item) => item['is_header'] != true)
|
|
313
|
+
.map((item) => item['path'])
|
|
314
|
+
.toList();
|
|
315
|
+
expect(allImagePaths.contains('img2.jpg'), isFalse);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('Empty image list returns empty result', () {
|
|
319
|
+
final result = groupImages([], ImageGrouping.day);
|
|
320
|
+
expect(result.isEmpty, isTrue);
|
|
321
|
+
|
|
322
|
+
final resultWeek = groupImages([], ImageGrouping.week);
|
|
323
|
+
expect(resultWeek.isEmpty, isTrue);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('Single image creates single group', () {
|
|
327
|
+
final singleImage = [
|
|
328
|
+
{
|
|
329
|
+
'path': 'single.jpg',
|
|
330
|
+
'filename': 'single.jpg',
|
|
331
|
+
'modified': DateTime(2024, 1, 15).millisecondsSinceEpoch ~/ 1000,
|
|
332
|
+
'size': 1024,
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
final result = groupImages(singleImage, ImageGrouping.day);
|
|
337
|
+
expect(result.length, equals(2)); // 1 header + 1 image
|
|
338
|
+
|
|
339
|
+
final headers = result
|
|
340
|
+
.where((item) => item['is_header'] == true)
|
|
341
|
+
.toList();
|
|
342
|
+
expect(headers.length, equals(1));
|
|
343
|
+
expect(headers[0]['image_count'], equals(1));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('Day grouping includes day of week in header', () {
|
|
347
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
348
|
+
|
|
349
|
+
final headers = result
|
|
350
|
+
.where((item) => item['is_header'] == true)
|
|
351
|
+
.toList();
|
|
352
|
+
|
|
353
|
+
// Jan 15, 2024 was a Monday
|
|
354
|
+
final jan15Header = headers.firstWhere(
|
|
355
|
+
(h) => h['header_text'].toString().contains('2024-01-15'),
|
|
356
|
+
);
|
|
357
|
+
expect(jan15Header['header_text'], contains('Monday'));
|
|
358
|
+
|
|
359
|
+
// Jan 16, 2024 was a Tuesday
|
|
360
|
+
final jan16Header = headers.firstWhere(
|
|
361
|
+
(h) => h['header_text'].toString().contains('2024-01-16'),
|
|
362
|
+
);
|
|
363
|
+
expect(jan16Header['header_text'], contains('Tuesday'));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('Week grouping handles year boundary correctly', () {
|
|
367
|
+
final yearBoundaryImages = [
|
|
368
|
+
{
|
|
369
|
+
'path': 'dec31.jpg',
|
|
370
|
+
'filename': 'dec31.jpg',
|
|
371
|
+
'modified':
|
|
372
|
+
DateTime(2023, 12, 31).millisecondsSinceEpoch ~/ 1000, // Sunday
|
|
373
|
+
'size': 1024,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
'path': 'jan1.jpg',
|
|
377
|
+
'filename': 'jan1.jpg',
|
|
378
|
+
'modified':
|
|
379
|
+
DateTime(2024, 1, 1).millisecondsSinceEpoch ~/ 1000, // Monday
|
|
380
|
+
'size': 1024,
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
final result = groupImages(yearBoundaryImages, ImageGrouping.week);
|
|
385
|
+
|
|
386
|
+
// These should be in different weeks
|
|
387
|
+
final headers = result
|
|
388
|
+
.where((item) => item['is_header'] == true)
|
|
389
|
+
.toList();
|
|
390
|
+
expect(headers.length, equals(2));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('Groups are sorted with most recent first for both day and week', () {
|
|
394
|
+
// Test with deliberately unsorted input
|
|
395
|
+
final unsortedImages = [
|
|
396
|
+
{
|
|
397
|
+
'path': 'old.jpg',
|
|
398
|
+
'filename': 'old.jpg',
|
|
399
|
+
'modified': DateTime(2024, 1, 1).millisecondsSinceEpoch ~/ 1000,
|
|
400
|
+
'size': 1024,
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
'path': 'new.jpg',
|
|
404
|
+
'filename': 'new.jpg',
|
|
405
|
+
'modified': DateTime(2024, 1, 31).millisecondsSinceEpoch ~/ 1000,
|
|
406
|
+
'size': 1024,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
'path': 'middle.jpg',
|
|
410
|
+
'filename': 'middle.jpg',
|
|
411
|
+
'modified': DateTime(2024, 1, 15).millisecondsSinceEpoch ~/ 1000,
|
|
412
|
+
'size': 1024,
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
// Test day grouping
|
|
417
|
+
final dayResult = groupImages(unsortedImages, ImageGrouping.day);
|
|
418
|
+
final dayHeaders = dayResult
|
|
419
|
+
.where((item) => item['is_header'] == true)
|
|
420
|
+
.toList();
|
|
421
|
+
|
|
422
|
+
expect(
|
|
423
|
+
dayHeaders[0]['header_text'],
|
|
424
|
+
contains('2024-01-31'),
|
|
425
|
+
); // Most recent
|
|
426
|
+
expect(dayHeaders[1]['header_text'], contains('2024-01-15')); // Middle
|
|
427
|
+
expect(dayHeaders[2]['header_text'], contains('2024-01-01')); // Oldest
|
|
428
|
+
|
|
429
|
+
// Test week grouping
|
|
430
|
+
final weekResult = groupImages(unsortedImages, ImageGrouping.week);
|
|
431
|
+
final weekHeaders = weekResult
|
|
432
|
+
.where((item) => item['is_header'] == true)
|
|
433
|
+
.toList();
|
|
434
|
+
|
|
435
|
+
// Should still be ordered most recent first
|
|
436
|
+
expect(weekHeaders.length, greaterThan(0));
|
|
437
|
+
|
|
438
|
+
// First header should contain the most recent date range
|
|
439
|
+
final firstHeaderText = weekHeaders[0]['header_text'] as String;
|
|
440
|
+
expect(firstHeaderText, contains('Jan'));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('Handles float timestamps from backend (st_mtime)', () {
|
|
444
|
+
// Backend returns st_mtime as float, not int
|
|
445
|
+
final imagesWithFloatTimestamps = [
|
|
446
|
+
{
|
|
447
|
+
'path': 'img1.jpg',
|
|
448
|
+
'filename': 'img1.jpg',
|
|
449
|
+
'modified': 1705320600.5, // Float with fractional seconds
|
|
450
|
+
'size': 1024,
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
'path': 'img2.jpg',
|
|
454
|
+
'filename': 'img2.jpg',
|
|
455
|
+
'modified': 1705320650.123, // Float with more precision
|
|
456
|
+
'size': 2048,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
'path': 'img3.jpg',
|
|
460
|
+
'filename': 'img3.jpg',
|
|
461
|
+
'modified': 1705407000.0, // Float that's a whole number
|
|
462
|
+
'size': 1536,
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
// Should group successfully without errors
|
|
467
|
+
final resultDay = groupImages(
|
|
468
|
+
imagesWithFloatTimestamps,
|
|
469
|
+
ImageGrouping.day,
|
|
470
|
+
);
|
|
471
|
+
expect(resultDay.length, greaterThan(0));
|
|
472
|
+
|
|
473
|
+
final headersDay = resultDay
|
|
474
|
+
.where((item) => item['is_header'] == true)
|
|
475
|
+
.toList();
|
|
476
|
+
expect(headersDay.length, greaterThan(0));
|
|
477
|
+
|
|
478
|
+
// Should also work with week grouping
|
|
479
|
+
final resultWeek = groupImages(
|
|
480
|
+
imagesWithFloatTimestamps,
|
|
481
|
+
ImageGrouping.week,
|
|
482
|
+
);
|
|
483
|
+
expect(resultWeek.length, greaterThan(0));
|
|
484
|
+
|
|
485
|
+
final headersWeek = resultWeek
|
|
486
|
+
.where((item) => item['is_header'] == true)
|
|
487
|
+
.toList();
|
|
488
|
+
expect(headersWeek.length, greaterThan(0));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('Handles mixed int and float timestamps', () {
|
|
492
|
+
final mixedImages = [
|
|
493
|
+
{
|
|
494
|
+
'path': 'int_timestamp.jpg',
|
|
495
|
+
'filename': 'int_timestamp.jpg',
|
|
496
|
+
'modified': 1705320600, // Integer timestamp
|
|
497
|
+
'size': 1024,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
'path': 'float_timestamp.jpg',
|
|
501
|
+
'filename': 'float_timestamp.jpg',
|
|
502
|
+
'modified': 1705320650.5, // Float timestamp
|
|
503
|
+
'size': 2048,
|
|
504
|
+
},
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
// Should handle both types without errors
|
|
508
|
+
final result = groupImages(mixedImages, ImageGrouping.day);
|
|
509
|
+
expect(result.length, greaterThan(0));
|
|
510
|
+
|
|
511
|
+
// Both images should be included
|
|
512
|
+
final images = result.where((item) => item['is_header'] != true).toList();
|
|
513
|
+
expect(images.length, equals(2));
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('Group headers include group_images field', () {
|
|
517
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
518
|
+
|
|
519
|
+
// Find all headers
|
|
520
|
+
final headers = result
|
|
521
|
+
.where((item) => item['is_header'] == true)
|
|
522
|
+
.toList();
|
|
523
|
+
|
|
524
|
+
// Each header should have group_images field
|
|
525
|
+
for (final header in headers) {
|
|
526
|
+
expect(header.containsKey('group_images'), isTrue);
|
|
527
|
+
expect(header['group_images'], isNotNull);
|
|
528
|
+
expect(header['group_images'], isA<List<Map<String, dynamic>>>());
|
|
529
|
+
|
|
530
|
+
// Verify group_images contains the right number of images
|
|
531
|
+
final groupImages =
|
|
532
|
+
header['group_images'] as List<Map<String, dynamic>>;
|
|
533
|
+
expect(groupImages.length, equals(header['image_count']));
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('Group images can be used for selection operations', () {
|
|
538
|
+
final result = groupImages(sampleImages, ImageGrouping.day);
|
|
539
|
+
|
|
540
|
+
// Get first group header
|
|
541
|
+
final firstHeader = result.firstWhere(
|
|
542
|
+
(item) => item['is_header'] == true,
|
|
543
|
+
);
|
|
544
|
+
final imagesInGroup =
|
|
545
|
+
firstHeader['group_images'] as List<Map<String, dynamic>>;
|
|
546
|
+
|
|
547
|
+
// Verify all images in group_images are actual image objects (not headers)
|
|
548
|
+
for (final image in imagesInGroup) {
|
|
549
|
+
expect(image['is_header'], isNot(true));
|
|
550
|
+
expect(image.containsKey('filename'), isTrue);
|
|
551
|
+
expect(image.containsKey('path'), isTrue);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|