From 3c924570409eb27115d735d3ee61f30449b6f327 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Mon, 1 May 2023 21:34:00 +0200 Subject: [PATCH] Adapted clients screen --- lib/screens/clients/active_client_tile.dart | 100 +++++++ lib/screens/clients/added_client_tile.dart | 250 ++++++++++++++++++ lib/screens/clients/added_list.dart | 156 ++++------- lib/screens/clients/client_screen.dart | 179 +++++++++---- lib/screens/clients/clients.dart | 226 ++++++++++------ lib/screens/clients/clients_desktop_view.dart | 161 +++++++++++ lib/screens/clients/clients_list.dart | 56 ++-- lib/screens/clients/fab.dart | 32 ++- lib/screens/clients/logs_list_client.dart | 223 ++++++++++++++++ lib/screens/clients/safe_search_modal.dart | 187 ++++++------- lib/screens/clients/search_clients.dart | 38 ++- lib/screens/logs/log_tile.dart | 2 +- lib/widgets/custom_list_tile.dart | 5 +- lib/widgets/tab_content_list.dart | 7 +- 14 files changed, 1249 insertions(+), 373 deletions(-) create mode 100644 lib/screens/clients/active_client_tile.dart create mode 100644 lib/screens/clients/added_client_tile.dart create mode 100644 lib/screens/clients/clients_desktop_view.dart create mode 100644 lib/screens/clients/logs_list_client.dart diff --git a/lib/screens/clients/active_client_tile.dart b/lib/screens/clients/active_client_tile.dart new file mode 100644 index 0000000..cabbb6c --- /dev/null +++ b/lib/screens/clients/active_client_tile.dart @@ -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), + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/added_client_tile.dart b/lib/screens/clients/added_client_tile.dart new file mode 100644 index 0000000..d8a4b8f --- /dev/null +++ b/lib/screens/clients/added_client_tile.dart @@ -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 createState() => _AddedClientTileState(); +} + +class _AddedClientTileState extends State { + bool hover = false; + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(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, + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 922d2de..9bb3ab5 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -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 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 { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(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 { 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 { } 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 { } 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 { ), ), 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( diff --git a/lib/screens/clients/client_screen.dart b/lib/screens/clients/client_screen.dart index 461ba42..2e282f5 100644 --- a/lib/screens/clients/client_screen.dart +++ b/lib/screens/clients/client_screen.dart @@ -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 { ), ); } - - 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 { 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 { ), 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) + ); + } } } \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index 549b242..3b52760 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -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 with TickerProviderStateM @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(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() + ) + ); + } } } \ No newline at end of file diff --git a/lib/screens/clients/clients_desktop_view.dart b/lib/screens/clients/clients_desktop_view.dart new file mode 100644 index 0000000..8f0f772 --- /dev/null +++ b/lib/screens/clients/clients_desktop_view.dart @@ -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 createState() => _ClientsDesktopViewState(); +} + +class _ClientsDesktopViewState extends State 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(context); + final appConfigProvider = Provider.of(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(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index af1ee86..3ca250b 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -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 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(context); - final logsProvider = Provider.of(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( diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart index 4eff04e..83cf864 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -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(context); final appConfigProvider = Provider.of(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( diff --git a/lib/screens/clients/logs_list_client.dart b/lib/screens/clients/logs_list_client.dart new file mode 100644 index 0000000..ef910fd --- /dev/null +++ b/lib/screens/clients/logs_list_client.dart @@ -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 createState() => _LogsListClientState(); +} + +class _LogsListClientState extends State { + 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(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/safe_search_modal.dart b/lib/screens/clients/safe_search_modal.dart index 9d51f01..1be694c 100644 --- a/lib/screens/clients/safe_search_modal.dart +++ b/lib/screens/clients/safe_search_modal.dart @@ -64,109 +64,114 @@ class _SafeSearchModalState extends State { ) ], ), - 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( diff --git a/lib/screens/clients/search_clients.dart b/lib/screens/clients/search_clients.dart index ce5d2bf..e22e238 100644 --- a/lib/screens/clients/search_clients.dart +++ b/lib/screens/clients/search_clients.dart @@ -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 { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(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 { } 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) { diff --git a/lib/screens/logs/log_tile.dart b/lib/screens/logs/log_tile.dart index 5e10280..776bd0c 100644 --- a/lib/screens/logs/log_tile.dart +++ b/lib/screens/logs/log_tile.dart @@ -42,7 +42,7 @@ class LogTile extends StatelessWidget { required String text }) { return SizedBox( - width: 70, + width: 80, child: Column( children: [ Icon( diff --git a/lib/widgets/custom_list_tile.dart b/lib/widgets/custom_list_tile.dart index 34a7c80..bfd379d 100644 --- a/lib/widgets/custom_list_tile.dart +++ b/lib/widgets/custom_list_tile.dart @@ -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), diff --git a/lib/widgets/tab_content_list.dart b/lib/widgets/tab_content_list.dart index 9603c95..d1f2ad0 100644 --- a/lib/widgets/tab_content_list.dart +++ b/lib/widgets/tab_content_list.dart @@ -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: [ SliverOverlapInjector(