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: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';
List<AppScreen> screensSelectServer = [

View file

@ -16,4 +16,16 @@ class RoutesNames {
static const String filters = "/filters";
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> homeNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> clientsNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> clientsListNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> logsNavigatorKey = 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/connect/connect.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/settings/settings.dart';
import 'package:adguard_home_manager/widgets/layout.dart';
import 'package:adguard_home_manager/routes/router_globals.dart';
@ -31,44 +29,15 @@ final List<RouteBase> routes = [
path: RoutesNames.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(
navigatorKey: clientsNavigatorKey,
routes: [
ShellRoute(
GoRoute(
parentNavigatorKey: clientsNavigatorKey,
navigatorKey: clientsListNavigatorKey,
builder: (context, state, child) => Clients(child: child),
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']
)
)
]
path: RoutesNames.clients,
builder: (context, state) => const Clients(),
)
]
),
@ -76,6 +45,7 @@ final List<RouteBase> routes = [
navigatorKey: logsNavigatorKey,
routes: [
GoRoute(
parentNavigatorKey: logsNavigatorKey,
path: RoutesNames.logs,
builder: (context, state) => const Logs(),
)
@ -85,6 +55,7 @@ final List<RouteBase> routes = [
navigatorKey: filtersNavigatorKey,
routes: [
GoRoute(
parentNavigatorKey: filtersNavigatorKey,
path: RoutesNames.filters,
builder: (context, state) => const Filters(),
)
@ -94,7 +65,8 @@ final List<RouteBase> routes = [
navigatorKey: settingsNavigatorKey,
routes: [
GoRoute(
path: RoutesNames.settings,
parentNavigatorKey: settingsNavigatorKey,
path: RoutesNames.settings,
builder: (context, state) => const Settings(),
)
]

View file

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

View file

@ -1,213 +1,55 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:flutter_split_view/flutter_split_view.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/added_list.dart';
import 'package:adguard_home_manager/screens/clients/clients_lists.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';
class Clients extends StatefulWidget {
final Widget child;
const Clients({
Key? key,
required this.child,
}) : super(key: key);
const Clients({Key? key}) : super(key: key);
@override
State<Clients> createState() => _ClientsState();
}
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) {
return clients.where((client) => ips.contains(client.ip)).toList();
}
@override
Widget build(BuildContext context) {
final clientsProvider = Provider.of<ClientsProvider>(context);
final width = MediaQuery.of(context).size.width;
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)
],
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 1000) {
return SplitView.material(
hideDivider: true,
flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2),
placeholder: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
AppLocalizations.of(context)!.selectClientLeftColumn,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
),
),
),
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) => 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,
)
],
child: const ClientsLists(
splitView: true,
)
);
}
else {
return const ClientsLists(
splitView: false,
);
}
},
);
}
}

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';
class ClientsList extends StatelessWidget {
final ScrollController scrollController;
final List<AutoClient> data;
final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient;
@ -18,7 +17,6 @@ class ClientsList extends StatelessWidget {
const ClientsList({
Key? key,
required this.scrollController,
required this.data,
required this.onClientSelected,
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 FiltersTripleColumn(
onRemoveCustomRule: openRemoveCustomRuleModal,
onOpenDetailsModal: openListDetails,
actions: actions(),
);
}
else {
return FiltersTabsView(
appConfigProvider: appConfigProvider,
actions: actions(),
onRemoveCustomRule: openRemoveCustomRuleModal,
onOpenDetailsModal: openListDetails,
);
}
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 900) {
return FiltersTripleColumn(
onRemoveCustomRule: openRemoveCustomRuleModal,
onOpenDetailsModal: openListDetails,
actions: actions(),
);
}
else {
return FiltersTabsView(
appConfigProvider: appConfigProvider,
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/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/constants/enums.dart';
import 'package:adguard_home_manager/models/filtering.dart';
@ -47,6 +48,8 @@ class _FiltersTabsViewState extends State<FiltersTabsView> with TickerProviderSt
Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context);
final width = MediaQuery.of(context).size.width;
return DefaultTabController(
length: 3,
child: NestedScrollView(
@ -62,6 +65,7 @@ class _FiltersTabsViewState extends State<FiltersTabsView> with TickerProviderSt
forceElevated: innerBoxIsScrolled,
centerTitle: false,
actions: widget.actions,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
bottom: TabBar(
controller: tabController,
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/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/functions/snackbar.dart';
import 'package:adguard_home_manager/functions/copy_clipboard.dart';
@ -37,6 +38,8 @@ class FiltersTripleColumn extends StatelessWidget {
Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Widget? generateSubtitle(String rule) {
final allowRegex = RegExp(r'^@@.*$');
@ -325,6 +328,7 @@ class FiltersTripleColumn extends StatelessWidget {
return Scaffold(
appBar: AppBar(
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.filters),
actions: [
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/functions/desktop_mode.dart';
import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/constants/urls.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 statusProvider = Provider.of<StatusProvider>(context);
final width = MediaQuery.of(context).size.width;
Filter? getList(int id) {
try {
return statusProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () {
@ -288,6 +291,7 @@ class LogDetailsScreen extends StatelessWidget {
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.logDetails),
actions: [
IconButton(

View file

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

View file

@ -1,27 +1,10 @@
// 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/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 {
const Logs({Key? key}) : super(key: key);
@ -31,554 +14,48 @@ class Logs extends StatefulWidget {
}
class _LogsState extends State<Logs> {
bool showDivider = true;
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();
}
Log? _selectedLog;
@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,
};
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,
),
)
],
),
)
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 1000) {
return Material(
color: Colors.transparent,
child: Row(
children: [
Expanded(
flex: 2,
child: LogsListWidget(
twoColumns: true,
selectedLog: _selectedLog,
onLogSelected: (log) => setState(() => _selectedLog = log),
)
],
),
)
);
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,
),
),
),
]
),
),
)
],
),
),
),
)
);
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();
}
}
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,
Expanded(
flex: 3,
child: _selectedLog != null
? LogDetailsScreen(
log: _selectedLog!,
dialog: false,
)
: 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()
: const SizedBox()
)
],
),
Expanded(
flex: 2,
child: selectedLog != null
? LogDetailsScreen(
log: selectedLog!,
dialog: false,
)
: const SizedBox()
)
],
),
);
}
else {
return logsScreen();
}
);
}
else {
return LogsListWidget(
twoColumns: false,
selectedLog: _selectedLog,
onLogSelected: (log) => setState(() => _selectedLog = log),
);
}
},
);
}
}

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