1e97716636
plugins/window replaces the window_manager fork on every desktop. Changelog: Tray hotkeys, node delays and a delay test
293 lines
8.7 KiB
Dart
293 lines
8.7 KiB
Dart
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<AppStateManager> createState() => _AppStateManagerState();
|
|
}
|
|
|
|
class _AppStateManagerState extends ConsumerState<AppStateManager>
|
|
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<void> 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<NavigationItem> 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<NavigationSidebar>() !=
|
|
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,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|