Adapted clients screen

This commit is contained in:
Juan Gilsanz Polo 2023-05-01 21:34:00 +02:00
parent 5660aefdc5
commit 3c92457040
14 changed files with 1249 additions and 373 deletions

View file

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/models/clients.dart';
class ActiveClientTile extends StatelessWidget {
final AutoClient client;
final void Function(AutoClient) onTap;
final bool splitView;
final AutoClient? selectedClient;
const ActiveClientTile({
Key? key,
required this.client,
required this.onTap,
required this.splitView,
this.selectedClient
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (splitView == true) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(28),
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: () => onTap(client),
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: client == selectedClient
? Theme.of(context).colorScheme.primaryContainer
: null
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.name != ''
? client.name!
: client.ip,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
if (client.name != '') Text(client.ip)
],
),
)
],
),
),
Text(
client.source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
],
)
),
),
),
);
}
else {
return CustomListTile(
title: client.name != ''
? client.name!
: client.ip,
subtitle: client.name != ''
? client.ip
: null,
trailing: Text(
client.source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
onTap: () => onTap(client),
);
}
}
}

View file

@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class AddedClientTile extends StatefulWidget {
final Client client;
final void Function(Client) onTap;
final void Function(Client) onLongPress;
final void Function(Client) onEdit;
final Client? selectedClient;
final bool? splitView;
const AddedClientTile({
Key? key,
required this.client,
required this.onTap,
required this.onLongPress,
required this.onEdit,
this.selectedClient,
required this.splitView
}) : super(key: key);
@override
State<AddedClientTile> createState() => _AddedClientTileState();
}
class _AddedClientTileState extends State<AddedClientTile> {
bool hover = false;
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
if (widget.splitView == true) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(28),
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: () => widget.onTap(widget.client),
onHover: (v) => setState(() => hover = v),
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: widget.client == widget.selectedClient
? Theme.of(context).colorScheme.primaryContainer
: null
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: widget.client.filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: widget.client.safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: widget.client.parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serversProvider.serverStatus.data!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: widget.client.safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
)
],
),
),
if (hover == true) IconButton(
onPressed: () => widget.onEdit(widget.client),
icon: const Icon(Icons.edit_rounded)
)
],
)
),
),
),
);
}
else {
return CustomListTile(
onLongPress: () => widget.onLongPress(widget.client),
onTap: () => widget.onTap(widget.client),
onHover: (v) => setState(() => hover = v),
title: widget.client.name,
subtitleWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: widget.client.filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: widget.client.safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: widget.client.parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serversProvider.serverStatus.data!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: widget.client.safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
trailing: hover == true
? IconButton(
onPressed: () => widget.onEdit(widget.client),
icon: const Icon(Icons.edit_rounded)
)
: null,
);
}
}
}

View file

@ -1,12 +1,16 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client_screen.dart';
import 'package:adguard_home_manager/screens/clients/added_client_tile.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/fab.dart';
import 'package:adguard_home_manager/screens/clients/options_modal.dart';
@ -27,13 +31,19 @@ class AddedList extends StatefulWidget {
final LoadStatus loadStatus;
final List<Client> data;
final Future Function() fetchClients;
final void Function(Client) onClientSelected;
final Client? selectedClient;
final bool splitView;
const AddedList({
Key? key,
required this.scrollController,
required this.loadStatus,
required this.data,
required this.fetchClients
required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView
}) : super(key: key);
@override
@ -69,6 +79,8 @@ class _AddedListState extends State<AddedList> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmEditClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient);
@ -130,6 +142,10 @@ class _AddedListState extends State<AddedList> {
clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList();
serversProvider.setClientsData(clientsData);
if (widget.splitView == true) {
SplitView.of(context).popUntil(0);
}
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
@ -150,15 +166,31 @@ class _AddedListState extends State<AddedList> {
}
void openClientModal(Client client) {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
)
));
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: true,
)
);
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: false,
)
));
}
}
void openDeleteModal(Client client) {
@ -181,6 +213,10 @@ class _AddedListState extends State<AddedList> {
}
return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS),
listPadding: widget.splitView == true
? const EdgeInsets.only(top: 8)
: null,
loadingGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
@ -202,98 +238,14 @@ class _AddedListState extends State<AddedList> {
),
),
itemsCount: widget.data.length,
contentWidget: (index) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
isThreeLine: true,
onLongPress: () => openOptionsModal(widget.data[index]),
onTap: () => openClientModal(widget.data[index]),
title: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(
widget.data[index].name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface
),
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.data[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor
),
),
const SizedBox(height: 7),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: widget.data[index].filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: widget.data[index].safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: widget.data[index].parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serversProvider.serverStatus.data!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? widget.data[index].safeSearch != null && widget.data[index].safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: widget.data[index].safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
),
contentWidget: (index) => AddedClientTile(
selectedClient: widget.selectedClient,
client: widget.data[index],
onTap: widget.onClientSelected,
onLongPress: openOptionsModal,
onEdit: openClientModal,
splitView: widget.splitView,
),
noData: SizedBox(
width: double.maxFinite,
child: Column(

View file

@ -19,6 +19,7 @@ class ClientScreen extends StatefulWidget {
final String serverVersion;
final void Function(Client) onConfirm;
final void Function(Client)? onDelete;
final bool dialog;
const ClientScreen({
Key? key,
@ -26,6 +27,7 @@ class ClientScreen extends StatefulWidget {
required this.serverVersion,
required this.onConfirm,
this.onDelete,
required this.dialog
}) : super(key: key);
@override
@ -300,51 +302,13 @@ class _ClientScreenState extends State<ClientScreen> {
),
);
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close)
),
title: Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient
),
actions: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
),
body: ListView(
Widget content(bool withPaddingTop) {
return ListView(
padding: const EdgeInsets.only(top: 0),
children: [
const SizedBox(height: 24),
if (withPaddingTop == true) const SizedBox(height: 24),
if (withPaddingTop == false) const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -693,10 +657,7 @@ class _ClientScreenState extends State<ClientScreen> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: editMode == true
? MediaQuery.of(context).size.width - 108
: MediaQuery.of(context).size.width - 40,
Expanded(
child: TextFormField(
enabled: editMode,
controller: controller['controller'],
@ -751,7 +712,125 @@ class _ClientScreenState extends State<ClientScreen> {
),
const SizedBox(height: 20)
],
),
);
);
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded)
),
const SizedBox(width: 8),
Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient,
style: const TextStyle(
fontSize: 22
),
),
],
),
Row(
children: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
)
],
),
),
Flexible(
child: content(false)
)
],
),
),
);
}
else {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close)
),
title: Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient
),
actions: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
),
body: content(true)
);
}
}
}

View file

@ -1,9 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.dart';
import 'package:adguard_home_manager/screens/clients/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart';
import 'package:adguard_home_manager/models/app_log.dart';
@ -90,83 +95,154 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
return DefaultTabController(
length: 2,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(AppLocalizations.of(context)!.clients),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
actions: [
if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[
IconButton(
onPressed: () => {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const SearchClients()
))
},
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: TabBar(
controller: tabController,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
icon: const Icon(Icons.devices),
text: AppLocalizations.of(context)!.activeClients,
),
Tab(
icon: const Icon(Icons.add_rounded),
text: AppLocalizations.of(context)!.added,
),
]
)
),
)
];
}),
body: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1)
)
)
PreferredSizeWidget tabBar() {
return TabBar(
controller: tabController,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.devices),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.activeClients)
],
),
),
child: TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.autoClientsData : [],
fetchClients: fetchClients,
),
AddedList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.clients : [],
fetchClients: fetchClients,
),
]
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add_rounded),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.added)
],
),
),
]
);
}
Widget tabBarView() {
return TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.autoClientsData : [],
fetchClients: fetchClients,
onClientSelected: (client) => Navigator.push(context, MaterialPageRoute(
builder: (context) => LogsListClient(
ip: client.ip,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider
)
)),
splitView: false,
),
AddedList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.clients : [],
fetchClients: fetchClients,
onClientSelected: (client) => Navigator.push(context, MaterialPageRoute(
builder: (context) => LogsListClient(
ip: client.ids[0],
serversProvider: serversProvider,
appConfigProvider: appConfigProvider
)
)),
splitView: false,
),
]
);
}
if (!(Platform.isAndroid || Platform.isIOS)) {
if (width > 900) {
return SplitView.material(
breakpoint: 900,
hideDivider: true,
child: ClientsDesktopView(
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
fetchClients: fetchClients,
)
),
)
);
);
}
else {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.clients),
centerTitle: false,
actions: [
if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[
IconButton(
onPressed: () => {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const SearchClients()
))
},
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
body: tabBarView(),
),
);
}
}
else {
return DefaultTabController(
length: 2,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(AppLocalizations.of(context)!.clients),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
actions: [
if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[
IconButton(
onPressed: () => {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const SearchClients()
))
},
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
)
];
}),
body: tabBarView()
)
);
}
}
}

View file

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.dart';
class ClientsDesktopView extends StatefulWidget {
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
final Future Function() fetchClients;
const ClientsDesktopView({
Key? key,
required this.serversProvider,
required this.appConfigProvider,
required this.fetchClients
}) : super(key: key);
@override
State<ClientsDesktopView> createState() => _ClientsDesktopViewState();
}
class _ClientsDesktopViewState extends State<ClientsDesktopView> with TickerProviderStateMixin {
late TabController tabController;
final ScrollController scrollController = ScrollController();
AutoClient? selectedActiveClient;
Client? selectedAddedClient;
@override
void initState() {
super.initState();
tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
tabController.addListener(() => widget.appConfigProvider.setSelectedClientsTab(tabController.index));
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
PreferredSizeWidget tabBar() {
return TabBar(
controller: tabController,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.devices),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.activeClients)
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add_rounded),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.added)
],
),
),
]
);
}
Widget tabBarView() {
return TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.autoClientsData : [],
fetchClients: widget.fetchClients,
onClientSelected: (client) => setState(() {
selectedAddedClient = null;
selectedActiveClient = client;
SplitView.of(context).setSecondary(
LogsListClient(
ip: client.ip,
name: client.name,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
)
);
}),
selectedClient: selectedActiveClient,
splitView: true
),
AddedList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.clients : [],
fetchClients: widget.fetchClients,
onClientSelected: (client) => setState(() {
selectedActiveClient = null;
selectedAddedClient = client;
SplitView.of(context).setSecondary(
LogsListClient(
ip: client.ids[0],
name: client.name,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
)
);
}),
selectedClient: selectedAddedClient,
splitView: true,
),
]
);
}
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.clients),
centerTitle: false,
actions: [
if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[
IconButton(
onPressed: () => {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const SearchClients()
))
},
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
body: tabBarView(),
),
);
}
}

View file

@ -1,36 +1,42 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/screens/clients/active_client_tile.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/models/applied_filters.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsList extends StatelessWidget {
final ScrollController scrollController;
final LoadStatus loadStatus;
final List<AutoClient> data;
final Future Function() fetchClients;
final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient;
final bool splitView;
const ClientsList({
Key? key,
required this.scrollController,
required this.loadStatus,
required this.data,
required this.fetchClients
required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView
}) : super(key: key);
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
return CustomTabContentList(
listPadding: splitView == true
? const EdgeInsets.only(top: 8)
: null,
noSliver: !(Platform.isAndroid || Platform.isIOS),
loadingGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
@ -52,32 +58,12 @@ class ClientsList extends StatelessWidget {
),
),
itemsCount: data.length,
contentWidget: (index) => CustomListTile(
title: data[index].name != ''
? data[index].name!
: data[index].ip,
subtitle: data[index].name != ''
? data[index].ip
: null,
trailing: Text(
data[index].source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
onTap: () {
logsProvider.setSearchText(null);
logsProvider.setSelectedClients([data[index].ip]);
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: 'all',
searchText: null,
clients: [data[index].ip]
)
);
appConfigProvider.setSelectedScreen(2);
},
),
contentWidget: (index) => ActiveClientTile(
client: data[index],
onTap: onClientSelected,
splitView: splitView,
selectedClient: selectedClient,
),
noData: SizedBox(
width: double.maxFinite,
child: Column(

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -23,6 +25,8 @@ class ClientsFab extends StatelessWidget {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmAddClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient);
@ -65,13 +69,27 @@ class ClientsFab extends StatelessWidget {
}
void openAddClient() {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmAddClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
)
));
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmAddClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
dialog: true,
)
);
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmAddClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
dialog: false,
)
));
}
}
return FloatingActionButton(

View file

@ -0,0 +1,223 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
class LogsListClient extends StatefulWidget {
final String ip;
final String? name;
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
const LogsListClient({
Key? key,
required this.ip,
this.name,
required this.serversProvider,
required this.appConfigProvider
}) : super(key: key);
@override
State<LogsListClient> createState() => _LogsListClientState();
}
class _LogsListClientState extends State<LogsListClient> {
late ScrollController scrollController;
bool isLoadingMore = false;
int logsQuantity = 100;
int offset = 0;
int loadStatus = 0;
LogsData? logsData;
String previousIp = "";
bool showDivider = true;
Future fetchLogs({
int? inOffset,
bool? loadingMore,
String? responseStatus,
String? searchText,
}) async {
int offst = inOffset ?? offset;
if (loadingMore != null && loadingMore == true) {
setState(() => isLoadingMore = true);
}
final result = await getLogs(
server: widget.serversProvider.selectedServer!,
count: logsQuantity,
offset: offst,
search: '"${widget.ip}"'
);
if (loadingMore != null && loadingMore == true) {
setState(() => isLoadingMore = false);
}
if (mounted) {
if (result['result'] == 'success') {
setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity);
if (loadingMore != null && loadingMore == true && logsData != null) {
LogsData newLogsData = result['data'];
newLogsData.data = [...logsData!.data, ...result['data'].data];
setState(() => logsData = newLogsData);
}
else {
LogsData newLogsData = result['data'];
setState(() => logsData = newLogsData);
}
setState(() => loadStatus = 1);
}
else {
setState(() => loadStatus = 2);
widget.appConfigProvider.addLog(result['log']);
}
}
}
void scrollListener() {
if (scrollController.position.extentAfter < 500 && isLoadingMore == false) {
fetchLogs(loadingMore: true);
}
if (scrollController.position.pixels > 0) {
setState(() => showDivider = false);
}
else {
setState(() => showDivider = true);
}
}
@override
void initState() {
scrollController = ScrollController()..addListener(scrollListener);
fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip);
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ip != previousIp) {
setState(() => loadStatus = 0);
if (scrollController.hasClients) scrollController.animateTo(0, duration: const Duration(milliseconds: 1), curve: Curves.ease);
fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip);
}
Widget status() {
switch (loadStatus) {
case 0:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingLogs,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
);
case 1:
return RefreshIndicator(
onRefresh: fetchLogs,
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.only(top: 0),
itemCount: isLoadingMore == true
? logsData!.data.length+1
: logsData!.data.length,
itemBuilder: (context, index) {
if (isLoadingMore == true && index == logsData!.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(),
),
);
}
else {
return LogTile(
log: logsData!.data[index],
index: index,
length: logsData!.data.length,
onLogTap: (log) {
}
);
}
}
),
);
case 2:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.logsNotLoaded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
);
default:
return const SizedBox();
}
}
return Scaffold(
appBar: AppBar(
title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip),
centerTitle: true,
actions: [
if (!(Platform.isAndroid || Platform.isIOS)) ...[
IconButton(
onPressed: fetchLogs,
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.refresh,
),
const SizedBox(width: 8)
]
],
),
body: status(),
);
}
}

View file

@ -64,109 +64,114 @@ class _SafeSearchModalState extends State<SafeSearchModal> {
)
],
),
content: Wrap(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: widget.disabled == true
? null
: () => setState(() => generalEnabled = !generalEnabled),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400
),
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.enable,
style: TextStyle(
fontSize: 16,
color: widget.disabled == true
? Colors.grey
: Theme.of(context).colorScheme.onSurface
child: InkWell(
onTap: widget.disabled == true
? null
: () => setState(() => generalEnabled = !generalEnabled),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.enable,
style: TextStyle(
fontSize: 16,
color: widget.disabled == true
? Colors.grey
: Theme.of(context).colorScheme.onSurface
),
),
),
Switch(
value: generalEnabled,
onChanged: widget.disabled == true
? null
: (value) => setState(() => generalEnabled = value),
)
],
Switch(
value: generalEnabled,
onChanged: widget.disabled == true
? null
: (value) => setState(() => generalEnabled = value),
)
],
),
),
),
),
),
),
const SizedBox(height: 4, width: double.maxFinite),
CustomCheckboxListTile(
value: bingEnabled,
onChanged: (value) => setState(() => bingEnabled = value),
title: "Bing",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
const SizedBox(height: 4, width: double.maxFinite),
CustomCheckboxListTile(
value: bingEnabled,
onChanged: (value) => setState(() => bingEnabled = value),
title: "Bing",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
CustomCheckboxListTile(
value: duckduckgoEnabled,
onChanged: (value) => setState(() => duckduckgoEnabled = value),
title: "DuckDuckGo",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
CustomCheckboxListTile(
value: duckduckgoEnabled,
onChanged: (value) => setState(() => duckduckgoEnabled = value),
title: "DuckDuckGo",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
CustomCheckboxListTile(
value: googleEnabled,
onChanged: (value) => setState(() => googleEnabled = value),
title: "Google",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
CustomCheckboxListTile(
value: googleEnabled,
onChanged: (value) => setState(() => googleEnabled = value),
title: "Google",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
CustomCheckboxListTile(
value: pixabayEnabled,
onChanged: (value) => setState(() => pixabayEnabled = value),
title: "Pixabay",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
CustomCheckboxListTile(
value: pixabayEnabled,
onChanged: (value) => setState(() => pixabayEnabled = value),
title: "Pixabay",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
CustomCheckboxListTile(
value: yandexEnabled,
onChanged: (value) => setState(() => yandexEnabled = value),
title: "Yandex",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
CustomCheckboxListTile(
value: yandexEnabled,
onChanged: (value) => setState(() => yandexEnabled = value),
title: "Yandex",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
CustomCheckboxListTile(
value: youtubeEnabled,
onChanged: (value) => setState(() => youtubeEnabled = value),
title: "YouTube",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
CustomCheckboxListTile(
value: youtubeEnabled,
onChanged: (value) => setState(() => youtubeEnabled = value),
title: "YouTube",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
),
],
],
),
),
actions: [
TextButton(

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:provider/provider.dart';
@ -99,6 +101,8 @@ class _SearchClientsWidgetState extends State<SearchClientsWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void deleteClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.removingClient);
@ -183,15 +187,31 @@ class _SearchClientsWidgetState extends State<SearchClientsWidget> {
}
void openClientModal(Client client) {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
onDelete: deleteClient,
client: client,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
)
));
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: true,
)
);
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: false,
)
));
}
}
void openDeleteModal(Client client) {

View file

@ -42,7 +42,7 @@ class LogTile extends StatelessWidget {
required String text
}) {
return SizedBox(
width: 70,
width: 80,
child: Column(
children: [
Icon(

View file

@ -10,6 +10,7 @@ class CustomListTile extends StatelessWidget {
final EdgeInsets? padding;
final void Function()? onLongPress;
final bool? disabled;
final void Function(bool)? onHover;
const CustomListTile({
Key? key,
@ -21,7 +22,8 @@ class CustomListTile extends StatelessWidget {
this.trailing,
this.padding,
this.onLongPress,
this.disabled
this.disabled,
this.onHover
}) : super(key: key);
@override
@ -30,6 +32,7 @@ class CustomListTile extends StatelessWidget {
color: Colors.transparent,
child: InkWell(
onTap: onTap,
onHover: onHover,
onLongPress: onLongPress,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),

View file

@ -16,6 +16,7 @@ class CustomTabContentList extends StatelessWidget {
final Widget? fab;
final bool? fabVisible;
final bool? noSliver;
final EdgeInsets? listPadding;
const CustomTabContentList({
Key? key,
@ -29,7 +30,8 @@ class CustomTabContentList extends StatelessWidget {
this.refreshIndicatorOffset,
this.fab,
this.fabVisible,
this.noSliver
this.noSliver,
this.listPadding
}) : super(key: key);
@override
@ -73,6 +75,7 @@ class CustomTabContentList extends StatelessWidget {
return Stack(
children: [
ListView.builder(
padding: listPadding,
itemCount: itemsCount,
itemBuilder: (context, index) => contentWidget(index),
),
@ -117,7 +120,7 @@ class CustomTabContentList extends StatelessWidget {
builder: (BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
edgeOffset: refreshIndicatorOffset ?? 95,
edgeOffset: refreshIndicatorOffset ?? 70,
child: CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(