diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index f53e4ec5..5de8f211 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,7 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/core/router/routes.dart'; import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -9,6 +10,12 @@ import 'package:sentry_flutter/sentry_flutter.dart'; part 'app_router.g.dart'; +bool _debugMobileRouter = false; + +final useMobileRouter = + !PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter); +final GlobalKey rootNavigatorKey = GlobalKey(); + // TODO: test and improve handling of deep link @riverpod GoRouter router(RouterRef ref) { @@ -31,7 +38,7 @@ GoRouter router(RouterRef ref) { navigatorKey: rootNavigatorKey, initialLocation: initialLocation, debugLogDiagnostics: true, - routes: $routes, + routes: useMobileRouter ? mobileRoutes : desktopRoutes, refreshListenable: notifier, redirect: notifier.redirect, observers: [ diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index f557c5a6..aaa1ea0c 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -1,2 +1,2 @@ export 'app_router.dart'; -export 'routes/routes.dart'; +export 'routes.dart'; diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart new file mode 100644 index 00000000..5b9acd92 --- /dev/null +++ b/lib/core/router/routes.dart @@ -0,0 +1,16 @@ +import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop; +import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile; +import 'package:hiddify/core/router/routes/shared_routes.dart' as shared; + +export 'routes/mobile_routes.dart'; +export 'routes/shared_routes.dart' hide $appRoutes; + +final mobileRoutes = [ + ...shared.$appRoutes, + ...mobile.$appRoutes, +]; + +final desktopRoutes = [ + ...shared.$appRoutes, + ...desktop.$appRoutes, +]; diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 8444d76f..20c1f87d 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; import 'package:hiddify/features/settings/view/view.dart'; -import 'package:hiddify/features/wrapper/wrapper.dart'; part 'desktop_routes.g.dart'; @@ -61,7 +61,7 @@ class DesktopWrapperRoute extends ShellRouteData { @override Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return DesktopWrapper(navigator); + return AdaptiveRootScaffold(navigator); } } diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 1846cbf2..8cfd3ca3 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; import 'package:hiddify/features/settings/view/view.dart'; -import 'package:hiddify/features/wrapper/wrapper.dart'; part 'mobile_routes.g.dart'; @@ -65,7 +66,7 @@ class MobileWrapperRoute extends ShellRouteData { @override Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return MobileWrapper(navigator); + return AdaptiveRootScaffold(navigator); } } diff --git a/lib/core/router/routes/routes.dart b/lib/core/router/routes/routes.dart deleted file mode 100644 index 5805cd24..00000000 --- a/lib/core/router/routes/routes.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop; -import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile; -import 'package:hiddify/core/router/routes/shared_routes.dart' as shared; -import 'package:hiddify/utils/utils.dart'; - -export 'mobile_routes.dart'; -export 'shared_routes.dart' hide $appRoutes; - -List get $routes => [ - ...shared.$appRoutes, - if (PlatformUtils.isDesktop) - ...desktop.$appRoutes - else - ...mobile.$appRoutes, - ]; diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart index 8fdadd06..74d43ebf 100644 --- a/lib/core/router/routes/shared_routes.dart +++ b/lib/core/router/routes/shared_routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/features/home/view/view.dart'; import 'package:hiddify/features/intro/view/view.dart'; import 'package:hiddify/features/profile_detail/view/view.dart'; @@ -9,8 +10,6 @@ import 'package:hiddify/utils/utils.dart'; part 'shared_routes.g.dart'; -final GlobalKey rootNavigatorKey = GlobalKey(); - class HomeRoute extends GoRouteData { const HomeRoute(); static const path = '/'; diff --git a/lib/features/about/view/about_page.dart b/lib/features/about/view/about_page.dart index 4261c6db..68669bdd 100644 --- a/lib/features/about/view/about_page.dart +++ b/lib/features/about/view/about_page.dart @@ -4,6 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/common/common.dart'; import 'package:hiddify/features/common/new_version_dialog.dart'; import 'package:hiddify/gen/assets.gen.dart'; @@ -71,7 +72,7 @@ class AboutPage extends HookConsumerWidget { return Scaffold( body: CustomScrollView( slivers: [ - SliverAppBar( + NestedAppBar( title: Text(t.about.pageTitle), actions: [ PopupMenuButton( diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart new file mode 100644 index 00000000..e0a9786c --- /dev/null +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +abstract interface class RootScaffold { + static final stateKey = GlobalKey(); + + static bool canShowDrawer(BuildContext context) => + Breakpoints.small.isActive(context); +} + +class AdaptiveRootScaffold extends HookConsumerWidget { + const AdaptiveRootScaffold(this.navigator, {super.key}); + + final Widget navigator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final selectedIndex = getCurrentIndex(context); + + final destinations = [ + NavigationDestination( + icon: const Icon(Icons.power_settings_new), + label: t.home.pageTitle, + ), + NavigationDestination( + icon: const Icon(Icons.filter_list), + label: t.proxies.pageTitle, + ), + NavigationDestination( + icon: const Icon(Icons.article), + label: t.logs.pageTitle, + ), + NavigationDestination( + icon: const Icon(Icons.settings), + label: t.settings.pageTitle, + ), + NavigationDestination( + icon: const Icon(Icons.info), + label: t.about.pageTitle, + ), + ]; + + return _CustomAdaptiveScaffold( + selectedIndex: selectedIndex, + onSelectedIndexChange: (index) { + RootScaffold.stateKey.currentState?.closeDrawer(); + switchTab(index, context); + }, + destinations: destinations, + drawerDestinationRange: useMobileRouter ? (2, null) : (0, null), + bottomDestinationRange: (0, 2), + useBottomSheet: useMobileRouter, + body: navigator, + ); + } +} + +class _CustomAdaptiveScaffold extends HookConsumerWidget { + const _CustomAdaptiveScaffold({ + required this.selectedIndex, + required this.onSelectedIndexChange, + required this.destinations, + required this.drawerDestinationRange, + required this.bottomDestinationRange, + this.useBottomSheet = false, + required this.body, + }); + + final int selectedIndex; + final Function(int) onSelectedIndexChange; + final List destinations; + final (int, int?) drawerDestinationRange; + final (int, int?) bottomDestinationRange; + final bool useBottomSheet; + final Widget body; + + List destinationsSlice((int, int?) range) => + destinations.sublist(range.$1, range.$2); + + int? selectedWithOffset((int, int?) range) { + final index = selectedIndex - range.$1; + return index < 0 || (range.$2 != null && index > (range.$2! - 1)) + ? null + : index; + } + + void selectWithOffset(int index, (int, int?) range) => + onSelectedIndexChange(index + range.$1); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + key: RootScaffold.stateKey, + drawer: Breakpoints.small.isActive(context) + ? SafeArea( + child: Drawer( + width: (MediaQuery.sizeOf(context).width * 0.88).clamp(0, 304), + child: NavigationRail( + extended: true, + selectedIndex: selectedWithOffset(drawerDestinationRange), + destinations: destinationsSlice(drawerDestinationRange) + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: (index) => + selectWithOffset(index, drawerDestinationRange), + ), + ), + ) + : null, + body: AdaptiveLayout( + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primaryNavigation'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedIndex, + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: onSelectedIndexChange, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primaryNavigation1'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + extended: true, + selectedIndex: selectedIndex, + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: onSelectedIndexChange, + ), + ), + }, + ), + bottomNavigation: useBottomSheet || + Breakpoints.smallMobile.isActive(context) + ? SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottomNavigation'), + builder: (_) => + AdaptiveScaffold.standardBottomNavigationBar( + currentIndex: selectedWithOffset(bottomDestinationRange), + destinations: destinationsSlice(bottomDestinationRange), + onDestinationSelected: (index) => + selectWithOffset(index, bottomDestinationRange), + ), + ), + }, + ) + : null, + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: (context) => body, + ), + }, + ), + ), + ); + } +} diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/common/app_update_notifier.dart index 66d13581..72d57fd7 100644 --- a/lib/features/common/app_update_notifier.dart +++ b/lib/features/common/app_update_notifier.dart @@ -1,7 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/features/common/new_version_dialog.dart'; diff --git a/lib/features/common/common.dart b/lib/features/common/common.dart index aca01a77..bed2a3db 100644 --- a/lib/features/common/common.dart +++ b/lib/features/common/common.dart @@ -1,6 +1,5 @@ export 'app_update_notifier.dart'; export 'confirmation_dialogs.dart'; -export 'custom_app_bar.dart'; export 'general_pref_tiles.dart'; export 'profile_tile.dart'; export 'qr_code_scanner_screen.dart'; diff --git a/lib/features/common/custom_app_bar.dart b/lib/features/common/custom_app_bar.dart deleted file mode 100644 index e08b1000..00000000 --- a/lib/features/common/custom_app_bar.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -abstract class RootScaffold { - static final stateKey = GlobalKey(); -} - -class NestedTabAppBar extends SliverAppBar { - NestedTabAppBar({ - super.key, - super.title, - super.actions, - super.pinned = true, - super.forceElevated, - super.bottom, - }) : super( - leading: RootScaffold.stateKey.currentState?.hasDrawer ?? false - ? DrawerButton( - onPressed: () { - RootScaffold.stateKey.currentState?.openDrawer(); - }, - ) - : null, - ); -} diff --git a/lib/features/common/nested_app_bar.dart b/lib/features/common/nested_app_bar.dart new file mode 100644 index 00000000..99b3c1b3 --- /dev/null +++ b/lib/features/common/nested_app_bar.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; + +bool showDrawerButton(BuildContext context) { + if (!useMobileRouter) return true; + final String location = GoRouterState.of(context).uri.path; + if (location == const HomeRoute().location) return true; + if (location.startsWith(const ProxiesRoute().location)) return true; + return false; +} + +class NestedAppBar extends StatelessWidget { + const NestedAppBar({ + super.key, + this.title, + this.actions, + this.pinned = true, + this.forceElevated = false, + this.bottom, + }); + + final Widget? title; + final List? actions; + final bool pinned; + final bool forceElevated; + final PreferredSizeWidget? bottom; + + @override + Widget build(BuildContext context) { + RootScaffold.canShowDrawer(context); + + return SliverAppBar( + leading: (RootScaffold.stateKey.currentState?.hasDrawer ?? false) && + showDrawerButton(context) + ? DrawerButton( + onPressed: () { + RootScaffold.stateKey.currentState?.openDrawer(); + }, + ) + : null, + title: title, + actions: actions, + pinned: pinned, + forceElevated: forceElevated, + bottom: bottom, + ); + } +} diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index 099f1675..22073d41 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; diff --git a/lib/features/common/window/window_controller.dart b/lib/features/common/window/window_controller.dart index bd1f3c68..6d7adb56 100644 --- a/lib/features/common/window/window_controller.dart +++ b/lib/features/common/window/window_controller.dart @@ -14,9 +14,10 @@ class WindowController extends _$WindowController Future build() async { await windowManager.ensureInitialized(); const size = Size(868, 668); + const minumumSize = Size(368, 568); const windowOptions = WindowOptions( size: size, - minimumSize: size, + minimumSize: minumumSize, center: true, ); await windowManager.setPreventClose(true); diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 4d05bf97..2ce3e82d 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -6,7 +6,8 @@ import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart'; -import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/common/profile_tile.dart'; import 'package:hiddify/features/home/widgets/widgets.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -28,7 +29,7 @@ class HomePage extends HookConsumerWidget { children: [ CustomScrollView( slivers: [ - NestedTabAppBar( + NestedAppBar( title: Row( children: [ Text(t.general.appTitle), diff --git a/lib/features/logs/view/logs_page.dart b/lib/features/logs/view/logs_page.dart index 3a6bee65..c332e83d 100644 --- a/lib/features/logs/view/logs_page.dart +++ b/lib/features/logs/view/logs_page.dart @@ -6,10 +6,12 @@ import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/logs/notifier/notifier.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:sliver_tools/sliver_tools.dart'; class LogsPage extends HookConsumerWidget with PresLogger { const LogsPage({super.key}); @@ -49,145 +51,174 @@ class LogsPage extends HookConsumerWidget with PresLogger { : []; return Scaffold( - appBar: AppBar( - // TODO: fix height - toolbarHeight: 90, - title: Text(t.logs.pageTitle), - actions: [ - if (state.paused) - IconButton( - onPressed: notifier.resume, - icon: const Icon(Icons.play_arrow), - tooltip: t.logs.resumeTooltip, - ) - else - IconButton( - onPressed: notifier.pause, - icon: const Icon(Icons.pause), - tooltip: t.logs.pauseTooltip, - ), - IconButton( - onPressed: notifier.clear, - icon: const Icon(Icons.clear_all), - tooltip: t.logs.clearTooltip, - ), - if (popupButtons.isNotEmpty) - PopupMenuButton( - itemBuilder: (context) { - return popupButtons; - }, - ), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(36), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - Flexible( - child: TextFormField( - controller: filterController, - onChanged: notifier.filterMessage, - decoration: InputDecoration( - isDense: true, - hintText: t.logs.filterHint, - ), + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: MultiSliver( + children: [ + NestedAppBar( + forceElevated: innerBoxIsScrolled, + title: Text(t.logs.pageTitle), + actions: [ + if (state.paused) + IconButton( + onPressed: notifier.resume, + icon: const Icon(Icons.play_arrow), + tooltip: t.logs.resumeTooltip, + ) + else + IconButton( + onPressed: notifier.pause, + icon: const Icon(Icons.pause), + tooltip: t.logs.pauseTooltip, + ), + IconButton( + onPressed: notifier.clear, + icon: const Icon(Icons.clear_all), + tooltip: t.logs.clearTooltip, + ), + if (popupButtons.isNotEmpty) + PopupMenuButton( + itemBuilder: (context) { + return popupButtons; + }, + ), + ], ), - ), - const Gap(16), - DropdownButton>( - value: optionOf(state.levelFilter), - onChanged: (v) { - if (v == null) return; - notifier.filterLevel(v.toNullable()); - }, - padding: const EdgeInsets.symmetric(horizontal: 8), - borderRadius: BorderRadius.circular(4), - items: [ - DropdownMenuItem( - value: none(), - child: Text(t.logs.allLevelsFilter), - ), - ...LogLevel.choices.map( - (e) => DropdownMenuItem( - value: some(e), - child: Text(e.name), + SliverPinnedHeader( + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, ), - ), - ], - ), - ], - ), - ), - ), - ), - body: switch (state.logs) { - AsyncData(value: final logs) => SelectionArea( - child: ListView.builder( - itemCount: logs.length, - reverse: true, - itemBuilder: (context, index) { - final log = logs[index]; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (log.level != null) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - log.level!.name.toUpperCase(), - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith(color: log.level!.color), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + children: [ + Flexible( + child: TextFormField( + controller: filterController, + onChanged: notifier.filterMessage, + decoration: InputDecoration( + isDense: true, + hintText: t.logs.filterHint, ), - if (log.time != null) - Text( - log.time!.toString(), - style: - Theme.of(context).textTheme.labelSmall, + ), + ), + const Gap(16), + DropdownButton>( + value: optionOf(state.levelFilter), + onChanged: (v) { + if (v == null) return; + notifier.filterLevel(v.toNullable()); + }, + padding: + const EdgeInsets.symmetric(horizontal: 8), + borderRadius: BorderRadius.circular(4), + items: [ + DropdownMenuItem( + value: none(), + child: Text(t.logs.allLevelsFilter), + ), + ...LogLevel.choices.map( + (e) => DropdownMenuItem( + value: some(e), + child: Text(e.name), ), + ), ], ), - Text( - log.message, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + ], + ), ), ), - if (index != 0) - const Divider( - indent: 16, - endIndent: 16, - height: 4, - ), - ], - ); - }, + ), + ], + ), ), - ), - AsyncError(:final error) => CustomScrollView( - slivers: [ - SliverErrorBodyPlaceholder(t.presentShortError(error)), - ], - ), - _ => const CustomScrollView( - slivers: [ - SliverLoadingBodyPlaceholder(), - ], - ), - }, + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + reverse: true, + slivers: [ + switch (state.logs) { + AsyncData(value: final logs) => SliverList.builder( + itemCount: logs.length, + itemBuilder: (context, index) { + final log = logs[index]; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (log.level != null) + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + log.level!.name.toUpperCase(), + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + color: log.level!.color), + ), + if (log.time != null) + Text( + log.time!.toString(), + style: Theme.of(context) + .textTheme + .labelSmall, + ), + ], + ), + Text( + log.message, + style: + Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + if (index != 0) + const Divider( + indent: 16, + endIndent: 16, + height: 4, + ), + ], + ); + }, + ), + AsyncError(:final error) => SliverErrorBodyPlaceholder( + t.presentShortError(error), + ), + _ => const SliverLoadingBodyPlaceholder(), + }, + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + ], + ); + }, + ), + ), ); } } diff --git a/lib/features/proxies/view/proxies_page.dart b/lib/features/proxies/view/proxies_page.dart index b42fc05a..c5952064 100644 --- a/lib/features/proxies/view/proxies_page.dart +++ b/lib/features/proxies/view/proxies_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/proxies/notifier/notifier.dart'; import 'package:hiddify/features/proxies/widgets/widgets.dart'; import 'package:hiddify/utils/utils.dart'; @@ -29,7 +29,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger { return Scaffold( body: CustomScrollView( slivers: [ - NestedTabAppBar( + NestedAppBar( title: Text(t.proxies.pageTitle), ), SliverFillRemaining( @@ -50,7 +50,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger { return Scaffold( body: CustomScrollView( slivers: [ - NestedTabAppBar( + NestedAppBar( title: Text(t.proxies.pageTitle), actions: [ PopupMenuButton( @@ -140,7 +140,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger { return Scaffold( body: CustomScrollView( slivers: [ - NestedTabAppBar( + NestedAppBar( title: Text(t.proxies.pageTitle), ), SliverErrorBodyPlaceholder( @@ -155,7 +155,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger { return Scaffold( body: CustomScrollView( slivers: [ - NestedTabAppBar( + NestedAppBar( title: Text(t.proxies.pageTitle), ), const SliverLoadingBodyPlaceholder(), diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 21290819..164641fe 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/settings/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,18 +13,22 @@ class SettingsPage extends HookConsumerWidget { final t = ref.watch(translationsProvider); return Scaffold( - appBar: AppBar( - title: Text(t.settings.pageTitle), - ), - body: ListView( - children: [ - SettingsSection(t.settings.general.sectionTitle), - const GeneralSettingTiles(), - const PlatformSettingsTiles(), - const SettingsDivider(), - SettingsSection(t.settings.advanced.sectionTitle), - const AdvancedSettingTiles(), - const Gap(16), + body: CustomScrollView( + slivers: [ + NestedAppBar( + title: Text(t.settings.pageTitle), + ), + SliverList.list( + children: [ + SettingsSection(t.settings.general.sectionTitle), + const GeneralSettingTiles(), + const PlatformSettingsTiles(), + const SettingsDivider(), + SettingsSection(t.settings.advanced.sectionTitle), + const AdvancedSettingTiles(), + const Gap(16), + ], + ), ], ), ); diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index c2cd9fdd..0f1ed75d 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/common/common.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/wrapper/view/desktop_wrapper.dart b/lib/features/wrapper/view/desktop_wrapper.dart deleted file mode 100644 index 4556a76c..00000000 --- a/lib/features/wrapper/view/desktop_wrapper.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/stats/stats_overview.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class DesktopWrapper extends HookConsumerWidget { - const DesktopWrapper(this.navigator, {super.key}); - - final Widget navigator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = ref.watch(translationsProvider); - - final currentIndex = getCurrentIndex(context); - - final destinations = [ - NavigationRailDestination( - icon: const Icon(Icons.power_settings_new), - label: Text(t.home.pageTitle), - ), - NavigationRailDestination( - icon: const Icon(Icons.filter_list), - label: Text(t.proxies.pageTitle), - ), - NavigationRailDestination( - icon: const Icon(Icons.article), - label: Text(t.logs.pageTitle), - ), - NavigationRailDestination( - icon: const Icon(Icons.settings), - label: Text(t.settings.pageTitle), - ), - NavigationRailDestination( - icon: const Icon(Icons.info), - label: Text(t.about.pageTitle), - ), - ]; - - return Scaffold( - body: Row( - children: [ - SizedBox( - width: 192, - child: NavigationRail( - extended: true, - minExtendedWidth: 192, - destinations: destinations, - selectedIndex: currentIndex, - onDestinationSelected: (index) => switchTab(index, context), - trailing: const Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: StatsOverview(), - ), - ), - ), - ), - Expanded(child: navigator), - ], - ), - ); - } -} diff --git a/lib/features/wrapper/view/mobile_wrapper.dart b/lib/features/wrapper/view/mobile_wrapper.dart deleted file mode 100644 index e2d873ce..00000000 --- a/lib/features/wrapper/view/mobile_wrapper.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/common.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class MobileWrapper extends HookConsumerWidget { - const MobileWrapper(this.navigator, {super.key}); - - final Widget navigator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = ref.watch(translationsProvider); - - final currentIndex = getCurrentIndex(context); - final location = GoRouterState.of(context).uri.path; - - return Scaffold( - key: RootScaffold.stateKey, - body: navigator, - drawer: SafeArea( - child: Drawer( - width: (MediaQuery.of(context).size.width * 0.88).clamp(0, 304), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Gap(16), - DrawerTile( - label: t.settings.pageTitle, - icon: Icons.settings, - selected: location == SettingsRoute.path, - onSelect: () => const SettingsRoute().push(context), - ), - DrawerTile( - label: t.logs.pageTitle, - icon: Icons.article, - selected: location == LogsRoute.path, - onSelect: () => const LogsRoute().push(context), - ), - DrawerTile( - label: t.about.pageTitle, - icon: Icons.info, - selected: location == AboutRoute.path, - onSelect: () => const AboutRoute().push(context), - ), - const Gap(16), - ], - ), - ), - ), - bottomNavigationBar: NavigationBar( - destinations: [ - NavigationDestination( - icon: const Icon(Icons.power_settings_new), - label: t.home.pageTitle, - ), - NavigationDestination( - icon: const Icon(Icons.filter_list), - label: t.proxies.pageTitle, - ), - ], - selectedIndex: currentIndex > 1 ? 0 : currentIndex, - onDestinationSelected: (index) => switchTab(index, context), - ), - ); - } -} - -class DrawerTile extends StatelessWidget { - const DrawerTile({ - super.key, - required this.label, - required this.icon, - required this.selected, - required this.onSelect, - }); - - final String label; - final IconData icon; - final bool selected; - final VoidCallback onSelect; - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(label), - leading: Icon(icon), - selected: selected, - onTap: selected ? () {} : onSelect, - ); - } -} diff --git a/lib/features/wrapper/wrapper.dart b/lib/features/wrapper/wrapper.dart deleted file mode 100644 index 867e049a..00000000 --- a/lib/features/wrapper/wrapper.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'view/desktop_wrapper.dart'; -export 'view/mobile_wrapper.dart';