Files
MWClash/lib/controller.dart

543 lines
14 KiB
Dart
Raw Normal View History

2024-04-30 23:38:49 +08:00
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
2024-04-30 23:38:49 +08:00
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
2024-04-30 23:38:49 +08:00
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
2024-04-30 23:38:49 +08:00
import 'package:provider/provider.dart';
2024-05-31 09:59:18 +08:00
import 'package:url_launcher/url_launcher.dart';
2024-04-30 23:38:49 +08:00
import 'clash/core.dart';
import 'models/models.dart';
import 'common/common.dart';
class AppController {
final BuildContext context;
late AppState appState;
late Config config;
late ClashConfig clashConfig;
late Function updateClashConfigDebounce;
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
2024-04-30 23:38:49 +08:00
AppController(this.context) {
appState = context.read<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
2024-07-13 16:36:08 +08:00
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
updateGroupDebounce = debounce(() async {
await updateGroups();
});
2024-04-30 23:38:49 +08:00
}
updateStatus(bool isStart) async {
2024-04-30 23:38:49 +08:00
if (isStart) {
await globalState.handleStart(
2024-04-30 23:38:49 +08:00
config: config,
clashConfig: clashConfig,
);
updateRunTime();
updateTraffic();
globalState.updateFunctionLists = [
updateRunTime,
updateTraffic,
];
applyProfileDebounce();
2024-04-30 23:38:49 +08:00
} else {
await globalState.handleStop();
clashCore.resetTraffic();
2024-04-30 23:38:49 +08:00
appState.traffics = [];
appState.totalTraffic = Traffic();
2024-04-30 23:38:49 +08:00
appState.runTime = null;
addCheckIpNumDebounce();
2024-04-30 23:38:49 +08:00
}
}
updateCoreVersionInfo() {
globalState.updateCoreVersionInfo(appState);
}
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
final startTimeStamp = 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(
appState: appState,
);
}
addProfile(Profile profile) async {
2024-04-30 23:38:49 +08:00
config.setProfile(profile);
if (config.currentProfileId != null) return;
await changeProfile(profile.id);
2024-04-30 23:38:49 +08:00
}
deleteProfile(String id) async {
config.deleteProfileById(id);
clashCore.clearEffect(id);
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 {
2024-09-09 09:48:26 +08:00
changeProfile(null);
updateStatus(false);
2024-04-30 23:38:49 +08:00
}
}
}
Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update();
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
2024-04-30 23:38:49 +08:00
}
Future<void> updateClashConfig({bool isPatch = true}) async {
await globalState.updateClashConfig(
2024-04-30 23:38:49 +08:00
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
}
Future applyProfile({bool isPrue = false}) async {
if (isPrue) {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} else {
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
}
addCheckIpNumDebounce();
2024-07-13 16:36:08 +08:00
}
2024-04-30 23:38:49 +08:00
changeProfile(String? value) async {
if (value == config.currentProfileId) return;
config.currentProfileId = value;
}
autoUpdateProfiles() async {
for (final profile in config.profiles) {
if (!profile.autoUpdate) continue;
2024-04-30 23:38:49 +08:00
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
2024-05-06 10:32:39 +08:00
profile.autoUpdateDuration,
)
.isBeforeNow;
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
continue;
}
try {
updateProfile(profile);
} catch (e) {
appState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
}
}
}
updateProfiles() async {
for (final profile in config.profiles) {
if (profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile);
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);
}
changeProxy({
required String groupName,
required String proxyName,
}) {
globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
);
addCheckIpNumDebounce();
}
2024-04-30 23:38:49 +08:00
handleBackOrExit() async {
if (config.isMinimizeOnExit) {
if (system.isDesktop) {
await savePreferences();
}
await system.back();
} else {
await handleExit();
}
}
handleExit() async {
await updateStatus(false);
await proxy?.stopProxy();
2024-04-30 23:38:49 +08:00
await savePreferences();
clashCore.shutdown();
system.exit();
}
updateLogStatus() {
if (config.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
appState.logs = [];
}
}
autoCheckUpdate() async {
2024-05-31 09:59:18 +08:00
if (!config.autoCheckUpdate) return;
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
2024-05-31 09:59:18 +08:00
}
checkUpdateResultHandle({
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
2024-05-31 09:59:18 +08:00
final submits = other.parseReleaseBody(body);
final textTheme = context.textTheme;
2024-05-31 09:59:18 +08:00
globalState.showMessage(
title: appLocalizations.discoverNewVersion,
message: TextSpan(
text: "$tagName \n",
style: textTheme.headlineSmall,
2024-05-31 09:59:18 +08:00
children: [
TextSpan(
text: "\n",
style: textTheme.bodyMedium,
2024-05-31 09:59:18 +08:00
),
for (final submit in submits)
TextSpan(
text: "- $submit \n",
style: textTheme.bodyMedium,
2024-05-31 09:59:18 +08:00
),
],
),
onTab: () {
launchUrl(
Uri.parse("https://github.com/$repository/releases/latest"),
);
},
confirmText: appLocalizations.goDownload,
);
} else if (handleError) {
2024-05-31 09:59:18 +08:00
globalState.showMessage(
title: appLocalizations.checkUpdate,
message: TextSpan(
text: appLocalizations.checkUpdateError,
),
);
}
}
init() async {
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
if (Platform.isAndroid) {
globalState.updateStartTime();
}
if (globalState.isStart) {
await updateStatus(true);
2024-05-06 10:32:39 +08:00
} else {
await updateStatus(config.autoRun);
2024-05-06 10:32:39 +08:00
}
autoUpdateProfiles();
autoCheckUpdate();
2024-04-30 23:38:49 +08:00
}
updateTray() {
globalState.updateTray(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
2024-04-30 23:38:49 +08:00
setDelay(Delay delay) {
appState.setDelay(delay);
}
toPage(int index, {bool hasAnimate = false}) {
if (index > appState.currentNavigationItems.length - 1) {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
2024-04-30 23:38:49 +08:00
if ((config.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
index,
duration: kTabScrollDuration,
curve: Curves.easeOut,
);
} else {
globalState.pageController?.jumpToPage(index);
}
}
toProfiles() {
final index = appState.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);
},
);
},
);
}
showSnackBar(String message) {
globalState.showSnackBar(context, message: message);
}
addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
}
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile>(
() async {
return await Profile.normal(
url: url,
).update();
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
2024-04-30 23:38:49 +08:00
addProfileFormFile() async {
final platformFile = await globalState.safeRun(picker.pickerFile);
final bytes = platformFile?.bytes;
if (bytes == null) {
return null;
}
2024-04-30 23:38:49 +08:00
if (!context.mounted) return;
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile?>(
2024-05-06 10:32:39 +08:00
() async {
2024-04-30 23:38:49 +08:00
await Future.delayed(const Duration(milliseconds: 300));
return await Profile.normal(label: platformFile?.name).saveFile(bytes);
2024-04-30 23:38:49 +08:00
},
title: "${appLocalizations.add}${appLocalizations.profile}",
2024-04-30 23:38:49 +08:00
);
if (profile != null) {
await addProfile(profile);
}
2024-04-30 23:38:49 +08:00
}
addProfileFormQrCode() async {
final url = await globalState.safeRun(picker.pickerConfigQRCode);
if (url == null) return;
addProfileFormURL(url);
}
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
});
}
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
2024-08-05 19:25:35 +08:00
(a, b) => other.sortByChar(
other.getPinyin(a.name),
other.getPinyin(b.name),
2024-08-05 19:25:35 +08:00
),
);
}
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
return proxies = List.of(proxies)
..sort(
(a, b) {
final aDelay = appState.getDelay(a.name);
final bDelay = appState.getDelay(b.name);
if (aDelay == null && bDelay == null) {
return 0;
}
if (aDelay == null || aDelay == -1) {
return 1;
}
if (bDelay == null || bDelay == -1) {
return -1;
}
return aDelay.compareTo(bDelay);
},
);
}
List<Proxy> getSortProxies(List<Proxy> proxies) {
return switch (config.proxiesSortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies),
ProxiesSortType.name => _sortOfName(proxies),
};
}
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,
) async {
final archive = await Isolate.run<Archive>(() {
final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data);
});
final homeDirPath = await appPath.getHomeDirPath();
final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles =
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex];
final tempConfig = Config.fromJson(
json.decode(
utf8.decode(configFile.content),
),
);
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
}
if (recoveryOption == RecoveryOption.onlyProfiles) {
config.update(tempConfig, RecoveryOption.onlyProfiles);
} else {
config.update(tempConfig, RecoveryOption.all);
clashConfig.update(tempClashConfig);
}
}
2024-04-30 23:38:49 +08:00
}