Add android shortcuts

Fix init params issues

Fix dynamic color issues

Optimize navigator animate

Optimize window init

Optimize fab

Optimize save
This commit is contained in:
chen08209
2024-10-27 16:59:23 +08:00
parent c94f64cf78
commit 3a43dc2fe9
52 changed files with 612 additions and 460 deletions

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -58,10 +60,18 @@ class ApplicationState extends State<Application> {
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
},
);
@@ -93,6 +103,7 @@ class ApplicationState extends State<Application> {
}
await globalState.appController.init();
globalState.appController.initLink();
app?.initShortcuts();
});
}

View File

@@ -29,4 +29,5 @@ export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'network.dart';
export 'network.dart';
export 'navigator.dart';

11
lib/common/navigator.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
return await Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) => child,
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
@@ -101,14 +102,13 @@ class Other {
}
String getTrayIconPath({
required bool isStart,
required Brightness brightness,
}) {
if(Platform.isMacOS){
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";
if (isStart && Platform.isWindows) {
if (Platform.isWindows) {
return "assets/images/icon.$suffix";
}
return switch (brightness) {
@@ -188,10 +188,8 @@ class Other {
return parameters[fileNameKey];
}
double getViewWidth() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
final size = view.physicalSize / view.devicePixelRatio;
return size.width;
FlutterView getScreen() {
return WidgetsBinding.instance.platformDispatcher.views.first;
}
List<String> parseReleaseBody(String? body) {

View File

@@ -1,5 +1,7 @@
import 'dart:io';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
@@ -21,14 +23,7 @@ class Window {
size: Size(props.width, props.height),
minimumSize: const Size(380, 500),
);
if (props.left != null || props.top != null) {
await windowManager.setPosition(
Offset(props.left ?? 0, props.top ?? 0),
);
} else {
await windowManager.setAlignment(Alignment.center);
}
if(!Platform.isMacOS || version > 10){
if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
await windowManager.waitUntilReadyToShow(windowOptions, () async {

View File

@@ -29,6 +29,7 @@ class AppController {
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -38,6 +39,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
savePreferencesDebounce = debounce<Function()>(() async {
await savePreferences();
});
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
@@ -51,10 +55,7 @@ class AppController {
updateStatus(bool isStart) async {
if (isStart) {
await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
await globalState.handleStart();
updateRunTime();
updateTraffic();
globalState.updateFunctionLists = [
@@ -202,17 +203,8 @@ class AppController {
}
savePreferences() async {
await saveConfigPreferences();
await saveClashConfigPreferences();
}
saveConfigPreferences() async {
debugPrint("saveConfigPreferences");
debugPrint("[APP] savePreferences");
await preferences.saveConfig(config);
}
saveClashConfigPreferences() async {
debugPrint("saveClashConfigPreferences");
await preferences.saveClashConfig(clashConfig);
}
@@ -231,7 +223,7 @@ class AppController {
handleBackOrExit() async {
if (config.appSetting.minimizeOnExit) {
if (system.isDesktop) {
await savePreferences();
await savePreferencesDebounce();
}
await system.back();
} else {
@@ -608,7 +600,6 @@ class AppController {
}
Future _updateSystemTray({
required bool isStart,
required Brightness? brightness,
bool force = false,
}) async {
@@ -617,7 +608,6 @@ class AppController {
}
await trayManager.setIcon(
other.getTrayIconPath(
isStart: isStart,
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
@@ -633,7 +623,6 @@ class AppController {
updateTray([bool focus = false]) async {
if (!Platform.isLinux) {
await _updateSystemTray(
isStart: appFlowingState.isStart,
brightness: appState.brightness,
force: focus,
);
@@ -697,15 +686,18 @@ class AppController {
},
checked: config.appSetting.autoLaunch,
);
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: config.appSetting.adminAutoLaunch,
);
menuItems.add(autoStartMenuItem);
menuItems.add(adminAutoStartMenuItem);
if(Platform.isWindows){
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: config.appSetting.adminAutoLaunch,
);
menuItems.add(adminAutoStartMenuItem);
}
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
@@ -718,7 +710,6 @@ class AppController {
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
isStart: appFlowingState.isStart,
brightness: appState.brightness,
force: focus,
);

View File

@@ -21,12 +21,25 @@ class DashboardFragment extends StatefulWidget {
}
class _DashboardFragmentState extends State<DashboardFragment> {
_initFab(bool isCurrent) {
if(!isCurrent){
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = const StartButton();
});
}
@override
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: const FloatWrapper(
child: StartButton(),
),
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initFab(isCurrent);
return child!;
},
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(

View File

@@ -19,9 +19,10 @@ class _StartButtonState extends State<StartButton>
@override
void initState() {
super.initState();
isStart = globalState.appController.appFlowingState.isStart;
_controller = AnimationController(
vsync: this,
value: 0,
value: isStart ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
}
@@ -85,58 +86,58 @@ class _StartButtonState extends State<StartButton>
)
.width +
16;
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
return _updateControllerContainer(
AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
),
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
),
),
),
],
],
),
),
),
);
},
child: child,
);
},
child: child,
),
);
},
child: _updateControllerContainer(
Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
),
child: Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
),
);
}

View File

@@ -7,7 +7,10 @@ import 'package:flutter/material.dart';
class AddProfile extends StatelessWidget {
final BuildContext context;
const AddProfile({super.key, required this.context,});
const AddProfile({
super.key,
required this.context,
});
_handleAddProfileFormFile() async {
globalState.appController.addProfileFormFile();
@@ -18,14 +21,16 @@ class AddProfile extends StatelessWidget {
}
_toScan() async {
if(system.isDesktop){
if (system.isDesktop) {
globalState.appController.addProfileFormQrCode();
return;
}
final url = await Navigator.of(context)
.push<String>(MaterialPageRoute(builder: (_) => const ScanPage()));
final url = await BaseNavigator.push(
context,
const ScanPage(),
);
if (url != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleAddProfileFormURL(url);
});
}
@@ -44,12 +49,12 @@ class AddProfile extends StatelessWidget {
Widget build(context) {
return ListView(
children: [
ListItem(
leading: const Icon(Icons.qr_code),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.qr_code),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.upload_file),
title: Text(appLocalizations.file),

View File

@@ -80,7 +80,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
_initScaffoldState() {
_initScaffold() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
@@ -112,71 +112,67 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
iconSize: 26,
),
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
},
);
}
@override
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: FloatWrapper(
child: FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
return ActiveBuilder(
label: "profiles",
builder: (isCurrent,child){
if(isCurrent){
_initScaffold();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
),
),
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
},
),
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
);
},
),
);
}

View File

@@ -191,7 +191,7 @@ class ProxiesSetting extends StatelessWidget {
_buildGroupStyleSetting() {
return generateSection(
title: "图标样式",
title: appLocalizations.iconStyle,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),

View File

@@ -278,6 +278,23 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
);
}
initFab(bool isCurrent, List<Proxy> proxies) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest(
proxies,
);
},
);
});
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
@@ -303,11 +320,11 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
proxies,
);
_lastProxies = sortedProxies;
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
return ActiveBuilder(
label: "proxies",
builder: (isCurrent, child) {
initFab(isCurrent, proxies);
return child!;
},
child: Align(
alignment: Alignment.topCenter,
@@ -344,22 +361,19 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
class DelayTestButton extends StatefulWidget {
final Future Function() onClick;
const DelayTestButtonContainer({
const DelayTestButton({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
State<DelayTestButton> createState() => _DelayTestButtonState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
class _DelayTestButtonState extends State<DelayTestButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
@@ -401,29 +415,23 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
child: widget.child,
);
}
}

View File

@@ -163,7 +163,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,

View File

@@ -322,5 +322,6 @@
"adminAutoLaunch": "Admin auto launch",
"adminAutoLaunchDesc": "Boot up by using admin mode",
"fontFamily": "FontFamily",
"systemFont": "System font"
"systemFont": "System font",
"toggle": "Toggle"
}

View File

@@ -322,5 +322,6 @@
"adminAutoLaunch": "管理员自启动",
"adminAutoLaunchDesc": "使用管理员模式开机自启动",
"fontFamily": "字体",
"systemFont": "系统字体"
"systemFont": "系统字体",
"toggle": "切换"
}

View File

@@ -447,6 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"toggle": MessageLookupByLibrary.simpleMessage("Toggle"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),

View File

@@ -359,6 +359,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),

View File

@@ -3289,6 +3289,16 @@ class AppLocalizations {
args: [],
);
}
/// `Toggle`
String get toggle {
return Intl.message(
'Toggle',
name: 'toggle',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -53,6 +53,10 @@ Future<void> vpnService() async {
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final appState = AppState(
mode: clashConfig.mode,
selectedMap: config.currentSelectedMap,
@@ -98,15 +102,8 @@ Future<void> vpnService() async {
},
),
);
final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
await app?.tip(appLocalizations.startVpn);
await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -18,7 +20,6 @@ class AppStateManager extends StatefulWidget {
class _AppStateManagerState extends State<AppStateManager>
with WidgetsBindingObserver {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
@@ -45,6 +46,22 @@ class _AppStateManagerState extends State<AppStateManager>
);
}
_cacheStateChange(Widget child) {
return Selector2<Config, ClashConfig, String>(
selector: (_, config, clashConfig) => "$clashConfig $config",
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.savePreferencesDebounce();
}
return prev != next;
},
builder: (context, state, child) {
return child!;
},
child: child,
);
}
@override
void initState() {
super.initState();
@@ -61,7 +78,7 @@ class _AppStateManagerState extends State<AppStateManager>
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
final isPaused = state == AppLifecycleState.paused;
if (isPaused) {
await globalState.appController.savePreferences();
globalState.appController.savePreferencesDebounce();
}
}
@@ -73,8 +90,10 @@ class _AppStateManagerState extends State<AppStateManager>
@override
Widget build(BuildContext context) {
return _updateNavigationsContainer(
widget.child,
return _cacheStateChange(
_updateNavigationsContainer(
widget.child,
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -29,7 +30,7 @@ class _TileContainerState extends State<TileManager> with TileListener {
}
@override
void onStop() {
Future<void> onStop() async {
globalState.appController.updateStatus(false);
super.onStop();
}

View File

@@ -1,14 +1,9 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_ext/window_ext.dart';
class TrayManager extends StatefulWidget {
final Widget child;

View File

@@ -20,7 +20,8 @@ class WindowManager extends StatefulWidget {
State<WindowManager> createState() => _WindowContainerState();
}
class _WindowContainerState extends State<WindowManager> with WindowListener, WindowExtListener {
class _WindowContainerState extends State<WindowManager>
with WindowListener, WindowExtListener {
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) {
@@ -82,7 +83,7 @@ class _WindowContainerState extends State<WindowManager> with WindowListener, Wi
@override
void onWindowMinimize() async {
await globalState.appController.savePreferences();
globalState.appController.savePreferencesDebounce();
super.onWindowMinimize();
}

View File

@@ -371,4 +371,9 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> json) {
return _$ClashConfigFromJson(json);
}
@override
String toString() {
return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _ipv6: $_ipv6, _geodataLoader: $_geodataLoader, _logLevel: $_logLevel, _externalController: $_externalController, _mode: $_mode, _findProcessMode: $_findProcessMode, _keepAliveInterval: $_keepAliveInterval, _unifiedDelay: $_unifiedDelay, _tcpConcurrent: $_tcpConcurrent, _tun: $_tun, _dns: $_dns, _geoXUrl: $_geoXUrl, _rules: $_rules, _globalRealUa: $_globalRealUa, _hosts: $_hosts}';
}
}

View File

@@ -431,7 +431,6 @@ class HotKeyAction with _$HotKeyAction {
_$HotKeyActionFromJson(json);
}
typedef Validator = String? Function(String? value);
@freezed
@@ -441,4 +440,4 @@ class Field with _$Field {
required String value,
Validator? validator,
}) = _Field;
}
}

View File

@@ -10,7 +10,9 @@ part 'generated/config.g.dart';
part 'generated/config.freezed.dart';
const defaultAppSetting = AppSetting();
final defaultAppSetting = const AppSetting().copyWith(
isAnimateToPage: system.isDesktop ? false : true,
);
@freezed
class AppSetting with _$AppSetting {
@@ -36,10 +38,11 @@ class AppSetting with _$AppSetting {
_$AppSettingFromJson(json);
factory AppSetting.realFromJson(Map<String, Object?>? json) {
final appSetting =
json == null ? defaultAppSetting : AppSetting.fromJson(json);
final appSetting = json == null
? defaultAppSetting
: AppSetting.fromJson(json);
return appSetting.copyWith(
isAnimateToPage: system.isDesktop ? false : true,
isAnimateToPage: system.isDesktop ? false : appSetting.isAnimateToPage,
);
}
}
@@ -68,7 +71,7 @@ extension AccessControlExt on AccessControl {
@freezed
class WindowProps with _$WindowProps {
const factory WindowProps({
@Default(1000) double width,
@Default(900) double width,
@Default(600) double height,
double? top,
double? left,
@@ -141,37 +144,39 @@ class ProxiesStyle with _$ProxiesStyle {
json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json);
}
const defaultThemeProps = ThemeProps();
final defaultThemeProps = Platform.isWindows
? const ThemeProps().copyWith(
fontFamily: FontFamily.miSans,
primaryColor: defaultPrimaryColor.value,
)
: const ThemeProps().copyWith(
primaryColor: defaultPrimaryColor.value,
);
@freezed
class ThemeProps with _$ThemeProps {
const factory ThemeProps({
@Default(0xFF795548) int? primaryColor,
int? primaryColor,
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(false) bool prueBlack,
@Default(FontFamily.system) FontFamily fontFamily,
}) = _ThemeProps;
factory ThemeProps.fromJson(Map<String, Object?> json) => _$ThemePropsFromJson(json);
factory ThemeProps.fromJson(Map<String, Object?> json) =>
_$ThemePropsFromJson(json);
factory ThemeProps.realFromJson(Map<String, Object?>? json) {
if (json == null) {
return Platform.isWindows
? defaultThemeProps.copyWith(fontFamily: FontFamily.miSans)
: defaultThemeProps;
return defaultThemeProps;
}
try {
return ThemeProps.fromJson(json);
} catch (_) {
return Platform.isWindows
? defaultThemeProps.copyWith(fontFamily: FontFamily.miSans)
: defaultThemeProps;
return defaultThemeProps;
}
}
}
const defaultCustomFontSizeScale = 1.0;
@JsonSerializable()
class Config extends ChangeNotifier {
AppSetting _appSetting;
@@ -479,4 +484,9 @@ class Config extends ChangeNotifier {
factory Config.fromJson(Map<String, dynamic> json) {
return _$ConfigFromJson(json);
}
@override
String toString() {
return 'Config{_appSetting: $_appSetting, _profiles: $_profiles, _currentProfileId: $_currentProfileId, _isAccessControl: $_isAccessControl, _accessControl: $_accessControl, _dav: $_dav, _windowProps: $_windowProps, _themeProps: $_themeProps, _vpnProps: $_vpnProps, _desktopProps: $_desktopProps, _overrideDns: $_overrideDns, _hotKeyActions: $_hotKeyActions, _proxiesStyle: $_proxiesStyle}';
}
}

View File

@@ -840,7 +840,7 @@ class __$$WindowPropsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$WindowPropsImpl implements _WindowProps {
const _$WindowPropsImpl(
{this.width = 1000, this.height = 600, this.top, this.left});
{this.width = 900, this.height = 600, this.top, this.left});
factory _$WindowPropsImpl.fromJson(Map<String, dynamic> json) =>
_$$WindowPropsImplFromJson(json);
@@ -1667,7 +1667,7 @@ class __$$ThemePropsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$ThemePropsImpl implements _ThemeProps {
const _$ThemePropsImpl(
{this.primaryColor = 0xFF795548,
{this.primaryColor,
this.themeMode = ThemeMode.system,
this.prueBlack = false,
this.fontFamily = FontFamily.system});
@@ -1676,7 +1676,6 @@ class _$ThemePropsImpl implements _ThemeProps {
_$$ThemePropsImplFromJson(json);
@override
@JsonKey()
final int? primaryColor;
@override
@JsonKey()

View File

@@ -128,7 +128,7 @@ const _$AccessSortTypeEnumMap = {
_$WindowPropsImpl _$$WindowPropsImplFromJson(Map<String, dynamic> json) =>
_$WindowPropsImpl(
width: (json['width'] as num?)?.toDouble() ?? 1000,
width: (json['width'] as num?)?.toDouble() ?? 900,
height: (json['height'] as num?)?.toDouble() ?? 600,
top: (json['top'] as num?)?.toDouble(),
left: (json['left'] as num?)?.toDouble(),
@@ -234,7 +234,7 @@ const _$ProxyCardTypeEnumMap = {
_$ThemePropsImpl _$$ThemePropsImplFromJson(Map<String, dynamic> json) =>
_$ThemePropsImpl(
primaryColor: (json['primaryColor'] as num?)?.toInt() ?? 0xFF795548,
primaryColor: (json['primaryColor'] as num?)?.toInt(),
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system,
prueBlack: json['prueBlack'] as bool? ?? false,

View File

@@ -3,9 +3,11 @@ import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
class App {
static App? _instance;
@@ -20,6 +22,12 @@ class App {
if (onExit != null) {
await onExit!();
}
case "getText":
try {
return Intl.message(call.arguments as String);
} catch (_) {
return "";
}
default:
throw MissingPluginException();
}
@@ -78,6 +86,13 @@ class App {
});
}
Future<bool?> initShortcuts() async {
return await methodChannel.invokeMethod<bool>(
"initShortcuts",
appLocalizations.toggle,
);
}
Future<bool?> updateExcludeFromRecents(bool value) async {
return await methodChannel.invokeMethod<bool>("updateExcludeFromRecents", {
"value": value,

View File

@@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
class FadePage<T> extends Page<T> {
final Widget child;
final bool maintainState;
final bool fullscreenDialog;
final bool allowSnapshotting;
const FadePage({
required this.child,
this.maintainState = true,
this.fullscreenDialog = false,
this.allowSnapshotting = true,
super.key,
super.name,
super.arguments,
super.restorationId,
});
@override
Route<T> createRoute(BuildContext context) {
return FadePageRoute<T>(page: this);
}
}
class FadePageRoute<T> extends PageRoute<T> {
final FadePage page;
FadePageRoute({
required this.page,
}) : super(settings: page);
FadePage<T> get _page => settings as FadePage<T>;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return _page.child;
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: child,
);
}
@override
Duration get transitionDuration => const Duration(milliseconds: 600);
@override
bool get maintainState => false;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
}

View File

@@ -70,10 +70,7 @@ class GlobalState {
appState.versionInfo = clashCore.getVersionInfo();
}
handleStart({
required Config config,
required ClashConfig clashConfig,
}) async {
handleStart() async {
clashCore.start();
if (globalState.isVpnService) {
await vpn?.startVpn();
@@ -81,8 +78,6 @@ class GlobalState {
return;
}
startTime ??= DateTime.now();
await preferences.saveClashConfig(clashConfig);
await preferences.saveConfig(config);
await service?.init();
startListenUpdate();
}

View File

@@ -68,6 +68,8 @@ class ProxiesActionsBuilder extends StatelessWidget {
typedef StateWidgetBuilder<T> = Widget Function(T state);
typedef StateAndChildWidgetBuilder<T> = Widget Function(T state, Widget? child);
class LocaleBuilder extends StatelessWidget {
final StateWidgetBuilder<String?> builder;
@@ -86,3 +88,30 @@ class LocaleBuilder extends StatelessWidget {
);
}
}
class ActiveBuilder extends StatelessWidget {
final String label;
final StateAndChildWidgetBuilder<bool> builder;
final Widget? child;
const ActiveBuilder({
super.key,
required this.label,
required this.builder,
required this.child,
});
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == label,
builder: (_, state, child) {
return builder(
state,
child,
);
},
child: child,
);
}
}

View File

@@ -359,15 +359,12 @@ class ListItem<T> extends StatelessWidget {
);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CommonScaffold(
key: Key(nextDelegate.title),
body: nextDelegate.widget,
title: nextDelegate.title,
),
),
);
BaseNavigator.push(context, CommonScaffold(
key: Key(nextDelegate.title),
body: nextDelegate.widget,
title: nextDelegate.title,
));
},
);
}

View File

@@ -50,6 +50,7 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<dynamic> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) {
@@ -58,6 +59,12 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget floatingActionButton) {
if (_floatingActionButton.value != floatingActionButton) {
_floatingActionButton.value = floatingActionButton;
}
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
@@ -82,6 +89,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
void dispose() {
_actions.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@@ -90,6 +98,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -99,60 +108,66 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
Widget build(BuildContext context) {
final scaffold = Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ValueListenableBuilder<List<Widget>>(
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
final scaffold = ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, value, __) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ValueListenableBuilder<List<Widget>>(
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
return AppBar(
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarIconBrightness:
systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent,
),
automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: [
...?realActions,
const SizedBox(
width: 8,
)
],
);
},
systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent,
),
automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: [
...?realActions,
const SizedBox(
width: 8,
)
],
);
},
),
ValueListenableBuilder(
valueListenable: _loading,
builder: (_, value, __) {
return value == true
? const LinearProgressIndicator()
: Container();
},
),
],
),
ValueListenableBuilder(
valueListenable: _loading,
builder: (_, value, __) {
return value == true
? const LinearProgressIndicator()
: Container();
},
),
],
),
),
body: body,
bottomNavigationBar: widget.bottomNavigationBar,
),
body: body,
floatingActionButton: value,
bottomNavigationBar: widget.bottomNavigationBar,
);
},
);
return _sideNavigationBar != null
? Row(

View File

@@ -2,6 +2,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'side_sheet.dart';
@@ -23,12 +24,11 @@ showExtendPage(
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (isMobile) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CommonScaffold(
title: title,
body: uniqueBody,
),
BaseNavigator.push(
context,
CommonScaffold(
title: title,
body: uniqueBody,
),
);
return;