Implemented go router

This commit is contained in:
Juan Gilsanz Polo 2023-10-28 22:38:49 +02:00
parent 2ad739ed4f
commit 621171c5b1
23 changed files with 614 additions and 234 deletions

View file

@ -101,7 +101,7 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
child: child, child: child,
) )
), ),
child: screens[appConfigProvider.selectedScreen].body, child: SizedBox()
), ),
), ),
], ],

View file

@ -13,12 +13,10 @@ List<AppScreen> screensSelectServer = [
const AppScreen( const AppScreen(
name: "connect", name: "connect",
icon: Icons.link_rounded, icon: Icons.link_rounded,
body: Connect(),
), ),
const AppScreen( const AppScreen(
name: "settings", name: "settings",
icon: Icons.settings_rounded, icon: Icons.settings_rounded,
body: Settings()
) )
]; ];
@ -26,26 +24,21 @@ List<AppScreen> screensServerConnected = [
const AppScreen( const AppScreen(
name: "home", name: "home",
icon: Icons.home_rounded, icon: Icons.home_rounded,
body: Home(),
), ),
const AppScreen( const AppScreen(
name: "clients", name: "clients",
icon: Icons.devices, icon: Icons.devices,
body: Clients()
), ),
const AppScreen( const AppScreen(
name: "logs", name: "logs",
icon: Icons.list_alt_rounded, icon: Icons.list_alt_rounded,
body: Logs(),
), ),
const AppScreen( const AppScreen(
name: "filters", name: "filters",
icon: Icons.shield_rounded, icon: Icons.shield_rounded,
body: Filters(),
), ),
const AppScreen( const AppScreen(
name: "settings", name: "settings",
icon: Icons.settings_rounded, icon: Icons.settings_rounded,
body: Settings()
) )
]; ];

1
lib/config/sizes.dart Normal file
View file

@ -0,0 +1 @@
const double desktopBreakpoint = 1000;

View file

@ -0,0 +1,19 @@
class RoutesNames {
static const String connect = "/connect";
static const String home = "/home";
static const String queriedDomains = "/home/queried-domains";
static const String blockedDomains = "/home/blocked-domains";
static const String recurrentClients = "/home/recurrent-clients";
static const String clients = "/clients";
static const String clientsList = "/clients/list";
static const String clientPlaceholder = "/clients/list/placeholder";
static const String client = "/clients/list:id";
static const String logs = "/logs";
static const String filters = "/filters";
static const String settings = "/settings";
}

View file

@ -0,0 +1,5 @@
import 'package:adguard_home_manager/config/sizes.dart';
bool isDesktop(double width) {
return width > desktopBreakpoint;
}

View file

@ -667,5 +667,7 @@
"showChart": "Show chart", "showChart": "Show chart",
"hideChart": "Hide chart", "hideChart": "Hide chart",
"showTopItemsChart": "Show top items chart", "showTopItemsChart": "Show top items chart",
"showTopItemsChartDescription": "Shows by default the ring chart on the top items sections. Only affects to the mobile view." "showTopItemsChartDescription": "Shows by default the ring chart on the top items sections. Only affects to the mobile view.",
"openMenu": "Open menu",
"closeMenu": "Close menu"
} }

View file

@ -667,5 +667,7 @@
"showChart": "Mostrar gráfico", "showChart": "Mostrar gráfico",
"hideChart": "Ocultar gráfico", "hideChart": "Ocultar gráfico",
"showTopItemsChart": "Mostrar gráfico en top de items", "showTopItemsChart": "Mostrar gráfico en top de items",
"showTopItemsChartDescription": "Muestra por defecto el gráfico de anillo en las secciones de top de items. Sólo afecta a la vista móvil." "showTopItemsChartDescription": "Muestra por defecto el gráfico de anillo en las secciones de top de items. Sólo afecta a la vista móvil.",
"openMenu": "Abrir menú",
"closeMenu": "Cerrar menú"
} }

View file

@ -15,8 +15,7 @@ import 'package:window_size/window_size.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/base.dart'; import 'package:adguard_home_manager/routes/router.dart';
import 'package:adguard_home_manager/providers/logs_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/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/providers/clients_provider.dart';
@ -204,7 +203,7 @@ class _MainState extends State<Main> {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => MaterialApp( builder: (lightDynamic, darkDynamic) => MaterialApp.router(
title: 'AdGuard Home Manager', title: 'AdGuard Home Manager',
theme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31 theme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31
? appConfigProvider.useDynamicColor == true ? appConfigProvider.useDynamicColor == true
@ -243,7 +242,7 @@ class _MainState extends State<Main> {
child: child!, child: child!,
); );
}, },
home: const Base(), routerConfig: goRouter,
), ),
); );
} }

View file

@ -4,14 +4,12 @@ class AppScreen {
final String name; final String name;
final IconData icon; final IconData icon;
final PreferredSizeWidget? appBar; final PreferredSizeWidget? appBar;
final Widget body;
final Widget? fab; final Widget? fab;
const AppScreen({ const AppScreen({
required this.name, required this.name,
required this.icon, required this.icon,
this.appBar, this.appBar,
required this.body,
this.fab this.fab
}); });
} }

20
lib/routes/router.dart Normal file
View file

@ -0,0 +1,20 @@
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:adguard_home_manager/routes/router_globals.dart';
import 'package:adguard_home_manager/routes/routes.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/constants/routes_names.dart';
final goRouter = GoRouter(
navigatorKey: rootNavigatorKey,
redirect: (context, state) {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
if (serversProvider.selectedServer == null) {
return RoutesNames.connect;
}
return null;
},
initialLocation: RoutesNames.home,
routes: routes,
);

View file

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
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>();

113
lib/routes/routes.dart Normal file
View file

@ -0,0 +1,113 @@
import 'package:go_router/go_router.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/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/widgets/layout.dart';
import 'package:adguard_home_manager/routes/router_globals.dart';
import 'package:adguard_home_manager/constants/routes_names.dart';
final List<RouteBase> routes = [
GoRoute(
path: "/",
redirect: (context, state) => RoutesNames.home,
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => Layout(
navigationShell: navigationShell
),
branches: [
StatefulShellBranch(
navigatorKey: homeNavigatorKey,
routes: [
GoRoute(
parentNavigatorKey: homeNavigatorKey,
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(
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']
)
)
]
)
]
),
StatefulShellBranch(
navigatorKey: logsNavigatorKey,
routes: [
GoRoute(
path: RoutesNames.logs,
builder: (context, state) => const Logs(),
)
]
),
StatefulShellBranch(
navigatorKey: filtersNavigatorKey,
routes: [
GoRoute(
path: RoutesNames.filters,
builder: (context, state) => const Filters(),
)
]
),
StatefulShellBranch(
navigatorKey: settingsNavigatorKey,
routes: [
GoRoute(
path: RoutesNames.settings,
builder: (context, state) => const Settings(),
)
]
),
StatefulShellBranch(
navigatorKey: connectNavigatorKey,
routes: [
GoRoute(
path: RoutesNames.connect,
builder: (context, state) => const Connect(),
)
]
),
]
)
];

View file

@ -1,7 +1,5 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -157,7 +155,6 @@ class _AddedListState extends State<AddedList> {
} }
return CustomTabContentList( return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS),
listPadding: widget.splitView == true listPadding: widget.splitView == true
? const EdgeInsets.only(top: 8) ? const EdgeInsets.only(top: 8)
: null, : null,

View file

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class ClientPlaceholder extends StatelessWidget {
const ClientPlaceholder({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text("Select a client"),
);
}
}

View file

@ -1,7 +1,9 @@
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';
@ -12,18 +14,12 @@ 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 StatefulWidget { class LogsListClient extends StatefulHookWidget {
final String ip; final String id;
final String? name;
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
const LogsListClient({ const LogsListClient({
Key? key, Key? key,
required this.ip, required this.id,
this.name,
required this.serversProvider,
required this.appConfigProvider
}) : super(key: key); }) : super(key: key);
@override @override
@ -38,11 +34,9 @@ class _LogsListClientState extends State<LogsListClient> {
int logsQuantity = 100; int logsQuantity = 100;
int offset = 0; int offset = 0;
int loadStatus = 0; LoadStatus loadStatus = LoadStatus.loading;
LogsData? logsData; LogsData? logsData;
String previousIp = "";
bool showDivider = true; bool showDivider = true;
CancelableOperation? cancelableRequest; CancelableOperation? cancelableRequest;
@ -67,7 +61,7 @@ class _LogsListClientState extends State<LogsListClient> {
serversProvider.apiClient!.getLogs( serversProvider.apiClient!.getLogs(
count: logsQuantity, count: logsQuantity,
offset: offst, offset: offst,
search: '"${widget.ip}"' search: '"${widget.id}"'
) )
); );
@ -90,11 +84,11 @@ class _LogsListClientState extends State<LogsListClient> {
LogsData newLogsData = result['data']; LogsData newLogsData = result['data'];
setState(() => logsData = newLogsData); setState(() => logsData = newLogsData);
} }
setState(() => loadStatus = 1); setState(() => loadStatus = LoadStatus.loaded);
} }
else { else {
setState(() => loadStatus = 2); setState(() => loadStatus = LoadStatus.error);
widget.appConfigProvider.addLog(result['log']); Provider.of<AppConfigProvider>(context, listen: false).addLog(result['log']);
} }
} }
} }
@ -116,8 +110,6 @@ 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();
} }
@ -125,15 +117,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;
if (widget.ip != previousIp) { useEffect(() {
setState(() => loadStatus = 0); setState(() => loadStatus = LoadStatus.loading);
fetchLogs(inOffset: 0); fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip); return null;
} }, [widget.id]);
Widget status() { Widget status() {
switch (loadStatus) { switch (loadStatus) {
case 0: case LoadStatus.loading:
return SizedBox( return SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
@ -154,7 +146,7 @@ class _LogsListClientState extends State<LogsListClient> {
), ),
); );
case 1: case LoadStatus.loaded:
if (logsData!.data.isNotEmpty) { if (logsData!.data.isNotEmpty) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: fetchLogs, onRefresh: fetchLogs,
@ -217,7 +209,7 @@ class _LogsListClientState extends State<LogsListClient> {
); );
} }
case 2: case LoadStatus.error:
return SizedBox( return SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
@ -249,7 +241,7 @@ class _LogsListClientState extends State<LogsListClient> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip), title: Text(widget.id),
centerTitle: true, centerTitle: true,
actions: [ actions: [
if (!(Platform.isAndroid || Platform.isIOS)) ...[ if (!(Platform.isAndroid || Platform.isIOS)) ...[

View file

@ -1,24 +1,25 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:go_router/go_router.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';
import 'package:adguard_home_manager/screens/clients/clients_list.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.dart';
import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart'; import 'package:adguard_home_manager/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/clients_provider.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/constants/enums.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';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class Clients extends StatefulWidget { class Clients extends StatefulWidget {
const Clients({Key? key}) : super(key: key); final Widget child;
const Clients({
Key? key,
required this.child,
}) : super(key: key);
@override @override
State<Clients> createState() => _ClientsState(); State<Clients> createState() => _ClientsState();
@ -53,9 +54,7 @@ class _ClientsState extends State<Clients> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final clientsProvider = Provider.of<ClientsProvider>(context); final clientsProvider = Provider.of<ClientsProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
@ -96,163 +95,119 @@ class _ClientsState extends State<Clients> with TickerProviderStateMixin {
scrollController: scrollController, scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredActiveClients : [], ? clientsProvider.filteredActiveClients : [],
onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( onClientSelected: (client) => context.go(
builder: (context) => LogsListClient( RoutesNames.client,
ip: client.ip, extra: {
serversProvider: serversProvider, "id": client.name != null && client.name != ""
appConfigProvider: appConfigProvider ? client.name
) : client.ip
)), }
splitView: false, ),
sliver: sliver, splitView: isDesktop(width),
), ),
AddedList( AddedList(
scrollController: scrollController, scrollController: scrollController,
data: clientsProvider.loadStatus == LoadStatus.loaded data: clientsProvider.loadStatus == LoadStatus.loaded
? clientsProvider.filteredAddedClients : [], ? clientsProvider.filteredAddedClients : [],
onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( onClientSelected: (client) => context.go(
builder: (context) => LogsListClient( RoutesNames.client,
ip: client.ids[0], extra: { "id": client.name }
serversProvider: serversProvider, ),
appConfigProvider: appConfigProvider splitView: isDesktop(width),
)
)),
splitView: false,
), ),
] ]
); );
} }
if (width > 900) { return Row(
return SplitView.material( children: [
hideDivider: true, SizedBox(
flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), width: isDesktop(width) ? 300 : width,
placeholder: Center( height: double.maxFinite,
child: Padding( child: Material(
padding: const EdgeInsets.all(24), child: DefaultTabController(
child: Text( length: 2,
AppLocalizations.of(context)!.selectClientLeftColumn, child: NestedScrollView(
textAlign: TextAlign.center, controller: scrollController,
style: TextStyle( headerSliverBuilder: ((context, innerBoxIsScrolled) {
fontSize: 24, return [
color: Theme.of(context).colorScheme.onSurfaceVariant 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)
)
), ),
), ),
), ),
child: ClientsDesktopView( if (isDesktop(width) == true) Expanded(
serversProvider: serversProvider, child: widget.child,
appConfigProvider: appConfigProvider,
) )
); ],
} );
else {
if (!(Platform.isAndroid || Platform.isIOS)) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.clients),
centerTitle: false,
actions: [
if (clientsProvider.loadStatus == LoadStatus.loaded) ...[
IconButton(
onPressed: () => {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const SearchClients()
))
},
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.searchClients,
),
const SizedBox(width: 10),
]
],
bottom: tabBar()
),
body: tabBarView(false),
),
);
}
else {
return 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)
)
);
}
}
} }
} }

View file

@ -97,18 +97,10 @@ class _ClientsDesktopViewState extends State<ClientsDesktopView> with TickerPro
onClientSelected: (client) => setState(() { onClientSelected: (client) => setState(() {
selectedAddedClient = null; selectedAddedClient = null;
selectedActiveClient = client; selectedActiveClient = client;
SplitView.of(context).setSecondary(
LogsListClient(
ip: client.ip,
name: client.name,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
)
);
}), }),
selectedClient: selectedActiveClient, selectedClient: selectedActiveClient,
splitView: true, splitView: true,
sliver: sliver,
), ),
AddedList( AddedList(
scrollController: scrollController, scrollController: scrollController,
@ -117,14 +109,7 @@ class _ClientsDesktopViewState extends State<ClientsDesktopView> with TickerPro
onClientSelected: (client) => setState(() { onClientSelected: (client) => setState(() {
selectedActiveClient = null; selectedActiveClient = null;
selectedAddedClient = client; selectedAddedClient = client;
SplitView.of(context).setSecondary(
LogsListClient(
ip: client.ids[0],
name: client.name,
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
)
);
}), }),
selectedClient: selectedAddedClient, selectedClient: selectedAddedClient,
splitView: true, splitView: true,

View file

@ -15,7 +15,6 @@ class ClientsList extends StatelessWidget {
final void Function(AutoClient) onClientSelected; final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient; final AutoClient? selectedClient;
final bool splitView; final bool splitView;
final bool sliver;
const ClientsList({ const ClientsList({
Key? key, Key? key,
@ -24,7 +23,6 @@ class ClientsList extends StatelessWidget {
required this.onClientSelected, required this.onClientSelected,
this.selectedClient, this.selectedClient,
required this.splitView, required this.splitView,
required this.sliver
}) : super(key: key); }) : super(key: key);
@override @override
@ -35,7 +33,6 @@ class ClientsList extends StatelessWidget {
listPadding: splitView == true listPadding: splitView == true
? const EdgeInsets.only(top: 8) ? const EdgeInsets.only(top: 8)
: null, : null,
noSliver: !sliver,
loadingGenerator: () => SizedBox( loadingGenerator: () => SizedBox(
width: double.maxFinite, width: double.maxFinite,
height: MediaQuery.of(context).size.height-171, height: MediaQuery.of(context).size.height-171,

View file

@ -1,3 +1,4 @@
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -25,6 +26,8 @@ class HomeAppBar extends StatelessWidget {
final statusProvider = Provider.of<StatusProvider>(context); final statusProvider = Provider.of<StatusProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
final Server? server = serversProvider.selectedServer; final Server? server = serversProvider.selectedServer;
void navigateServers() { void navigateServers() {
@ -40,6 +43,7 @@ class HomeAppBar extends StatelessWidget {
floating: true, floating: true,
centerTitle: false, centerTitle: false,
forceElevated: innerBoxScrolled, forceElevated: innerBoxScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
leading: Stack( leading: Stack(
children: [ children: [
Center( Center(

View file

@ -84,21 +84,21 @@ class BottomNavBar extends StatelessWidget {
), ),
label: translatedName(screen.name) label: translatedName(screen.name)
)).toList(), )).toList(),
onDestinationSelected: (value) { // onDestinationSelected: (value) {
// Reset clients tab to 0 when changing screen // // Reset clients tab to 0 when changing screen
if (value != 1) { // if (value != 1) {
appConfigProvider.setSelectedClientsTab(0); // appConfigProvider.setSelectedClientsTab(0);
} // }
// Reset logs filters when changing screen // // Reset logs filters when changing screen
if (value != 2) { // if (value != 2) {
logsProvider.resetFilters(); // logsProvider.resetFilters();
} // }
// Reset settings selected screen // // Reset settings selected screen
if (value != screens.length-1) { // if (value != screens.length-1) {
appConfigProvider.setSelectedSettingsScreen(screen: null); // appConfigProvider.setSelectedSettingsScreen(screen: null);
} // }
appConfigProvider.setSelectedScreen(value); // appConfigProvider.setSelectedScreen(value);
}, // },
); );
} }
} }

250
lib/widgets/layout.dart Normal file
View file

@ -0,0 +1,250 @@
import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/config/app_screens.dart';
import 'package:adguard_home_manager/config/sizes.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class Layout extends StatefulWidget {
final StatefulNavigationShell navigationShell;
const Layout({
Key? key,
required this.navigationShell,
}) : super(key: key);
@override
State<Layout> createState() => _LayoutState();
}
class _LayoutState extends State<Layout> {
bool _drawerExpanded = true;
void _goBranch(int index) {
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final serversProvider = Provider.of<ServersProvider>(context);
String translatedName(String key) {
switch (key) {
case 'home':
return AppLocalizations.of(context)!.home;
case 'settings':
return AppLocalizations.of(context)!.settings;
case 'connect':
return AppLocalizations.of(context)!.connect;
case 'clients':
return AppLocalizations.of(context)!.clients;
case 'logs':
return AppLocalizations.of(context)!.logs;
case 'filters':
return AppLocalizations.of(context)!.filters;
default:
return '';
}
}
if (width > desktopBreakpoint) {
return Material(
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
width: _drawerExpanded ? 250 : 90,
child: ListView(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16
),
child: IconButton(
onPressed: () => setState(() => _drawerExpanded = !_drawerExpanded),
icon: const Icon(Icons.menu_open_rounded),
tooltip: _drawerExpanded == true
? AppLocalizations.of(context)!.closeMenu
: AppLocalizations.of(context)!.openMenu,
),
),
],
),
if (serversProvider.selectedServer != null)
...screensServerConnected.asMap().entries.map(
(s) => DrawerTile(
icon: s.value.icon,
title: translatedName(s.value.name),
isSelected:
widget.navigationShell.currentIndex == s.key,
onSelect: () => _goBranch(s.key),
withoutTitle: !_drawerExpanded,
),
),
if (serversProvider.selectedServer == null)
...screensSelectServer.asMap().entries.map(
(s) => DrawerTile(
icon: s.value.icon,
title: translatedName(s.value.name),
isSelected:
widget.navigationShell.currentIndex == s.key,
onSelect: () => _goBranch(s.key),
withoutTitle: !_drawerExpanded,
),
),
],
),
),
Expanded(
child: widget.navigationShell
),
],
),
);
}
else {
final screens = serversProvider.selectedServer != null && serversProvider.apiClient != null
? screensServerConnected
: screensSelectServer;
return Scaffold(
body: widget.navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient == null) && widget.navigationShell.currentIndex > 1
? 0
: widget.navigationShell.currentIndex,
onDestinationSelected: (s) => _goBranch(s),
destinations: screens.asMap().entries.map((screen) => NavigationDestination(
icon: Stack(
children: [
Icon(
screen.value.icon,
color: widget.navigationShell.currentIndex == screen.key
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
),
if (
screen.value.name == 'settings' &&
serversProvider.updateAvailable.data != null &&
serversProvider.updateAvailable.data!.canAutoupdate == true
) Positioned(
bottom: 0,
right: -12,
child: Container(
width: 10,
height: 10,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.red
),
),
)
],
),
label: translatedName(screen.value.name)
)).toList(),
)
);
}
}
}
class DrawerTile extends StatelessWidget {
final IconData icon;
final String title;
final bool isSelected;
final void Function() onSelect;
final bool? withoutTitle;
const DrawerTile({
super.key,
required this.icon,
required this.title,
required this.isSelected,
required this.onSelect,
this.withoutTitle,
});
@override
Widget build(BuildContext context) {
Widget iconWidget = withoutTitle == true
? Tooltip(
message: title,
child: Icon(
icon,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
: Icon(
icon,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
);
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(30),
bottomRight: Radius.circular(30),
),
onTap: onSelect,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.secondaryContainer
: null,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Row(children: [
iconWidget,
const SizedBox(width: 16),
Flexible(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
]),
),
),
),
);
}
}

View file

@ -222,6 +222,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "5.1.0"
flutter_hooks:
dependency: "direct main"
description:
name: flutter_hooks
sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec"
url: "https://pub.dev"
source: hosted
version: "0.20.3"
flutter_html: flutter_html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -310,6 +318,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: e156bc1b2088eb5ece9351bccd48c3e1719a4858eacbd44e59162e98a68205d1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
html: html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -382,6 +398,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -74,6 +74,8 @@ dependencies:
flutter_dotenv: ^5.0.2 flutter_dotenv: ^5.0.2
flutter_reorderable_list: ^1.3.1 flutter_reorderable_list: ^1.3.1
pie_chart: ^5.3.2 pie_chart: ^5.3.2
go_router: ^12.0.1
flutter_hooks: ^0.20.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: