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
@ -76,56 +81,38 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
? 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,
onChanged: search,
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.search,
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
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: [ actions: [
if (searchActive == false) IconButton( IconButton(
onPressed: () => setState(() => searchActive = true), key: _searchButtonKey,
icon: const Icon(Icons.search), onPressed: showSearchDialog,
icon: const Icon(Icons.search_rounded),
tooltip: AppLocalizations.of(context)!.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( PopupMenuButton(
icon: const Icon(Icons.sort_rounded), icon: const Icon(Icons.sort_rounded),
itemBuilder: (context) => [ itemBuilder: (context) => [
@ -175,18 +162,47 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
), ),
const SizedBox(width: 8) const SizedBox(width: 8)
], ],
), )
)
],
body: SafeArea( body: SafeArea(
child: _Content( top: false,
buildValue: widget.buildValue, 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,
),
),
),
),
if (sortedValues.isNotEmpty) SliverList.builder(
itemCount: sortedValues.length,
itemBuilder: (context, index) => _Item(
data: sortedValues[index],
isClient: widget.isClient, isClient: widget.isClient,
onTapEntry: widget.onTapEntry,
options: widget.options, options: widget.options,
screenData: sortedValues,
total: total, total: total,
withProgressBar: widget.withProgressBar, withProgressBar: widget.withProgressBar,
onTapEntry: widget.onTapEntry,
buildValue: widget.buildValue,
), ),
), ),
],
),
)
)
), ),
); );
} }
@ -242,14 +258,29 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
), ),
), ),
Expanded( Expanded(
child: _Content( child: sortedValues.isNotEmpty ? ListView.builder(
buildValue: widget.buildValue, itemCount: sortedValues.length,
itemBuilder: (context, index) => _Item(
data: sortedValues[index],
isClient: widget.isClient, isClient: widget.isClient,
onTapEntry: widget.onTapEntry,
options: widget.options, options: widget.options,
screenData: sortedValues,
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,15 +314,10 @@ 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) {
return ListView.builder(
padding: const EdgeInsets.only(top: 0),
itemCount: screenData.length,
itemBuilder: (context, index) {
String? name; String? name;
if (isClient != null && isClient == true) { if (isClient != null && isClient == true) {
try { try {
name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(data.keys.toList()[0])).name;
} catch (e) { } catch (e) {
// ---- // // ---- //
} }
@ -299,7 +325,7 @@ class _Content extends StatelessWidget {
return OptionsMenu( return OptionsMenu(
options: options, options: options,
value: screenData[index].keys.toList()[0], value: data.keys.toList()[0],
onTap: onTapEntry != null onTap: onTapEntry != null
? (v) { ? (v) {
onTapEntry!(v); onTapEntry!(v);
@ -307,9 +333,9 @@ class _Content extends StatelessWidget {
} }
: null, : null,
child: CustomListTile( child: CustomListTile(
title: screenData[index].keys.toList()[0], title: data.keys.toList()[0],
trailing: Text( trailing: Text(
buildValue(screenData[index].values.toList()[0]), buildValue(data.values.toList()[0]),
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant color: Theme.of(context).colorScheme.onSurfaceVariant
), ),
@ -332,7 +358,7 @@ class _Content extends StatelessWidget {
SizedBox( SizedBox(
width: 50, width: 50,
child: Text( child: Text(
"${doubleFormat((screenData[index].values.toList()[0]/total*100), Platform.localeName)}%", "${doubleFormat((data.values.toList()[0]/total*100), Platform.localeName)}%",
style: TextStyle( style: TextStyle(
color: Theme.of(context).listTileTheme.textColor color: Theme.of(context).listTileTheme.textColor
), ),
@ -345,7 +371,7 @@ class _Content extends StatelessWidget {
lineHeight: 4, lineHeight: 4,
animationDuration: 500, animationDuration: 500,
curve: Curves.easeOut, curve: Curves.easeOut,
percent: screenData[index].values.toList()[0]/total, percent: data.values.toList()[0]/total,
barRadius: const Radius.circular(5), barRadius: const Radius.circular(5),
progressColor: Theme.of(context).colorScheme.primary, progressColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
@ -359,22 +385,4 @@ class _Content extends StatelessWidget {
), ),
); );
} }
);
}
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,
),
),
),
);
}
}
} }

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),
),
),
),
),
),
),
)
],
);
}
),
),
);
}
}