Skip to content

Commit 90b8366

Browse files
authored
feat: Events review (#228)
* Improved events filtering time and improved the time that the events are loaded and showed to the user; and * Redesigned the events screen on small screens. This addresses the issue that blocked the UI until all the events were loaded; and * The devices selection is persisted across app sessions (#177)
2 parents d67b08c + b464000 commit 90b8366

21 files changed

+673
-679
lines changed

lib/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@
317317
},
318318
"noRecords": "This camera has no records in the current period.",
319319
"filter": "Filter",
320+
"loadEvents": "{n, plural, =0{Load} =1{Load from 1 device} other{Load from {n} devices}}",
321+
"@loadEvents": {
322+
"placeholders": {
323+
"n": {
324+
"type": "int"
325+
}
326+
}
327+
},
320328
"timeFilter": "Time filter",
321329
"fromDate": "From",
322330
"toDate": "To",

lib/l10n/app_fr.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@
295295
},
296296
"noRecords": "Cette caméra n'a aucun enregistrement dans la période actuelle",
297297
"filter": "Filtre",
298+
"loadEvents": "{n, plural, =0{Load} =1{Load from 1 device} other{Load from {n} devices}}",
299+
"@loadEvents": {
300+
"placeholders": {
301+
"n": {
302+
"type": "int"
303+
}
304+
}
305+
},
298306
"timeFilter": "Filtre par temps",
299307
"fromDate": "De",
300308
"toDate": "À",

lib/l10n/app_pl.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@
317317
},
318318
"noRecords": "Ta kamera nie ma nagrań w tym zakresie.",
319319
"filter": "Filtr",
320+
"loadEvents": "{n, plural, =0{Load} =1{Load from 1 device} other{Load from {n} devices}}",
321+
"@loadEvents": {
322+
"placeholders": {
323+
"n": {
324+
"type": "int"
325+
}
326+
}
327+
},
320328
"timeFilter": "Filtr czasu",
321329
"fromDate": "Od",
322330
"toDate": "Do",

lib/l10n/app_pt.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@
317317
},
318318
"noRecords": "Essa câmera não tem gravações neste período.",
319319
"filter": "Filtrar",
320+
"loadEvents": "{n, plural, =0{Buscar} =1{Buscar de 1 dispositivo} other{Buscar de {n} dispositivos}}",
321+
"@loadEvents": {
322+
"placeholders": {
323+
"n": {
324+
"type": "int"
325+
}
326+
}
327+
},
320328
"timeFilter": "Filtro de tempo",
321329
"fromDate": "De",
322330
"toDate": "à",

lib/main.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import 'package:bluecherry_client/models/layout.dart';
3030
import 'package:bluecherry_client/models/server.dart';
3131
import 'package:bluecherry_client/providers/desktop_view_provider.dart';
3232
import 'package:bluecherry_client/providers/downloads_provider.dart';
33-
import 'package:bluecherry_client/providers/events_playback_provider.dart';
33+
import 'package:bluecherry_client/providers/events_provider.dart';
3434
import 'package:bluecherry_client/providers/home_provider.dart';
3535
import 'package:bluecherry_client/providers/mobile_view_provider.dart';
3636
import 'package:bluecherry_client/providers/server_provider.dart';
@@ -168,8 +168,8 @@ Future<void> main(List<String> args) async {
168168
MobileViewProvider.ensureInitialized(),
169169
DesktopViewProvider.ensureInitialized(),
170170
ServersProvider.ensureInitialized(),
171-
EventsProvider.ensureInitialized(),
172171
UpdateManager.ensureInitialized(),
172+
EventsProvider.ensureInitialized(),
173173
]);
174174

175175
/// Firebase messaging isn't available on windows nor linux
@@ -295,15 +295,15 @@ class _UnityAppState extends State<UnityApp>
295295
ChangeNotifierProvider<ServersProvider>.value(
296296
value: ServersProvider.instance,
297297
),
298-
ChangeNotifierProvider<EventsProvider>.value(
299-
value: EventsProvider.instance,
300-
),
301298
ChangeNotifierProvider<UpdateManager>.value(
302299
value: UpdateManager.instance,
303300
),
304301
ChangeNotifierProvider<UnityPlayers>.value(
305302
value: UnityPlayers.instance,
306303
),
304+
ChangeNotifierProvider<EventsProvider>.value(
305+
value: EventsProvider.instance,
306+
),
307307
],
308308
child: Consumer<SettingsProvider>(
309309
builder: (context, settings, _) => MaterialApp(

lib/models/device.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ class Device {
7676
/// [Uri] to the RTSP stream associated with the device.
7777
final int id;
7878

79-
/// `true` [status] indicates that device device is working correctly or is `Online`.
79+
/// `true` [status] indicates that device device is working correctly or is
80+
/// `Online`.
8081
final bool status;
8182

8283
/// Horizontal resolution of the device device.

lib/providers/events_playback_provider.dart

Lines changed: 0 additions & 104 deletions
This file was deleted.

lib/providers/events_provider.dart

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity).
3+
*
4+
* Copyright 2022 Bluecherry, LLC
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU General Public License as
8+
* published by the Free Software Foundation; either version 3 of
9+
* the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
import 'package:bluecherry_client/api/api.dart';
21+
import 'package:bluecherry_client/models/event.dart';
22+
import 'package:bluecherry_client/models/server.dart';
23+
import 'package:bluecherry_client/providers/app_provider_interface.dart';
24+
import 'package:bluecherry_client/providers/server_provider.dart';
25+
import 'package:bluecherry_client/screens/events_browser/filter.dart';
26+
import 'package:bluecherry_client/utils/constants.dart';
27+
import 'package:bluecherry_client/utils/storage.dart';
28+
import 'package:flutter/foundation.dart';
29+
30+
typedef EventsData = Map<Server, List<Event>>;
31+
32+
class LoadedEvents {
33+
final EventsData events;
34+
final List<Server> invalidResponses;
35+
36+
factory LoadedEvents() {
37+
return LoadedEvents.raw(
38+
events: {},
39+
invalidResponses: List.empty(growable: true),
40+
);
41+
}
42+
43+
const LoadedEvents.raw({
44+
required this.events,
45+
required this.invalidResponses,
46+
});
47+
48+
List<Event> get filteredEvents => events.values.expand((e) => e).toList();
49+
}
50+
51+
class EventsProvider extends UnityProvider {
52+
EventsProvider._();
53+
54+
static late final EventsProvider instance;
55+
static Future<EventsProvider> ensureInitialized() async {
56+
instance = EventsProvider._();
57+
await instance.initialize();
58+
debugPrint('EventsProvider initialized');
59+
return instance;
60+
}
61+
62+
Set<String> selectedDevices = {};
63+
void toggleDevice(String device) {
64+
if (selectedDevices.contains(device)) {
65+
selectedDevices.remove(device);
66+
} else {
67+
selectedDevices.add(device);
68+
}
69+
save();
70+
}
71+
72+
void selectDevices(Iterable<String> devices) {
73+
selectedDevices.addAll(devices);
74+
save();
75+
}
76+
77+
void unselectDevices(Iterable<String> devices) {
78+
selectedDevices.removeAll(devices);
79+
save();
80+
}
81+
82+
DateTime? startTime, endTime;
83+
EventsMinLevelFilter _levelFilter = EventsMinLevelFilter.any;
84+
EventsMinLevelFilter get levelFilter => _levelFilter;
85+
set levelFilter(EventsMinLevelFilter value) {
86+
_levelFilter = value;
87+
notifyListeners();
88+
}
89+
90+
LoadedEvents? loadedEvents;
91+
92+
@override
93+
Future<void> initialize() async {
94+
await tryReadStorage(() => initializeStorage(events, kStorageEvents));
95+
}
96+
97+
@override
98+
Future<void> save({bool notifyListeners = true}) async {
99+
try {
100+
await events.write({
101+
kStorageEvents: kStorageEvents,
102+
'selectedDevices': selectedDevices.toList(),
103+
});
104+
} catch (error, stackTrace) {
105+
debugPrint('Failed to save events:\n $error\n$stackTrace');
106+
}
107+
108+
super.save(notifyListeners: notifyListeners);
109+
}
110+
111+
@override
112+
Future<void> restore({bool notifyListeners = true}) async {
113+
final data = await tryReadStorage(() => events.read());
114+
115+
selectedDevices = (data['selectedDevices'] as List).toSet().cast<String>();
116+
117+
super.restore(notifyListeners: notifyListeners);
118+
}
119+
120+
void _notify() => notifyListeners();
121+
}
122+
123+
extension EventsScreenProvider on EventsProvider {
124+
Future<void> loadEvents() async {
125+
loadedEvents = LoadedEvents();
126+
_notify();
127+
128+
// Load the events at the same time
129+
await Future.wait(ServersProvider.instance.servers.map((server) async {
130+
if (!server.online || server.devices.isEmpty) return;
131+
132+
server = await API.instance.checkServerCredentials(server);
133+
134+
try {
135+
final allowedDevices = server.devices
136+
.where((d) => d.status && selectedDevices.contains(d.streamURL));
137+
138+
// Perform a query for each selected device
139+
await Future.wait(allowedDevices.map((device) async {
140+
final iterable = (await API.instance.getEvents(
141+
server,
142+
startTime: startTime,
143+
endTime: endTime,
144+
device: device,
145+
))
146+
.toList()
147+
..removeWhere((event) {
148+
switch (levelFilter) {
149+
case EventsMinLevelFilter.alarming:
150+
if (event.isAlarm) return true;
151+
break;
152+
case EventsMinLevelFilter.warning:
153+
if (event.priority == EventPriority.warning) return true;
154+
break;
155+
default:
156+
break;
157+
}
158+
return false;
159+
});
160+
161+
loadedEvents!.events[server] ??= [];
162+
loadedEvents!.events[server]!.addAll(iterable);
163+
_notify();
164+
}));
165+
} catch (exception, stacktrace) {
166+
debugPrint(exception.toString());
167+
debugPrint(stacktrace.toString());
168+
loadedEvents!.invalidResponses.add(server);
169+
}
170+
}));
171+
}
172+
}

0 commit comments

Comments
 (0)