import 'dart:async'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/permission.dart'; import 'package:fl_clash/common/system_dns.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/animated_visibility.dart'; import 'package:fl_clash/widgets/icon.dart'; import 'package:fl_clash/widgets/sidebar.dart'; import 'package:flutter/foundation.dart'; import 'package:material_ui/material_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class AppStateManager extends ConsumerStatefulWidget { final Widget child; const AppStateManager({super.key, required this.child}); @override ConsumerState createState() => _AppStateManagerState(); } class _AppStateManagerState extends ConsumerState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // A desktop build started to the tray mounts while hidden, and a paused // render never runs a post-frame callback. scheduleMicrotask(() { if (mounted) { _updateVisible(WidgetsBinding.instance.lifecycleState); } }); ref.listenManual(configProvider, (prev, next) { if (prev != next) { ref.read(storeActionProvider.notifier).savePreferencesDebounce(); } }); ref.listenManual(groupsProvider, (prev, next) { if (prev != next) { unawaited(precacheTargetIcons(next.map((group) => group.icon))); } }, fireImmediately: true); ref.listenManual(needUpdateGroupsProvider, (prev, next) { if (prev != next) { ref.read(proxiesActionProvider.notifier).updateGroupsDebounce(); } }); ref.listenManual(suspendProvider, (prev, next) { final isStart = ref.read(isStartProvider); if (prev != next && isStart && !ref.read(safeModeProvider)) { debouncer.call(FunctionTag.suspend, () async { final core = ref.read(coreHandlerProvider); if (next == true) { await core.stopListener(); } else { await core.startListener(); } }); } }); final systemDns = systemDnsCoordinator; if (systemDns != null) { ref.listenManual(shouldPatchSystemDnsProvider, (prev, next) { unawaited(systemDns.sync(next)); }, fireImmediately: true); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } void _updateVisible(AppLifecycleState? state) { ref.read(appVisibleProvider.notifier).value = switch (state) { null || AppLifecycleState.resumed || AppLifecycleState.inactive => true, AppLifecycleState.hidden || AppLifecycleState.paused || AppLifecycleState.detached => false, }; } @override Future didChangeAppLifecycleState(AppLifecycleState state) async { commonPrint.log('$state'); _updateVisible(state); if (state == AppLifecycleState.resumed) { permissions.check(ref.read); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } ref.read(routeTrackerProvider.notifier).markResumed(); }); } } @override void didChangePlatformBrightness() { ref.read(themeActionProvider.notifier).updateBrightness(); } @override Widget build(BuildContext context) { return widget.child; } } class AppEnvManager extends ConsumerWidget { final Widget child; const AppEnvManager({super.key, required this.child}); String? _bannerMessage({required bool safeMode}) { if (safeMode) { return 'SAFE MODE'; } if (!globalState.isPre) { return null; } return kDebugMode ? 'DEBUG' : globalState.appEnv.toUpperCase(); } @override Widget build(BuildContext context, WidgetRef ref) { final message = _bannerMessage(safeMode: ref.watch(safeModeProvider)); if (message == null) { return child; } return Banner( message: message, location: BannerLocation.topEnd, child: child, ); } } class _Sidebar extends ConsumerWidget { const _Sidebar({ required this.items, required this.currentIndex, required this.viewMode, required this.onSelected, }); final List items; final int currentIndex; final ViewMode viewMode; final void Function(int index) onSelected; @override Widget build(BuildContext context, WidgetRef ref) { final canExpand = canExpandSidebar(viewMode); final sidebarExpanded = ref.watch( appSettingProvider.select((state) => state.sidebarExpanded), ); final version = ref.watch(versionProvider); return NavigationSidebar( destinations: [ for (final item in items) SidebarDestination(glyph: item.glyph, label: item.label.label), ], selectedIndex: currentIndex, expanded: canExpand && sidebarExpanded, onSelected: onSelected, onToggle: canExpand ? () { ref .read(appSettingProvider.notifier) .update( (state) => state.copyWith(sidebarExpanded: !state.sidebarExpanded), ); } : null, windowControls: windowControlsOverSidebar( isMacOS: system.isMacOS, version: version, ), ); } } class AppSidebarContainer extends ConsumerWidget { final Widget child; const AppSidebarContainer({super.key, required this.child}); Widget _buildBackground({ required Color color, required Color edge, required Widget child, }) { return Material( color: color, shape: BorderDirectional(end: BorderSide(color: edge)), child: child, ); } void _updateSideBarWidth(WidgetRef ref, double contentWidth) { WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(sideWidthProvider.notifier).value = ref.read(viewSizeProvider.select((state) => state.width)) - contentWidth; }); } void _handleToPage(WidgetRef ref, PageLabel pageLabel) { final focusNode = FocusManager.instance.primaryFocus; final focusContext = focusNode?.context; final preserveNavigationFocus = focusContext?.findAncestorWidgetOfExactType() != null; ref.read(currentPageLabelProvider.notifier).toPage(pageLabel); if (!preserveNavigationFocus || focusNode == null) { return; } WidgetsBinding.instance.addPostFrameCallback((_) { if (focusNode.context != null && focusNode.canRequestFocus) { focusNode.requestFocus(); } }); } @override Widget build(BuildContext context, WidgetRef ref) { final navigationState = ref.watch(navigationStateProvider); final navigationItems = navigationState.navigationItems; final viewMode = navigationState.viewMode; final currentIndex = navigationState.currentIndex; final blur = ref.watch(windowBlurProvider); final colorScheme = context.colorScheme; final surfaceContainer = colorScheme.surfaceContainer; void onSelected(int index) { _handleToPage(ref, navigationItems[index].label); } final hasSidebar = viewMode != ViewMode.mobile; final isLtr = Directionality.of(context) == TextDirection.ltr; return Container( color: blur ? Colors.transparent : surfaceContainer, child: Row( children: [ AnimatedVisibility.sidebar( visible: hasSidebar, child: _buildBackground( color: blur ? surfaceContainer.withValues(alpha: kSidebarBlurOpacity) : surfaceContainer, edge: colorScheme.outlineVariant, child: MediaQuery.removePadding( context: context, removeLeft: !isLtr, removeRight: isLtr, child: _Sidebar( items: navigationItems, currentIndex: currentIndex, viewMode: viewMode, onSelected: onSelected, ), ), ), ), Expanded( flex: 1, child: ClipRect( child: LayoutBuilder( builder: (_, constraints) { _updateSideBarWidth(ref, constraints.maxWidth); return MediaQuery.removePadding( context: context, removeLeft: hasSidebar && isLtr, removeRight: hasSidebar && !isLtr, child: child, ); }, ), ), ), ], ), ); } }