From dc838e4d65237c0506a6d70ab1b4593f31974feb Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 27 Jun 2023 18:02:30 -0300 Subject: [PATCH 1/6] Moves filtering logic outside of UI --- lib/widgets/events/events_screen.dart | 58 +++++++++++++++++++++------ windows/runner/flutter_window.cpp | 5 +++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 70438426..4a8c3d6d 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -38,6 +38,7 @@ import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/events/event_player_desktop.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -66,6 +67,8 @@ class _EventsScreenState extends State { final EventsData events = {}; Map invalid = {}; + List filteredEvents = []; + /// The devices that can't be displayed in the list List disabledDevices = []; @@ -101,6 +104,9 @@ class _EventsScreenState extends State { debugPrint(exception.toString()); debugPrint(stacktrace.toString()); } + + await computeFiltered(); + home.notLoading(UnityLoadingReason.fetchingEventsHistory); if (mounted) { setState(() { @@ -109,16 +115,23 @@ class _EventsScreenState extends State { } } - @override - Widget build(BuildContext context) { - final hasDrawer = Scaffold.hasDrawer(context); - final loc = AppLocalizations.of(context); + Future computeFiltered() async { + filteredEvents = await compute(_updateFiltered, { + 'events': events, + 'allowedServers': allowedServers, + 'timeFilter': timeFilter, + 'levelFilter': levelFilter, + 'disabledDevices': disabledDevices, + }); + } - if (ServersProvider.instance.servers.isEmpty) { - return const NoServerWarning(); - } + static List _updateFiltered(Map data) { + final events = data['events'] as EventsData; + final allowedServers = data['allowedServers'] as List; + final timeFilter = data['timeFilter'] as EventsTimeFilter; + final levelFilter = data['levelFilter'] as EventsMinLevelFilter; + final disabledDevices = data['disabledDevices'] as List; - final now = DateTime.now(); final hourRange = { EventsTimeFilter.last12Hours: 12, EventsTimeFilter.last24Hours: 24, @@ -127,7 +140,8 @@ class _EventsScreenState extends State { EventsTimeFilter.any: -1, }[timeFilter]!; - final events = this.events.values.expand((events) sync* { + final now = DateTime.now(); + return events.values.expand((events) sync* { for (final event in events) { // allow events from the allowed servers if (!allowedServers.any((element) => event.server.ip == element.ip)) { @@ -163,6 +177,26 @@ class _EventsScreenState extends State { yield event; } }).toList(); + } + + /// We override setState because we need to update the filtered events + @override + void setState(VoidCallback fn) { + super.setState(fn); + // computes the events based on the filter, then update the screen + computeFiltered().then((_) { + if (mounted) super.setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + final hasDrawer = Scaffold.hasDrawer(context); + final loc = AppLocalizations.of(context); + + if (ServersProvider.instance.servers.isEmpty) { + return const NoServerWarning(); + } return LayoutBuilder(builder: (context, consts) { if (hasDrawer || consts.maxWidth < kMobileBreakpoint.width) { @@ -183,8 +217,8 @@ class _EventsScreenState extends State { ), Expanded( child: EventsScreenMobile( - events: events, - loadedServers: this.events.keys, + events: filteredEvents, + loadedServers: events.keys, refresh: fetch, // isFirstTimeLoading: isFirstTimeLoading, invalid: invalid, @@ -264,7 +298,7 @@ class _EventsScreenState extends State { ), ), const VerticalDivider(width: 1), - Expanded(child: EventsScreenDesktop(events: events)), + Expanded(child: EventsScreenDesktop(events: filteredEvents)), ]); }); } diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363e..955ee303 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } From 4ae00a446f7e4b7e37f5f5852241c18adda16bb4 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 27 Jun 2023 18:21:32 -0300 Subject: [PATCH 2/6] Move requesting and parsing to another isolate This increases performance, being able to parse 1000 events in no-time --- lib/api/api.dart | 56 +++++++++++-------- lib/api/api_helpers.dart | 1 - lib/utils/constants.dart | 3 + lib/widgets/events/events_screen.dart | 5 ++ lib/widgets/events/events_screen_desktop.dart | 3 +- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/lib/api/api.dart b/lib/api/api.dart index e8ef6a9a..f979193a 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/api/ptz.dart'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/models/server.dart'; +import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:xml2json/xml2json.dart'; @@ -137,40 +138,45 @@ class API { return null; } - /// Gets [Event]s present on the [server] after login. - /// [limit] defines the number of events to be fetched. + /// Gets the [Event]s present on the [server]. /// - Future> getEvents( - Server server, { - int limit = 50, - }) async { + /// If server is offline, then it returns an empty list. + Future> getEvents(Server server) async { if (!server.online) { debugPrint('Can not get events of an offline server: $server'); return []; } - try { - assert(server.serverUUID != null && server.cookie != null); - final response = await get( - Uri.https( - '${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}', - '/events/', - { - 'XML': '1', - 'limit': '$limit', - }, - ), - headers: { - 'Cookie': server.cookie!, + return compute(_getEvents, server); + } + + static Future> _getEvents(Server server) async { + if (!server.online) { + debugPrint('Can not get events of an offline server: $server'); + return []; + } + + assert(server.serverUUID != null && server.cookie != null); + final response = await get( + Uri.https( + '${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}', + '/events/', + { + 'XML': '1', + 'limit': '$kEventsLimit', }, - ); + ), + headers: { + 'Cookie': server.cookie!, + }, + ); + try { debugPrint('Getting events for server ${server.name}'); // debugPrint(response.body); final parser = Xml2Json()..parse(response.body); - return (await compute(jsonDecode, parser.toGData()))['feed']['entry'] - .map((e) { + final events = jsonDecode(parser.toGData())['feed']['entry'].map((e) { if (!e.containsKey('content')) debugPrint(e.toString()); return Event( server, @@ -197,6 +203,12 @@ class API { ), ); }).cast(); + + debugPrint('Loaded ${events.length} events for server ${server.name}'); + return events; + } on Xml2JsonException { + debugPrint('Failed to parse XML response from server $server'); + debugPrint(response.body); } catch (exception, stacktrace) { debugPrint('Failed to getEvents on server $server'); debugPrint(exception.toString()); diff --git a/lib/api/api_helpers.dart b/lib/api/api_helpers.dart index 9211e1ab..bb41972c 100644 --- a/lib/api/api_helpers.dart +++ b/lib/api/api_helpers.dart @@ -85,7 +85,6 @@ abstract class APIHelpers { }) async { final events = await API.instance.getEvents( await API.instance.checkServerCredentials(server), - limit: attempts, ); debugPrint(events.map((e) => e.id).toList().toString()); Future getThumbnailForMediaID(int mediaID) async { diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index cf763b29..d52bf947 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -64,3 +64,6 @@ const uuid = Uuid(); /// If the screen size is smaller or equal to this, a mobile view shall be used const kMobileBreakpoint = Size(800, 500); + +/// The amount of events to load at once +const kEventsLimit = 1000; diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 4a8c3d6d..70ff223c 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -84,6 +84,8 @@ class _EventsScreenState extends State { try { // Load the events at the same time await Future.wait(ServersProvider.instance.servers.map((server) async { + if (!server.online) return; + try { final iterable = await API.instance.getEvents( await API.instance.checkServerCredentials(server), @@ -184,8 +186,11 @@ class _EventsScreenState extends State { void setState(VoidCallback fn) { super.setState(fn); // computes the events based on the filter, then update the screen + final home = context.read() + ..loading(UnityLoadingReason.fetchingEventsHistory); computeFiltered().then((_) { if (mounted) super.setState(() {}); + home.notLoading(UnityLoadingReason.fetchingEventsHistory); }); } diff --git a/lib/widgets/events/events_screen_desktop.dart b/lib/widgets/events/events_screen_desktop.dart index 6ae31310..89713f95 100644 --- a/lib/widgets/events/events_screen_desktop.dart +++ b/lib/widgets/events/events_screen_desktop.dart @@ -56,7 +56,8 @@ class EventsScreenDesktop extends StatelessWidget { ], showCheckboxColumn: false, rows: events.map((Event event) { - final index = events.indexOf(event); + // final index = events.indexOf(event); + final index = event.id; return DataRow( key: ValueKey(event), From b2808592a817d3372389e3ac88b8a1b42da9f981 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 27 Jun 2023 19:01:27 -0300 Subject: [PATCH 3/6] Add refresh button on desktop --- lib/firebase_messaging_background_handler.dart | 2 +- lib/widgets/desktop_buttons.dart | 12 ++++++++++++ lib/widgets/events/events_screen.dart | 14 ++++++++------ lib/widgets/events/events_screen_desktop.dart | 2 +- lib/widgets/home.dart | 4 +++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/firebase_messaging_background_handler.dart b/lib/firebase_messaging_background_handler.dart index ef303e26..1eebf62e 100644 --- a/lib/firebase_messaging_background_handler.dart +++ b/lib/firebase_messaging_background_handler.dart @@ -256,7 +256,7 @@ Future _backgroundClickAction(ReceivedAction action) async { _mutex = 'events_screen'; await navigatorKey.currentState?.push( MaterialPageRoute( - builder: (context) => const EventsScreen(), + builder: (context) => EventsScreen(key: eventsScreenKey), ), ); _mutex = null; diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index c2f21f9e..aaf04821 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -24,9 +24,11 @@ import 'package:bluecherry_client/main.dart'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/widgets/events/events_screen.dart'; import 'package:bluecherry_client/widgets/home.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; import 'package:window_manager/window_manager.dart'; @@ -113,6 +115,7 @@ class _WindowButtonsState extends State with WindowListener { if (!isDesktop || isMobile) return const SizedBox.shrink(); final theme = Theme.of(context); + final loc = AppLocalizations.of(context); final home = context.watch(); final tab = home.tab; @@ -197,6 +200,15 @@ class _WindowButtonsState extends State with WindowListener { const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: UnityLoadingIndicator(), + ) + else if (home.tab == UnityTab.eventsScreen.index) + IconButton( + onPressed: () { + eventsScreenKey.currentState?.fetch(); + }, + icon: const Icon(Icons.refresh), + iconSize: 20.0, + tooltip: loc.refresh, ), SizedBox( width: 138, diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 70ff223c..9ae7b689 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -49,10 +49,12 @@ part 'event_player_mobile.dart'; part 'events_screen_desktop.dart'; part 'events_screen_mobile.dart'; -typedef EventsData = Map>; +typedef EventsData = Map>; + +final eventsScreenKey = GlobalKey<_EventsScreenState>(); class EventsScreen extends StatefulWidget { - const EventsScreen({super.key}); + const EventsScreen({required super.key}); @override State createState() => _EventsScreenState(); @@ -67,7 +69,7 @@ class _EventsScreenState extends State { final EventsData events = {}; Map invalid = {}; - List filteredEvents = []; + Iterable filteredEvents = []; /// The devices that can't be displayed in the list List disabledDevices = []; @@ -92,7 +94,7 @@ class _EventsScreenState extends State { ); if (mounted) { setState(() { - events[server] = iterable.toList(); + events[server] = iterable; invalid[server] = false; }); } @@ -127,7 +129,7 @@ class _EventsScreenState extends State { }); } - static List _updateFiltered(Map data) { + static Iterable _updateFiltered(Map data) { final events = data['events'] as EventsData; final allowedServers = data['allowedServers'] as List; final timeFilter = data['timeFilter'] as EventsTimeFilter; @@ -178,7 +180,7 @@ class _EventsScreenState extends State { yield event; } - }).toList(); + }); } /// We override setState because we need to update the filtered events diff --git a/lib/widgets/events/events_screen_desktop.dart b/lib/widgets/events/events_screen_desktop.dart index 89713f95..745fd6b3 100644 --- a/lib/widgets/events/events_screen_desktop.dart +++ b/lib/widgets/events/events_screen_desktop.dart @@ -20,7 +20,7 @@ part of 'events_screen.dart'; class EventsScreenDesktop extends StatelessWidget { - final List events; + final Iterable events; const EventsScreenDesktop({super.key, required this.events}); diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 0d48e4c3..10e676a6 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -154,7 +154,9 @@ class _MobileHomeState extends State { return const DirectCameraScreen(); }, UnityTab.eventsPlayback: () => const EventsPlayback(), - UnityTab.eventsScreen: () => const EventsScreen(), + UnityTab.eventsScreen: () => EventsScreen( + key: eventsScreenKey, + ), UnityTab.addServer: () => AddServerWizard( onFinish: () async => home.setTab(0, context), ), From 6e6c1f2419b04bcfb3e3c122b4ae7b64d42bca1d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 27 Jun 2023 19:45:10 -0300 Subject: [PATCH 4/6] Implement table lazy loading and update UI Lazy loading and fixed extent are now implemented in the desktop table view. This increases scrolling performance by a lot --- lib/main.dart | 2 +- lib/widgets/events/event_player_desktop.dart | 2 +- lib/widgets/events/event_player_mobile.dart | 2 +- lib/widgets/events/events_screen_desktop.dart | 177 +++++++++++------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 306ae702..1379f902 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -203,7 +203,7 @@ class UnityApp extends StatelessWidget { if (settings.name == '/events') { final data = settings.arguments! as Map; final event = data['event'] as Event; - final upcomingEvents = data['upcoming'] as List; + final upcomingEvents = data['upcoming'] as Iterable; return MaterialPageRoute( settings: RouteSettings( diff --git a/lib/widgets/events/event_player_desktop.dart b/lib/widgets/events/event_player_desktop.dart index 179a7c99..c5631564 100644 --- a/lib/widgets/events/event_player_desktop.dart +++ b/lib/widgets/events/event_player_desktop.dart @@ -41,7 +41,7 @@ const kSliderControlerWidth = 100.0; class EventPlayerDesktop extends StatefulWidget { final Event event; - final List upcomingEvents; + final Iterable upcomingEvents; const EventPlayerDesktop({ super.key, diff --git a/lib/widgets/events/event_player_mobile.dart b/lib/widgets/events/event_player_mobile.dart index 4c5a4d5f..aee7b46c 100644 --- a/lib/widgets/events/event_player_mobile.dart +++ b/lib/widgets/events/event_player_mobile.dart @@ -21,7 +21,7 @@ part of 'events_screen.dart'; class EventPlayerScreen extends StatelessWidget { final Event event; - final List upcomingEvents; + final Iterable upcomingEvents; const EventPlayerScreen({ super.key, diff --git a/lib/widgets/events/events_screen_desktop.dart b/lib/widgets/events/events_screen_desktop.dart index 745fd6b3..4fb4524e 100644 --- a/lib/widgets/events/events_screen_desktop.dart +++ b/lib/widgets/events/events_screen_desktop.dart @@ -19,6 +19,18 @@ part of 'events_screen.dart'; +Widget _buildTilePart({required Widget child, int flex = 1}) { + return Expanded( + flex: flex, + child: Container( + height: 40.0, + margin: const EdgeInsetsDirectional.only(start: 10.0), + alignment: AlignmentDirectional.centerStart, + child: child, + ), + ); +} + class EventsScreenDesktop extends StatelessWidget { final Iterable events; @@ -27,7 +39,6 @@ class EventsScreenDesktop extends StatelessWidget { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - final theme = Theme.of(context); final settings = context.watch(); if (events.isEmpty) { @@ -39,84 +50,118 @@ class EventsScreenDesktop extends StatelessWidget { ); } - return SingleChildScrollView( - child: SizedBox( - width: double.infinity, - child: DataTable( - horizontalMargin: 20.0, - columnSpacing: 30.0, - columns: [ - const DataColumn(label: SizedBox.shrink()), - DataColumn(label: Text(loc.server)), - DataColumn(label: Text(loc.device)), - DataColumn(label: Text(loc.event)), - DataColumn(label: Text(loc.duration)), - DataColumn(label: Text(loc.priority)), - DataColumn(label: Text(loc.date)), - ], - showCheckboxColumn: false, - rows: events.map((Event event) { - // final index = events.indexOf(event); - final index = event.id; + return Material( + child: CustomScrollView(slivers: [ + SliverPersistentHeader(delegate: _TableHeader(), pinned: true), + SliverFixedExtentList.builder( + itemCount: events.length, + itemExtent: 50.0, + itemBuilder: (context, index) { + final event = events.elementAt(index); - return DataRow( - key: ValueKey(event), - color: index.isEven - ? MaterialStateProperty.resolveWith((states) { - return theme.appBarTheme.backgroundColor - ?.withOpacity(0.75); - }) - : MaterialStateProperty.resolveWith((states) { - return theme.appBarTheme.backgroundColor - ?.withOpacity(0.25); - }), - onSelectChanged: event.mediaURL == null + return InkWell( + onTap: event.mediaURL == null ? null - : (_) { + : () { debugPrint('Displaying event $event'); Navigator.of(context).pushNamed( '/events', - arguments: { - 'event': event, - 'upcoming': events, - }, + arguments: {'event': event, 'upcoming': events}, ); }, - cells: [ - // icon - DataCell(Container( - width: 40.0, - height: 40.0, - alignment: AlignmentDirectional.center, - child: DownloadIndicator(event: event), - )), - // server - DataCell(Text(event.server.name)), - // device - DataCell(Text(event.deviceName)), - // event - DataCell(Text(event.type.locale(context).uppercaseFirst())), - // duration - DataCell( - Text( - event.duration + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Row(children: [ + Container( + width: 40.0, + height: 40.0, + alignment: AlignmentDirectional.center, + child: DownloadIndicator(event: event), + ), + _buildTilePart(child: Text(event.server.name), flex: 2), + _buildTilePart(child: Text(event.deviceName)), + _buildTilePart( + child: Text(event.type.locale(context).uppercaseFirst()), + ), + _buildTilePart( + child: Text(event.duration .humanReadableCompact(context) - .uppercaseFirst(), + .uppercaseFirst()), + ), + _buildTilePart( + child: + Text(event.priority.locale(context).uppercaseFirst()), ), - ), - // priority - DataCell(Text(event.priority.locale(context).uppercaseFirst())), - // date - DataCell( - Text( - '${settings.formatDate(event.updated.toLocal())} ${settings.formatTime(event.updated).toUpperCase()}', + _buildTilePart( + child: Text( + '${settings.formatDate(event.updated.toLocal())} ${settings.formatTime(event.updated).toUpperCase()}', + ), + flex: 2, ), - ), - ], + ]), + ), ); - }).toList(), + }, + ), + ]), + ); + } +} + +class _TableHeader extends SliverPersistentHeaderDelegate { + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + + return Material( + child: Card( + child: Container( + height: 50, + margin: const EdgeInsets.symmetric(horizontal: 20.0), + child: DefaultTextStyle( + style: theme.textTheme.headlineSmall ?? const TextStyle(), + child: Row(children: [ + const SizedBox(width: 40.0, height: 40.0), + _buildTilePart( + child: Text(loc.server), + flex: 2, + ), + _buildTilePart( + child: Text(loc.device), + ), + _buildTilePart( + child: Text(loc.event), + ), + _buildTilePart( + child: Text(loc.duration), + ), + _buildTilePart( + child: Text(loc.priority), + ), + _buildTilePart( + child: Text(loc.date), + flex: 2, + ), + ]), + ), ), ), ); } + + @override + double get maxExtent => 50; + + @override + double get minExtent => 50; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { + return false; + } } From 561488eaa4b6ccd8833cc81504084898db6e8042 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Jun 2023 10:03:33 -0300 Subject: [PATCH 5/6] Add the amount of events beside each device --- lib/widgets/desktop_buttons.dart | 2 +- lib/widgets/events/events_screen.dart | 22 ++++++++++++++----- lib/widgets/events/events_screen_desktop.dart | 16 ++++++++++++-- lib/widgets/events/events_screen_mobile.dart | 3 ++- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index aaf04821..997b01d8 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -201,7 +201,7 @@ class _WindowButtonsState extends State with WindowListener { padding: EdgeInsets.symmetric(horizontal: 8.0), child: UnityLoadingIndicator(), ) - else if (home.tab == UnityTab.eventsScreen.index) + else if (home.tab == UnityTab.eventsScreen.index && !canPop) IconButton( onPressed: () { eventsScreenKey.currentState?.fetch(); diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 9ae7b689..fa46c482 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -349,6 +349,7 @@ class _EventsScreenState extends State { final isTriState = disabledDevices .any((d) => server.devices.any((device) => device.rtspURL == d)); final isOffline = !server.online; + final serverEvents = events[server]; return TreeNode( content: Row(children: [ @@ -396,6 +397,8 @@ class _EventsScreenState extends State { final enabled = isOffline || !allowedServers.contains(server) ? false : !disabledDevices.contains(device.rtspURL); + final eventsForDevice = + serverEvents?.where((event) => event.deviceID == device.id); return TreeNode( content: Row(children: [ IgnorePointer( @@ -418,12 +421,21 @@ class _EventsScreenState extends State { ), ), SizedBox(width: gapCheckboxText), - Text( - device.name, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, + Flexible( + child: Text( + device.name, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + ), ), + if (eventsForDevice != null) ...[ + Text( + ' (${eventsForDevice.length})', + style: theme.textTheme.labelSmall, + ), + const SizedBox(width: 10.0), + ], ]), ); }).toList(); diff --git a/lib/widgets/events/events_screen_desktop.dart b/lib/widgets/events/events_screen_desktop.dart index 4fb4524e..52a9b3ee 100644 --- a/lib/widgets/events/events_screen_desktop.dart +++ b/lib/widgets/events/events_screen_desktop.dart @@ -19,14 +19,20 @@ part of 'events_screen.dart'; -Widget _buildTilePart({required Widget child, int flex = 1}) { +Widget _buildTilePart({required Widget child, Widget? icon, int flex = 1}) { return Expanded( flex: flex, child: Container( height: 40.0, margin: const EdgeInsetsDirectional.only(start: 10.0), alignment: AlignmentDirectional.centerStart, - child: child, + child: Row(children: [ + if (icon != null) ...[ + IconTheme.merge(data: const IconThemeData(size: 14.0), child: icon), + const SizedBox(width: 6.0), + ], + Flexible(child: child), + ]), ), ); } @@ -128,23 +134,29 @@ class _TableHeader extends SliverPersistentHeaderDelegate { child: Row(children: [ const SizedBox(width: 40.0, height: 40.0), _buildTilePart( + icon: const Icon(Icons.dns), child: Text(loc.server), flex: 2, ), _buildTilePart( child: Text(loc.device), + icon: const Icon(Icons.camera), ), _buildTilePart( child: Text(loc.event), + icon: const Icon(Icons.subscriptions), ), _buildTilePart( child: Text(loc.duration), + icon: const Icon(Icons.timer), ), _buildTilePart( child: Text(loc.priority), + icon: const Icon(Icons.priority_high), ), _buildTilePart( child: Text(loc.date), + icon: const Icon(Icons.calendar_today), flex: 2, ), ]), diff --git a/lib/widgets/events/events_screen_mobile.dart b/lib/widgets/events/events_screen_mobile.dart index 9547490b..f21d86e5 100644 --- a/lib/widgets/events/events_screen_mobile.dart +++ b/lib/widgets/events/events_screen_mobile.dart @@ -86,10 +86,11 @@ class EventsScreenMobile extends StatelessWidget { loc.offline, style: TextStyle( color: theme.colorScheme.error, + fontWeight: FontWeight.w600, ), ) : Text( - '${loc.nDevices(server.devices.length)} • ${server.ip}', + '${loc.nDevices(server.devices.length)} • ${server.ip} • ${serverEvents.length} events', ), children: !hasEvents ? [ From edbfaa2e58e73a7450f12acdc1f30045d8d0fe6a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Jun 2023 10:05:54 -0300 Subject: [PATCH 6/6] Add refresh button to mobile --- lib/widgets/events/events_screen.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index fa46c482..61597d71 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -212,6 +212,14 @@ class _EventsScreenState extends State { leading: MaybeUnityDrawerButton(context), title: Text(loc.eventBrowser), actions: [ + IconButton( + onPressed: () { + eventsScreenKey.currentState?.fetch(); + }, + icon: const Icon(Icons.refresh), + iconSize: 20.0, + tooltip: loc.refresh, + ), Padding( padding: const EdgeInsetsDirectional.only(end: 15.0), child: IconButton( @@ -242,10 +250,7 @@ class _EventsScreenState extends State { shape: const RoundedRectangleBorder(), child: DropdownButtonHideUnderline( child: Column(children: [ - SubHeader( - loc.servers, - height: 40.0, - ), + SubHeader(loc.servers, height: 40.0), Expanded( child: SingleChildScrollView( child: buildTreeView(context, setState: setState),