Changed design top items screen

This commit is contained in:
Juan Gilsanz Polo 2024-09-11 18:51:57 +02:00
parent f9149056fd
commit 9eb200f5da
3 changed files with 339 additions and 313 deletions

View file

@ -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/options_menu.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.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/models/menu_option.dart';
import 'package:adguard_home_manager/constants/enums.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'; import 'package:adguard_home_manager/providers/status_provider.dart';
enum _SortingOptions { highestToLowest, lowestToHighest } enum _SortingOptions { highestToLowest, lowestToHighest }
final GlobalKey _searchButtonKey = GlobalKey();
class TopItemsScreen extends StatefulWidget { class TopItemsScreen extends StatefulWidget {
final HomeTopItems type; final HomeTopItems type;
@ -47,15 +49,18 @@ class TopItemsScreen extends StatefulWidget {
class _TopItemsScreenState extends State<TopItemsScreen> { class _TopItemsScreenState extends State<TopItemsScreen> {
_SortingOptions _sortingOptions = _SortingOptions.highestToLowest; _SortingOptions _sortingOptions = _SortingOptions.highestToLowest;
bool searchActive = false;
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
String? _currentSearchValue = "";
List<Map<String, dynamic>> data = []; List<Map<String, dynamic>> data = [];
List<Map<String, dynamic>> screenData = []; List<Map<String, dynamic>> screenData = [];
void search(String value) { void search(String value) {
List<Map<String, dynamic>> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList(); List<Map<String, dynamic>> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList();
setState(() => screenData = newValues); setState(() {
screenData = newValues;
_currentSearchValue = searchController.text;
});
} }
@override @override
@ -75,118 +80,129 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
final sortedValues = _sortingOptions == _SortingOptions.lowestToHighest final sortedValues = _sortingOptions == _SortingOptions.lowestToHighest
? screenData.reversed.toList() ? screenData.reversed.toList()
: screenData.toList(); : screenData.toList();
void showSearchDialog() {
showDialog(
context: context,
builder: (context) => FloatingSearchBar(
existingSearchValue: _currentSearchValue,
searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?,
onSearchCompleted: (v) {
List<Map<String, dynamic>> newValues = widget.data.where((item) => item.keys.toList()[0].contains(v)).toList();
setState(() {
screenData = newValues;
_currentSearchValue = v;
});
},
),
);
}
if (widget.isFullscreen == true) { if (widget.isFullscreen == true) {
return Dialog.fullscreen( return Scaffold(
child: Scaffold( body: NestedScrollView(
appBar: AppBar( headerSliverBuilder: (context, innerBoxIsScrolled) => [
title: searchActive == true SliverOverlapAbsorber(
? Padding( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
padding: const EdgeInsets.only(bottom: 3), sliver: SliverAppBar.large(
child: TextFormField( title: Text(widget.title),
controller: searchController, actions: [
onChanged: search, IconButton(
decoration: InputDecoration( key: _searchButtonKey,
hintText: AppLocalizations.of(context)!.search, onPressed: showSearchDialog,
hintStyle: const TextStyle( icon: const Icon(Icons.search_rounded),
fontWeight: FontWeight.normal, tooltip: AppLocalizations.of(context)!.search,
fontSize: 18 ),
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, PopupMenuItem(
), onTap: () => setState(() => _sortingOptions = _SortingOptions.lowestToHighest),
style: const TextStyle( child: Row(
fontWeight: FontWeight.normal, children: [
fontSize: 18 const Icon(Icons.arrow_upward_rounded),
), const SizedBox(width: 8),
autofocus: true, Expanded(
), child: Text(AppLocalizations.of(context)!.fromLowestToHighest)
) ),
: Text(widget.title), const SizedBox(width: 16),
leading: searchActive == true ? Icon(
IconButton( _sortingOptions == _SortingOptions.lowestToHighest
onPressed: () => setState(() { ? Icons.radio_button_checked_rounded
searchActive = false; : Icons.radio_button_unchecked_rounded,
searchController.text = ''; color: _sortingOptions == _SortingOptions.lowestToHighest
screenData = data; ? Theme.of(context).colorScheme.primary
}), : Theme.of(context).colorScheme.onSurfaceVariant,
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( const SizedBox(width: 8)
onTap: () => setState(() => _sortingOptions = _SortingOptions.lowestToHighest), ],
child: Row( )
children: [ )
const Icon(Icons.arrow_upward_rounded), ],
const SizedBox(width: 8), body: SafeArea(
Expanded( top: false,
child: Text(AppLocalizations.of(context)!.fromLowestToHighest) 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 if (sortedValues.isNotEmpty) SliverList.builder(
: Icons.radio_button_unchecked_rounded, itemCount: sortedValues.length,
color: _sortingOptions == _SortingOptions.lowestToHighest itemBuilder: (context, index) => _Item(
? Theme.of(context).colorScheme.primary data: sortedValues[index],
: Theme.of(context).colorScheme.onSurfaceVariant, 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<TopItemsScreen> {
), ),
), ),
Expanded( Expanded(
child: _Content( child: sortedValues.isNotEmpty ? ListView.builder(
buildValue: widget.buildValue, itemCount: sortedValues.length,
isClient: widget.isClient, itemBuilder: (context, index) => _Item(
onTapEntry: widget.onTapEntry, data: sortedValues[index],
options: widget.options, isClient: widget.isClient,
screenData: sortedValues, options: widget.options,
total: total, withProgressBar: widget.withProgressBar,
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<TopItemsScreen> {
} }
} }
class _Content extends StatelessWidget { class _Item extends StatelessWidget {
final List<Map<String, dynamic>> screenData; final dynamic data;
final bool? isClient; final bool? isClient;
final List<MenuOption> Function(dynamic) options; final List<MenuOption> Function(dynamic) options;
final bool withProgressBar; final bool withProgressBar;
@ -269,8 +300,8 @@ class _Content extends StatelessWidget {
final String Function(dynamic) buildValue; final String Function(dynamic) buildValue;
final double total; final double total;
const _Content({ const _Item({
required this.screenData, required this.data,
required this.isClient, required this.isClient,
required this.options, required this.options,
required this.withProgressBar, required this.withProgressBar,
@ -283,98 +314,75 @@ class _Content extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final statusProvider = Provider.of<StatusProvider>(context); final statusProvider = Provider.of<StatusProvider>(context);
if (screenData.isNotEmpty) { String? name;
return ListView.builder( if (isClient != null && isClient == true) {
padding: const EdgeInsets.only(top: 0), try {
itemCount: screenData.length, name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(data.keys.toList()[0])).name;
itemBuilder: (context, index) { } catch (e) {
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),
],
),
],
)
),
);
}
);
} }
else {
return Center( return OptionsMenu(
child: Padding( options: options,
padding: const EdgeInsets.symmetric(horizontal: 20), value: data.keys.toList()[0],
child: Text( onTap: onTapEntry != null
AppLocalizations.of(context)!.noItemsSearch, ? (v) {
textAlign: TextAlign.center, onTapEntry!(v);
style: TextStyle( Navigator.pop(context);
fontSize: 22, }
color: Theme.of(context).colorScheme.onSurfaceVariant, : 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),
],
),
],
)
),
);
} }
} }

View file

@ -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/live/live_logs_screen.dart';
import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.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/constants/enums.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/providers/live_logs_provider.dart'; import 'package:adguard_home_manager/providers/live_logs_provider.dart';
@ -61,9 +61,10 @@ class LogsListAppBar extends StatelessWidget {
void showSearchDialog() { void showSearchDialog() {
showDialog( showDialog(
context: context, context: context,
builder: (context) => _Search( builder: (context) => FloatingSearchBar(
existingSearchValue: logsProvider.appliedFilters.searchText ?? "",
searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?, searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?,
onSearch: (v) { onSearchCompleted: (v) {
logsProvider.setAppliedFilters( logsProvider.setAppliedFilters(
AppliedFiters( AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
@ -73,6 +74,16 @@ class LogsListAppBar extends StatelessWidget {
); );
logsProvider.filterLogs(); 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<LogsProvider>(context, listen: false);
_searchController.text = logsProvider.appliedFilters.searchText ?? "";
super.initState();
}
@override
Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(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),
),
),
),
),
),
),
)
],
);
}
),
),
);
}
}

View file

@ -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<FloatingSearchBar> createState() => _SearchState();
}
class _SearchState extends State<FloatingSearchBar> {
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),
),
),
),
),
),
),
)
],
);
}
),
),
);
}
}