From 9eb200f5daef69374fad1893ff574a13ab2a401a Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Wed, 11 Sep 2024 18:51:57 +0200 Subject: [PATCH] Changed design top items screen --- .../home/top_items/top_items_screen.dart | 422 +++++++++--------- lib/screens/logs/logs_list_appbar.dart | 120 +---- lib/widgets/floating_search_bar.dart | 110 +++++ 3 files changed, 339 insertions(+), 313 deletions(-) create mode 100644 lib/widgets/floating_search_bar.dart diff --git a/lib/screens/home/top_items/top_items_screen.dart b/lib/screens/home/top_items/top_items_screen.dart index 94675ad..0f7465f 100644 --- a/lib/screens/home/top_items/top_items_screen.dart +++ b/lib/screens/home/top_items/top_items_screen.dart @@ -9,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/options_menu.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/floating_search_bar.dart'; import 'package:adguard_home_manager/models/menu_option.dart'; import 'package:adguard_home_manager/constants/enums.dart'; @@ -16,6 +17,7 @@ import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; enum _SortingOptions { highestToLowest, lowestToHighest } +final GlobalKey _searchButtonKey = GlobalKey(); class TopItemsScreen extends StatefulWidget { final HomeTopItems type; @@ -47,15 +49,18 @@ class TopItemsScreen extends StatefulWidget { class _TopItemsScreenState extends State { _SortingOptions _sortingOptions = _SortingOptions.highestToLowest; - bool searchActive = false; final TextEditingController searchController = TextEditingController(); + String? _currentSearchValue = ""; List> data = []; List> screenData = []; void search(String value) { List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList(); - setState(() => screenData = newValues); + setState(() { + screenData = newValues; + _currentSearchValue = searchController.text; + }); } @override @@ -75,118 +80,129 @@ class _TopItemsScreenState extends State { final sortedValues = _sortingOptions == _SortingOptions.lowestToHighest ? screenData.reversed.toList() : screenData.toList(); + + void showSearchDialog() { + showDialog( + context: context, + builder: (context) => FloatingSearchBar( + existingSearchValue: _currentSearchValue, + searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?, + onSearchCompleted: (v) { + List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(v)).toList(); + setState(() { + screenData = newValues; + _currentSearchValue = v; + }); + }, + ), + ); + } if (widget.isFullscreen == true) { - return Dialog.fullscreen( - child: Scaffold( - appBar: AppBar( - title: searchActive == true - ? Padding( - padding: const EdgeInsets.only(bottom: 3), - child: TextFormField( - controller: searchController, - onChanged: search, - decoration: InputDecoration( - hintText: AppLocalizations.of(context)!.search, - hintStyle: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 18 + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + title: Text(widget.title), + actions: [ + IconButton( + key: _searchButtonKey, + onPressed: showSearchDialog, + icon: const Icon(Icons.search_rounded), + tooltip: AppLocalizations.of(context)!.search, + ), + PopupMenuButton( + icon: const Icon(Icons.sort_rounded), + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => setState(() => _sortingOptions = _SortingOptions.highestToLowest), + child: Row( + children: [ + const Icon(Icons.arrow_downward_rounded), + const SizedBox(width: 8), + Expanded( + child: Text(AppLocalizations.of(context)!.fromHighestToLowest) + ), + const SizedBox(width: 16), + Icon( + _sortingOptions == _SortingOptions.highestToLowest + ? Icons.radio_button_checked_rounded + : Icons.radio_button_unchecked_rounded, + color: _sortingOptions == _SortingOptions.highestToLowest + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ) ), - border: InputBorder.none, - ), - style: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 18 - ), - autofocus: true, - ), - ) - : Text(widget.title), - leading: searchActive == true ? - IconButton( - onPressed: () => setState(() { - searchActive = false; - searchController.text = ''; - screenData = data; - }), - icon: const Icon(Icons.arrow_back), - tooltip: AppLocalizations.of(context)!.exitSearch, - ) : null, - actions: [ - if (searchActive == false) IconButton( - onPressed: () => setState(() => searchActive = true), - icon: const Icon(Icons.search), - tooltip: AppLocalizations.of(context)!.search, - ), - if (searchActive == true) IconButton( - onPressed: () => setState(() { - searchController.text = ''; - screenData = data; - }), - icon: const Icon(Icons.clear_rounded), - tooltip: AppLocalizations.of(context)!.clearSearch, - ), - PopupMenuButton( - icon: const Icon(Icons.sort_rounded), - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => setState(() => _sortingOptions = _SortingOptions.highestToLowest), - child: Row( - children: [ - const Icon(Icons.arrow_downward_rounded), - const SizedBox(width: 8), - Expanded( - child: Text(AppLocalizations.of(context)!.fromHighestToLowest) - ), - const SizedBox(width: 16), - Icon( - _sortingOptions == _SortingOptions.highestToLowest - ? Icons.radio_button_checked_rounded - : Icons.radio_button_unchecked_rounded, - color: _sortingOptions == _SortingOptions.highestToLowest - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant, + PopupMenuItem( + onTap: () => setState(() => _sortingOptions = _SortingOptions.lowestToHighest), + child: Row( + children: [ + const Icon(Icons.arrow_upward_rounded), + const SizedBox(width: 8), + Expanded( + child: Text(AppLocalizations.of(context)!.fromLowestToHighest) + ), + const SizedBox(width: 16), + Icon( + _sortingOptions == _SortingOptions.lowestToHighest + ? Icons.radio_button_checked_rounded + : Icons.radio_button_unchecked_rounded, + color: _sortingOptions == _SortingOptions.lowestToHighest + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], ) - ], - ) + ), + ], ), - PopupMenuItem( - onTap: () => setState(() => _sortingOptions = _SortingOptions.lowestToHighest), - child: Row( - children: [ - const Icon(Icons.arrow_upward_rounded), - const SizedBox(width: 8), - Expanded( - child: Text(AppLocalizations.of(context)!.fromLowestToHighest) + const SizedBox(width: 8) + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (sortedValues.isEmpty) Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.noItemsSearch, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - const SizedBox(width: 16), - Icon( - _sortingOptions == _SortingOptions.lowestToHighest - ? Icons.radio_button_checked_rounded - : Icons.radio_button_unchecked_rounded, - color: _sortingOptions == _SortingOptions.lowestToHighest - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant, - ) - ], - ) + ), + ), + ), + if (sortedValues.isNotEmpty) SliverList.builder( + itemCount: sortedValues.length, + itemBuilder: (context, index) => _Item( + data: sortedValues[index], + isClient: widget.isClient, + options: widget.options, + total: total, + withProgressBar: widget.withProgressBar, + onTapEntry: widget.onTapEntry, + buildValue: widget.buildValue, + ), ), ], ), - const SizedBox(width: 8) - ], - ), - body: SafeArea( - child: _Content( - buildValue: widget.buildValue, - isClient: widget.isClient, - onTapEntry: widget.onTapEntry, - options: widget.options, - screenData: sortedValues, - total: total, - withProgressBar: widget.withProgressBar, - ), - ), + ) + ) ), ); } @@ -242,14 +258,29 @@ class _TopItemsScreenState extends State { ), ), Expanded( - child: _Content( - buildValue: widget.buildValue, - isClient: widget.isClient, - onTapEntry: widget.onTapEntry, - options: widget.options, - screenData: sortedValues, - total: total, - withProgressBar: widget.withProgressBar, + child: sortedValues.isNotEmpty ? ListView.builder( + itemCount: sortedValues.length, + itemBuilder: (context, index) => _Item( + data: sortedValues[index], + isClient: widget.isClient, + options: widget.options, + withProgressBar: widget.withProgressBar, + onTapEntry: widget.onTapEntry, + buildValue: widget.buildValue, + total: total, + ), + ) : Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.noItemsSearch, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ), ), ], @@ -260,8 +291,8 @@ class _TopItemsScreenState extends State { } } -class _Content extends StatelessWidget { - final List> screenData; +class _Item extends StatelessWidget { + final dynamic data; final bool? isClient; final List Function(dynamic) options; final bool withProgressBar; @@ -269,8 +300,8 @@ class _Content extends StatelessWidget { final String Function(dynamic) buildValue; final double total; - const _Content({ - required this.screenData, + const _Item({ + required this.data, required this.isClient, required this.options, required this.withProgressBar, @@ -283,98 +314,75 @@ class _Content extends StatelessWidget { Widget build(BuildContext context) { final statusProvider = Provider.of(context); - if (screenData.isNotEmpty) { - return ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: screenData.length, - itemBuilder: (context, index) { - String? name; - if (isClient != null && isClient == true) { - try { - name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return OptionsMenu( - options: options, - value: screenData[index].keys.toList()[0], - onTap: onTapEntry != null - ? (v) { - onTapEntry!(v); - Navigator.pop(context); - } - : null, - child: CustomListTile( - title: screenData[index].keys.toList()[0], - trailing: Text( - buildValue(screenData[index].values.toList()[0]), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - subtitleWidget: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (name != null) ...[ - Text( - name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 5), - ], - if (withProgressBar == true) Row( - children: [ - SizedBox( - width: 50, - child: Text( - "${doubleFormat((screenData[index].values.toList()[0]/total*100), Platform.localeName)}%", - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor - ), - ), - ), - const SizedBox(width: 10), - Flexible( - child: LinearPercentIndicator( - animation: true, - lineHeight: 4, - animationDuration: 500, - curve: Curves.easeOut, - percent: screenData[index].values.toList()[0]/total, - barRadius: const Radius.circular(5), - progressColor: Theme.of(context).colorScheme.primary, - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - ), - const SizedBox(width: 10), - ], - ), - ], - ) - ), - ); - } - ); + String? name; + if (isClient != null && isClient == true) { + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(data.keys.toList()[0])).name; + } catch (e) { + // ---- // + } } - else { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.noItemsSearch, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + + return OptionsMenu( + options: options, + value: data.keys.toList()[0], + onTap: onTapEntry != null + ? (v) { + onTapEntry!(v); + Navigator.pop(context); + } + : null, + child: CustomListTile( + title: data.keys.toList()[0], + trailing: Text( + buildValue(data.values.toList()[0]), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant ), ), - ); - } + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (name != null) ...[ + Text( + name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 5), + ], + if (withProgressBar == true) Row( + children: [ + SizedBox( + width: 50, + child: Text( + "${doubleFormat((data.values.toList()[0]/total*100), Platform.localeName)}%", + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor + ), + ), + ), + const SizedBox(width: 10), + Flexible( + child: LinearPercentIndicator( + animation: true, + lineHeight: 4, + animationDuration: 500, + curve: Curves.easeOut, + percent: data.values.toList()[0]/total, + barRadius: const Radius.circular(5), + progressColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ) + ), + ); } } \ No newline at end of file diff --git a/lib/screens/logs/logs_list_appbar.dart b/lib/screens/logs/logs_list_appbar.dart index caab9fa..2a052f6 100644 --- a/lib/screens/logs/logs_list_appbar.dart +++ b/lib/screens/logs/logs_list_appbar.dart @@ -8,8 +8,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/logs/live/live_logs_screen.dart'; import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.dart'; +import 'package:adguard_home_manager/widgets/floating_search_bar.dart'; -import 'package:adguard_home_manager/config/globals.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/providers/live_logs_provider.dart'; @@ -61,9 +61,10 @@ class LogsListAppBar extends StatelessWidget { void showSearchDialog() { showDialog( context: context, - builder: (context) => _Search( + builder: (context) => FloatingSearchBar( + existingSearchValue: logsProvider.appliedFilters.searchText ?? "", searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?, - onSearch: (v) { + onSearchCompleted: (v) { logsProvider.setAppliedFilters( AppliedFiters( selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, @@ -73,6 +74,16 @@ class LogsListAppBar extends StatelessWidget { ); logsProvider.filterLogs(); }, + onSearchFieldUpdated: (v) { + if (v == "") { + logsProvider.setSearchText(null); + return; + } + logsProvider.setSearchText(v); + }, + onSearchFieldCleared: () { + logsProvider.setSearchText(null); + }, ), ); } @@ -279,106 +290,3 @@ class LogsListAppBar extends StatelessWidget { } } -class _Search extends StatefulWidget { - final void Function(String) onSearch; - final RenderBox? searchButtonRenderBox; - - const _Search({ - required this.onSearch, - required this.searchButtonRenderBox, - }); - - @override - State<_Search> createState() => _SearchState(); -} - -class _SearchState extends State<_Search> { - final _searchController = TextEditingController(); - - @override - void initState() { - final logsProvider = Provider.of(context, listen: false); - _searchController.text = logsProvider.appliedFilters.searchText ?? ""; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - - final position = widget.searchButtonRenderBox?.localToGlobal(Offset.zero); - final topPadding = MediaQuery.of(globalNavigatorKey.currentContext!).viewPadding.top; - - return GestureDetector( - onTap: () => Navigator.pop(context), - child: Material( - color: Colors.transparent, - child: LayoutBuilder( - builder: (context, constraints) { - final double width = constraints.maxWidth - 32 > 500 ? 500 : constraints.maxWidth - 32; - return Stack( - alignment: Alignment.topCenter, - children: [ - Positioned( - top: position != null ? position.dy - topPadding : topPadding, - child: SizedBox( - width: width, - child: GestureDetector( - onTap: () => {}, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16) - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: TextFormField( - controller: _searchController, - onChanged: (v) { - if (v == "") { - logsProvider.setSearchText(null); - return; - } - logsProvider.setSearchText(v); - }, - onFieldSubmitted: (v) { - widget.onSearch(v); - Navigator.pop(context); - }, - autofocus: true, - decoration: InputDecoration( - hintText: AppLocalizations.of(context)!.search, - prefixIcon: const Icon(Icons.search_rounded), - border: InputBorder.none, - filled: true, - fillColor: Colors.grey.withOpacity(0.2), - suffixIcon: _searchController.text != "" - ? IconButton( - onPressed: () { - _searchController.text = ""; - logsProvider.setSearchText(null); - }, - icon: const Icon( - Icons.close_rounded, - size: 20, - ), - tooltip: AppLocalizations.of(context)!.clearSearch, - ) - : null, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - ), - ), - ), - ), - ), - ), - ) - ], - ); - } - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/floating_search_bar.dart b/lib/widgets/floating_search_bar.dart new file mode 100644 index 0000000..bc09058 --- /dev/null +++ b/lib/widgets/floating_search_bar.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/config/globals.dart'; + +class FloatingSearchBar extends StatefulWidget { + final void Function(String) onSearchCompleted; + final RenderBox? searchButtonRenderBox; + final String? existingSearchValue; + final void Function(String)? onSearchFieldUpdated; + final void Function()? onSearchFieldCleared; + + const FloatingSearchBar({ + super.key, + required this.onSearchCompleted, + required this.searchButtonRenderBox, + this.existingSearchValue, + this.onSearchFieldUpdated, + this.onSearchFieldCleared, + }); + + @override + State createState() => _SearchState(); +} + +class _SearchState extends State { + final _searchController = TextEditingController(); + + @override + void initState() { + if (widget.existingSearchValue != null) { + _searchController.text = widget.existingSearchValue!; + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final position = widget.searchButtonRenderBox?.localToGlobal(Offset.zero); + final topPadding = MediaQuery.of(globalNavigatorKey.currentContext!).viewPadding.top; + + return GestureDetector( + onTap: () => Navigator.pop(context), + child: Material( + color: Colors.transparent, + child: LayoutBuilder( + builder: (context, constraints) { + final double width = constraints.maxWidth - 32 > 500 ? 500 : constraints.maxWidth - 32; + return Stack( + alignment: Alignment.topCenter, + children: [ + Positioned( + top: position != null ? position.dy - topPadding : topPadding, + child: SizedBox( + width: width, + child: GestureDetector( + onTap: () => {}, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16) + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: TextFormField( + controller: _searchController, + onChanged: widget.onSearchFieldUpdated, + onFieldSubmitted: (v) { + widget.onSearchCompleted(v); + Navigator.pop(context); + }, + autofocus: true, + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search_rounded), + border: InputBorder.none, + filled: true, + fillColor: Colors.grey.withOpacity(0.2), + suffixIcon: _searchController.text != "" + ? IconButton( + onPressed: () { + setState(() => _searchController.text = ""); + if (widget.onSearchFieldCleared != null) { + widget.onSearchFieldCleared!(); + } + }, + icon: const Icon( + Icons.close_rounded, + size: 20, + ), + tooltip: AppLocalizations.of(context)!.clearSearch, + ) + : null, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ), + ), + ), + ) + ], + ); + } + ), + ), + ); + } +} \ No newline at end of file