2024-04-30 23:38:49 +08:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:fl_clash/state.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
|
|
|
|
import 'clash/core.dart';
|
|
|
|
|
import 'enum/enum.dart';
|
|
|
|
|
import 'models/models.dart';
|
|
|
|
|
import 'common/common.dart';
|
|
|
|
|
|
|
|
|
|
class AppController {
|
|
|
|
|
final BuildContext context;
|
|
|
|
|
late AppState appState;
|
|
|
|
|
late Config config;
|
|
|
|
|
late ClashConfig clashConfig;
|
|
|
|
|
late Measure measure;
|
|
|
|
|
late Function updateClashConfigDebounce;
|
|
|
|
|
|
|
|
|
|
AppController(this.context) {
|
|
|
|
|
appState = context.read<AppState>();
|
|
|
|
|
config = context.read<Config>();
|
|
|
|
|
clashConfig = context.read<ClashConfig>();
|
|
|
|
|
updateClashConfigDebounce = debounce<Function()>(() async {
|
|
|
|
|
await updateClashConfig();
|
|
|
|
|
});
|
|
|
|
|
measure = Measure.of(context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> updateSystemProxy(bool isStart) async {
|
|
|
|
|
if (isStart) {
|
|
|
|
|
await globalState.startSystemProxy(
|
|
|
|
|
config: config,
|
|
|
|
|
clashConfig: clashConfig,
|
|
|
|
|
);
|
|
|
|
|
updateRunTime();
|
|
|
|
|
updateTraffic();
|
|
|
|
|
globalState.updateFunctionLists = [
|
|
|
|
|
updateRunTime,
|
|
|
|
|
updateTraffic,
|
|
|
|
|
];
|
2024-05-17 15:39:41 +08:00
|
|
|
clearShowProxyDelay();
|
|
|
|
|
testShowProxyDelay();
|
2024-04-30 23:38:49 +08:00
|
|
|
} else {
|
|
|
|
|
await globalState.stopSystemProxy();
|
|
|
|
|
appState.traffics = [];
|
|
|
|
|
appState.runTime = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateCoreVersionInfo() {
|
|
|
|
|
globalState.updateCoreVersionInfo(appState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateRunTime() {
|
|
|
|
|
if (proxyManager.startTime != null) {
|
|
|
|
|
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
|
2024-05-06 10:32:39 +08:00
|
|
|
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
2024-04-30 23:38:49 +08:00
|
|
|
appState.runTime = nowTimeStamp - startTimeStamp;
|
|
|
|
|
} else {
|
|
|
|
|
appState.runTime = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTraffic() {
|
|
|
|
|
globalState.updateTraffic(
|
|
|
|
|
config: config,
|
|
|
|
|
appState: appState,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changeProxy() {
|
2024-05-02 00:32:11 +08:00
|
|
|
globalState.changeProxy(
|
|
|
|
|
appState: appState,
|
|
|
|
|
config: config,
|
|
|
|
|
clashConfig: clashConfig,
|
2024-04-30 23:38:49 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addProfile(Profile profile) {
|
|
|
|
|
config.setProfile(profile);
|
|
|
|
|
if (config.currentProfileId != null) return;
|
|
|
|
|
changeProfile(profile.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deleteProfile(String id) async {
|
|
|
|
|
config.deleteProfileById(id);
|
|
|
|
|
final profilePath = await appPath.getProfilePath(id);
|
|
|
|
|
if (profilePath == null) return;
|
2024-05-10 10:11:27 +08:00
|
|
|
clashCore.clearEffect(profilePath);
|
2024-04-30 23:38:49 +08:00
|
|
|
if (config.currentProfileId == id) {
|
|
|
|
|
if (config.profiles.isNotEmpty) {
|
|
|
|
|
final updateId = config.profiles.first.id;
|
|
|
|
|
changeProfile(updateId);
|
|
|
|
|
} else {
|
|
|
|
|
changeProfile(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateProfile(String id) async {
|
|
|
|
|
final profile = config.getCurrentProfileForId(id);
|
|
|
|
|
if (profile != null) {
|
|
|
|
|
final res = await profile.update();
|
|
|
|
|
if (res.type == ResultType.success) {
|
|
|
|
|
config.setProfile(profile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-05 02:40:35 +08:00
|
|
|
Future<String> updateClashConfig({bool isPatch = true}) async {
|
2024-04-30 23:38:49 +08:00
|
|
|
return await globalState.updateClashConfig(
|
|
|
|
|
clashConfig: clashConfig,
|
|
|
|
|
config: config,
|
|
|
|
|
isPatch: isPatch,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-05 02:40:35 +08:00
|
|
|
applyProfile() async {
|
|
|
|
|
await globalState.applyProfile(
|
2024-05-04 16:39:21 +08:00
|
|
|
appState: appState,
|
|
|
|
|
config: config,
|
|
|
|
|
clashConfig: clashConfig,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 10:32:39 +08:00
|
|
|
Function? _changeProfileDebounce;
|
2024-05-04 21:51:40 +08:00
|
|
|
|
|
|
|
|
changeProfileDebounce(String? profileId) {
|
|
|
|
|
if (profileId == config.currentProfileId) return;
|
|
|
|
|
config.currentProfileId = profileId;
|
|
|
|
|
_changeProfileDebounce ??= debounce<Function(String?)>((profileId) async {
|
2024-05-05 02:40:35 +08:00
|
|
|
await applyProfile();
|
|
|
|
|
appState.delayMap = {};
|
|
|
|
|
saveConfigPreferences();
|
2024-05-04 21:51:40 +08:00
|
|
|
});
|
|
|
|
|
_changeProfileDebounce!([profileId]);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
changeProfile(String? value) async {
|
|
|
|
|
if (value == config.currentProfileId) return;
|
|
|
|
|
config.currentProfileId = value;
|
2024-05-04 16:39:21 +08:00
|
|
|
await applyProfile();
|
2024-04-30 23:38:49 +08:00
|
|
|
appState.delayMap = {};
|
|
|
|
|
saveConfigPreferences();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
autoUpdateProfiles() async {
|
|
|
|
|
for (final profile in config.profiles) {
|
|
|
|
|
if (!profile.autoUpdate) return;
|
|
|
|
|
final isNotNeedUpdate = profile.lastUpdateDate
|
|
|
|
|
?.add(
|
2024-05-06 10:32:39 +08:00
|
|
|
profile.autoUpdateDuration,
|
|
|
|
|
)
|
2024-04-30 23:38:49 +08:00
|
|
|
.isBeforeNow();
|
|
|
|
|
if (isNotNeedUpdate == false) continue;
|
2024-05-04 16:39:21 +08:00
|
|
|
await profile.update();
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-03 14:31:10 +08:00
|
|
|
Future<void> updateGroups() async {
|
|
|
|
|
await globalState.updateGroups(appState);
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
|
|
|
|
|
appState.systemColorSchemes = systemColorSchemes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
savePreferences() async {
|
|
|
|
|
await saveConfigPreferences();
|
|
|
|
|
await saveClashConfigPreferences();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveConfigPreferences() async {
|
|
|
|
|
debugPrint("saveConfigPreferences");
|
|
|
|
|
await preferences.saveConfig(config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveClashConfigPreferences() async {
|
|
|
|
|
debugPrint("saveClashConfigPreferences");
|
|
|
|
|
await preferences.saveClashConfig(clashConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleBackOrExit() async {
|
|
|
|
|
if (config.isMinimizeOnExit) {
|
|
|
|
|
if (system.isDesktop) {
|
|
|
|
|
await savePreferences();
|
|
|
|
|
}
|
|
|
|
|
await system.back();
|
|
|
|
|
} else {
|
|
|
|
|
await handleExit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleExit() async {
|
|
|
|
|
await updateSystemProxy(false);
|
|
|
|
|
await savePreferences();
|
|
|
|
|
clashCore.shutdown();
|
|
|
|
|
system.exit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateLogStatus() {
|
|
|
|
|
if (config.openLogs) {
|
|
|
|
|
clashCore.startLog();
|
|
|
|
|
} else {
|
|
|
|
|
clashCore.stopLog();
|
|
|
|
|
appState.logs = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
afterInit() async {
|
2024-05-06 10:32:39 +08:00
|
|
|
if (config.autoRun) {
|
|
|
|
|
await updateSystemProxy(true);
|
|
|
|
|
} else {
|
|
|
|
|
await proxyManager.updateStartTime();
|
|
|
|
|
await updateSystemProxy(proxyManager.isStart);
|
|
|
|
|
}
|
|
|
|
|
autoUpdateProfiles();
|
|
|
|
|
updateLogStatus();
|
|
|
|
|
if (!config.silentLaunch) {
|
|
|
|
|
window?.show();
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 14:29:49 +08:00
|
|
|
healthcheck() {
|
2024-05-17 15:39:41 +08:00
|
|
|
if(globalState.healthcheckLock) return;
|
2024-05-06 14:29:49 +08:00
|
|
|
for (final delay in appState.delayMap.entries) {
|
|
|
|
|
setDelay(
|
|
|
|
|
Delay(
|
|
|
|
|
name: delay.key,
|
|
|
|
|
value: 0,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
clashCore.healthcheck();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
setDelay(Delay delay) {
|
|
|
|
|
appState.setDelay(delay);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-10 10:11:27 +08:00
|
|
|
updateDelayMap() async {
|
|
|
|
|
appState.delayMap = await clashCore.getDelayMap();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
toPage(int index, {bool hasAnimate = false}) {
|
|
|
|
|
final nextLabel = globalState.currentNavigationItems[index].label;
|
|
|
|
|
appState.currentLabel = nextLabel;
|
|
|
|
|
if ((config.isAnimateToPage || hasAnimate)) {
|
|
|
|
|
globalState.pageController?.animateToPage(
|
|
|
|
|
index,
|
|
|
|
|
duration: kTabScrollDuration,
|
|
|
|
|
curve: Curves.easeOut,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
globalState.pageController?.jumpToPage(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updatePackages() async {
|
|
|
|
|
await globalState.updatePackages(appState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toProfiles() {
|
|
|
|
|
final index = globalState.currentNavigationItems.indexWhere(
|
2024-05-06 10:32:39 +08:00
|
|
|
(element) => element.label == "profiles",
|
2024-04-30 23:38:49 +08:00
|
|
|
);
|
|
|
|
|
if (index != -1) {
|
|
|
|
|
toPage(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initLink() {
|
|
|
|
|
linkManager.initAppLinksListen(
|
2024-05-06 10:32:39 +08:00
|
|
|
(url) {
|
2024-04-30 23:38:49 +08:00
|
|
|
globalState.showMessage(
|
|
|
|
|
title: "${appLocalizations.add}${appLocalizations.profile}",
|
|
|
|
|
message: TextSpan(
|
|
|
|
|
children: [
|
|
|
|
|
TextSpan(text: appLocalizations.doYouWantToPass),
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: " $url ",
|
|
|
|
|
style: TextStyle(
|
2024-05-06 10:32:39 +08:00
|
|
|
color: Theme.of(context).colorScheme.primary,
|
2024-04-30 23:38:49 +08:00
|
|
|
decoration: TextDecoration.underline,
|
2024-05-06 10:32:39 +08:00
|
|
|
decorationColor: Theme.of(context).colorScheme.primary,
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
TextSpan(
|
|
|
|
|
text:
|
2024-05-06 10:32:39 +08:00
|
|
|
"${appLocalizations.create}${appLocalizations.profile}"),
|
2024-04-30 23:38:49 +08:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
onTab: () {
|
|
|
|
|
addProfileFormURL(url);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-11 17:02:34 +08:00
|
|
|
addProfileFormURL(String url) async {
|
|
|
|
|
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
|
|
|
|
toProfiles();
|
|
|
|
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
|
|
|
|
if (commonScaffoldState?.mounted != true) return;
|
|
|
|
|
commonScaffoldState?.loadingRun(
|
|
|
|
|
() async {
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
|
|
|
final profile = Profile(
|
|
|
|
|
url: url,
|
|
|
|
|
);
|
|
|
|
|
final res = await profile.update();
|
|
|
|
|
if (res.type == ResultType.success) {
|
|
|
|
|
addProfile(profile);
|
|
|
|
|
} else {
|
|
|
|
|
debugPrint(res.message);
|
|
|
|
|
globalState.showMessage(
|
|
|
|
|
title: "${appLocalizations.add}${appLocalizations.profile}",
|
|
|
|
|
message: TextSpan(text: res.message!),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
addProfileFormFile() async {
|
2024-05-11 17:02:34 +08:00
|
|
|
final result = await picker.pickerConfigFile();
|
2024-04-30 23:38:49 +08:00
|
|
|
if (result.type == ResultType.error) return;
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
|
|
|
|
toProfiles();
|
|
|
|
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
|
|
|
|
if (commonScaffoldState?.mounted != true) return;
|
|
|
|
|
commonScaffoldState?.loadingRun(
|
2024-05-06 10:32:39 +08:00
|
|
|
() async {
|
2024-04-30 23:38:49 +08:00
|
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
|
|
|
final bytes = result.data?.bytes;
|
|
|
|
|
if (bytes == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final profile = Profile(label: result.data?.name);
|
|
|
|
|
final sRes = await profile.saveFile(bytes);
|
|
|
|
|
if (sRes.type == ResultType.error) {
|
|
|
|
|
debugPrint(sRes.message);
|
|
|
|
|
globalState.showMessage(
|
|
|
|
|
title: "${appLocalizations.add}${appLocalizations.profile}",
|
|
|
|
|
message: TextSpan(text: sRes.message!),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
addProfile(profile);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-05-10 10:11:27 +08:00
|
|
|
|
2024-05-11 17:02:34 +08:00
|
|
|
addProfileFormQrCode() async {
|
|
|
|
|
final result = await picker.pickerConfigQRCode();
|
|
|
|
|
if (result.type == ResultType.error) {
|
|
|
|
|
if(result.message != null){
|
|
|
|
|
globalState.showMessage(
|
|
|
|
|
title: appLocalizations.tip,
|
|
|
|
|
message: TextSpan(
|
|
|
|
|
text: result.message,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
addProfileFormURL(result.data!);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-10 10:11:27 +08:00
|
|
|
clearShowProxyDelay() {
|
|
|
|
|
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
|
|
|
|
|
if (showProxyDelay != null) {
|
|
|
|
|
appState.setDelay(
|
|
|
|
|
Delay(name: showProxyDelay, value: null),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-17 15:39:41 +08:00
|
|
|
|
|
|
|
|
testShowProxyDelay() {
|
|
|
|
|
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
|
|
|
|
|
if (showProxyDelay != null) {
|
|
|
|
|
globalState.updateCurrentDelay(showProxyDelay);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|