Implemented new router

This commit is contained in:
Juan Gilsanz Polo 2023-10-29 02:19:00 +01:00
parent 621171c5b1
commit 96fe7eb730
17 changed files with 985 additions and 1090 deletions

View file

@ -1,12 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adguard_home_manager/screens/filters/filters.dart';
import 'package:adguard_home_manager/screens/logs/logs.dart';
import 'package:adguard_home_manager/screens/connect/connect.dart';
import 'package:adguard_home_manager/screens/home/home.dart';
import 'package:adguard_home_manager/screens/clients/clients.dart';
import 'package:adguard_home_manager/screens/settings/settings.dart';
import 'package:adguard_home_manager/models/app_screen.dart'; import 'package:adguard_home_manager/models/app_screen.dart';
List<AppScreen> screensSelectServer = [ List<AppScreen> screensSelectServer = [

View file

@ -16,4 +16,16 @@ class RoutesNames {
static const String filters = "/filters"; static const String filters = "/filters";
static const String settings = "/settings"; static const String settings = "/settings";
static const String safeSearch = "/settings/safe-search";
static const String accessSettings = "/settings/access-settigs";
static const String dhcpSettings = "/settings/dhcp-settings";
static const String dnsSettings = "/settings/dns-settings";
static const String encryptionSettings = "/settings/encryption-settings";
static const String dnsRewrites = "/settings/dns-rewrites";
static const String serverUpdates = "/settings/server-updates";
static const String serverInfo = "/settings/server-info";
static const String customization = "/settings/customization";
static const String servers = "/settings/servers";
static const String generalSettings = "/settings/general-settings";
static const String advancedSettings = "/settings/advanced-settings";
} }

View file

@ -4,7 +4,7 @@ final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> connectNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> connectNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> homeNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> homeNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> clientsNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> clientsNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> clientsListNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> logsNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> logsNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> filtersNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> filtersNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> settingsNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> settingsNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> settingsListNavigatorKey = GlobalKey<NavigatorState>();

View file

@ -4,10 +4,8 @@ import 'package:adguard_home_manager/screens/home/home.dart';
import 'package:adguard_home_manager/screens/clients/clients.dart'; import 'package:adguard_home_manager/screens/clients/clients.dart';
import 'package:adguard_home_manager/screens/connect/connect.dart'; import 'package:adguard_home_manager/screens/connect/connect.dart';
import 'package:adguard_home_manager/screens/filters/filters.dart'; import 'package:adguard_home_manager/screens/filters/filters.dart';
import 'package:adguard_home_manager/screens/settings/settings.dart';
import 'package:adguard_home_manager/screens/clients/client/client_placeholder.dart';
import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart';
import 'package:adguard_home_manager/screens/logs/logs.dart'; import 'package:adguard_home_manager/screens/logs/logs.dart';
import 'package:adguard_home_manager/screens/settings/settings.dart';
import 'package:adguard_home_manager/widgets/layout.dart'; import 'package:adguard_home_manager/widgets/layout.dart';
import 'package:adguard_home_manager/routes/router_globals.dart'; import 'package:adguard_home_manager/routes/router_globals.dart';
@ -31,44 +29,15 @@ final List<RouteBase> routes = [
path: RoutesNames.home, path: RoutesNames.home,
builder: (context, state) => const Home(), builder: (context, state) => const Home(),
), ),
GoRoute(
parentNavigatorKey: homeNavigatorKey,
path: RoutesNames.queriedDomains,
builder: (context, state) => const Home(),
),
GoRoute(
parentNavigatorKey: homeNavigatorKey,
path: RoutesNames.blockedDomains,
builder: (context, state) => const Home(),
),
GoRoute(
parentNavigatorKey: homeNavigatorKey,
path: RoutesNames.recurrentClients,
builder: (context, state) => const Home(),
),
] ]
), ),
StatefulShellBranch( StatefulShellBranch(
navigatorKey: clientsNavigatorKey, navigatorKey: clientsNavigatorKey,
routes: [ routes: [
ShellRoute( GoRoute(
parentNavigatorKey: clientsNavigatorKey, parentNavigatorKey: clientsNavigatorKey,
navigatorKey: clientsListNavigatorKey, path: RoutesNames.clients,
builder: (context, state, child) => Clients(child: child), builder: (context, state) => const Clients(),
routes: [
GoRoute(
path: RoutesNames.clientPlaceholder,
parentNavigatorKey: clientsListNavigatorKey,
builder: (context, state) => const ClientPlaceholder(),
),
GoRoute(
path: RoutesNames.client,
parentNavigatorKey: clientsListNavigatorKey,
builder: (context, state) => LogsListClient(
id: (state.extra as Map?)?['id']
)
)
]
) )
] ]
), ),
@ -76,6 +45,7 @@ final List<RouteBase> routes = [
navigatorKey: logsNavigatorKey, navigatorKey: logsNavigatorKey,
routes: [ routes: [
GoRoute( GoRoute(
parentNavigatorKey: logsNavigatorKey,
path: RoutesNames.logs, path: RoutesNames.logs,
builder: (context, state) => const Logs(), builder: (context, state) => const Logs(),
) )
@ -85,6 +55,7 @@ final List<RouteBase> routes = [
navigatorKey: filtersNavigatorKey, navigatorKey: filtersNavigatorKey,
routes: [ routes: [
GoRoute( GoRoute(
parentNavigatorKey: filtersNavigatorKey,
path: RoutesNames.filters, path: RoutesNames.filters,
builder: (context, state) => const Filters(), builder: (context, state) => const Filters(),
) )
@ -94,7 +65,8 @@ final List<RouteBase> routes = [
navigatorKey: settingsNavigatorKey, navigatorKey: settingsNavigatorKey,
routes: [ routes: [
GoRoute( GoRoute(
path: RoutesNames.settings, parentNavigatorKey: settingsNavigatorKey,
path: RoutesNames.settings,
builder: (context, state) => const Settings(), builder: (context, state) => const Settings(),
) )
] ]

View file

@ -1,9 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -14,12 +12,20 @@ import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/providers/app_config_provider.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/providers/servers_provider.dart';
class LogsListClient extends StatefulHookWidget { class LogsListClient extends StatefulWidget {
final String id; final String ip;
final String? name;
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
final bool splitView;
const LogsListClient({ const LogsListClient({
Key? key, Key? key,
required this.id, required this.ip,
this.name,
required this.serversProvider,
required this.appConfigProvider,
required this.splitView,
}) : super(key: key); }) : super(key: key);
@override @override
@ -34,9 +40,11 @@ class _LogsListClientState extends State<LogsListClient> {
int logsQuantity = 100; int logsQuantity = 100;
int offset = 0; int offset = 0;
LoadStatus loadStatus = LoadStatus.loading; int loadStatus = 0;
LogsData? logsData; LogsData? logsData;
String previousIp = "";
bool showDivider = true; bool showDivider = true;
CancelableOperation? cancelableRequest; CancelableOperation? cancelableRequest;
@ -61,7 +69,7 @@ class _LogsListClientState extends State<LogsListClient> {
serversProvider.apiClient!.getLogs( serversProvider.apiClient!.getLogs(
count: logsQuantity, count: logsQuantity,
offset: offst, offset: offst,
search: '"${widget.id}"' search: '"${widget.ip}"'
) )
); );
@ -84,11 +92,11 @@ class _LogsListClientState extends State<LogsListClient> {
LogsData newLogsData = result['data']; LogsData newLogsData = result['data'];
setState(() => logsData = newLogsData); setState(() => logsData = newLogsData);
} }
setState(() => loadStatus = LoadStatus.loaded); setState(() => loadStatus = 1);
} }
else { else {
setState(() => loadStatus = LoadStatus.error); setState(() => loadStatus = 2);
Provider.of<AppConfigProvider>(context, listen: false).addLog(result['log']); widget.appConfigProvider.addLog(result['log']);
} }
} }
} }
@ -110,6 +118,8 @@ class _LogsListClientState extends State<LogsListClient> {
@override @override
void initState() { void initState() {
scrollController = ScrollController()..addListener(scrollListener); scrollController = ScrollController()..addListener(scrollListener);
fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip);
super.initState(); super.initState();
} }
@ -117,15 +127,15 @@ class _LogsListClientState extends State<LogsListClient> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
useEffect(() { if (widget.ip != previousIp) {
setState(() => loadStatus = LoadStatus.loading); setState(() => loadStatus = 0);
fetchLogs(inOffset: 0); fetchLogs(inOffset: 0);
return null; setState(() => previousIp = widget.ip);
}, [widget.id]); }
Widget status() { Widget status() {
switch (loadStatus) { switch (loadStatus) {
case LoadStatus.loading: case 0:
return SizedBox( return SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
@ -146,7 +156,7 @@ class _LogsListClientState extends State<LogsListClient> {
), ),
); );
case LoadStatus.loaded: case 1:
if (logsData!.data.isNotEmpty) { if (logsData!.data.isNotEmpty) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: fetchLogs, onRefresh: fetchLogs,
@ -189,7 +199,8 @@ class _LogsListClientState extends State<LogsListClient> {
) )
)) ))
} }
} },
twoColumns: widget.splitView,
); );
} }
} }
@ -209,7 +220,7 @@ class _LogsListClientState extends State<LogsListClient> {
); );
} }
case LoadStatus.error: case 2:
return SizedBox( return SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
@ -241,7 +252,7 @@ class _LogsListClientState extends State<LogsListClient> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.id), title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip),
centerTitle: true, centerTitle: true,
actions: [ actions: [
if (!(Platform.isAndroid || Platform.isIOS)) ...[ if (!(Platform.isAndroid || Platform.isIOS)) ...[

View file

@ -1,213 +1,55 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.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:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart'; import 'package:adguard_home_manager/screens/clients/clients_lists.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart';
import 'package:adguard_home_manager/constants/routes_names.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/providers/clients_provider.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/models/clients.dart';
class Clients extends StatefulWidget { class Clients extends StatefulWidget {
final Widget child; const Clients({Key? key}) : super(key: key);
const Clients({
Key? key,
required this.child,
}) : super(key: key);
@override @override
State<Clients> createState() => _ClientsState(); State<Clients> createState() => _ClientsState();
} }
class _ClientsState extends State<Clients> with TickerProviderStateMixin { class _ClientsState extends State<Clients> with TickerProviderStateMixin {
late TabController tabController;
final ScrollController scrollController = ScrollController();
bool searchMode = false;
final TextEditingController searchController = TextEditingController();
@override
void initState() {
final clientsProvider = Provider.of<ClientsProvider>(context, listen: false);
clientsProvider.fetchClients(updateLoading: true);
super.initState();
tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
tabController.addListener(
() => Provider.of<AppConfigProvider>(context, listen: false).setSelectedClientsTab(tabController.index)
);
}
List<AutoClient> generateClientsList(List<AutoClient> clients, List<String> ips) { List<AutoClient> generateClientsList(List<AutoClient> clients, List<String> ips) {
return clients.where((client) => ips.contains(client.ip)).toList(); return clients.where((client) => ips.contains(client.ip)).toList();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final clientsProvider = Provider.of<ClientsProvider>(context); return LayoutBuilder(
builder: (context, constraints) {
final width = MediaQuery.of(context).size.width; if (constraints.maxWidth > 1000) {
return SplitView.material(
PreferredSizeWidget tabBar() { hideDivider: true,
return TabBar( flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2),
controller: tabController, placeholder: Center(
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, child: Padding(
tabs: [ padding: const EdgeInsets.all(24),
Tab( child: Text(
child: Row( AppLocalizations.of(context)!.selectClientLeftColumn,
mainAxisSize: MainAxisSize.min, textAlign: TextAlign.center,
children: [ style: TextStyle(
const Icon(Icons.devices), fontSize: 24,
const SizedBox(width: 8), color: Theme.of(context).colorScheme.onSurfaceVariant
Text(AppLocalizations.of(context)!.activeClients) ),
], ),
),
), ),
), child: const ClientsLists(
Tab( splitView: true,
child: Row( )
mainAxisSize: MainAxisSize.min, );
children: [ }
const Icon(Icons.add_rounded), else {
const SizedBox(width: 8), return const ClientsLists(
Text(AppLocalizations.of(context)!.added) splitView: false,
], );
), }
), },
]
);
}
Widget tabBarView(bool sliver) {
return TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredActiveClients : [],
onClientSelected: (client) => context.go(
RoutesNames.client,
extra: {
"id": client.name != null && client.name != ""
? client.name
: client.ip
}
),
splitView: isDesktop(width),
),
AddedList(
scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredAddedClients : [],
onClientSelected: (client) => context.go(
RoutesNames.client,
extra: { "id": client.name }
),
splitView: isDesktop(width),
),
]
);
}
return Row(
children: [
SizedBox(
width: isDesktop(width) ? 300 : width,
height: double.maxFinite,
child: Material(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: searchMode == true
? Row(
children: [
IconButton(
onPressed: () {
setState(() {
searchMode = false;
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.arrow_back_rounded),
tooltip: AppLocalizations.of(context)!.exitSearch,
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: searchController,
onChanged: (value) => clientsProvider.setSearchTermClients(value),
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setState(() {
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.clear_rounded)
),
hintText: AppLocalizations.of(context)!.search,
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
border: InputBorder.none,
),
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
autofocus: true,
),
)
],
)
: Text(AppLocalizations.of(context)!.clients),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
actions: [
if (clientsProvider.loadStatus == LoadStatus.loaded && searchMode == false) ...[
IconButton(
onPressed: () => setState(() => searchMode = true),
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
)
];
}),
body: tabBarView(true)
)
),
),
),
if (isDesktop(width) == true) Expanded(
child: widget.child,
)
],
); );
} }
} }

View file

@ -1,229 +0,0 @@
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/client/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/providers/clients_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';
class ClientsDesktopView extends StatefulWidget {
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
const ClientsDesktopView({
Key? key,
required this.serversProvider,
required this.appConfigProvider,
}) : 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;
bool searchMode = false;
final TextEditingController searchController = TextEditingController();
@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 clientsProvider = Provider.of<ClientsProvider>(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(bool sliver) {
return TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredActiveClients : [],
onClientSelected: (client) => setState(() {
selectedAddedClient = null;
selectedActiveClient = client;
}),
selectedClient: selectedActiveClient,
splitView: true,
),
AddedList(
scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredAddedClients : [],
onClientSelected: (client) => setState(() {
selectedActiveClient = null;
selectedAddedClient = client;
}),
selectedClient: selectedAddedClient,
splitView: true,
),
]
);
}
Widget title() {
if (searchMode == true) {
return Row(
children: [
IconButton(
onPressed: () {
setState(() {
searchMode = false;
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.arrow_back_rounded)
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: searchController,
onChanged: (value) => clientsProvider.setSearchTermClients(value),
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setState(() {
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.clear_rounded)
),
hintText: AppLocalizations.of(context)!.search,
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
border: InputBorder.none,
),
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
),
)
],
);
}
else {
return Text(AppLocalizations.of(context)!.clients);
}
}
if (!(Platform.isAndroid || Platform.isIOS)) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: title(),
centerTitle: false,
actions: [
if (clientsProvider.loadStatus == LoadStatus.loaded && searchMode == false) ...[
IconButton(
onPressed: () => setState(() => searchMode = true),
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
body: tabBarView(false),
),
);
}
else {
return DefaultTabController(
length: 2,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: title(),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
actions: [
if (clientsProvider.loadStatus == LoadStatus.loaded && searchMode == false) ...[
IconButton(
onPressed: () => setState(() => searchMode = true),
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
)
];
}),
body: tabBarView(true)
)
);
}
}
}

View file

@ -10,7 +10,6 @@ import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients.dart';
class ClientsList extends StatelessWidget { class ClientsList extends StatelessWidget {
final ScrollController scrollController;
final List<AutoClient> data; final List<AutoClient> data;
final void Function(AutoClient) onClientSelected; final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient; final AutoClient? selectedClient;
@ -18,7 +17,6 @@ class ClientsList extends StatelessWidget {
const ClientsList({ const ClientsList({
Key? key, Key? key,
required this.scrollController,
required this.data, required this.data,
required this.onClientSelected, required this.onClientSelected,
this.selectedClient, this.selectedClient,

View file

@ -0,0 +1,222 @@
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/added_list.dart';
import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class ClientsLists extends StatefulWidget {
final bool splitView;
const ClientsLists({
Key? key,
required this.splitView,
}) : super(key: key);
@override
State<ClientsLists> createState() => _ClientsListsState();
}
class _ClientsListsState extends State<ClientsLists> with TickerProviderStateMixin {
late TabController tabController;
final ScrollController scrollController = ScrollController();
bool searchMode = false;
final TextEditingController searchController = TextEditingController();
AutoClient? _selectedAutoClient;
Client? _selectedClient;
@override
void initState() {
final clientsProvider = Provider.of<ClientsProvider>(context, listen: false);
clientsProvider.fetchClients(updateLoading: true);
super.initState();
tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
tabController.addListener(
() => Provider.of<AppConfigProvider>(context, listen: false).setSelectedClientsTab(tabController.index)
);
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final clientsProvider = Provider.of<ClientsProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
void onAutoClientSelected(AutoClient client) {
setState(() => _selectedAutoClient = client);
final w = LogsListClient(
ip: client.ip,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
splitView: widget.splitView,
);
if (widget.splitView) {
SplitView.of(context).push(w);
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => w,
));
}
}
void onClientSelected(Client client) {
setState(() => _selectedClient = client);
final w = LogsListClient(
ip: client.ids[0],
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
splitView: widget.splitView,
);
if (widget.splitView) {
SplitView.of(context).push(w);
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => w,
));
}
}
return DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: searchMode == true
? Row(
children: [
IconButton(
onPressed: () {
setState(() {
searchMode = false;
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.arrow_back_rounded),
tooltip: AppLocalizations.of(context)!.exitSearch,
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: searchController,
onChanged: (value) => clientsProvider.setSearchTermClients(value),
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setState(() {
searchController.text = "";
clientsProvider.setSearchTermClients(null);
});
},
icon: const Icon(Icons.clear_rounded)
),
hintText: AppLocalizations.of(context)!.search,
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
border: InputBorder.none,
),
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 18
),
autofocus: true,
),
)
],
)
: Text(AppLocalizations.of(context)!.clients),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(MediaQuery.of(context).size.width)
? Colors.transparent
: null,
actions: [
if (clientsProvider.loadStatus == LoadStatus.loaded && searchMode == false) ...[
IconButton(
onPressed: () => setState(() => searchMode = true),
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(
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)
],
),
),
]
)
),
)
];
}),
body: TabBarView(
controller: tabController,
children: [
ClientsList(
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredActiveClients : [],
onClientSelected: onAutoClientSelected,
selectedClient: _selectedAutoClient,
splitView: widget.splitView,
),
AddedList(
scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredAddedClients : [],
onClientSelected: onClientSelected,
selectedClient: _selectedClient,
splitView: widget.splitView,
),
]
)
)
);
}
}

View file

@ -324,20 +324,24 @@ class _FiltersState extends State<Filters> {
} }
} }
if (width > 1200) { return LayoutBuilder(
return FiltersTripleColumn( builder: (context, constraints) {
onRemoveCustomRule: openRemoveCustomRuleModal, if (constraints.maxWidth > 900) {
onOpenDetailsModal: openListDetails, return FiltersTripleColumn(
actions: actions(), onRemoveCustomRule: openRemoveCustomRuleModal,
); onOpenDetailsModal: openListDetails,
} actions: actions(),
else { );
return FiltersTabsView( }
appConfigProvider: appConfigProvider, else {
actions: actions(), return FiltersTabsView(
onRemoveCustomRule: openRemoveCustomRuleModal, appConfigProvider: appConfigProvider,
onOpenDetailsModal: openListDetails, actions: actions(),
); onRemoveCustomRule: openRemoveCustomRuleModal,
} onOpenDetailsModal: openListDetails,
);
}
},
);
} }
} }

View file

@ -5,6 +5,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart';
import 'package:adguard_home_manager/screens/filters/filters_list.dart'; import 'package:adguard_home_manager/screens/filters/filters_list.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/providers/filtering_provider.dart'; import 'package:adguard_home_manager/providers/filtering_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/models/filtering.dart'; import 'package:adguard_home_manager/models/filtering.dart';
@ -47,6 +48,8 @@ class _FiltersTabsViewState extends State<FiltersTabsView> with TickerProviderSt
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context); final filteringProvider = Provider.of<FilteringProvider>(context);
final width = MediaQuery.of(context).size.width;
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
child: NestedScrollView( child: NestedScrollView(
@ -62,6 +65,7 @@ class _FiltersTabsViewState extends State<FiltersTabsView> with TickerProviderSt
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
centerTitle: false, centerTitle: false,
actions: widget.actions, actions: widget.actions,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
bottom: TabBar( bottom: TabBar(
controller: tabController, controller: tabController,
isScrollable: true, isScrollable: true,

View file

@ -13,6 +13,7 @@ import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/widgets/options_modal.dart'; import 'package:adguard_home_manager/widgets/options_modal.dart';
import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/models/menu_option.dart'; import 'package:adguard_home_manager/models/menu_option.dart';
import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/functions/copy_clipboard.dart'; import 'package:adguard_home_manager/functions/copy_clipboard.dart';
@ -37,6 +38,8 @@ class FiltersTripleColumn extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context); final filteringProvider = Provider.of<FilteringProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Widget? generateSubtitle(String rule) { Widget? generateSubtitle(String rule) {
final allowRegex = RegExp(r'^@@.*$'); final allowRegex = RegExp(r'^@@.*$');
@ -325,6 +328,7 @@ class FiltersTripleColumn extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.filters), title: Text(AppLocalizations.of(context)!.filters),
actions: [ actions: [
IconButton( IconButton(

View file

@ -8,6 +8,7 @@ import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/screens/logs/log_list_tile.dart'; import 'package:adguard_home_manager/screens/logs/log_list_tile.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/constants/urls.dart'; import 'package:adguard_home_manager/constants/urls.dart';
import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/classes/process_modal.dart';
@ -34,6 +35,8 @@ class LogDetailsScreen extends StatelessWidget {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context); final statusProvider = Provider.of<StatusProvider>(context);
final width = MediaQuery.of(context).size.width;
Filter? getList(int id) { Filter? getList(int id) {
try { try {
return statusProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () { return statusProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () {
@ -288,6 +291,7 @@ class LogDetailsScreen extends StatelessWidget {
floating: true, floating: true,
centerTitle: false, centerTitle: false,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.logDetails), title: Text(AppLocalizations.of(context)!.logDetails),
actions: [ actions: [
IconButton( IconButton(

View file

@ -17,6 +17,7 @@ class LogTile extends StatelessWidget {
final bool? isLogSelected; final bool? isLogSelected;
final void Function(Log) onLogTap; final void Function(Log) onLogTap;
final bool? useAlwaysNormalTile; final bool? useAlwaysNormalTile;
final bool twoColumns;
const LogTile({ const LogTile({
Key? key, Key? key,
@ -25,7 +26,8 @@ class LogTile extends StatelessWidget {
required this.index, required this.index,
this.isLogSelected, this.isLogSelected,
required this.onLogTap, required this.onLogTap,
this.useAlwaysNormalTile this.useAlwaysNormalTile,
required this.twoColumns,
}) : super(key: key); }) : super(key: key);
@override @override
@ -83,7 +85,7 @@ class LogTile extends StatelessWidget {
} }
} }
if (width > 1100 && !(useAlwaysNormalTile == true)) { if (twoColumns && !(useAlwaysNormalTile == true)) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: InkWell( child: InkWell(

View file

@ -1,27 +1,10 @@
// ignore_for_file: use_build_context_synchronously // 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';
import 'package:adguard_home_manager/screens/logs/logs_filters_modal.dart';
import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/screens/logs/log_details_screen.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/models/applied_filters.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/models/logs.dart'; import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:flutter/material.dart';
import 'package:adguard_home_manager/screens/logs/logs_list.dart';
import 'package:adguard_home_manager/screens/logs/log_details_screen.dart';
class Logs extends StatefulWidget { class Logs extends StatefulWidget {
const Logs({Key? key}) : super(key: key); const Logs({Key? key}) : super(key: key);
@ -31,554 +14,48 @@ class Logs extends StatefulWidget {
} }
class _LogsState extends State<Logs> { class _LogsState extends State<Logs> {
bool showDivider = true; Log? _selectedLog;
Log? selectedLog;
void fetchFilteringRules() async {
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
final result = await statusProvider.getFilteringRules();
if (mounted && result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red
);
}
}
Future fetchClients() async {
final clientsProvider = Provider.of<ClientsProvider>(context, listen: false);
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
final result = await clientsProvider.fetchClients();
if (mounted && result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red
);
}
}
bool scrollListener(ScrollUpdateNotification scrollNotification) {
final logsProvider = Provider.of<LogsProvider>(context, listen: false);
if (scrollNotification.metrics.extentAfter < 500 && logsProvider.isLoadingMore == false) {
logsProvider.fetchLogs(loadingMore: true);
}
if (scrollNotification.metrics.pixels > 0) {
setState(() => showDivider = false);
}
else {
setState(() => showDivider = true);
}
return false;
}
@override
void initState() {
final logsProvider = Provider.of<LogsProvider>(context, listen: false);
logsProvider.fetchLogs(inOffset: 0);
fetchFilteringRules();
fetchClients();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width; return LayoutBuilder(
builder: (context, constraints) {
void updateConfig(Map<String, dynamic> data) async { if (constraints.maxWidth > 1000) {
ProcessModal processModal = ProcessModal(context: context); return Material(
processModal.open(AppLocalizations.of(context)!.updatingSettings); color: Colors.transparent,
child: Row(
final result = serverVersionIsAhead( children: [
currentVersion: statusProvider.serverStatus!.serverVersion, Expanded(
referenceVersion: 'v0.107.28', flex: 2,
referenceVersionBeta: 'v0.108.0-b.33' child: LogsListWidget(
) == true twoColumns: true,
? await serversProvider.apiClient!.updateQueryLogParameters(data: data) selectedLog: _selectedLog,
: await serversProvider.apiClient!.updateQueryLogParametersLegacy(data: data); onLogSelected: (log) => setState(() => _selectedLog = log),
processModal.close();
if (result['result'] == 'success') {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
);
}
}
void clearQueries() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient!.clearLogs();
processModal.close();
if (result['result'] == 'success') {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsCleared,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsNotCleared,
color: Colors.red
);
}
}
void openFilersModal() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: true,
),
barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
}
final Map<String, String> translatedString = {
"all": AppLocalizations.of(context)!.all,
"filtered": AppLocalizations.of(context)!.filtered,
"processed": AppLocalizations.of(context)!.processedRow,
"whitelisted": AppLocalizations.of(context)!.processedWhitelistRow,
"blocked": AppLocalizations.of(context)!.blocked,
"blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow,
"blocked_parental": AppLocalizations.of(context)!.blockedParentalRow,
"safe_search": AppLocalizations.of(context)!.safeSearch,
};
Widget generateBody() {
switch (logsProvider.loadStatus) {
case LoadStatus.loading:
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverFillRemaining(
child: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingLogs,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
)
) )
],
),
)
);
case LoadStatus.loaded:
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => RefreshIndicator(
onRefresh: () async {
await logsProvider.fetchLogs(inOffset: 0);
},
displacement: 95,
child: NotificationListener(
onNotification: scrollListener,
child: CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
if (logsProvider.logsData!.data.isNotEmpty) SliverList.builder(
itemCount: logsProvider.isLoadingMore
? logsProvider.logsData!.data.length + 1
: logsProvider.logsData!.data.length,
itemBuilder: (context, index) {
if (logsProvider.isLoadingMore == true && index == logsProvider.logsData!.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(),
),
);
}
else if (logsProvider.logsData!.data[index].question.name != null) {
return LogTile(
log: logsProvider.logsData!.data[index],
index: index,
length: logsProvider.logsData!.data.length,
isLogSelected: selectedLog != null && selectedLog == logsProvider.logsData!.data[index],
onLogTap: (log) {
if (width <= 1100) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false,
)
));
}
setState(() => selectedLog = log);
}
);
}
else {
return null;
}
}
),
if (logsProvider.logsData!.data.isEmpty) SliverFillRemaining(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context)!.noLogsDisplay,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (logsProvider.logsOlderThan != null) Padding(
padding: const EdgeInsets.only(
top: 30,
left: 20,
right: 20
),
child: Text(
AppLocalizations.of(context)!.noLogsThatOld,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
]
),
),
)
],
),
), ),
), Expanded(
) flex: 3,
); child: _selectedLog != null
? LogDetailsScreen(
case LoadStatus.error: log: _selectedLog!,
return SafeArea( dialog: false,
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverFillRemaining(
child: 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,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
)
)
],
),
)
);
default:
return const SizedBox();
}
}
Widget logsScreen() {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
title: Text(AppLocalizations.of(context)!.logs),
expandedHeight: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null
? 170 : null,
actions: [
if (!(Platform.isAndroid || Platform.isIOS)) IconButton(
onPressed: () => logsProvider.fetchLogs(inOffset: 0),
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.refresh,
),
logsProvider.loadStatus == LoadStatus.loaded
? IconButton(
onPressed: openFilersModal,
icon: const Icon(Icons.filter_list_rounded),
tooltip: AppLocalizations.of(context)!.filters,
) )
: const SizedBox(), : const SizedBox()
if (statusProvider.serverStatus != null) IconButton( )
tooltip: AppLocalizations.of(context)!.settings, ],
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: true,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
barrierDismissible: false
)
}
else {
showModalBottomSheet(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: false,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
},
icon: const Icon(Icons.settings)
),
const SizedBox(width: 5),
],
bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null
? PreferredSize(
preferredSize: const Size(double.maxFinite, 70),
child: Container(
height: 50,
width: double.maxFinite,
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: showDivider == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.1)
: Colors.transparent,
)
)
),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
if (logsProvider.appliedFilters.searchText != null) ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.search_rounded,
),
label: Row(
children: [
Text(
logsProvider.appliedFilters.searchText!,
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
searchText: null,
clients: logsProvider.appliedFilters.clients
)
);
logsProvider.setSearchText(null);
logsProvider.fetchLogs(
inOffset: 0,
searchText: ''
);
},
),
],
if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.shield_rounded,
),
label: Row(
children: [
Text(
translatedString[logsProvider.appliedFilters.selectedResultStatus]!,
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: 'all',
searchText: logsProvider.appliedFilters.searchText,
clients: logsProvider.appliedFilters.clients
)
);
logsProvider.setSelectedResultStatus('all');
logsProvider.fetchLogs(
inOffset: 0,
responseStatus: 'all'
);
},
),
],
if (logsProvider.appliedFilters.clients != null) ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.smartphone_rounded,
),
label: Row(
children: [
Text(
logsProvider.appliedFilters.clients!.length == 1
? logsProvider.appliedFilters.clients![0]
: "${logsProvider.appliedFilters.clients!.length} ${AppLocalizations.of(context)!.clients}",
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
searchText: logsProvider.appliedFilters.searchText,
clients: null
)
);
logsProvider.setSelectedClients(null);
logsProvider.fetchLogs(
inOffset: 0,
responseStatus: logsProvider.appliedFilters.selectedResultStatus
);
},
),
],
const SizedBox(width: 15),
],
),
)
)
: null,
),
)
],
body: generateBody()
),
);
}
if (width > 1100) {
return Material(
color: Colors.transparent,
child: Row(
children: [
Expanded(
flex: 1,
child: logsScreen()
), ),
Expanded( );
flex: 2, }
child: selectedLog != null else {
? LogDetailsScreen( return LogsListWidget(
log: selectedLog!, twoColumns: false,
dialog: false, selectedLog: _selectedLog,
) onLogSelected: (log) => setState(() => _selectedLog = log),
: const SizedBox() );
) }
], },
), );
);
}
else {
return logsScreen();
}
} }
} }

View file

@ -0,0 +1,566 @@
// 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';
import 'package:adguard_home_manager/screens/logs/log_details_screen.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart';
import 'package:adguard_home_manager/screens/logs/logs_filters_modal.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/models/applied_filters.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/clients_provider.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
class LogsListWidget extends StatefulWidget {
final Log? selectedLog;
final bool twoColumns;
final void Function(Log) onLogSelected;
const LogsListWidget({
Key? key,
required this.twoColumns,
required this.selectedLog,
required this.onLogSelected,
}) : super(key: key);
@override
State<LogsListWidget> createState() => _LogsListWidgetState();
}
class _LogsListWidgetState extends State<LogsListWidget> {
bool showDivider = true;
void fetchFilteringRules() async {
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
final result = await statusProvider.getFilteringRules();
if (mounted && result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red
);
}
}
Future fetchClients() async {
final clientsProvider = Provider.of<ClientsProvider>(context, listen: false);
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
final result = await clientsProvider.fetchClients();
if (mounted && result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red
);
}
}
bool scrollListener(ScrollUpdateNotification scrollNotification) {
final logsProvider = Provider.of<LogsProvider>(context, listen: false);
if (scrollNotification.metrics.extentAfter < 500 && logsProvider.isLoadingMore == false) {
logsProvider.fetchLogs(loadingMore: true);
}
if (scrollNotification.metrics.pixels > 0) {
setState(() => showDivider = false);
}
else {
setState(() => showDivider = true);
}
return false;
}
@override
void initState() {
final logsProvider = Provider.of<LogsProvider>(context, listen: false);
logsProvider.fetchLogs(inOffset: 0);
fetchFilteringRules();
fetchClients();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width;
void updateConfig(Map<String, dynamic> data) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = serverVersionIsAhead(
currentVersion: statusProvider.serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? await serversProvider.apiClient!.updateQueryLogParameters(data: data)
: await serversProvider.apiClient!.updateQueryLogParametersLegacy(data: data);
processModal.close();
if (result['result'] == 'success') {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
);
}
}
void clearQueries() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient!.clearLogs();
processModal.close();
if (result['result'] == 'success') {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsCleared,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsNotCleared,
color: Colors.red
);
}
}
void openFilersModal() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: true,
),
barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
}
final Map<String, String> translatedString = {
"all": AppLocalizations.of(context)!.all,
"filtered": AppLocalizations.of(context)!.filtered,
"processed": AppLocalizations.of(context)!.processedRow,
"whitelisted": AppLocalizations.of(context)!.processedWhitelistRow,
"blocked": AppLocalizations.of(context)!.blocked,
"blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow,
"blocked_parental": AppLocalizations.of(context)!.blockedParentalRow,
"safe_search": AppLocalizations.of(context)!.safeSearch,
};
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.logs),
expandedHeight: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null
? 170 : null,
actions: [
if (!(Platform.isAndroid || Platform.isIOS)) IconButton(
onPressed: () => logsProvider.fetchLogs(inOffset: 0),
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.refresh,
),
logsProvider.loadStatus == LoadStatus.loaded
? IconButton(
onPressed: openFilersModal,
icon: const Icon(Icons.filter_list_rounded),
tooltip: AppLocalizations.of(context)!.filters,
)
: const SizedBox(),
if (statusProvider.serverStatus != null) IconButton(
tooltip: AppLocalizations.of(context)!.settings,
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: true,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
barrierDismissible: false
)
}
else {
showModalBottomSheet(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: false,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
},
icon: const Icon(Icons.settings)
),
const SizedBox(width: 5),
],
bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null
? PreferredSize(
preferredSize: const Size(double.maxFinite, 70),
child: Container(
height: 50,
width: double.maxFinite,
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: showDivider == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.1)
: Colors.transparent,
)
)
),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
if (logsProvider.appliedFilters.searchText != null) ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.search_rounded,
),
label: Row(
children: [
Text(
logsProvider.appliedFilters.searchText!,
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
searchText: null,
clients: logsProvider.appliedFilters.clients
)
);
logsProvider.setSearchText(null);
logsProvider.fetchLogs(
inOffset: 0,
searchText: ''
);
},
),
],
if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.shield_rounded,
),
label: Row(
children: [
Text(
translatedString[logsProvider.appliedFilters.selectedResultStatus]!,
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: 'all',
searchText: logsProvider.appliedFilters.searchText,
clients: logsProvider.appliedFilters.clients
)
);
logsProvider.setSelectedResultStatus('all');
logsProvider.fetchLogs(
inOffset: 0,
responseStatus: 'all'
);
},
),
],
if (logsProvider.appliedFilters.clients != null) ...[
const SizedBox(width: 15),
Chip(
avatar: const Icon(
Icons.smartphone_rounded,
),
label: Row(
children: [
Text(
logsProvider.appliedFilters.clients!.length == 1
? logsProvider.appliedFilters.clients![0]
: "${logsProvider.appliedFilters.clients!.length} ${AppLocalizations.of(context)!.clients}",
),
],
),
deleteIcon: const Icon(
Icons.clear,
size: 18,
),
onDeleted: () {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
searchText: logsProvider.appliedFilters.searchText,
clients: null
)
);
logsProvider.setSelectedClients(null);
logsProvider.fetchLogs(
inOffset: 0,
responseStatus: logsProvider.appliedFilters.selectedResultStatus
);
},
),
],
const SizedBox(width: 15),
],
),
)
)
: null,
),
)
],
body: Builder(
builder: (context) {
switch (logsProvider.loadStatus) {
case LoadStatus.loading:
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverFillRemaining(
child: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingLogs,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
)
)
],
),
)
);
case LoadStatus.loaded:
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => RefreshIndicator(
onRefresh: () async {
await logsProvider.fetchLogs(inOffset: 0);
},
displacement: 95,
child: NotificationListener(
onNotification: scrollListener,
child: CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
if (logsProvider.logsData!.data.isNotEmpty) SliverList.builder(
itemCount: logsProvider.isLoadingMore
? logsProvider.logsData!.data.length + 1
: logsProvider.logsData!.data.length,
itemBuilder: (context, index) {
if (logsProvider.isLoadingMore == true && index == logsProvider.logsData!.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(),
),
);
}
else if (logsProvider.logsData!.data[index].question.name != null) {
return LogTile(
log: logsProvider.logsData!.data[index],
index: index,
length: logsProvider.logsData!.data.length,
isLogSelected: widget.selectedLog != null && widget.selectedLog == logsProvider.logsData!.data[index],
onLogTap: (log) {
if (!widget.twoColumns) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false,
)
));
}
widget.onLogSelected(log);
},
twoColumns: widget.twoColumns,
);
}
else {
return null;
}
}
),
if (logsProvider.logsData!.data.isEmpty) SliverFillRemaining(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context)!.noLogsDisplay,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (logsProvider.logsOlderThan != null) Padding(
padding: const EdgeInsets.only(
top: 30,
left: 20,
right: 20
),
child: Text(
AppLocalizations.of(context)!.noLogsThatOld,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
]
),
),
)
],
),
),
),
)
);
case LoadStatus.error:
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverFillRemaining(
child: 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,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
)
)
],
),
)
);
default:
return const SizedBox();
}
},
)
),
);
}
}

View file

@ -23,6 +23,7 @@ import 'package:adguard_home_manager/widgets/custom_settings_tile.dart';
import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/constants/strings.dart'; import 'package:adguard_home_manager/constants/strings.dart';
import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/functions/compare_versions.dart';
@ -36,35 +37,46 @@ class Settings extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; return LayoutBuilder(
builder: (context, constraints) {
if (width > 900) { if (constraints.maxWidth > 900) {
return SplitView.material( return SplitView.material(
hideDivider: true, hideDivider: true,
flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2),
placeholder: Center( placeholder: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Text( child: Text(
AppLocalizations.of(context)!.selectOptionLeftColumn, AppLocalizations.of(context)!.selectOptionLeftColumn,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
), ),
), ),
), child: const SettingsWidget(
), twoColumns: true,
child: const SettingsWidget(), ),
); );
} }
else { else {
return const SettingsWidget(); return const SettingsWidget(
} twoColumns: false,
);
}
},
);
} }
} }
class SettingsWidget extends StatelessWidget { class SettingsWidget extends StatelessWidget {
const SettingsWidget({Key? key}) : super(key: key); final bool twoColumns;
const SettingsWidget({
Key? key,
required this.twoColumns,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -74,7 +86,7 @@ class SettingsWidget extends StatelessWidget {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
if (width <= 900 && appConfigProvider.selectedSettingsScreen != null) { if (!twoColumns && appConfigProvider.selectedSettingsScreen != null) {
appConfigProvider.setSelectedSettingsScreen(screen: null); appConfigProvider.setSelectedSettingsScreen(screen: null);
} }
@ -86,7 +98,7 @@ class SettingsWidget extends StatelessWidget {
required Widget screenToNavigate, required Widget screenToNavigate,
required int thisItem required int thisItem
}) { }) {
if (width > 900) { if (twoColumns) {
return CustomSettingsTile( return CustomSettingsTile(
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
@ -125,6 +137,7 @@ class SettingsWidget extends StatelessWidget {
floating: true, floating: true,
centerTitle: false, centerTitle: false,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.settings), title: Text(AppLocalizations.of(context)!.settings),
) )
) )