7e7f1f89f3
Support custom global-ua Update core Optimize some details
997 lines
31 KiB
Dart
997 lines
31 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/core/core.dart';
|
|
import 'package:fl_clash/database/database.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/plugins/app.dart';
|
|
import 'package:fl_clash/plugins/service.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
part 'generated/action.g.dart';
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class CommonAction extends _$CommonAction {
|
|
@override
|
|
void build() {}
|
|
|
|
void updateStart() {
|
|
ref
|
|
.read(setupActionProvider.notifier)
|
|
.updateStatus(!ref.read(isStartProvider));
|
|
}
|
|
|
|
void updateSpeedStatistics() {
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => state.copyWith(showTrayTitle: !state.showTrayTitle));
|
|
}
|
|
|
|
void updateMode() {
|
|
ref.read(patchClashConfigProvider.notifier).update((state) {
|
|
final index = Mode.values.indexWhere((item) => item == state.mode);
|
|
if (index == -1) return state;
|
|
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
|
return state.copyWith(mode: Mode.values[nextIndex]);
|
|
});
|
|
}
|
|
|
|
void updateRunTime() {
|
|
final startTime = ref.read(setupActionProvider.notifier).startTime;
|
|
if (startTime != null) {
|
|
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
|
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
|
ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
|
|
} else {
|
|
ref.read(runTimeProvider.notifier).value = null;
|
|
}
|
|
}
|
|
|
|
Future<void> updateTraffic() async {
|
|
final onlyStatisticsProxy = ref.read(
|
|
appSettingProvider.select((state) => state.onlyStatisticsProxy),
|
|
);
|
|
final traffic = await coreController.getTraffic(onlyStatisticsProxy);
|
|
ref.read(trafficsProvider.notifier).addTraffic(traffic);
|
|
ref.read(totalTrafficProvider.notifier).value = await coreController
|
|
.getTotalTraffic(onlyStatisticsProxy);
|
|
}
|
|
|
|
Future<void> autoCheckUpdate() async {
|
|
if (!ref.read(appSettingProvider).autoCheckUpdate) return;
|
|
final res = await request.checkForUpdate();
|
|
checkUpdateResultHandle(data: res);
|
|
}
|
|
|
|
Future<void> checkUpdateResultHandle({
|
|
Map<String, dynamic>? data,
|
|
bool isUser = false,
|
|
}) async {
|
|
if (data != null) {
|
|
final tagName = data['tag_name'];
|
|
final body = data['body'];
|
|
final submits = utils.parseReleaseBody(body);
|
|
final context = globalState.navigatorKey.currentContext!;
|
|
final textTheme = context.textTheme;
|
|
final res = await globalState.showMessage(
|
|
title: currentAppLocalizations.discoverNewVersion,
|
|
message: TextSpan(
|
|
text: '$tagName \n',
|
|
style: textTheme.headlineSmall,
|
|
children: [
|
|
TextSpan(text: '\n', style: textTheme.bodyMedium),
|
|
for (final submit in submits)
|
|
TextSpan(text: '- $submit \n', style: textTheme.bodyMedium),
|
|
],
|
|
),
|
|
confirmText: currentAppLocalizations.goDownload,
|
|
cancelText: isUser ? null : currentAppLocalizations.noLongerRemind,
|
|
);
|
|
if (res == true) {
|
|
launchUrl(Uri.parse('https://github.com/$repository/releases/latest'));
|
|
} else if (!isUser && res == false) {
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => state.copyWith(autoCheckUpdate: false));
|
|
}
|
|
} else if (isUser) {
|
|
globalState.showMessage(
|
|
title: currentAppLocalizations.checkUpdate,
|
|
message: TextSpan(text: currentAppLocalizations.checkUpdateError),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class SetupAction extends _$SetupAction {
|
|
Timer? _updateTimer;
|
|
DateTime? startTime;
|
|
|
|
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
|
|
|
@override
|
|
void build() {}
|
|
|
|
SetupParams get _setupParams {
|
|
final selectedMap = ref.read(selectedMapProvider);
|
|
final testUrl = ref.read(
|
|
appSettingProvider.select((state) => state.testUrl),
|
|
);
|
|
return SetupParams(selectedMap: selectedMap, testUrl: testUrl);
|
|
}
|
|
|
|
void fullSetup() {
|
|
if (!ref.read(initProvider)) return;
|
|
ref.read(delayDataSourceProvider.notifier).value = {};
|
|
applyProfile(force: true);
|
|
ref.read(logsProvider.notifier).value = FixedList(500);
|
|
ref.read(requestsProvider.notifier).value = FixedList(500);
|
|
}
|
|
|
|
Future<void> _handleStart() async {
|
|
startTime ??= DateTime.now();
|
|
//The local status must be updated when performing the run task
|
|
ref.read(commonActionProvider.notifier).updateRunTime();
|
|
ref.read(commonActionProvider.notifier).updateTraffic();
|
|
if (!ref.read(suspendProvider)) {
|
|
await coreController.startListener();
|
|
}
|
|
_updateTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
ref.read(commonActionProvider.notifier).updateRunTime();
|
|
ref.read(commonActionProvider.notifier).updateTraffic();
|
|
});
|
|
}
|
|
|
|
Future _updateStartTime() async {
|
|
startTime = await service?.getRunTime();
|
|
}
|
|
|
|
Future handleStop() async {
|
|
startTime = null;
|
|
_updateTimer?.cancel();
|
|
_updateTimer = null;
|
|
await coreController.stopListener();
|
|
}
|
|
|
|
Future<void> initStatus() async {
|
|
if (!globalState.needInitStatus) {
|
|
commonPrint.log('init status cancel');
|
|
return;
|
|
}
|
|
commonPrint.log('init status');
|
|
if (system.isAndroid) {
|
|
await _updateStartTime();
|
|
}
|
|
final status = isStart == true
|
|
? true
|
|
: ref.read(appSettingProvider).autoRun;
|
|
if (status == true) {
|
|
await updateStatus(true, isInit: true);
|
|
} else {
|
|
await applyProfile(force: true);
|
|
}
|
|
}
|
|
|
|
Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
|
|
if (isStart) {
|
|
if (!isInit) {
|
|
final res = await ref
|
|
.read(coreActionProvider.notifier)
|
|
.tryStartCore(true);
|
|
if (res) return;
|
|
if (!ref.read(initProvider)) return;
|
|
await _handleStart();
|
|
applyProfileDebounce(force: true, silence: true);
|
|
} else {
|
|
globalState.needInitStatus = false;
|
|
ref.read(runTimeProvider.notifier).value = 0;
|
|
try {
|
|
await applyProfile(
|
|
force: true,
|
|
preloadInvoke: () async {
|
|
await _handleStart();
|
|
},
|
|
);
|
|
} catch (_) {
|
|
ref.read(runTimeProvider.notifier).value = null;
|
|
}
|
|
}
|
|
} else {
|
|
await handleStop();
|
|
coreController.resetTraffic();
|
|
ref.read(trafficsProvider.notifier).clear();
|
|
ref.read(totalTrafficProvider.notifier).value = const Traffic();
|
|
ref.read(runTimeProvider.notifier).value = null;
|
|
ref.read(checkIpNumProvider.notifier).add();
|
|
}
|
|
}
|
|
|
|
Future<void> updateConfigDebounce() async {
|
|
debouncer.call(FunctionTag.updateConfig, () async {
|
|
await globalState.safeRun(() async {
|
|
final updateParams = ref.read(updateParamsProvider);
|
|
final res = await _requestAdmin(updateParams.tun.enable);
|
|
if (res.isError) return;
|
|
final realTunEnable = ref.read(realTunEnableProvider);
|
|
final message = await coreController.updateConfig(
|
|
updateParams.copyWith.tun(enable: realTunEnable),
|
|
);
|
|
ref.read(checkIpNumProvider.notifier).add();
|
|
if (message.isNotEmpty) throw message;
|
|
});
|
|
});
|
|
}
|
|
|
|
void tryCheckIp() {
|
|
final isTimeout = ref.read(
|
|
networkDetectionProvider.select(
|
|
(state) => state.ipInfo == null && state.isLoading == false,
|
|
),
|
|
);
|
|
if (!isTimeout) return;
|
|
ref.read(checkIpNumProvider.notifier).add();
|
|
}
|
|
|
|
void applyProfileDebounce({bool silence = false, bool force = false}) {
|
|
debouncer.call(FunctionTag.applyProfile, (silence, force) {
|
|
applyProfile(silence: silence, force: force);
|
|
}, args: [silence, force]);
|
|
}
|
|
|
|
void changeMode(Mode mode) {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => state.copyWith(mode: mode));
|
|
if (mode == Mode.global) {
|
|
ref
|
|
.read(proxiesActionProvider.notifier)
|
|
.updateCurrentGroupName(GroupName.GLOBAL.name);
|
|
}
|
|
}
|
|
|
|
void autoApplyProfile() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
applyProfile();
|
|
});
|
|
}
|
|
|
|
Future<void> applyProfile({
|
|
bool silence = false,
|
|
bool force = false,
|
|
VoidCallback? preloadInvoke,
|
|
}) async {
|
|
await _setupConfig(
|
|
force: force,
|
|
silence: silence,
|
|
preloadInvoke: preloadInvoke,
|
|
onUpdated: () async {
|
|
await ref.read(proxiesActionProvider.notifier).updateGroups();
|
|
await ref.read(providersProvider.notifier).syncProviders();
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<VM2<String, String>> getProfile({
|
|
required SetupState setupState,
|
|
required PatchClashConfig patchConfig,
|
|
}) async {
|
|
final profileId = setupState.profileId;
|
|
if (profileId == null) return const VM2('', '');
|
|
final defaultUA = globalState.packageInfo.ua;
|
|
final networkVM2 = ref.read(
|
|
networkSettingProvider.select(
|
|
(state) => VM2(state.appendSystemDns, state.routeMode),
|
|
),
|
|
);
|
|
final overrideDns = ref.read(overrideDnsProvider);
|
|
final appendSystemDns = networkVM2.a;
|
|
final routeMode = networkVM2.b;
|
|
final configMap = await coreController.getConfig(profileId);
|
|
String? scriptContent;
|
|
final List<Rule> addedRules = [];
|
|
final List<ProxyGroup> proxyGroups = [];
|
|
final List<Rule> rules = [];
|
|
if (setupState.overwriteType == OverwriteType.script) {
|
|
scriptContent = await setupState.script?.content;
|
|
} else if (setupState.overwriteType == OverwriteType.standard) {
|
|
addedRules.addAll(setupState.addedRules);
|
|
} else {
|
|
proxyGroups.addAll(setupState.proxyGroups);
|
|
rules.addAll(setupState.rules);
|
|
}
|
|
final realPatchConfig = patchConfig.copyWith(
|
|
tun: patchConfig.tun.getRealTun(routeMode),
|
|
);
|
|
Map<String, dynamic> rawConfig = configMap;
|
|
if (scriptContent?.isNotEmpty == true) {
|
|
rawConfig = await handleEvaluate(scriptContent!, rawConfig);
|
|
}
|
|
final directory = await appPath.profilesPath;
|
|
final res = makeRealProfileTask(
|
|
MakeRealProfileState(
|
|
rules: rules,
|
|
proxyGroups: proxyGroups,
|
|
profilesPath: directory,
|
|
profileId: profileId,
|
|
rawConfig: rawConfig,
|
|
realPatchConfig: realPatchConfig,
|
|
overrideDns: overrideDns,
|
|
appendSystemDns: appendSystemDns,
|
|
addedRules: addedRules,
|
|
defaultUA: defaultUA,
|
|
),
|
|
);
|
|
return res;
|
|
}
|
|
|
|
Future<String> getProfileWithId(int profileId) async {
|
|
try {
|
|
final setupState = await ref.read(setupStateProvider(profileId).future);
|
|
final patchClashConfig = ref.read(patchClashConfigProvider);
|
|
final res = await getProfile(
|
|
setupState: setupState,
|
|
patchConfig: patchClashConfig,
|
|
);
|
|
return res.a;
|
|
} catch (e) {
|
|
globalState.showNotifier(e.toString());
|
|
}
|
|
return '';
|
|
}
|
|
|
|
Future<Result<bool>> _requestAdmin(bool enableTun) async {
|
|
final realTunEnable = ref.read(realTunEnableProvider);
|
|
if (enableTun != realTunEnable && realTunEnable == false) {
|
|
final code = await system.authorizeCore();
|
|
switch (code) {
|
|
case AuthorizeCode.success:
|
|
await ref.read(coreActionProvider.notifier).restartCore();
|
|
return Result.error('');
|
|
case AuthorizeCode.none:
|
|
break;
|
|
case AuthorizeCode.error:
|
|
enableTun = false;
|
|
break;
|
|
}
|
|
}
|
|
ref.read(realTunEnableProvider.notifier).value = enableTun;
|
|
return Result.success(enableTun);
|
|
}
|
|
|
|
Future<void> _setupConfig({
|
|
bool force = false,
|
|
bool silence = false,
|
|
VoidCallback? preloadInvoke,
|
|
FutureOr Function()? onUpdated,
|
|
}) async {
|
|
var profile = ref.read(currentProfileProvider);
|
|
final nextProfile = await profile?.checkAndUpdateAndCopy();
|
|
if (nextProfile != null) {
|
|
profile = nextProfile;
|
|
ref.read(profilesProvider.notifier).put(nextProfile);
|
|
}
|
|
commonPrint.log('setup ===> ${profile?.id}');
|
|
final patchConfig = ref.read(patchClashConfigProvider);
|
|
final res = await _requestAdmin(patchConfig.tun.enable);
|
|
if (res.isError) return;
|
|
final realTunEnable = ref.read(realTunEnableProvider);
|
|
final realPatchConfig = patchConfig.copyWith.tun(enable: realTunEnable);
|
|
final setupState = await ref.read(setupStateProvider(profile?.id).future);
|
|
if (system.isAndroid) {
|
|
globalState.lastVpnState = ref.read(vpnStateProvider);
|
|
final sharedState = ref.read(sharedStateProvider);
|
|
preferences.saveShareState(sharedState);
|
|
}
|
|
final vm2 = await getProfile(
|
|
setupState: setupState,
|
|
patchConfig: realPatchConfig,
|
|
);
|
|
final yamlString = vm2.a;
|
|
final yamlMd5 = vm2.b;
|
|
if (yamlMd5 == globalState.lastConfigMd5 && force == false) return;
|
|
await globalState.loadingRun(
|
|
() async {
|
|
final configFilePath = await appPath.configFilePath;
|
|
await File(configFilePath).safeWriteAsString(yamlString);
|
|
globalState.lastConfigMd5 = yamlMd5;
|
|
final message = await coreController.setupConfig(
|
|
setupState: setupState,
|
|
params: _setupParams,
|
|
preloadInvoke: preloadInvoke,
|
|
);
|
|
if (message.isNotEmpty && !message.endsWith('is empty')) {
|
|
throw message;
|
|
}
|
|
ref.read(checkIpNumProvider.notifier).add();
|
|
await onUpdated?.call();
|
|
},
|
|
silence: true,
|
|
tag: !silence ? LoadingTag.proxies : null,
|
|
);
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class BackupAction extends _$BackupAction {
|
|
@override
|
|
void build() {}
|
|
|
|
Future<String> backup() async {
|
|
final res = await Future.wait([
|
|
database.profilesDao.fileNames().get(),
|
|
database.scriptsDao.fileNames().get(),
|
|
]);
|
|
final profileFileNames = res[0];
|
|
final scriptFileNames = res[1];
|
|
final configMap = ref.read(configProvider).toJson();
|
|
configMap['version'] = await preferences.getVersion();
|
|
return backupTask(configMap, [...profileFileNames, ...scriptFileNames]);
|
|
}
|
|
|
|
Future<void> restore(RestoreOption option) async {
|
|
final restoreDirPath = await appPath.restoreDirPath;
|
|
final restoreDir = Directory(restoreDirPath);
|
|
final restoreStrategy = ref.read(
|
|
appSettingProvider.select((state) => state.restoreStrategy),
|
|
);
|
|
final isOverride = restoreStrategy == RestoreStrategy.override;
|
|
try {
|
|
final migrationData = await restoreTask();
|
|
if (!await restoreDir.exists()) {
|
|
throw currentAppLocalizations.restoreException;
|
|
}
|
|
await database.restore(
|
|
migrationData.profiles,
|
|
migrationData.scripts,
|
|
migrationData.rules,
|
|
migrationData.links,
|
|
migrationData.proxyGroups,
|
|
isOverride: isOverride,
|
|
);
|
|
final configMap = migrationData.configMap;
|
|
if (option == RestoreOption.onlyProfiles || configMap == null) return;
|
|
final config = Config.fromJson(configMap);
|
|
ref.read(patchClashConfigProvider.notifier).value =
|
|
config.patchClashConfig;
|
|
ref.read(appSettingProvider.notifier).value = config.appSettingProps;
|
|
ref.read(currentProfileIdProvider.notifier).value =
|
|
config.currentProfileId;
|
|
ref.read(davSettingProvider.notifier).value = config.davProps;
|
|
ref.read(themeSettingProvider.notifier).value = config.themeProps;
|
|
ref.read(windowSettingProvider.notifier).value = config.windowProps;
|
|
ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
|
|
ref.read(proxiesStyleSettingProvider.notifier).value =
|
|
config.proxiesStyleProps;
|
|
ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
|
|
ref.read(networkSettingProvider.notifier).value = config.networkProps;
|
|
ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
|
|
return;
|
|
} finally {
|
|
await restoreDir.safeDelete(recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class CoreAction extends _$CoreAction {
|
|
@override
|
|
void build() {}
|
|
|
|
Future<void> initCore() async {
|
|
final isInit = await coreController.isInit;
|
|
|
|
final version = ref.read(versionProvider);
|
|
if (!isInit) {
|
|
final res = await coreController.init(version);
|
|
commonPrint.log('init result: $res');
|
|
} else {
|
|
await ref.read(proxiesActionProvider.notifier).updateGroups();
|
|
}
|
|
}
|
|
|
|
Future<void> connectCore() async {
|
|
ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
|
|
final result = await Future.wait([
|
|
coreController.preload(),
|
|
Future.delayed(const Duration(milliseconds: 300)),
|
|
]);
|
|
final String message = result[0];
|
|
if (message.isNotEmpty) {
|
|
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
|
globalState.showNotifier(message);
|
|
return;
|
|
}
|
|
ref.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
|
}
|
|
|
|
Future<Result<bool>> requestAdmin(bool enableTun) async {
|
|
final realTunEnable = ref.read(realTunEnableProvider);
|
|
if (enableTun != realTunEnable && realTunEnable == false) {
|
|
final code = await system.authorizeCore();
|
|
switch (code) {
|
|
case AuthorizeCode.success:
|
|
await restartCore();
|
|
return Result.error('');
|
|
case AuthorizeCode.none:
|
|
break;
|
|
case AuthorizeCode.error:
|
|
enableTun = false;
|
|
break;
|
|
}
|
|
}
|
|
ref.read(realTunEnableProvider.notifier).value = enableTun;
|
|
return Result.success(enableTun);
|
|
}
|
|
|
|
Future<void> restartCore([bool start = false]) async {
|
|
final isDisconnected =
|
|
ref.read(coreStatusProvider) == CoreStatus.disconnected;
|
|
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
|
await coreController.shutdown(!isDisconnected);
|
|
await connectCore();
|
|
await initCore();
|
|
if (start || ref.read(isStartProvider)) {
|
|
await ref
|
|
.read(setupActionProvider.notifier)
|
|
.updateStatus(true, isInit: true);
|
|
} else {
|
|
await ref.read(setupActionProvider.notifier).applyProfile(force: true);
|
|
}
|
|
}
|
|
|
|
Future<bool> tryStartCore([bool start = false]) async {
|
|
if (coreController.isCompleted) return false;
|
|
await restartCore(start);
|
|
return true;
|
|
}
|
|
|
|
void handleCoreDisconnected() {
|
|
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class SystemAction extends _$SystemAction {
|
|
@override
|
|
void build() {}
|
|
|
|
Future<List<Package>> getPackages() async {
|
|
if (ref.read(isMobileViewProvider)) {
|
|
await Future.delayed(commonDuration);
|
|
}
|
|
if (ref.read(packagesProvider).isEmpty) {
|
|
ref.read(packagesProvider.notifier).value =
|
|
await app?.getPackages() ?? [];
|
|
}
|
|
return ref.read(packagesProvider);
|
|
}
|
|
|
|
Future<void> handleExit([bool needSave = false]) async {
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
system.exit();
|
|
});
|
|
try {
|
|
await Future.wait([
|
|
if (needSave) preferences.saveConfig(ref.read(configProvider)),
|
|
if (macOS != null) macOS!.updateDns(true),
|
|
if (proxy != null) proxy!.stopProxy(),
|
|
if (tray != null) tray!.destroy(),
|
|
]);
|
|
await window?.close();
|
|
await coreController.destroy();
|
|
commonPrint.log('exit');
|
|
} finally {
|
|
system.exit();
|
|
}
|
|
}
|
|
|
|
Future<void> handleClose([bool exit = true]) async {
|
|
if (!system.isDesktop) {
|
|
if (ref.read(backBlockProvider)) return;
|
|
}
|
|
if (ref.read(appSettingProvider).minimizeOnExit || !exit) {
|
|
if (system.isDesktop) {
|
|
await preferences.saveConfig(ref.read(configProvider));
|
|
}
|
|
await system.back();
|
|
} else {
|
|
await handleExit();
|
|
}
|
|
}
|
|
|
|
Future<void> updateVisible() async {
|
|
final visible = await window?.isVisible;
|
|
if (visible != null && !visible) {
|
|
window?.show();
|
|
} else {
|
|
window?.hide();
|
|
}
|
|
}
|
|
|
|
void updateTun() {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => state.copyWith.tun(enable: !state.tun.enable));
|
|
}
|
|
|
|
void updateSystemProxy() {
|
|
ref
|
|
.read(networkSettingProvider.notifier)
|
|
.update((state) => state.copyWith(systemProxy: !state.systemProxy));
|
|
}
|
|
|
|
void updateAutoLaunch() {
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => state.copyWith(autoLaunch: !state.autoLaunch));
|
|
}
|
|
|
|
Future<void> updateTray() async {
|
|
tray?.update(
|
|
trayState: ref.read(trayStateProvider),
|
|
traffic: ref.read(
|
|
trafficsProvider.select(
|
|
(state) => state.list.safeLast(const Traffic()),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> updateLocalIp() async {
|
|
ref.read(localIpProvider.notifier).value = null;
|
|
await Future.delayed(commonDuration);
|
|
ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress();
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class StoreAction extends _$StoreAction {
|
|
@override
|
|
void build() {}
|
|
|
|
Future<void> shakingStore() async {
|
|
final profileIds = ref.read(
|
|
profilesProvider.select((state) => state.map((item) => item.id)),
|
|
);
|
|
final scriptIds = await ref.read(
|
|
scriptsProvider.future.select(
|
|
(state) async => (await state).map((item) => item.id),
|
|
),
|
|
);
|
|
final pathsToDelete = await shakingProfileTask(VM2(profileIds, scriptIds));
|
|
if (pathsToDelete.isNotEmpty) {
|
|
final deleteFutures = pathsToDelete.map((path) async {
|
|
try {
|
|
final res = await coreController.deleteFile(path);
|
|
if (res.isNotEmpty) throw res;
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
});
|
|
await Future.wait(deleteFutures);
|
|
}
|
|
}
|
|
|
|
void savePreferencesDebounce() {
|
|
debouncer.call(FunctionTag.savePreferences, () async {
|
|
await preferences.saveConfig(ref.read(configProvider));
|
|
});
|
|
}
|
|
|
|
Future handleClear() async {
|
|
await preferences.clearPreferences();
|
|
commonPrint.log('clear preferences');
|
|
await database.close();
|
|
await File(await appPath.databasePath).safeDelete(recursive: true);
|
|
final homeDir = Directory(await appPath.profilesPath);
|
|
await for (final file in homeDir.list(recursive: true)) {
|
|
await coreController.deleteFile(file.path);
|
|
}
|
|
await preferences.clearPreferences();
|
|
ref.read(systemActionProvider.notifier).handleExit(false);
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class ThemeAction extends _$ThemeAction {
|
|
@override
|
|
void build() {}
|
|
|
|
void updateBrightness() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
ref.read(systemBrightnessProvider.notifier).value =
|
|
WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
|
});
|
|
}
|
|
|
|
void updateViewSize(Size size) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
ref.read(viewSizeProvider.notifier).value = size;
|
|
});
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class ProxiesAction extends _$ProxiesAction {
|
|
@override
|
|
void build() {}
|
|
|
|
void updateGroupsDebounce([Duration? duration]) {
|
|
debouncer.call(FunctionTag.updateGroups, updateGroups, duration: duration);
|
|
}
|
|
|
|
void changeProxyDebounce(String groupName, String proxyName) {
|
|
debouncer.call(FunctionTag.changeProxy, (
|
|
String groupName,
|
|
String proxyName,
|
|
) async {
|
|
await changeProxy(groupName: groupName, proxyName: proxyName);
|
|
updateGroupsDebounce();
|
|
}, args: [groupName, proxyName]);
|
|
}
|
|
|
|
Future<void> updateGroups() async {
|
|
try {
|
|
commonPrint.log('updateGroups');
|
|
ref.read(groupsProvider.notifier).value = await retry(
|
|
task: () async {
|
|
final sortType = ref.read(
|
|
proxiesStyleSettingProvider.select((state) => state.sortType),
|
|
);
|
|
final delayMap = ref.read(delayDataSourceProvider);
|
|
final testUrl = ref.read(
|
|
appSettingProvider.select((state) => state.testUrl),
|
|
);
|
|
final selectedMap = ref.read(
|
|
currentProfileProvider.select((state) => state?.selectedMap ?? {}),
|
|
);
|
|
return coreController.getProxiesGroups(
|
|
selectedMap: selectedMap,
|
|
sortType: sortType,
|
|
delayMap: delayMap,
|
|
defaultTestUrl: testUrl,
|
|
);
|
|
},
|
|
retryIf: (res) => res.isEmpty,
|
|
);
|
|
} catch (e) {
|
|
commonPrint.log('updateGroups error: $e');
|
|
ref.read(groupsProvider.notifier).value = [];
|
|
}
|
|
}
|
|
|
|
void updateCurrentGroupName(String groupName) {
|
|
final profile = ref.read(currentProfileProvider);
|
|
if (profile == null || profile.currentGroupName == groupName) return;
|
|
ref
|
|
.read(profilesProvider.notifier)
|
|
.put(profile.copyWith(currentGroupName: groupName));
|
|
}
|
|
|
|
void updateCurrentUnfoldSet(Set<String> value) {
|
|
final currentProfile = ref.read(currentProfileProvider);
|
|
if (currentProfile == null) return;
|
|
ref
|
|
.read(profilesProvider.notifier)
|
|
.put(currentProfile.copyWith(unfoldSet: value));
|
|
}
|
|
|
|
void setDelay(Delay delay) {
|
|
ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
|
}
|
|
|
|
Future<void> changeProxy({
|
|
required String groupName,
|
|
required String proxyName,
|
|
}) async {
|
|
await coreController.changeProxy(
|
|
ChangeProxyParams(groupName: groupName, proxyName: proxyName),
|
|
);
|
|
if (ref.read(appSettingProvider).closeConnections) {
|
|
await coreController.closeConnections();
|
|
} else {
|
|
await coreController.resetConnections();
|
|
}
|
|
ref.read(checkIpNumProvider.notifier).add();
|
|
}
|
|
|
|
Future<String> updateProvider(
|
|
ExternalProvider provider, {
|
|
bool showLoading = false,
|
|
}) async {
|
|
try {
|
|
if (showLoading) {
|
|
ref.read(isUpdatingProvider(provider.updatingKey).notifier).value =
|
|
true;
|
|
}
|
|
final message = await coreController.updateExternalProvider(
|
|
providerName: provider.name,
|
|
);
|
|
if (message.isNotEmpty) return message;
|
|
ref
|
|
.read(providersProvider.notifier)
|
|
.setProvider(await coreController.getExternalProvider(provider.name));
|
|
return '';
|
|
} finally {
|
|
ref.read(isUpdatingProvider(provider.updatingKey).notifier).value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class ProfilesAction extends _$ProfilesAction {
|
|
@override
|
|
void build() {}
|
|
|
|
void updateCurrentSelectedMap(String groupName, String proxyName) {
|
|
final currentProfile = ref.read(currentProfileProvider);
|
|
if (currentProfile != null &&
|
|
currentProfile.selectedMap[groupName] != proxyName) {
|
|
final selectedMap = Map<String, String>.from(currentProfile.selectedMap)
|
|
..[groupName] = proxyName;
|
|
ref
|
|
.read(profilesProvider.notifier)
|
|
.put(currentProfile.copyWith(selectedMap: selectedMap));
|
|
}
|
|
}
|
|
|
|
Future<void> deleteProfile(int id) async {
|
|
ref.read(profilesProvider.notifier).del(id);
|
|
clearEffect(id);
|
|
final currentProfileId = ref.read(currentProfileIdProvider);
|
|
if (currentProfileId == id) {
|
|
final profiles = ref.read(profilesProvider);
|
|
if (profiles.isNotEmpty) {
|
|
final updateId = profiles.first.id;
|
|
ref.read(currentProfileIdProvider.notifier).value = updateId;
|
|
} else {
|
|
ref.read(currentProfileIdProvider.notifier).value = null;
|
|
ref.read(setupActionProvider.notifier).updateStatus(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> autoUpdateProfiles() async {
|
|
for (final profile in ref.read(profilesProvider)) {
|
|
if (!profile.autoUpdate) continue;
|
|
final isNotNeedUpdate = profile.lastUpdateDate
|
|
?.add(profile.autoUpdateDuration)
|
|
.isBeforeNow;
|
|
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
|
|
continue;
|
|
}
|
|
try {
|
|
await updateProfile(profile);
|
|
} catch (e) {
|
|
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
void putProfile(Profile profile) {
|
|
ref.read(profilesProvider.notifier).put(profile);
|
|
if (ref.read(currentProfileIdProvider) != null) return;
|
|
ref.read(currentProfileIdProvider.notifier).value = profile.id;
|
|
}
|
|
|
|
Future<void> updateProfiles() async {
|
|
for (final profile in ref.read(profilesProvider)) {
|
|
if (profile.type == ProfileType.file) continue;
|
|
await updateProfile(profile);
|
|
}
|
|
}
|
|
|
|
Future<void> updateProfile(
|
|
Profile profile, {
|
|
bool showLoading = false,
|
|
}) async {
|
|
try {
|
|
if (showLoading) {
|
|
ref.read(isUpdatingProvider(profile.updatingKey).notifier).value = true;
|
|
}
|
|
ref.read(profilesProvider.notifier).put(profile);
|
|
final newProfile = await profile.update();
|
|
ref.read(profilesProvider.notifier).put(newProfile);
|
|
if (profile.id == ref.read(currentProfileIdProvider)) {
|
|
ref
|
|
.read(setupActionProvider.notifier)
|
|
.applyProfileDebounce(silence: true);
|
|
}
|
|
} finally {
|
|
ref.read(isUpdatingProvider(profile.updatingKey).notifier).value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> addProfileFormFile() async {
|
|
final platformFile = await globalState.safeRun(picker.pickerFile);
|
|
if (platformFile == null) return;
|
|
final bytes = await platformFile.readBytes();
|
|
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
|
ref.read(currentPageLabelProvider.notifier).toProfiles();
|
|
final profile = await globalState.loadingRun(
|
|
tag: LoadingTag.profiles,
|
|
() async {
|
|
return Profile.normal(label: platformFile.name).saveFile(bytes);
|
|
},
|
|
title: currentAppLocalizations.addProfile,
|
|
);
|
|
if (profile != null) {
|
|
putProfile(profile);
|
|
}
|
|
}
|
|
|
|
Future<void> addProfileFormURL(String url) async {
|
|
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
|
|
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
|
}
|
|
ref.read(currentPageLabelProvider.notifier).value = PageLabel.profiles;
|
|
final profile = await globalState.loadingRun(
|
|
tag: LoadingTag.profiles,
|
|
() async {
|
|
return Profile.normal(url: url).update();
|
|
},
|
|
title: currentAppLocalizations.addProfile,
|
|
);
|
|
if (profile != null) {
|
|
putProfile(profile);
|
|
}
|
|
}
|
|
|
|
void setProfileAndAutoApply(Profile profile) {
|
|
ref.read(profilesProvider.notifier).put(profile);
|
|
if (profile.id == ref.read(currentProfileIdProvider)) {
|
|
ref.read(setupActionProvider.notifier).applyProfileDebounce();
|
|
}
|
|
}
|
|
|
|
Future<void> addProfileFormQrCode() async {
|
|
final url = await globalState.safeRun(picker.pickerConfigQRCode);
|
|
if (url == null) return;
|
|
addProfileFormURL(url);
|
|
}
|
|
|
|
void reorder(List<Profile> profiles) {
|
|
ref.read(profilesProvider.notifier).reorder(profiles);
|
|
}
|
|
|
|
Future<void> clearEffect(int profileId) async {
|
|
final profilePath = await appPath.getProfilePath(profileId.toString());
|
|
final providersDirPath = await appPath.getProvidersDirPath(
|
|
profileId.toString(),
|
|
);
|
|
final profileFile = File(profilePath);
|
|
final isExists = await profileFile.exists();
|
|
if (isExists) {
|
|
await profileFile.safeDelete(recursive: true);
|
|
}
|
|
await coreController.deleteFile(providersDirPath);
|
|
}
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
class GeoResourceAction extends _$GeoResourceAction {
|
|
@override
|
|
void build() {}
|
|
|
|
Future<void> updateGeoResource(GeoResource geoResource) async {
|
|
await coreController.updateGeoData(geoResource.name);
|
|
}
|
|
|
|
void updateGeoResourceUrl(GeoResource geoResource, String newUrl) {
|
|
if (!newUrl.isUrl) {
|
|
throw 'Invalid url';
|
|
}
|
|
ref.read(patchClashConfigProvider.notifier).update((state) {
|
|
return state.copyWith(geoXUrl: {...state.geoXUrl, geoResource: newUrl});
|
|
});
|
|
}
|
|
}
|