From d20f73f7408c5cb101902344ff37ffef335e0a70 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Wed, 24 Jan 2024 20:23:21 -0300 Subject: [PATCH 01/13] feat: Search feature on device grid --- lib/widgets/collapsable_sidebar.dart | 4 +- .../device_grid/desktop/desktop_sidebar.dart | 23 ++++++- .../device_grid/desktop/layout_manager.dart | 60 +++++++++++++++++-- pubspec.lock | 36 +++++++++-- 4 files changed, 107 insertions(+), 16 deletions(-) diff --git a/lib/widgets/collapsable_sidebar.dart b/lib/widgets/collapsable_sidebar.dart index 8a624efb..d6ac38b4 100644 --- a/lib/widgets/collapsable_sidebar.dart +++ b/lib/widgets/collapsable_sidebar.dart @@ -109,9 +109,7 @@ class _CollapsableSidebarState extends State<CollapsableSidebar> : AlignmentDirectional.topCenter, padding: collapsed ? EdgeInsetsDirectional.zero - : widget.left - ? const EdgeInsetsDirectional.symmetric(horizontal: 5.0) - : const EdgeInsetsDirectional.only(end: 5.0), + : const EdgeInsetsDirectional.symmetric(horizontal: 5.0), child: SquaredIconButton( key: collapseButtonKey, tooltip: collapsed ? loc.expand : loc.collapse, diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index fe2df170..51f890f7 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -34,6 +34,7 @@ class DesktopSidebar extends StatefulWidget { class _DesktopSidebarState extends State<DesktopSidebar> { bool isSidebarHovering = false; + String searchQuery = ''; @override Widget build(BuildContext context) { @@ -49,7 +50,12 @@ class _DesktopSidebarState extends State<DesktopSidebar> { child: Material( color: theme.canvasColor, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - LayoutManager(collapseButton: widget.collapseButton), + LayoutManager( + collapseButton: widget.collapseButton, + onSearchChanged: (text) { + setState(() => searchQuery = text); + }, + ), if (servers.servers.isEmpty) const Expanded(child: NoServers()) else @@ -66,8 +72,21 @@ class _DesktopSidebarState extends State<DesktopSidebar> { ..sort((a, b) => b.online.toString().compareTo(a.online.toString()))) () { - final devices = server.devices.sorted(); + final devices = server.devices + .where( + (device) => device.name.toLowerCase().contains( + searchQuery.toLowerCase(), + ), + ) + .sorted(); final isLoading = servers.isServerLoading(server); + if (!isLoading && + devices.isEmpty && + searchQuery.isNotEmpty) { + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } /// Whether all the online devices are in the current view. final isAllInView = devices diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index cd0ecdc1..0ba12d6a 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -37,7 +37,13 @@ import 'package:provider/provider.dart'; class LayoutManager extends StatefulWidget { final Widget collapseButton; - const LayoutManager({super.key, required this.collapseButton}); + final ValueChanged<String> onSearchChanged; + + const LayoutManager({ + super.key, + required this.collapseButton, + required this.onSearchChanged, + }); @override State<LayoutManager> createState() => _LayoutManagerState(); @@ -46,6 +52,10 @@ class LayoutManager extends StatefulWidget { class _LayoutManagerState extends State<LayoutManager> { Timer? timer; + bool searchVisible = false; + final searchController = TextEditingController(); + final searchFocusNode = FocusNode(); + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -70,6 +80,8 @@ class _LayoutManagerState extends State<LayoutManager> { @override void dispose() { timer?.cancel(); + searchController.dispose(); + searchFocusNode.dispose(); super.dispose(); } @@ -95,11 +107,22 @@ class _LayoutManagerState extends State<LayoutManager> { child: Row(children: [ widget.collapseButton, const SizedBox(width: 5.0), - Expanded( - child: Text( - loc.view, - maxLines: 1, + Expanded(child: Text(loc.view, maxLines: 1)), + SquaredIconButton( + icon: Icon( + !searchVisible ? Icons.search : Icons.search_off, + size: 18.0, + color: IconTheme.of(context).color, ), + tooltip: searchVisible + ? 'Disable Search' + : MaterialLocalizations.of(context).searchFieldLabel, + onPressed: () { + setState(() => searchVisible = !searchVisible); + if (searchVisible) { + searchFocusNode.requestFocus(); + } + }, ), SquaredIconButton( icon: Icon( @@ -146,6 +169,33 @@ class _LayoutManagerState extends State<LayoutManager> { }, ), ), + AnimatedSlide( + offset: searchVisible ? Offset.zero : const Offset(0, 1), + duration: kThemeChangeDuration, + curve: Curves.easeInOut, + child: Column(children: [ + const Divider(height: 1.0), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: searchController, + focusNode: searchFocusNode, + decoration: const InputDecoration( + hintText: 'Search', + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + ), + onChanged: widget.onSearchChanged, + ), + ) + ]), + ), const Divider(height: 1.0), ]), ); diff --git a/pubspec.lock b/pubspec.lock index 91e9528c..93b2ad4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -461,6 +461,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + url: "https://pub.dev" + source: hosted + version: "9.0.16" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + url: "https://pub.dev" + source: hosted + version: "1.0.5" lints: dependency: transitive description: @@ -489,10 +505,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" media_kit: dependency: transitive description: @@ -567,10 +583,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" msix: dependency: "direct dev" description: @@ -1131,6 +1147,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" volume_controller: dependency: transitive description: @@ -1159,10 +1183,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0" win32: dependency: transitive description: From 78372b714790b5fa9d4f1317404692476a7c4089 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Wed, 24 Jan 2024 20:46:48 -0300 Subject: [PATCH 02/13] feat(desktop): Search feature for timeline view --- lib/utils/extensions.dart | 9 +- lib/widgets/device_selector_screen.dart | 2 +- lib/widgets/events/events_screen.dart | 9 +- .../desktop/timeline_sidebar.dart | 87 +++++++++++++++++-- .../events_timeline/events_playback.dart | 3 +- 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index 2746aba5..414e24e5 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -150,8 +150,13 @@ extension DateTimeExtension on DateTime { extension DeviceListExtension on Iterable<Device> { /// Returns this device list sorted properly - List<Device> sorted([Iterable? available]) { - final list = [...this]..sort((a, b) => a.name.compareTo(b.name)); + List<Device> sorted({ + Iterable? available, + String searchQuery = '', + }) { + final list = where((device) => + device.name.toLowerCase().contains(searchQuery.toLowerCase())).toList() + ..sort((a, b) => a.name.compareTo(b.name)); if (available != null) list.sort((a, b) => available.contains(a) ? 0 : 1); list.sort((a, b) => a.status ? 0 : 1); diff --git a/lib/widgets/device_selector_screen.dart b/lib/widgets/device_selector_screen.dart index 694cc7a8..3224e745 100644 --- a/lib/widgets/device_selector_screen.dart +++ b/lib/widgets/device_selector_screen.dart @@ -131,7 +131,7 @@ class DeviceSelectorScreen extends StatelessWidget { addAutomaticKeepAlives: false, addRepaintBoundaries: false, itemBuilder: (context, index) { - final devices = server.devices.sorted(available); + final devices = server.devices.sorted(available: available); final device = devices[index]; final isSelected = selected.contains(device); diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 950119ba..a70f6361 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -218,6 +218,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { setState(() => disabledDevices.add(device)), onDisabledDeviceRemoved: (device) => setState(() => disabledDevices.remove(device)), + searchQuery: '', // TODO(bdlukaa): ), ), ), @@ -354,6 +355,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { setState(() => disabledDevices.add(device)), onDisabledDeviceRemoved: (device) => setState(() => disabledDevices.remove(device)), + searchQuery: '', // TODO(bldukaa): search for mobile ); }), ]); @@ -383,12 +385,15 @@ class EventsDevicesPicker extends StatelessWidget { final ValueChanged<String> onDisabledDeviceAdded; final ValueChanged<String> onDisabledDeviceRemoved; + final String searchQuery; + const EventsDevicesPicker({ super.key, required this.events, required this.disabledDevices, required this.onDisabledDeviceAdded, required this.onDisabledDeviceRemoved, + required this.searchQuery, this.checkboxScale = 0.8, this.gapCheckboxText = 0.0, }); @@ -440,7 +445,9 @@ class EventsDevicesPicker extends StatelessWidget { if (isOffline) { return <TreeNode>[]; } else { - return server.devices.sorted().map((device) { + return server.devices + .sorted(searchQuery: searchQuery) + .map((device) { final enabled = isOffline ? false : !disabledDevices.contains(device.streamURL); diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart index 725727f9..cd3a8738 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart @@ -18,6 +18,7 @@ */ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/events/events_screen.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; @@ -26,7 +27,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; -class TimelineSidebar extends StatelessWidget { +class TimelineSidebar extends StatefulWidget { const TimelineSidebar({ super.key, required this.date, @@ -39,6 +40,23 @@ class TimelineSidebar extends StatelessWidget { final VoidCallback onFetch; + @override + State<TimelineSidebar> createState() => _TimelineSidebarState(); +} + +class _TimelineSidebarState extends State<TimelineSidebar> { + bool searchVisible = false; + String searchQuery = ''; + final searchFocusNode = FocusNode(); + final searchController = TextEditingController(); + + @override + void dispose() { + searchFocusNode.dispose(); + searchController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -64,9 +82,60 @@ class TimelineSidebar extends StatelessWidget { SubHeader( loc.servers, height: 40.0, - trailing: collapseButton, + trailing: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SquaredIconButton( + icon: Icon( + searchVisible ? Icons.search_off : Icons.search, + size: 22.0, + ), + onPressed: () { + setState(() => searchVisible = !searchVisible); + if (searchVisible) { + searchFocusNode.requestFocus(); + } + }, + ), + collapseButton, + ], + ), padding: const EdgeInsetsDirectional.only(start: 16.0, end: 4.0), ), + AnimatedSize( + duration: kThemeChangeDuration, + curve: Curves.easeInOut, + child: Builder(builder: (context) { + if (!searchVisible) return const SizedBox.shrink(); + return Column(children: [ + const Divider(height: 1.0), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: searchController, + focusNode: searchFocusNode, + decoration: const InputDecoration( + hintText: 'Search', + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + ), + onChanged: (value) { + setState(() => searchQuery = value); + }, + ), + ), + const Divider(height: 1.0), + const SizedBox(height: 8.0), + ]); + }), + ), Expanded( child: StatefulBuilder(builder: (context, setState) { return EventsDevicesPicker( @@ -76,18 +145,20 @@ class TimelineSidebar extends StatelessWidget { setState(() => state.disabledDevices.add(device)), onDisabledDeviceRemoved: (device) => setState(() => state.disabledDevices.remove(device)), + searchQuery: searchQuery, ); }), ), + const Divider(), SubHeader(loc.timeFilter, height: 24.0), ListTile( title: AutoSizeText( () { final formatter = DateFormat.MEd(); - if (DateUtils.isSameDay(date, DateTime.now())) { + if (DateUtils.isSameDay(widget.date, DateTime.now())) { return loc.today; } else { - return formatter.format(date); + return formatter.format(widget.date); } }(), maxLines: 1, @@ -95,15 +166,15 @@ class TimelineSidebar extends StatelessWidget { onTap: () async { final result = await showDatePicker( context: context, - initialDate: date, + initialDate: widget.date, firstDate: DateTime.utc(1970), lastDate: DateTime.now(), initialEntryMode: DatePickerEntryMode.calendarOnly, - currentDate: date, + currentDate: widget.date, ); if (result != null) { - debugPrint('date picked: from $date to $result'); - onDateChanged(result); + debugPrint('date picked: from ${widget.date} to $result'); + widget.onDateChanged(result); } }, ), diff --git a/lib/widgets/events_timeline/events_playback.dart b/lib/widgets/events_timeline/events_playback.dart index bd1490e1..dfd0b3e1 100644 --- a/lib/widgets/events_timeline/events_playback.dart +++ b/lib/widgets/events_timeline/events_playback.dart @@ -149,8 +149,7 @@ class _EventsPlaybackState extends EventsScreenState<EventsPlayback> { timeline!.volume = 0.0; } return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyR || - event.logicalKey == LogicalKeyboardKey.f5) { + } else if (event.logicalKey == LogicalKeyboardKey.f5) { fetch(); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { From 655827819a3625a79e45b689d3a6bad0d452952c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Wed, 24 Jan 2024 20:57:33 -0300 Subject: [PATCH 03/13] feat(desktop): Search feature for Events History --- .../device_grid/desktop/layout_manager.dart | 2 +- lib/widgets/events/events_screen.dart | 44 ++++++++- .../desktop/timeline_sidebar.dart | 90 ++++++++++++------- 3 files changed, 100 insertions(+), 36 deletions(-) diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index 0ba12d6a..351642b0 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -115,7 +115,7 @@ class _LayoutManagerState extends State<LayoutManager> { color: IconTheme.of(context).color, ), tooltip: searchVisible - ? 'Disable Search' + ? 'Disable search' : MaterialLocalizations.of(context).searchFieldLabel, onPressed: () { setState(() => searchVisible = !searchVisible); diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index a70f6361..62eec5e4 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -37,6 +37,7 @@ import 'package:bluecherry_client/widgets/desktop_buttons.dart'; 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/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -172,6 +173,11 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { }); } + bool searchVisible = false; + final searchController = TextEditingController(); + final searchFocusNode = FocusNode(); + String searchQuery = ''; + @override Widget build(BuildContext context) { if (ServersProvider.instance.servers.isEmpty) { @@ -205,10 +211,42 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { SubHeader( loc.servers, height: 38.0, - trailing: Text( - '${ServersProvider.instance.servers.length}', + trailing: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 6.0), + child: SquaredIconButton( + icon: Icon( + searchVisible ? Icons.search_off : Icons.search, + size: 20.0, + ), + tooltip: searchVisible + ? 'Disable search' + : MaterialLocalizations.of(context) + .searchFieldLabel, + onPressed: () { + setState(() => searchVisible = !searchVisible); + if (searchVisible) { + searchFocusNode.requestFocus(); + } + }, + ), + ), + Text( + '${ServersProvider.instance.servers.length}', + ), + ], ), ), + EventsSearchBar( + searchVisible: searchVisible, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: (query) { + super.setState(() => searchQuery = query); + }, + ), Expanded( child: SingleChildScrollView( child: EventsDevicesPicker( @@ -218,7 +256,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { setState(() => disabledDevices.add(device)), onDisabledDeviceRemoved: (device) => setState(() => disabledDevices.remove(device)), - searchQuery: '', // TODO(bdlukaa): + searchQuery: searchQuery, ), ), ), diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart index cd3a8738..5daf4bce 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart @@ -91,6 +91,9 @@ class _TimelineSidebarState extends State<TimelineSidebar> { searchVisible ? Icons.search_off : Icons.search, size: 22.0, ), + tooltip: searchVisible + ? 'Disable search' + : MaterialLocalizations.of(context).searchFieldLabel, onPressed: () { setState(() => searchVisible = !searchVisible); if (searchVisible) { @@ -103,38 +106,11 @@ class _TimelineSidebarState extends State<TimelineSidebar> { ), padding: const EdgeInsetsDirectional.only(start: 16.0, end: 4.0), ), - AnimatedSize( - duration: kThemeChangeDuration, - curve: Curves.easeInOut, - child: Builder(builder: (context) { - if (!searchVisible) return const SizedBox.shrink(); - return Column(children: [ - const Divider(height: 1.0), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: searchController, - focusNode: searchFocusNode, - decoration: const InputDecoration( - hintText: 'Search', - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: EdgeInsetsDirectional.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - ), - onChanged: (value) { - setState(() => searchQuery = value); - }, - ), - ), - const Divider(height: 1.0), - const SizedBox(height: 8.0), - ]); - }), + EventsSearchBar( + searchVisible: searchVisible, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: (query) => setState(() => searchQuery = query), ), Expanded( child: StatefulBuilder(builder: (context, setState) { @@ -184,3 +160,53 @@ class _TimelineSidebarState extends State<TimelineSidebar> { ); } } + +class EventsSearchBar extends StatelessWidget { + final bool searchVisible; + final TextEditingController searchController; + final FocusNode searchFocusNode; + final ValueChanged<String> onSearchChanged; + + const EventsSearchBar({ + super.key, + required this.searchVisible, + required this.searchController, + required this.searchFocusNode, + required this.onSearchChanged, + }); + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: kThemeChangeDuration, + curve: Curves.easeInOut, + child: Builder(builder: (context) { + if (!searchVisible) return const SizedBox.shrink(); + return Column(children: [ + const Divider(height: 1.0), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: searchController, + focusNode: searchFocusNode, + decoration: const InputDecoration( + hintText: 'Search', + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + ), + onChanged: onSearchChanged, + ), + ), + const Divider(height: 1.0), + const SizedBox(height: 8.0), + ]); + }), + ); + } +} From 1cef53c3ec725d83b4d99d355bbe75c42e17965b Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Thu, 25 Jan 2024 11:10:37 -0300 Subject: [PATCH 04/13] feat(mobile): Search events on mobile events browser --- .../device_grid/desktop/layout_manager.dart | 13 +- lib/widgets/events/events_screen.dart | 197 ++------------- lib/widgets/events/filter.dart | 234 ++++++++++++++++++ .../desktop/timeline_sidebar.dart | 41 ++- 4 files changed, 296 insertions(+), 189 deletions(-) create mode 100644 lib/widgets/events/filter.dart diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index 351642b0..ee50e70c 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; +import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:file_picker/file_picker.dart'; @@ -108,15 +109,9 @@ class _LayoutManagerState extends State<LayoutManager> { widget.collapseButton, const SizedBox(width: 5.0), Expanded(child: Text(loc.view, maxLines: 1)), - SquaredIconButton( - icon: Icon( - !searchVisible ? Icons.search : Icons.search_off, - size: 18.0, - color: IconTheme.of(context).color, - ), - tooltip: searchVisible - ? 'Disable search' - : MaterialLocalizations.of(context).searchFieldLabel, + EventsSearchButton( + searchVisible: searchVisible, + iconSize: 18.0, onPressed: () { setState(() => searchVisible = !searchVisible); if (searchVisible) { diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index 62eec5e4..e4e8e60a 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -31,12 +31,12 @@ import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/utils/widgets/tree_view.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; 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/events/filter.dart'; import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/foundation.dart'; @@ -216,15 +216,8 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { children: [ Padding( padding: const EdgeInsetsDirectional.only(end: 6.0), - child: SquaredIconButton( - icon: Icon( - searchVisible ? Icons.search_off : Icons.search, - size: 20.0, - ), - tooltip: searchVisible - ? 'Disable search' - : MaterialLocalizations.of(context) - .searchFieldLabel, + child: EventsSearchButton( + searchVisible: searchVisible, onPressed: () { setState(() => searchVisible = !searchVisible); if (searchVisible) { @@ -345,7 +338,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { /// This is used to update the screen when the bottom sheet is closed. var hasChanged = false; - await showModalBottomSheet( + await showModalBottomSheet<bool>( context: context, isScrollControlled: true, showDragHandle: true, @@ -355,48 +348,29 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { maxChildSize: 0.85, initialChildSize: 0.85, builder: (context, controller) { - final loc = AppLocalizations.of(context); - return ListView(controller: controller, children: [ - SubHeader(loc.timeFilter, height: 20.0), - buildTimeFilterTile(onSelect: () => hasChanged = true), - // const SubHeader('Minimum level'), - // DropdownButtonHideUnderline( - // child: DropdownButton<EventsMinLevelFilter>( - // isExpanded: true, - // value: levelFilter, - // items: EventsMinLevelFilter.values.map((level) { - // return DropdownMenuItem( - // value: level, - // child: Text(level.name.uppercaseFirst()), - // ); - // }).toList(), - // onChanged: (v) => setState( - // () => levelFilter = v ?? levelFilter, - // ), - // ), - // ), - SubHeader(loc.servers, height: 36.0), - StatefulBuilder(builder: (context, localSetState) { - void setState(VoidCallback callback) { - callback(); + return PrimaryScrollController( + controller: controller, + child: MobileFilterSheet( + events: events, + disabledDevices: disabledDevices, + onDisabledDeviceAdded: (device) { + setState(() => disabledDevices.add(device)); hasChanged = true; - this.setState(() {}); - localSetState(() {}); - } - - return EventsDevicesPicker( - events: events, - disabledDevices: disabledDevices, - gapCheckboxText: 10.0, - checkboxScale: 1.15, - onDisabledDeviceAdded: (device) => - setState(() => disabledDevices.add(device)), - onDisabledDeviceRemoved: (device) => - setState(() => disabledDevices.remove(device)), - searchQuery: '', // TODO(bldukaa): search for mobile - ); - }), - ]); + }, + onDisabledDeviceRemoved: (device) { + setState(() => disabledDevices.remove(device)); + hasChanged = true; + }, + levelFilter: levelFilter, + onLevelFilterChanged: (filter) { + setState(() => levelFilter = filter); + hasChanged = true; + }, + timeFilterTile: buildTimeFilterTile(onSelect: () { + hasChanged = true; + }), + ), + ); }, ); }, @@ -405,122 +379,3 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { if (hasChanged) fetch(); } } - -enum EventsMinLevelFilter { - any, - info, - warning, - alarming, - critical, -} - -class EventsDevicesPicker extends StatelessWidget { - final EventsData events; - final Set<String> disabledDevices; - final double checkboxScale; - final double gapCheckboxText; - - final ValueChanged<String> onDisabledDeviceAdded; - final ValueChanged<String> onDisabledDeviceRemoved; - - final String searchQuery; - - const EventsDevicesPicker({ - super.key, - required this.events, - required this.disabledDevices, - required this.onDisabledDeviceAdded, - required this.onDisabledDeviceRemoved, - required this.searchQuery, - this.checkboxScale = 0.8, - this.gapCheckboxText = 0.0, - }); - - @override - Widget build(BuildContext context) { - final servers = context.watch<ServersProvider>(); - - return SingleChildScrollView( - child: TreeView( - indent: 56, - iconSize: 18.0, - nodes: servers.servers.map((server) { - final disabledDevicesForServer = disabledDevices.where( - (d) => server.devices.any((device) => device.streamURL == d)); - final isTriState = disabledDevices.any( - (d) => server.devices.any((device) => device.streamURL == d)); - final isOffline = !server.online; - final serverEvents = events[server]; - - return TreeNode( - content: buildCheckbox( - value: disabledDevicesForServer.length == server.devices.length || - isOffline - ? false - : isTriState - ? null - : true, - isError: isOffline, - onChanged: (v) { - if (v == true) { - for (final d in server.devices) { - onDisabledDeviceRemoved(d.streamURL); - } - } else if (v == null || !v) { - for (final d in server.devices) { - onDisabledDeviceAdded(d.streamURL); - } - } - }, - checkboxScale: checkboxScale, - text: server.name, - secondaryText: isOffline ? null : '${server.devices.length}', - gapCheckboxText: gapCheckboxText, - textFit: FlexFit.tight, - offlineIcon: Icons.domain_disabled_outlined, - ), - children: () { - if (isOffline) { - return <TreeNode>[]; - } else { - return server.devices - .sorted(searchQuery: searchQuery) - .map((device) { - final enabled = isOffline - ? false - : !disabledDevices.contains(device.streamURL); - final eventsForDevice = serverEvents - ?.where((event) => event.deviceID == device.id); - return TreeNode( - content: IgnorePointer( - ignoring: !device.status, - child: buildCheckbox( - value: device.status ? enabled : false, - isError: !device.status, - onChanged: (v) { - if (!device.status) return; - - if (enabled) { - onDisabledDeviceAdded(device.streamURL); - } else { - onDisabledDeviceRemoved(device.streamURL); - } - }, - checkboxScale: checkboxScale, - text: device.name, - secondaryText: eventsForDevice != null && device.status - ? ' (${eventsForDevice.length})' - : null, - gapCheckboxText: gapCheckboxText, - ), - ), - ); - }).toList(); - } - }(), - ); - }).toList(), - ), - ); - } -} diff --git a/lib/widgets/events/filter.dart b/lib/widgets/events/filter.dart new file mode 100644 index 00000000..0054c10b --- /dev/null +++ b/lib/widgets/events/filter.dart @@ -0,0 +1,234 @@ +/* + * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). + * + * Copyright 2022 Bluecherry, LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import 'package:bluecherry_client/providers/server_provider.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/widgets/tree_view.dart'; +import 'package:bluecherry_client/widgets/events/events_screen.dart'; +import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.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'; + +enum EventsMinLevelFilter { any, info, warning, alarming, critical } + +class EventsDevicesPicker extends StatelessWidget { + final EventsData events; + final Set<String> disabledDevices; + final double checkboxScale; + final double gapCheckboxText; + + final ValueChanged<String> onDisabledDeviceAdded; + final ValueChanged<String> onDisabledDeviceRemoved; + + final String searchQuery; + + const EventsDevicesPicker({ + super.key, + required this.events, + required this.disabledDevices, + required this.onDisabledDeviceAdded, + required this.onDisabledDeviceRemoved, + required this.searchQuery, + this.checkboxScale = 0.8, + this.gapCheckboxText = 0.0, + }); + + @override + Widget build(BuildContext context) { + final servers = context.watch<ServersProvider>(); + + return SingleChildScrollView( + child: TreeView( + indent: 56, + iconSize: 18.0, + nodes: servers.servers.map((server) { + final disabledDevicesForServer = disabledDevices.where( + (d) => server.devices.any((device) => device.streamURL == d)); + final isTriState = disabledDevices.any( + (d) => server.devices.any((device) => device.streamURL == d)); + final isOffline = !server.online; + final serverEvents = events[server]; + + return TreeNode( + content: buildCheckbox( + value: disabledDevicesForServer.length == server.devices.length || + isOffline + ? false + : isTriState + ? null + : true, + isError: isOffline, + onChanged: (v) { + if (v == true) { + for (final d in server.devices) { + onDisabledDeviceRemoved(d.streamURL); + } + } else if (v == null || !v) { + for (final d in server.devices) { + onDisabledDeviceAdded(d.streamURL); + } + } + }, + checkboxScale: checkboxScale, + text: server.name, + secondaryText: isOffline ? null : '${server.devices.length}', + gapCheckboxText: gapCheckboxText, + textFit: FlexFit.tight, + offlineIcon: Icons.domain_disabled_outlined, + ), + children: () { + if (isOffline) { + return <TreeNode>[]; + } else { + return server.devices + .sorted(searchQuery: searchQuery) + .map((device) { + final enabled = isOffline + ? false + : !disabledDevices.contains(device.streamURL); + final eventsForDevice = serverEvents + ?.where((event) => event.deviceID == device.id); + return TreeNode( + content: IgnorePointer( + ignoring: !device.status, + child: buildCheckbox( + value: device.status ? enabled : false, + isError: !device.status, + onChanged: (v) { + if (!device.status) return; + + if (enabled) { + onDisabledDeviceAdded(device.streamURL); + } else { + onDisabledDeviceRemoved(device.streamURL); + } + }, + checkboxScale: checkboxScale, + text: device.name, + secondaryText: eventsForDevice != null && device.status + ? ' (${eventsForDevice.length})' + : null, + gapCheckboxText: gapCheckboxText, + ), + ), + ); + }).toList(); + } + }(), + ); + }).toList(), + ), + ); + } +} + +class MobileFilterSheet extends StatefulWidget { + final EventsData events; + final Set<String> disabledDevices; + + final ValueChanged<String> onDisabledDeviceAdded; + final ValueChanged<String> onDisabledDeviceRemoved; + + final EventsMinLevelFilter levelFilter; + final ValueChanged<EventsMinLevelFilter> onLevelFilterChanged; + + final Widget timeFilterTile; + + const MobileFilterSheet({ + super.key, + required this.events, + required this.disabledDevices, + required this.onDisabledDeviceAdded, + required this.onDisabledDeviceRemoved, + required this.levelFilter, + required this.onLevelFilterChanged, + required this.timeFilterTile, + }); + + @override + State<MobileFilterSheet> createState() => _MobileFilterSheetState(); +} + +class _MobileFilterSheetState extends State<MobileFilterSheet> { + final searchController = TextEditingController(); + final searchFocusNode = FocusNode(); + String searchQuery = ''; + bool searchVisible = false; + + @override + void dispose() { + searchController.dispose(); + searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return ListView(primary: true, children: [ + SubHeader(loc.timeFilter, height: 20.0), + widget.timeFilterTile, + const SubHeader('Minimum level', height: 20.0), + DropdownButtonHideUnderline( + child: DropdownButton<EventsMinLevelFilter>( + isExpanded: true, + value: widget.levelFilter, + items: EventsMinLevelFilter.values.map((level) { + return DropdownMenuItem( + value: level, + child: Text(level.name.uppercaseFirst()), + ); + }).toList(), + onChanged: (filter) { + if (filter != null) { + widget.onLevelFilterChanged(filter); + } + }, + ), + ), + SubHeader( + loc.servers, + height: 38.0, + trailing: EventsSearchButton( + searchVisible: searchVisible, + onPressed: () => setState(() => searchVisible = !searchVisible), + ), + ), + EventsSearchBar( + searchVisible: searchVisible, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: (query) => setState(() => searchQuery = query), + ), + EventsDevicesPicker( + events: widget.events, + disabledDevices: widget.disabledDevices, + gapCheckboxText: 10.0, + checkboxScale: 1.15, + onDisabledDeviceAdded: (device) => + setState(() => widget.disabledDevices.add(device)), + onDisabledDeviceRemoved: (device) => + setState(() => widget.disabledDevices.remove(device)), + searchQuery: searchQuery, + ) + ]); + } +} diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart index 5daf4bce..9c8fff06 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart @@ -20,7 +20,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; +import 'package:bluecherry_client/widgets/events/filter.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; @@ -86,14 +86,9 @@ class _TimelineSidebarState extends State<TimelineSidebar> { mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ - SquaredIconButton( - icon: Icon( - searchVisible ? Icons.search_off : Icons.search, - size: 22.0, - ), - tooltip: searchVisible - ? 'Disable search' - : MaterialLocalizations.of(context).searchFieldLabel, + EventsSearchButton( + searchVisible: searchVisible, + iconSize: 22.0, onPressed: () { setState(() => searchVisible = !searchVisible); if (searchVisible) { @@ -161,6 +156,34 @@ class _TimelineSidebarState extends State<TimelineSidebar> { } } +class EventsSearchButton extends StatelessWidget { + final bool searchVisible; + final VoidCallback onPressed; + + final double iconSize; + + const EventsSearchButton({ + super.key, + required this.searchVisible, + required this.onPressed, + this.iconSize = 20.0, + }); + + @override + Widget build(BuildContext context) { + return SquaredIconButton( + icon: Icon( + searchVisible ? Icons.search_off : Icons.search, + size: iconSize, + ), + tooltip: searchVisible + ? 'Disable search' + : MaterialLocalizations.of(context).searchFieldLabel, + onPressed: onPressed, + ); + } +} + class EventsSearchBar extends StatelessWidget { final bool searchVisible; final TextEditingController searchController; From f0f97c81fe5157bce9759077d699fce1e3a9238d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Mon, 29 Jan 2024 14:38:22 -0300 Subject: [PATCH 05/13] feat(mobile): Search devices on direct camera --- lib/widgets/direct_camera.dart | 65 +++++++++++++++++++++++++++++++--- lib/widgets/home.dart | 32 ++++++++++++----- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/lib/widgets/direct_camera.dart b/lib/widgets/direct_camera.dart index bd85be1b..5acad4fd 100644 --- a/lib/widgets/direct_camera.dart +++ b/lib/widgets/direct_camera.dart @@ -25,28 +25,79 @@ import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; +import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.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:sliver_tools/sliver_tools.dart'; -class DirectCameraScreen extends StatelessWidget { +class DirectCameraScreen extends StatefulWidget { const DirectCameraScreen({super.key}); + @override + State<DirectCameraScreen> createState() => DirectCameraScreenState(); +} + +class DirectCameraScreenState extends State<DirectCameraScreen> { + bool _searchVisible = false; + bool get searchVisible => _searchVisible; + String _searchQuery = ''; + final _searchFocusNode = FocusNode(); + final _searchController = TextEditingController(); + + void toggleSearch() { + setState(() { + _searchVisible = !_searchVisible; + }); + if (_searchVisible) { + _searchFocusNode.requestFocus(); + } else { + _searchFocusNode.unfocus(); + } + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _searchController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final serversProviders = context.watch<ServersProvider>(); final loc = AppLocalizations.of(context); final hasDrawer = Scaffold.hasDrawer(context); + final searchBar = EventsSearchBar( + searchController: _searchController, + searchFocusNode: _searchFocusNode, + searchVisible: _searchVisible, + onSearchChanged: (text) { + setState(() => _searchQuery = text); + }, + ); + return SafeArea( child: Column(mainAxisSize: MainAxisSize.min, children: [ - if (hasDrawer) + if (hasDrawer) ...[ AppBar( leading: MaybeUnityDrawerButton(context), title: Text(loc.directCamera), + actions: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 12.0), + child: EventsSearchButton( + searchVisible: _searchVisible, + onPressed: toggleSearch, + ), + ), + ], ), + searchBar, + ] else + searchBar, Expanded(child: () { if (serversProviders.servers.isEmpty) { return const NoServerWarning(); @@ -60,6 +111,7 @@ class DirectCameraScreen extends StatelessWidget { server: server, isCompact: hasDrawer || consts.maxWidth < kMobileBreakpoint.width, + searchQuery: _searchVisible ? _searchQuery : '', ); }), ]), @@ -75,8 +127,13 @@ class DirectCameraScreen extends StatelessWidget { class _DevicesForServer extends StatelessWidget { final Server server; final bool isCompact; + final String searchQuery; - const _DevicesForServer({required this.server, required this.isCompact}); + const _DevicesForServer({ + required this.server, + required this.isCompact, + required this.searchQuery, + }); @override Widget build(BuildContext context) { @@ -122,7 +179,7 @@ class _DevicesForServer extends StatelessWidget { ]); } - final devices = server.devices.sorted(); + final devices = server.devices.sorted(searchQuery: searchQuery); if (isCompact) { return MultiSliver(pushPinnedChildren: true, children: [ diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 1bbf7952..950f5cb7 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:bluecherry_client/widgets/direct_camera.dart'; import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/events/events_screen.dart'; +import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/servers/add_server.dart'; import 'package:bluecherry_client/widgets/settings/settings.dart'; @@ -116,6 +117,8 @@ class _MobileHomeState extends State<Home> { }); } + final directCameraKey = GlobalKey<DirectCameraScreenState>(); + @override Widget build(BuildContext context) { final home = context.watch<HomeProvider>(); @@ -163,7 +166,8 @@ class _MobileHomeState extends State<Home> { }, child: switch (tab) { UnityTab.deviceGrid => const DeviceGrid(), - UnityTab.directCameraScreen => const DirectCameraScreen(), + UnityTab.directCameraScreen => + DirectCameraScreen(key: directCameraKey), UnityTab.eventsPlayback => EventsPlayback(), UnityTab.eventsScreen => EventsScreen(key: eventsScreenKey), @@ -334,14 +338,24 @@ class _MobileHomeState extends State<Home> { }, ), ), - SizedBox( - height: imageSize + 16.0, - child: () { - if (home.isLoading) { - return const Center(child: UnityLoadingIndicator()); - } - }(), - ), + if (directCameraKey.currentState != null) + EventsSearchButton( + searchVisible: directCameraKey.currentState!.searchVisible, + onPressed: () { + directCameraKey.currentState!.toggleSearch(); + setState(() {}); + }, + iconSize: 24.0, + ), + if (home.isLoading) + SizedBox( + height: imageSize + 16.0, + child: () { + if (home.isLoading) { + return const Center(child: UnityLoadingIndicator()); + } + }(), + ), ]), ); } From 6aba972408c1e638f4230561888c5853e30f6c4e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Mon, 29 Jan 2024 15:27:13 -0300 Subject: [PATCH 06/13] feat: Show the server devices when a new server is added --- lib/providers/server_provider.dart | 2 +- .../device_grid/desktop/desktop_sidebar.dart | 7 +- lib/widgets/servers/add_server.dart | 250 +++++++++++++----- 3 files changed, 186 insertions(+), 73 deletions(-) diff --git a/lib/providers/server_provider.dart b/lib/providers/server_provider.dart index 5fbadefa..0f7c43bc 100644 --- a/lib/providers/server_provider.dart +++ b/lib/providers/server_provider.dart @@ -65,7 +65,7 @@ class ServersProvider extends UnityProvider { servers.add(server); await save(); - refreshDevices(); + await refreshDevices(ids: [server.id]); if (isMobilePlatform) { // Register notification token. diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index 51f890f7..f93991a4 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -260,10 +260,12 @@ class DesktopDeviceSelectorTile extends StatefulWidget { super.key, required this.device, required this.selected, + this.selectable = true, }); final Device device; final bool selected; + final bool selectable; @override State<DesktopDeviceSelectorTile> createState() => @@ -298,7 +300,7 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { }, onLongPressDown: (details) => currentLongPressDeviceKind = details.kind, child: InkWell( - onTap: !widget.device.status + onTap: !widget.device.status || !widget.selectable ? null : () { if (widget.selected) { @@ -309,12 +311,15 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { }, child: MouseRegion( onEnter: (_) { + if (!widget.selectable) return; if (mounted) setState(() => hovering = true); }, onHover: (_) { + if (!widget.selectable) return; if (mounted && !hovering) setState(() => hovering = true); }, onExit: (_) { + if (!widget.selectable) return; if (mounted) setState(() => hovering = false); }, child: SizedBox( diff --git a/lib/widgets/servers/add_server.dart b/lib/widgets/servers/add_server.dart index 77b6c2bd..1a582f00 100644 --- a/lib/widgets/servers/add_server.dart +++ b/lib/widgets/servers/add_server.dart @@ -23,9 +23,11 @@ import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/stream_data.dart'; +import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/servers/error.dart'; import 'package:flutter/material.dart'; @@ -68,6 +70,7 @@ Widget _buildCardAppBar({ Text( description, style: theme.textTheme.headlineMedium, + softWrap: true, ), const SizedBox(height: 20.0), ], @@ -75,6 +78,12 @@ Widget _buildCardAppBar({ }); } +enum _ServerAddState { + none, + checkingServerCredentials, + gettingDevices; +} + class AddServerWizard extends StatefulWidget { final VoidCallback onFinish; @@ -343,6 +352,8 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { return Uri.parse(text).host; } + _ServerAddState state = _ServerAddState.none; + @override void dispose() { hostnameController.dispose(); @@ -612,14 +623,26 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { ), const SizedBox(height: 16.0), Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - if (disableFinishButton) + if (disableFinishButton) ...[ const SizedBox( - height: 24.0, - width: 24.0, + height: 18.0, + width: 18.0, child: CircularProgressIndicator.adaptive( strokeWidth: 2.0, ), ), + Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + ), + child: Text(switch (state) { + _ServerAddState.checkingServerCredentials => + 'Checking server credentials', + _ServerAddState.gettingDevices => 'Getting devices', + _ServerAddState.none => '', + }), + ), + ], FocusTraversalOrder( order: const NumericFocusOrder(8), child: MaterialButton( @@ -687,7 +710,13 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { return; } - if (mounted) setState(() => disableFinishButton = true); + if (mounted) { + setState(() { + disableFinishButton = true; + state = _ServerAddState.checkingServerCredentials; + }); + } + // TODO(bdlukaa): only allow numbers in the port fields final port = int.parse(portController.text.trim()); final server = await API.instance.checkServerCredentials( Server( @@ -704,9 +733,11 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { if (server.serverUUID != null && server.cookie != null) { widget.onServerChange(server); + state = _ServerAddState.gettingDevices; await ServersProvider.instance.add(server); widget.onNext(); } else { + state = _ServerAddState.none; if (context.mounted) { showDialog( context: context, @@ -801,7 +832,9 @@ class _AdditionalServerSettingsState extends State<AdditionalServerSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: MediaQuery.sizeOf(context).width / 2.5, + width: isDesktop + ? MediaQuery.sizeOf(context).width / 2.5 + : null, child: _buildCardAppBar( title: loc.serverSettings, description: loc.serverSettingsDescription, @@ -986,85 +1019,160 @@ class LetsGoScreen extends StatelessWidget { final theme = Theme.of(context); final loc = AppLocalizations.of(context); - return PopScope( - canPop: false, - child: IntrinsicWidth( - child: Container( - margin: const EdgeInsetsDirectional.all(16.0), - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width / 2.5, + final addedCard = Card( + elevation: 4.0, + margin: const EdgeInsetsDirectional.only(bottom: 8.0), + color: Color.alphaBlend( + Colors.green.withOpacity(0.2), + theme.cardColor, + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: Row(children: [ + Icon( + Icons.check, + color: Colors.green.shade400, + ), + const SizedBox(width: 16.0), + Expanded( + child: Text(loc.serverAdded), ), + ]), + ), + ); + + final tipsCard = Card( + elevation: 4.0, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: SelectionArea( child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (server != null) - Card( - elevation: 4.0, - margin: const EdgeInsetsDirectional.only(bottom: 8.0), - color: Color.alphaBlend( - Colors.green.withOpacity(0.2), - theme.cardColor, + Text( + loc.letsGoDescription, + style: theme.textTheme.headlineMedium, + ), + ...[loc.tip0, loc.tip1, loc.tip2, loc.tip3].map((tip) { + return Padding( + padding: const EdgeInsetsDirectional.only( + top: 8.0, ), - child: Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: Row(children: [ - Icon( - Icons.check, - color: Colors.green.shade400, - ), - const SizedBox(width: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text(' • '), + const SizedBox(width: 4.0), Expanded( - child: Text(loc.serverAdded), + child: Text(tip), ), - ]), + ], ), + ); + }), + ], + ), + ), + ), + ); + + final finishButton = Align( + alignment: AlignmentDirectional.centerEnd, + child: FloatingActionButton.extended( + onPressed: onFinish, + label: Text(loc.finish.toUpperCase()), + icon: const Icon(Icons.check), + ), + ); + + return LayoutBuilder(builder: (context, consts) { + if (consts.maxWidth < kMobileBreakpoint.width) { + return PopScope( + canPop: false, + child: ListView( + padding: const EdgeInsetsDirectional.all(24.0), + children: [ + SizedBox( + height: consts.maxHeight * 0.875, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (server != null) addedCard, + tipsCard, + const SizedBox(height: 8.0), + finishButton, + const SizedBox(height: 12.0), + ], ), + ), Card( - elevation: 4.0, - margin: EdgeInsets.zero, child: Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: SelectionArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loc.letsGoDescription, - style: theme.textTheme.headlineMedium, - ), - ...[loc.tip0, loc.tip1, loc.tip2, loc.tip3].map((tip) { - return Padding( - padding: const EdgeInsetsDirectional.only( - top: 8.0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text(' • '), - const SizedBox(width: 4.0), - Expanded( - child: Text(tip), - ), - ], - ), - ); - }), - ], - ), + padding: const EdgeInsetsDirectional.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: server!.devices.sorted().map((device) { + return DesktopDeviceSelectorTile( + device: device, + selected: false, + selectable: false, + ); + }).toList(), ), ), - ), - const SizedBox(height: 8.0), - FloatingActionButton.extended( - onPressed: onFinish, - label: Text(loc.finish.toUpperCase()), - icon: const Icon(Icons.check), - ), + ) ], ), - ), - ), - ); + ); + } else { + return PopScope( + canPop: false, + child: IntrinsicWidth( + child: Container( + margin: const EdgeInsetsDirectional.all(16.0), + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width / 2.5, + ), + child: Row(children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (server != null) addedCard, + tipsCard, + const SizedBox(height: 8.0), + finishButton, + ], + ), + ), + if (server != null && server!.devices.isNotEmpty) ...[ + const SizedBox(width: 16.0), + SizedBox( + width: kSidebarConstraints.maxWidth, + child: Card( + child: SingleChildScrollView( + padding: + const EdgeInsetsDirectional.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: server!.devices.sorted().map((device) { + return DesktopDeviceSelectorTile( + device: device, + selected: false, + selectable: false, + ); + }).toList(), + ), + ), + ), + ), + ] + ]), + ), + ), + ); + } + }); } } From ca2517c4939caaf7291e5be54197831d4a684735 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Mon, 29 Jan 2024 15:29:48 -0300 Subject: [PATCH 07/13] fix: Only allow numbers chars in the port field --- lib/widgets/servers/add_server.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/widgets/servers/add_server.dart b/lib/widgets/servers/add_server.dart index 1a582f00..ebc319aa 100644 --- a/lib/widgets/servers/add_server.dart +++ b/lib/widgets/servers/add_server.dart @@ -424,6 +424,9 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { hintText: '$kDefaultPort', border: const OutlineInputBorder(), ), + onChanged: (value) { + portController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); + }, ); final rtspPortField = TextFormField( @@ -445,6 +448,9 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { hintText: '$kDefaultRTSPPort', border: const OutlineInputBorder(), ), + onChanged: (value) { + rtspPortController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); + }, ); final nameField = TextFormField( @@ -625,10 +631,10 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { Row(mainAxisAlignment: MainAxisAlignment.end, children: [ if (disableFinishButton) ...[ const SizedBox( - height: 18.0, - width: 18.0, + height: 16.0, + width: 16.0, child: CircularProgressIndicator.adaptive( - strokeWidth: 2.0, + strokeWidth: 1.5, ), ), Padding( @@ -716,7 +722,6 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { state = _ServerAddState.checkingServerCredentials; }); } - // TODO(bdlukaa): only allow numbers in the port fields final port = int.parse(portController.text.trim()); final server = await API.instance.checkServerCredentials( Server( From 19ed92d19ac3165de1bd39c92709d3759695c7f8 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 10:46:16 -0300 Subject: [PATCH 08/13] fix(windows): window size --- lib/utils/window.dart | 6 +++--- windows/runner/main.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils/window.dart b/lib/utils/window.dart index be294b1c..5c44109c 100644 --- a/lib/utils/window.dart +++ b/lib/utils/window.dart @@ -57,9 +57,9 @@ Future<void> configureWindow() async { windowButtonVisibility: true, ), () async { - // if ((isDesktopPlatform && Platform.isMacOS) || kDebugMode) { - // await windowManager.setSize(kInitialWindowSize); - // } + if (kDebugMode) { + await windowManager.setSize(kInitialWindowSize); + } await windowManager.show(); }, ); diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 78bd43ed..16705b49 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -26,7 +26,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(900, 645); + Win32Window::Size size(1066, 645); if (!window.Create(L"Bluecherry", origin, size)) { return EXIT_FAILURE; } From eb1623048b43554a1cd8344536d5f5a512b5b607 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 11:01:16 -0300 Subject: [PATCH 09/13] feat: Server devices list dialog --- lib/widgets/settings/mobile/settings.dart | 1 + lib/widgets/settings/shared/server_tile.dart | 46 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index 82b8b360..0c9fc14d 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -27,6 +27,7 @@ import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/servers/edit_server.dart'; import 'package:bluecherry_client/widgets/servers/edit_server_settings.dart'; diff --git a/lib/widgets/settings/shared/server_tile.dart b/lib/widgets/settings/shared/server_tile.dart index c43b02ec..174d6dc8 100644 --- a/lib/widgets/settings/shared/server_tile.dart +++ b/lib/widgets/settings/shared/server_tile.dart @@ -405,13 +405,53 @@ Future showServerMenu({ ), const PopupMenuDivider(height: 1.0), PopupMenuItem( - child: Text( - server.online ? loc.refreshDevices : loc.refreshServer, - ), + child: Text(server.online ? loc.refreshDevices : loc.refreshServer), onTap: () async { servers.refreshDevices(ids: [server.id]); }, ), + if (server.online) + PopupMenuItem( + child: const Text('View devices'), + onTap: () async { + showDialog( + context: context, + builder: (context) => DevicesListDialog(server: server), + ); + }, + ), ], ); } + +class DevicesListDialog extends StatelessWidget { + final Server server; + + const DevicesListDialog({super.key, required this.server}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('${server.name} devices'), + contentPadding: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + content: SizedBox( + width: kSidebarConstraints.maxWidth, + child: ListView.builder( + itemCount: server.devices.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final device = server.devices[index]; + return DesktopDeviceSelectorTile( + device: device, + selected: false, + selectable: false, + ); + }, + ), + ), + ); + } +} From 932d468f7764e35091bd819581cc45c4d754a0d7 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 11:30:14 -0300 Subject: [PATCH 10/13] feat: Device info dialog --- .../desktop/desktop_device_grid.dart | 4 +- .../device_grid/desktop/desktop_sidebar.dart | 14 ++- .../desktop/device_info_dialog.dart | 119 ++++++++++++++++++ lib/widgets/device_grid/device_grid.dart | 1 + 4 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 lib/widgets/device_grid/desktop/device_info_dialog.dart diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/widgets/device_grid/desktop/desktop_device_grid.dart index c72e0bb2..a872c26f 100644 --- a/lib/widgets/device_grid/desktop/desktop_device_grid.dart +++ b/lib/widgets/device_grid/desktop/desktop_device_grid.dart @@ -91,9 +91,7 @@ class _DesktopDeviceGridState extends State<DesktopDeviceGrid> { ), child: Text( '${view.currentLayout.devices.length}', - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - ), + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), ), ), ]); diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index f93991a4..42b41a63 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -412,10 +412,7 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { offset.dx + size.width, offset.dy + size.height, ), - constraints: BoxConstraints( - maxWidth: size.width, - minWidth: size.width, - ), + constraints: BoxConstraints(maxWidth: size.width, minWidth: size.width), items: <PopupMenuEntry>[ PopupMenuLabel( label: Padding( @@ -471,6 +468,15 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { }); }, ), + const PopupMenuDivider(), + PopupMenuItem( + child: const Text('Device info'), + onTap: () async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await showDeviceInfoDialog(context, widget.device); + }); + }, + ), ], ); } diff --git a/lib/widgets/device_grid/desktop/device_info_dialog.dart b/lib/widgets/device_grid/desktop/device_info_dialog.dart new file mode 100644 index 00000000..32090bdc --- /dev/null +++ b/lib/widgets/device_grid/desktop/device_info_dialog.dart @@ -0,0 +1,119 @@ +/* + * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). + * + * Copyright 2022 Bluecherry, LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import 'package:bluecherry_client/models/device.dart'; +import 'package:bluecherry_client/utils/theme.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Future<void> showDeviceInfoDialog(BuildContext context, Device device) async { + await showDialog( + context: context, + builder: (context) => DeviceInfoDialog(device: device), + ); +} + +class DeviceInfoDialog extends StatefulWidget { + final Device device; + + const DeviceInfoDialog({super.key, required this.device}); + + @override + State<DeviceInfoDialog> createState() => _DeviceInfoDialogState(); +} + +class _DeviceInfoDialogState extends State<DeviceInfoDialog> { + bool _showStreamUrl = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + return AlertDialog( + title: Text(widget.device.name), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoTile(loc.serverName, widget.device.server.ip), + _buildInfoTile( + 'Status', + widget.device.status ? loc.online : loc.offline, + TextStyle( + color: widget.device.status + ? theme.extension<UnityColors>()!.successColor + : theme.colorScheme.error, + ), + ), + _buildInfoTile('Uri', widget.device.uri), + _buildInfoTile(loc.resolution, + '${widget.device.resolutionX}x${widget.device.resolutionY}'), + _buildInfoTile('Has PTZ?', widget.device.hasPTZ ? loc.yes : loc.no), + _buildInfoTileWidget( + loc.streamURL, + Row(children: [ + Text( + _showStreamUrl + ? widget.device.streamURL + : List.generate(widget.device.streamURL.length, (index) { + return '•'; + }).join(), + ), + const SizedBox(width: 6.0), + SquaredIconButton( + onPressed: () => + setState(() => _showStreamUrl = !_showStreamUrl), + tooltip: _showStreamUrl ? 'Hide' : 'Show', + icon: Icon( + _showStreamUrl ? Icons.visibility_off : Icons.visibility, + size: 18.0, + ), + ), + ]), + ), + ], + ), + ); + } + + Widget _buildInfoTile(String title, String value, [TextStyle? valueStyle]) { + return _buildInfoTileWidget(title, Text(value, style: valueStyle)); + } + + Widget _buildInfoTileWidget(String title, Widget value) { + return Builder(builder: (context) { + final theme = Theme.of(context); + return IntrinsicHeight( + child: Row(children: [ + SizedBox( + width: 100.0, + child: Text( + title, + style: theme.textTheme.labelLarge, + textAlign: TextAlign.end, + ), + ), + const VerticalDivider(), + Flexible(child: value), + ]), + ); + }); + } +} diff --git a/lib/widgets/device_grid/device_grid.dart b/lib/widgets/device_grid/device_grid.dart index 6d4435f0..fbb4a2ad 100644 --- a/lib/widgets/device_grid/device_grid.dart +++ b/lib/widgets/device_grid/device_grid.dart @@ -38,6 +38,7 @@ import 'package:bluecherry_client/utils/video_player.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; +import 'package:bluecherry_client/widgets/device_grid/desktop/device_info_dialog.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/layout_manager.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/multicast_view.dart'; From 9797b34f5304a06b3acd6fd15964a9612ca4af2c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 11:49:17 -0300 Subject: [PATCH 11/13] feat: Localizations --- lib/l10n/app_en.arb | 22 +++++++++++++++++-- lib/l10n/app_fr.arb | 20 ++++++++++++++++- lib/l10n/app_pl.arb | 20 ++++++++++++++++- lib/l10n/app_pt.arb | 22 +++++++++++++++++-- .../device_grid/desktop/desktop_sidebar.dart | 2 +- .../desktop/device_info_dialog.dart | 9 ++++---- .../device_grid/desktop/layout_manager.dart | 6 ++--- .../device_grid/video_status_label.dart | 2 +- .../desktop/timeline_sidebar.dart | 9 ++++---- lib/widgets/servers/add_server.dart | 5 +++-- lib/widgets/settings/shared/server_tile.dart | 5 +++-- 11 files changed, 99 insertions(+), 23 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ff2256b7..83eded0c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -15,10 +15,13 @@ "savePassword": "Save password", "showPassword": "Show password", "hidePassword": "Hide password", + "hide": "Hide", + "show": "Show", "useDefault": "Use Default", "connect": "Connect", "connectAutomaticallyAtStartup": "Connect automatically at startup", "connectAutomaticallyAtStartupDescription": "If enabled, the server will be automatically connected when the app starts.", + "checkingServerCredentials": "Checking server credentials", "skip": "Skip", "cancel": "Cancel", "disabled": "Disabled", @@ -89,6 +92,7 @@ "reloadCamera": "Reload Camera", "selectACamera": "Select a camera", "switchCamera": "Switch camera", + "status": "Status", "online": "Online", "offline": "Offline", "live": "LIVE", @@ -104,10 +108,12 @@ "streamURL": "Stream URL", "streamURLRequired": "The stream URL is required", "streamURLNotValid": "The stream URL is not valid", + "uri": "URI", "eventBrowser": "Events History", "eventsTimeline": "Timeline of Events", "server": "Server", "device": "Device", + "deviceInfo": "Device info", "event": "Event", "duration": "Duration", "priority": "Priority", @@ -165,7 +171,7 @@ "no": "No", "about": "About", "versionText": "Copyright © 2022, Bluecherry LLC.\nAll rights reserved.", - "gettingDevices": "Getting devices...", + "gettingDevices": "Getting devices", "noDevices": "No devices", "noEventsLoaded": "NO EVENTS LOADED", "noEventsLoadedTips": "• Select the cameras you want to see the events\n• Use the calendar to select a specific date or a date range \n• Use the \"Filter\" button to perform the search", @@ -183,6 +189,15 @@ "configureServer": "Configure server", "refreshDevices": "Refresh devices", "refreshServer": "Refresh server", + "viewDevices": "View devices", + "serverDevices": "{server} devices", + "@serverDevices": { + "placeholders": { + "server": { + "type": "String" + } + } + }, "refresh": "Refresh", "view": "View", "cameraRefreshPeriod": "Camera refresh period", @@ -344,6 +359,7 @@ "expand": "Expand", "more": "More", "@PTZ": {}, + "isPtzSupported": "Supports PTZ?", "ptzSupported": "PTZ is supported", "enabledPTZ": "PTZ is enabled", "disabledPTZ": "PTZ is disabled", @@ -508,5 +524,7 @@ "serverHostnameExample": "https://my-server.bluecherry.app:7001", "rackName": "Rack name", "rackNameExample": "Lab 1", - "openServer": "Open server" + "openServer": "Open server", + "@SEARCH": {}, + "disableSearch": "Disable search" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8fa760a6..a7968ba6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -15,10 +15,13 @@ "savePassword": "Sauvegarder mot de passe", "showPassword": "Afficher mot de passe", "hidePassword": "Masquer mot de passe", + "hide": "Hide", + "show": "Show", "useDefault": "Par défaut", "connect": "Connecter", "connectAutomaticallyAtStartup": "Connecter automatiquement au démarrage", "connectAutomaticallyAtStartupDescription": "If enabled, the app will automatically connect to the server when it starts.", + "checkingServerCredentials": "Checking server credentials", "skip": "Sauter", "cancel": "Annuler", "disabled": "Disabled", @@ -85,6 +88,7 @@ "reloadCamera": "Recharger caméra", "selectACamera": "Sélectionner une caméra", "switchCamera": "Switch camera", + "status": "Status", "online": "En ligne", "offline": "Hors ligne", "live": "EN DIRECT", @@ -100,10 +104,12 @@ "streamURL": "Stream URL", "streamURLRequired": "The stream URL is required", "streamURLNotValid": "The stream URL is not valid", + "uri": "URI", "eventBrowser": "Navigateur d'événements", "eventsTimeline": "Ligne du temps", "server": "Serveur", "device": "Appareil", + "deviceInfo": "Device info", "event": "Évènement", "duration": "Durée", "priority": "Priorité", @@ -173,6 +179,15 @@ "configureServer": "Configurer le serveur", "refreshDevices": "Actualiser les appareils", "refreshServer": "Actualiser le serveur", + "viewDevices": "View devices", + "serverDevices": "{server} devices", + "@serverDevices": { + "placeholders": { + "server": { + "type": "String" + } + } + }, "refresh": "Actualiser", "view": "Vue", "cameraRefreshPeriod": "Camera refresh period", @@ -322,6 +337,7 @@ "expand": "Développer", "more": "More", "@PTZ": {}, + "isPtzSupported": "Supports PTZ?", "ptzSupported": "Support PTZ", "enabledPTZ": "PTZ est activé", "disabledPTZ": "PTZ est désactivé", @@ -482,5 +498,7 @@ "serverHostnameExample": "https://my-server.bluecherry.app:7001", "rackName": "Rack name", "rackNameExample": "Lab 1", - "openServer": "Open server" + "openServer": "Open server", + "@SEARCH": {}, + "disableSearch": "Disable search" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index adfe41a9..653a985d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -15,10 +15,13 @@ "savePassword": "Zapisz hasło", "showPassword": "Show password", "hidePassword": "Hide password", + "hide": "Hide", + "show": "Show", "useDefault": "Użyj wartości domyślnych", "connect": "Połącz", "connectAutomaticallyAtStartup": "Połącz automatycznie przy uruchomieniu", "connectAutomaticallyAtStartupDescription": "If enabled, the app will automatically connect to the server when it starts.", + "checkingServerCredentials": "Checking server credentials", "skip": "Pomiń", "cancel": "Anuluj", "disabled": "Disabled", @@ -89,6 +92,7 @@ "reloadCamera": "Odśwież kamerę", "selectACamera": "Wybierz kamerę", "switchCamera": "Przełącz kamerę", + "status": "Status", "online": "Online", "offline": "Offline", "live": "NA ŻYWO", @@ -104,10 +108,12 @@ "streamURL": "Stream URL", "streamURLRequired": "The stream URL is required", "streamURLNotValid": "The stream URL is not valid", + "uri": "URI", "eventBrowser": "Historia zdarzeń", "eventsTimeline": "Oś czasu zdarzeń", "server": "Serwer", "device": "Urządzenie", + "deviceInfo": "Device info", "event": "Zdarzenie", "duration": "Czas trwania", "priority": "Priorytet", @@ -183,6 +189,15 @@ "configureServer": "Konfiguracja serwera", "refreshDevices": "Odśwież urządzenia", "refreshServer": "Odśwież serwer", + "viewDevices": "View devices", + "serverDevices": "{server} devices", + "@serverDevices": { + "placeholders": { + "server": { + "type": "String" + } + } + }, "refresh": "Odśwież", "view": "Widok", "cameraRefreshPeriod": "Camera refresh period", @@ -344,6 +359,7 @@ "expand": "Rozwiń", "more": "More", "@PTZ": {}, + "isPtzSupported": "Supports PTZ?", "ptzSupported": "PTZ jest wspierane", "enabledPTZ": "PTZ jest włączone", "disabledPTZ": "PTZ jest wyłączone", @@ -508,5 +524,7 @@ "serverHostnameExample": "https://my-server.bluecherry.app:7001", "rackName": "Rack name", "rackNameExample": "Lab 1", - "openServer": "Open server" + "openServer": "Open server", + "@SEARCH": {}, + "disableSearch": "Disable search" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 177fab8c..ff3de7a6 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -15,10 +15,13 @@ "savePassword": "Salvar senha", "showPassword": "Mostrar senha", "hidePassword": "Ocultar senha", + "hide": "Esconder", + "show": "Mostrar", "useDefault": "Usar Padrão", "connect": "Conectar", "connectAutomaticallyAtStartup": "Conectar automaticamente ao iniciar", "connectAutomaticallyAtStartupDescription": "Se ativado, o servidor será conectado automaticamente quando o aplicativo for iniciado.", + "checkingServerCredentials": "Verificando credenciais", "skip": "Pular", "cancel": "Cancelar", "disabled": "Desativado", @@ -89,6 +92,7 @@ "reloadCamera": "Recarregar Câmera", "selectACamera": "Selecione uma câmera", "switchCamera": "Trocar câmera", + "status": "Status", "online": "Online", "offline": "Offline", "live": "AO VIVO", @@ -104,10 +108,12 @@ "streamURL": "URL da Transmissão", "streamURLRequired": "A URL da transmissão é obrigatória", "streamURLNotValid": "A url não é válida", + "uri": "URI", "eventBrowser": "Histórico de eventos", "eventsTimeline": "Linha do tempo de eventos", "server": "Servidor", "device": "Dispositivo", + "deviceInfo": "Informações do dispositivo", "event": "Evento", "duration": "Duração", "priority": "Prioridade", @@ -183,6 +189,15 @@ "configureServer": "Configurar servidor", "refreshDevices": "Recarregar dispositivos", "refreshServer": "Recarregar servidor", + "viewDevices": "Ver dispositivos", + "serverDevices": "Dispositivos de {server}", + "@serverDevices": { + "placeholders": { + "server": { + "type": "String" + } + } + }, "refresh": "Recarregar", "view": "Layouts", "cameraRefreshPeriod": "Intervalo para recarregar cameras", @@ -344,7 +359,8 @@ "expand": "Expandir", "more": "Mais", "@PTZ": {}, - "ptzSupported": "PTZ é suportado", + "isPtzSupported": "Possui PTZ?", + "ptzSupported": "Possui PTZ", "enabledPTZ": "PTZ está ativado", "disabledPTZ": "PTZ está desativado", "move": "Movimento", @@ -508,5 +524,7 @@ "serverHostnameExample": "https://servidor.bluecherry.app:7001", "rackName": "Nome do rack", "rackNameExample": "Lab 1", - "openServer": "Abrir servidor" + "openServer": "Abrir servidor", + "@SEARCH": {}, + "disableSearch": "Desativar pesquisa" } \ No newline at end of file diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index 42b41a63..37d0dc1d 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -470,7 +470,7 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { ), const PopupMenuDivider(), PopupMenuItem( - child: const Text('Device info'), + child: Text(loc.deviceInfo), onTap: () async { WidgetsBinding.instance.addPostFrameCallback((_) async { await showDeviceInfoDialog(context, widget.device); diff --git a/lib/widgets/device_grid/desktop/device_info_dialog.dart b/lib/widgets/device_grid/desktop/device_info_dialog.dart index 32090bdc..b002dc54 100644 --- a/lib/widgets/device_grid/desktop/device_info_dialog.dart +++ b/lib/widgets/device_grid/desktop/device_info_dialog.dart @@ -54,7 +54,7 @@ class _DeviceInfoDialogState extends State<DeviceInfoDialog> { children: [ _buildInfoTile(loc.serverName, widget.device.server.ip), _buildInfoTile( - 'Status', + loc.status, widget.device.status ? loc.online : loc.offline, TextStyle( color: widget.device.status @@ -62,10 +62,11 @@ class _DeviceInfoDialogState extends State<DeviceInfoDialog> { : theme.colorScheme.error, ), ), - _buildInfoTile('Uri', widget.device.uri), + _buildInfoTile(loc.uri, widget.device.uri), _buildInfoTile(loc.resolution, '${widget.device.resolutionX}x${widget.device.resolutionY}'), - _buildInfoTile('Has PTZ?', widget.device.hasPTZ ? loc.yes : loc.no), + _buildInfoTile( + loc.isPtzSupported, widget.device.hasPTZ ? loc.yes : loc.no), _buildInfoTileWidget( loc.streamURL, Row(children: [ @@ -80,7 +81,7 @@ class _DeviceInfoDialogState extends State<DeviceInfoDialog> { SquaredIconButton( onPressed: () => setState(() => _showStreamUrl = !_showStreamUrl), - tooltip: _showStreamUrl ? 'Hide' : 'Show', + tooltip: _showStreamUrl ? loc.hide : loc.show, icon: Icon( _showStreamUrl ? Icons.visibility_off : Icons.visibility, size: 18.0, diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index ee50e70c..7e2e2d33 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -175,13 +175,13 @@ class _LayoutManagerState extends State<LayoutManager> { child: TextField( controller: searchController, focusNode: searchFocusNode, - decoration: const InputDecoration( - hintText: 'Search', + decoration: InputDecoration( + hintText: MaterialLocalizations.of(context).searchFieldLabel, isDense: true, border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, - contentPadding: EdgeInsetsDirectional.symmetric( + contentPadding: const EdgeInsetsDirectional.symmetric( horizontal: 8.0, vertical: 4.0, ), diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index 0b26a0d8..c708c889 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -241,7 +241,7 @@ class _DeviceVideoInfo extends StatelessWidget { server, _buildTextSpan( context, - title: loc.ptzSupported, + title: loc.isPtzSupported, data: device.hasPTZ ? loc.yes : loc.no, ), _buildTextSpan( diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart index 9c8fff06..8d44ed21 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart @@ -171,13 +171,14 @@ class EventsSearchButton extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return SquaredIconButton( icon: Icon( searchVisible ? Icons.search_off : Icons.search, size: iconSize, ), tooltip: searchVisible - ? 'Disable search' + ? loc.disableSearch : MaterialLocalizations.of(context).searchFieldLabel, onPressed: onPressed, ); @@ -212,13 +213,13 @@ class EventsSearchBar extends StatelessWidget { child: TextField( controller: searchController, focusNode: searchFocusNode, - decoration: const InputDecoration( - hintText: 'Search', + decoration: InputDecoration( + hintText: MaterialLocalizations.of(context).searchFieldLabel, isDense: true, border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, - contentPadding: EdgeInsetsDirectional.symmetric( + contentPadding: const EdgeInsetsDirectional.symmetric( horizontal: 8.0, vertical: 4.0, ), diff --git a/lib/widgets/servers/add_server.dart b/lib/widgets/servers/add_server.dart index ebc319aa..d61069f6 100644 --- a/lib/widgets/servers/add_server.dart +++ b/lib/widgets/servers/add_server.dart @@ -643,8 +643,9 @@ class _ConfigureDVRServerScreenState extends State<ConfigureDVRServerScreen> { ), child: Text(switch (state) { _ServerAddState.checkingServerCredentials => - 'Checking server credentials', - _ServerAddState.gettingDevices => 'Getting devices', + loc.checkingServerCredentials, + _ServerAddState.gettingDevices => + loc.gettingDevices, _ServerAddState.none => '', }), ), diff --git a/lib/widgets/settings/shared/server_tile.dart b/lib/widgets/settings/shared/server_tile.dart index 174d6dc8..8a1f0f3e 100644 --- a/lib/widgets/settings/shared/server_tile.dart +++ b/lib/widgets/settings/shared/server_tile.dart @@ -412,7 +412,7 @@ Future showServerMenu({ ), if (server.online) PopupMenuItem( - child: const Text('View devices'), + child: Text(loc.viewDevices), onTap: () async { showDialog( context: context, @@ -431,8 +431,9 @@ class DevicesListDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return AlertDialog( - title: Text('${server.name} devices'), + title: Text(loc.serverDevices(server.name)), contentPadding: const EdgeInsetsDirectional.symmetric( horizontal: 16.0, vertical: 12.0, From a8854dabaead64e907e2486916798b86eefb54a3 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 11:52:08 -0300 Subject: [PATCH 12/13] feat: Show devices info dialog on offline devices --- .../device_grid/desktop/desktop_sidebar.dart | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index 37d0dc1d..653a8499 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -311,15 +311,12 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { }, child: MouseRegion( onEnter: (_) { - if (!widget.selectable) return; if (mounted) setState(() => hovering = true); }, onHover: (_) { - if (!widget.selectable) return; if (mounted && !hovering) setState(() => hovering = true); }, onExit: (_) { - if (!widget.selectable) return; if (mounted) setState(() => hovering = false); }, child: SizedBox( @@ -361,7 +358,7 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { ), ), ), - if ((isMobile || hovering) && widget.device.status) + if (isMobile || hovering) Tooltip( message: loc.cameraOptions, preferBelow: false, @@ -369,7 +366,7 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { borderRadius: BorderRadius.circular(4.0), onTap: widget.device.status ? () => _displayOptions(context) - : null, + : () => showDeviceInfoDialog(context, widget.device), child: Icon(moreIconData, size: 20.0), ), ), @@ -426,40 +423,42 @@ class _DesktopDeviceSelectorTileState extends State<DesktopDeviceSelectorTile> { ), ), const PopupMenuDivider(), - PopupMenuItem( - child: Text( - widget.selected ? loc.removeFromView : loc.addToView, - ), - onTap: () { - if (widget.selected) { - view.remove(widget.device); - } else { - view.add(widget.device); - } - }, - ), - PopupMenuItem( - child: Text(loc.showFullscreenCamera), - onTap: () async { - WidgetsBinding.instance.addPostFrameCallback((_) async { - var player = UnityPlayers.players[widget.device.uuid]; - final isLocalController = player == null; - if (isLocalController) { - player = UnityPlayers.forDevice(widget.device); + if (widget.selectable) + PopupMenuItem( + child: Text( + widget.selected ? loc.removeFromView : loc.addToView, + ), + onTap: () { + if (widget.selected) { + view.remove(widget.device); + } else { + view.add(widget.device); } + }, + ), + if (widget.device.status) + PopupMenuItem( + child: Text(loc.showFullscreenCamera), + onTap: () async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + var player = UnityPlayers.players[widget.device.uuid]; + final isLocalController = player == null; + if (isLocalController) { + player = UnityPlayers.forDevice(widget.device); + } - await Navigator.of(context).pushNamed( - '/fullscreen', - arguments: { - 'device': widget.device, - 'player': player, - }, - ); - if (isLocalController) await player.dispose(); - }); - }, - ), - if (isDesktop) + await Navigator.of(context).pushNamed( + '/fullscreen', + arguments: { + 'device': widget.device, + 'player': player, + }, + ); + if (isLocalController) await player.dispose(); + }); + }, + ), + if (isDesktop && widget.device.status) PopupMenuItem( child: Text(loc.openInANewWindow), onTap: () async { From b8c47d518718efe4de8a9c97f70ff37bb02f42e6 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka <brunodlukaa@gmail.com> Date: Tue, 30 Jan 2024 18:28:32 -0300 Subject: [PATCH 13/13] chore: Rename EventsSearchButton to SearchToggleButton; Rename EventsSearchBar to ToggleSearcbBar --- .../device_grid/desktop/layout_manager.dart | 36 ++------ lib/widgets/direct_camera.dart | 6 +- lib/widgets/events/events_screen.dart | 6 +- lib/widgets/events/filter.dart | 6 +- .../desktop/timeline_sidebar.dart | 85 +----------------- lib/widgets/home.dart | 4 +- lib/widgets/search.dart | 87 +++++++++++++++++++ 7 files changed, 109 insertions(+), 121 deletions(-) create mode 100644 lib/widgets/search.dart diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index 7e2e2d33..16fc682e 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -26,9 +26,9 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -109,7 +109,7 @@ class _LayoutManagerState extends State<LayoutManager> { widget.collapseButton, const SizedBox(width: 5.0), Expanded(child: Text(loc.view, maxLines: 1)), - EventsSearchButton( + SearchToggleButton( searchVisible: searchVisible, iconSize: 18.0, onPressed: () { @@ -164,32 +164,12 @@ class _LayoutManagerState extends State<LayoutManager> { }, ), ), - AnimatedSlide( - offset: searchVisible ? Offset.zero : const Offset(0, 1), - duration: kThemeChangeDuration, - curve: Curves.easeInOut, - child: Column(children: [ - const Divider(height: 1.0), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: searchController, - focusNode: searchFocusNode, - decoration: InputDecoration( - hintText: MaterialLocalizations.of(context).searchFieldLabel, - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: const EdgeInsetsDirectional.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - ), - onChanged: widget.onSearchChanged, - ), - ) - ]), + ToggleSearchBar( + searchVisible: searchVisible, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: widget.onSearchChanged, + showBottomDivider: false, ), const Divider(height: 1.0), ]), diff --git a/lib/widgets/direct_camera.dart b/lib/widgets/direct_camera.dart index 5acad4fd..8941dd0b 100644 --- a/lib/widgets/direct_camera.dart +++ b/lib/widgets/direct_camera.dart @@ -25,8 +25,8 @@ import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -70,7 +70,7 @@ class DirectCameraScreenState extends State<DirectCameraScreen> { final loc = AppLocalizations.of(context); final hasDrawer = Scaffold.hasDrawer(context); - final searchBar = EventsSearchBar( + final searchBar = ToggleSearchBar( searchController: _searchController, searchFocusNode: _searchFocusNode, searchVisible: _searchVisible, @@ -88,7 +88,7 @@ class DirectCameraScreenState extends State<DirectCameraScreen> { actions: [ Padding( padding: const EdgeInsetsDirectional.only(end: 12.0), - child: EventsSearchButton( + child: SearchToggleButton( searchVisible: _searchVisible, onPressed: toggleSearch, ), diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index e4e8e60a..04029959 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -37,8 +37,8 @@ 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/events/filter.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -216,7 +216,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { children: [ Padding( padding: const EdgeInsetsDirectional.only(end: 6.0), - child: EventsSearchButton( + child: SearchToggleButton( searchVisible: searchVisible, onPressed: () { setState(() => searchVisible = !searchVisible); @@ -232,7 +232,7 @@ class EventsScreenState<T extends StatefulWidget> extends State<T> { ], ), ), - EventsSearchBar( + ToggleSearchBar( searchVisible: searchVisible, searchController: searchController, searchFocusNode: searchFocusNode, diff --git a/lib/widgets/events/filter.dart b/lib/widgets/events/filter.dart index 0054c10b..23c07ecd 100644 --- a/lib/widgets/events/filter.dart +++ b/lib/widgets/events/filter.dart @@ -21,8 +21,8 @@ import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/widgets/tree_view.dart'; import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -207,12 +207,12 @@ class _MobileFilterSheetState extends State<MobileFilterSheet> { SubHeader( loc.servers, height: 38.0, - trailing: EventsSearchButton( + trailing: SearchToggleButton( searchVisible: searchVisible, onPressed: () => setState(() => searchVisible = !searchVisible), ), ), - EventsSearchBar( + ToggleSearchBar( searchVisible: searchVisible, searchController: searchController, searchFocusNode: searchFocusNode, diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart index 8d44ed21..2e8ced7b 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/widgets/events_timeline/desktop/timeline_sidebar.dart @@ -18,11 +18,11 @@ */ import 'package:auto_size_text/auto_size_text.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/events/filter.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; @@ -86,7 +86,7 @@ class _TimelineSidebarState extends State<TimelineSidebar> { mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ - EventsSearchButton( + SearchToggleButton( searchVisible: searchVisible, iconSize: 22.0, onPressed: () { @@ -101,7 +101,7 @@ class _TimelineSidebarState extends State<TimelineSidebar> { ), padding: const EdgeInsetsDirectional.only(start: 16.0, end: 4.0), ), - EventsSearchBar( + ToggleSearchBar( searchVisible: searchVisible, searchController: searchController, searchFocusNode: searchFocusNode, @@ -155,82 +155,3 @@ class _TimelineSidebarState extends State<TimelineSidebar> { ); } } - -class EventsSearchButton extends StatelessWidget { - final bool searchVisible; - final VoidCallback onPressed; - - final double iconSize; - - const EventsSearchButton({ - super.key, - required this.searchVisible, - required this.onPressed, - this.iconSize = 20.0, - }); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return SquaredIconButton( - icon: Icon( - searchVisible ? Icons.search_off : Icons.search, - size: iconSize, - ), - tooltip: searchVisible - ? loc.disableSearch - : MaterialLocalizations.of(context).searchFieldLabel, - onPressed: onPressed, - ); - } -} - -class EventsSearchBar extends StatelessWidget { - final bool searchVisible; - final TextEditingController searchController; - final FocusNode searchFocusNode; - final ValueChanged<String> onSearchChanged; - - const EventsSearchBar({ - super.key, - required this.searchVisible, - required this.searchController, - required this.searchFocusNode, - required this.onSearchChanged, - }); - - @override - Widget build(BuildContext context) { - return AnimatedSize( - duration: kThemeChangeDuration, - curve: Curves.easeInOut, - child: Builder(builder: (context) { - if (!searchVisible) return const SizedBox.shrink(); - return Column(children: [ - const Divider(height: 1.0), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: searchController, - focusNode: searchFocusNode, - decoration: InputDecoration( - hintText: MaterialLocalizations.of(context).searchFieldLabel, - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: const EdgeInsetsDirectional.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - ), - onChanged: onSearchChanged, - ), - ), - const Divider(height: 1.0), - const SizedBox(height: 8.0), - ]); - }), - ); - } -} diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 950f5cb7..8ebe6298 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -26,8 +26,8 @@ import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:bluecherry_client/widgets/direct_camera.dart'; import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; +import 'package:bluecherry_client/widgets/search.dart'; import 'package:bluecherry_client/widgets/servers/add_server.dart'; import 'package:bluecherry_client/widgets/settings/settings.dart'; import 'package:flutter/material.dart'; @@ -339,7 +339,7 @@ class _MobileHomeState extends State<Home> { ), ), if (directCameraKey.currentState != null) - EventsSearchButton( + SearchToggleButton( searchVisible: directCameraKey.currentState!.searchVisible, onPressed: () { directCameraKey.currentState!.toggleSearch(); diff --git a/lib/widgets/search.dart b/lib/widgets/search.dart new file mode 100644 index 00000000..db9d2e9e --- /dev/null +++ b/lib/widgets/search.dart @@ -0,0 +1,87 @@ +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SearchToggleButton extends StatelessWidget { + final bool searchVisible; + final VoidCallback onPressed; + + final double iconSize; + + const SearchToggleButton({ + super.key, + required this.searchVisible, + required this.onPressed, + this.iconSize = 20.0, + }); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return SquaredIconButton( + icon: Icon( + searchVisible ? Icons.search_off : Icons.search, + size: iconSize, + ), + tooltip: searchVisible + ? loc.disableSearch + : MaterialLocalizations.of(context).searchFieldLabel, + onPressed: onPressed, + ); + } +} + +class ToggleSearchBar extends StatelessWidget { + final bool searchVisible; + final TextEditingController searchController; + final FocusNode searchFocusNode; + final ValueChanged<String> onSearchChanged; + + final bool showBottomDivider; + + const ToggleSearchBar({ + super.key, + required this.searchVisible, + required this.searchController, + required this.searchFocusNode, + required this.onSearchChanged, + this.showBottomDivider = true, + }); + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: kThemeChangeDuration, + curve: Curves.easeInOut, + child: Builder(builder: (context) { + if (!searchVisible) return const SizedBox.shrink(); + return Column(children: [ + const Divider(height: 1.0), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: searchController, + focusNode: searchFocusNode, + decoration: InputDecoration( + hintText: MaterialLocalizations.of(context).searchFieldLabel, + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: const EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + ), + onChanged: onSearchChanged, + ), + ), + if (showBottomDivider) ...[ + const Divider(height: 1.0), + const SizedBox(height: 8.0), + ], + ]); + }), + ); + } +}