diff --git a/lib/base.dart b/lib/base.dart index b2b593b..d57dd72 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -101,7 +101,7 @@ class _BaseState extends State with WidgetsBindingObserver { child: child, ) ), - child: screens[appConfigProvider.selectedScreen].body, + child: SizedBox() ), ), ], diff --git a/lib/config/app_screens.dart b/lib/config/app_screens.dart index 1835dc4..91d33f0 100644 --- a/lib/config/app_screens.dart +++ b/lib/config/app_screens.dart @@ -13,12 +13,10 @@ List screensSelectServer = [ const AppScreen( name: "connect", icon: Icons.link_rounded, - body: Connect(), ), const AppScreen( name: "settings", icon: Icons.settings_rounded, - body: Settings() ) ]; @@ -26,26 +24,21 @@ List screensServerConnected = [ const AppScreen( name: "home", icon: Icons.home_rounded, - body: Home(), ), const AppScreen( name: "clients", icon: Icons.devices, - body: Clients() ), const AppScreen( name: "logs", icon: Icons.list_alt_rounded, - body: Logs(), ), const AppScreen( name: "filters", icon: Icons.shield_rounded, - body: Filters(), ), const AppScreen( name: "settings", icon: Icons.settings_rounded, - body: Settings() ) ]; \ No newline at end of file diff --git a/lib/config/sizes.dart b/lib/config/sizes.dart new file mode 100644 index 0000000..0fc2640 --- /dev/null +++ b/lib/config/sizes.dart @@ -0,0 +1 @@ +const double desktopBreakpoint = 1000; \ No newline at end of file diff --git a/lib/constants/routes_names.dart b/lib/constants/routes_names.dart new file mode 100644 index 0000000..dae8723 --- /dev/null +++ b/lib/constants/routes_names.dart @@ -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"; +} \ No newline at end of file diff --git a/lib/functions/desktop_mode.dart b/lib/functions/desktop_mode.dart new file mode 100644 index 0000000..4586068 --- /dev/null +++ b/lib/functions/desktop_mode.dart @@ -0,0 +1,5 @@ +import 'package:adguard_home_manager/config/sizes.dart'; + +bool isDesktop(double width) { + return width > desktopBreakpoint; +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee0533d..d4962e3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -667,5 +667,7 @@ "showChart": "Show chart", "hideChart": "Hide 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" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4276b7b..2f202a8 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -667,5 +667,7 @@ "showChart": "Mostrar gráfico", "hideChart": "Ocultar gráfico", "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ú" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 98bb878..fafb46e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,8 +15,7 @@ import 'package:window_size/window_size.dart'; import 'package:flutter_localizations/flutter_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/app_config_provider.dart'; import 'package:adguard_home_manager/providers/clients_provider.dart'; @@ -204,7 +203,7 @@ class _MainState extends State
{ final appConfigProvider = Provider.of(context); return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) => MaterialApp( + builder: (lightDynamic, darkDynamic) => MaterialApp.router( title: 'AdGuard Home Manager', theme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31 ? appConfigProvider.useDynamicColor == true @@ -243,7 +242,7 @@ class _MainState extends State
{ child: child!, ); }, - home: const Base(), + routerConfig: goRouter, ), ); } diff --git a/lib/models/app_screen.dart b/lib/models/app_screen.dart index bce5cc3..1e377eb 100644 --- a/lib/models/app_screen.dart +++ b/lib/models/app_screen.dart @@ -4,14 +4,12 @@ class AppScreen { final String name; final IconData icon; final PreferredSizeWidget? appBar; - final Widget body; final Widget? fab; const AppScreen({ required this.name, required this.icon, this.appBar, - required this.body, this.fab }); } \ No newline at end of file diff --git a/lib/routes/router.dart b/lib/routes/router.dart new file mode 100644 index 0000000..9b1cfba --- /dev/null +++ b/lib/routes/router.dart @@ -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(context, listen: false); + if (serversProvider.selectedServer == null) { + return RoutesNames.connect; + } + return null; + }, + initialLocation: RoutesNames.home, + routes: routes, +); \ No newline at end of file diff --git a/lib/routes/router_globals.dart b/lib/routes/router_globals.dart new file mode 100644 index 0000000..739c50c --- /dev/null +++ b/lib/routes/router_globals.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; + +final GlobalKey rootNavigatorKey = GlobalKey(); +final GlobalKey connectNavigatorKey = GlobalKey(); +final GlobalKey homeNavigatorKey = GlobalKey(); +final GlobalKey clientsNavigatorKey = GlobalKey(); +final GlobalKey clientsListNavigatorKey = GlobalKey(); +final GlobalKey logsNavigatorKey = GlobalKey(); +final GlobalKey filtersNavigatorKey = GlobalKey(); +final GlobalKey settingsNavigatorKey = GlobalKey(); \ No newline at end of file diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart new file mode 100644 index 0000000..04bd218 --- /dev/null +++ b/lib/routes/routes.dart @@ -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 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(), + ) + ] + ), + ] + ) +]; \ No newline at end of file diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 24b69ae..eea9dd6 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -1,7 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:flutter/rendering.dart'; @@ -157,7 +155,6 @@ class _AddedListState extends State { } return CustomTabContentList( - noSliver: !(Platform.isAndroid || Platform.isIOS), listPadding: widget.splitView == true ? const EdgeInsets.only(top: 8) : null, diff --git a/lib/screens/clients/client/client_placeholder.dart b/lib/screens/clients/client/client_placeholder.dart new file mode 100644 index 0000000..cf4f485 --- /dev/null +++ b/lib/screens/clients/client/client_placeholder.dart @@ -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"), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/logs_list_client.dart b/lib/screens/clients/client/logs_list_client.dart index 0ddc99a..f35de0b 100644 --- a/lib/screens/clients/client/logs_list_client.dart +++ b/lib/screens/clients/client/logs_list_client.dart @@ -1,7 +1,9 @@ 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'; @@ -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/servers_provider.dart'; -class LogsListClient extends StatefulWidget { - final String ip; - final String? name; - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; +class LogsListClient extends StatefulHookWidget { + final String id; const LogsListClient({ Key? key, - required this.ip, - this.name, - required this.serversProvider, - required this.appConfigProvider + required this.id, }) : super(key: key); @override @@ -38,11 +34,9 @@ class _LogsListClientState extends State { int logsQuantity = 100; int offset = 0; - int loadStatus = 0; + LoadStatus loadStatus = LoadStatus.loading; LogsData? logsData; - String previousIp = ""; - bool showDivider = true; CancelableOperation? cancelableRequest; @@ -67,7 +61,7 @@ class _LogsListClientState extends State { serversProvider.apiClient!.getLogs( count: logsQuantity, offset: offst, - search: '"${widget.ip}"' + search: '"${widget.id}"' ) ); @@ -90,11 +84,11 @@ class _LogsListClientState extends State { LogsData newLogsData = result['data']; setState(() => logsData = newLogsData); } - setState(() => loadStatus = 1); + setState(() => loadStatus = LoadStatus.loaded); } else { - setState(() => loadStatus = 2); - widget.appConfigProvider.addLog(result['log']); + setState(() => loadStatus = LoadStatus.error); + Provider.of(context, listen: false).addLog(result['log']); } } } @@ -116,8 +110,6 @@ class _LogsListClientState extends State { @override void initState() { scrollController = ScrollController()..addListener(scrollListener); - fetchLogs(inOffset: 0); - setState(() => previousIp = widget.ip); super.initState(); } @@ -125,15 +117,15 @@ class _LogsListClientState extends State { Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; - if (widget.ip != previousIp) { - setState(() => loadStatus = 0); + useEffect(() { + setState(() => loadStatus = LoadStatus.loading); fetchLogs(inOffset: 0); - setState(() => previousIp = widget.ip); - } + return null; + }, [widget.id]); Widget status() { switch (loadStatus) { - case 0: + case LoadStatus.loading: return SizedBox( width: double.maxFinite, child: Column( @@ -154,7 +146,7 @@ class _LogsListClientState extends State { ), ); - case 1: + case LoadStatus.loaded: if (logsData!.data.isNotEmpty) { return RefreshIndicator( onRefresh: fetchLogs, @@ -217,7 +209,7 @@ class _LogsListClientState extends State { ); } - case 2: + case LoadStatus.error: return SizedBox( width: double.maxFinite, child: Column( @@ -249,7 +241,7 @@ class _LogsListClientState extends State { return Scaffold( appBar: AppBar( - title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip), + title: Text(widget.id), centerTitle: true, actions: [ if (!(Platform.isAndroid || Platform.isIOS)) ...[ diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index 4762e5b..45ccbd4 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -1,24 +1,25 @@ -import 'dart:io'; - 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:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart'; -import 'package:adguard_home_manager/screens/clients/search_clients.dart'; -import 'package:adguard_home_manager/screens/clients/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/constants/routes_names.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; 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 State createState() => _ClientsState(); @@ -53,9 +54,7 @@ class _ClientsState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); final clientsProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); final width = MediaQuery.of(context).size.width; @@ -96,163 +95,119 @@ class _ClientsState extends State with TickerProviderStateMixin { scrollController: scrollController, data: clientsProvider.loadStatus == LoadStatus.loaded ? clientsProvider.filteredActiveClients : [], - onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogsListClient( - ip: client.ip, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ) - )), - splitView: false, - sliver: sliver, + 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) => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogsListClient( - ip: client.ids[0], - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ) - )), - splitView: false, + onClientSelected: (client) => context.go( + RoutesNames.client, + extra: { "id": client.name } + ), + splitView: isDesktop(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)!.selectClientLeftColumn, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), + 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) + ) ), ), ), - child: ClientsDesktopView( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, + if (isDesktop(width) == true) Expanded( + child: widget.child, ) - ); - } - 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) - ) - ); - } - } + ], + ); } } \ No newline at end of file diff --git a/lib/screens/clients/clients_desktop_view.dart b/lib/screens/clients/clients_desktop_view.dart index 9e261ff..7356e15 100644 --- a/lib/screens/clients/clients_desktop_view.dart +++ b/lib/screens/clients/clients_desktop_view.dart @@ -97,18 +97,10 @@ class _ClientsDesktopViewState extends State with TickerPro onClientSelected: (client) => setState(() { selectedAddedClient = null; selectedActiveClient = client; - SplitView.of(context).setSecondary( - LogsListClient( - ip: client.ip, - name: client.name, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ) - ); + }), selectedClient: selectedActiveClient, splitView: true, - sliver: sliver, ), AddedList( scrollController: scrollController, @@ -117,14 +109,7 @@ class _ClientsDesktopViewState extends State with TickerPro onClientSelected: (client) => setState(() { selectedActiveClient = null; selectedAddedClient = client; - SplitView.of(context).setSecondary( - LogsListClient( - ip: client.ids[0], - name: client.name, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ) - ); + }), selectedClient: selectedAddedClient, splitView: true, diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index f3ee547..d0ecd33 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -15,7 +15,6 @@ class ClientsList extends StatelessWidget { final void Function(AutoClient) onClientSelected; final AutoClient? selectedClient; final bool splitView; - final bool sliver; const ClientsList({ Key? key, @@ -24,7 +23,6 @@ class ClientsList extends StatelessWidget { required this.onClientSelected, this.selectedClient, required this.splitView, - required this.sliver }) : super(key: key); @override @@ -35,7 +33,6 @@ class ClientsList extends StatelessWidget { listPadding: splitView == true ? const EdgeInsets.only(top: 8) : null, - noSliver: !sliver, loadingGenerator: () => SizedBox( width: double.maxFinite, height: MediaQuery.of(context).size.height-171, diff --git a/lib/screens/home/appbar.dart b/lib/screens/home/appbar.dart index 8660f54..34893b6 100644 --- a/lib/screens/home/appbar.dart +++ b/lib/screens/home/appbar.dart @@ -1,3 +1,4 @@ +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -25,6 +26,8 @@ class HomeAppBar extends StatelessWidget { final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + final Server? server = serversProvider.selectedServer; void navigateServers() { @@ -40,6 +43,7 @@ class HomeAppBar extends StatelessWidget { floating: true, centerTitle: false, forceElevated: innerBoxScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, leading: Stack( children: [ Center( diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index 366fe2c..0f16dcd 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -84,21 +84,21 @@ class BottomNavBar extends StatelessWidget { ), label: translatedName(screen.name) )).toList(), - onDestinationSelected: (value) { - // Reset clients tab to 0 when changing screen - if (value != 1) { - appConfigProvider.setSelectedClientsTab(0); - } - // Reset logs filters when changing screen - if (value != 2) { - logsProvider.resetFilters(); - } - // Reset settings selected screen - if (value != screens.length-1) { - appConfigProvider.setSelectedSettingsScreen(screen: null); - } - appConfigProvider.setSelectedScreen(value); - }, + // onDestinationSelected: (value) { + // // Reset clients tab to 0 when changing screen + // if (value != 1) { + // appConfigProvider.setSelectedClientsTab(0); + // } + // // Reset logs filters when changing screen + // if (value != 2) { + // logsProvider.resetFilters(); + // } + // // Reset settings selected screen + // if (value != screens.length-1) { + // appConfigProvider.setSelectedSettingsScreen(screen: null); + // } + // appConfigProvider.setSelectedScreen(value); + // }, ); } } \ No newline at end of file diff --git a/lib/widgets/layout.dart b/lib/widgets/layout.dart new file mode 100644 index 0000000..3513bb9 --- /dev/null +++ b/lib/widgets/layout.dart @@ -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 createState() => _LayoutState(); +} + +class _LayoutState extends State { + 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(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, + ), + ), + ) + ]), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c647bf5..a194327 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -222,6 +222,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -310,6 +318,14 @@ packages: description: flutter source: sdk 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: dependency: "direct main" description: @@ -382,6 +398,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" markdown: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2445ec4..06ae67d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,8 @@ dependencies: flutter_dotenv: ^5.0.2 flutter_reorderable_list: ^1.3.1 pie_chart: ^5.3.2 + go_router: ^12.0.1 + flutter_hooks: ^0.20.3 dev_dependencies: flutter_test: