import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.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:path/path.dart'; import 'package:url_launcher/url_launcher.dart'; import 'common/common.dart'; import 'models/models.dart'; class AppController { bool lastTunEnable = false; int? lastProfileModified; final BuildContext context; final WidgetRef _ref; AppController(this.context, WidgetRef ref) : _ref = ref; updateClashConfigDebounce() { debouncer.call(DebounceTag.updateClashConfig, () { updateClashConfig(true); }); } updateGroupsDebounce() { debouncer.call(DebounceTag.updateGroups, updateGroups); } addCheckIpNumDebounce() { debouncer.call(DebounceTag.addCheckIpNum, () { _ref.read(checkIpNumProvider.notifier).add(); }); } applyProfileDebounce({ bool silence = false, }) { debouncer.call(DebounceTag.applyProfile, (silence) { applyProfile(silence: silence); }, args: [silence]); } savePreferencesDebounce() { debouncer.call(DebounceTag.savePreferences, savePreferences); } changeProxyDebounce(String groupName, String proxyName) { debouncer.call(DebounceTag.changeProxy, (String groupName, String proxyName) async { await changeProxy( groupName: groupName, proxyName: proxyName, ); await updateGroups(); }, args: [groupName, proxyName]); } restartCore() async { await clashService?.reStart(); await initCore(); if (_ref.read(runTimeProvider.notifier).isStart) { await globalState.handleStart(); } } updateStatus(bool isStart) async { if (isStart) { await globalState.handleStart([ updateRunTime, updateTraffic, ]); final currentLastModified = await _ref.read(currentProfileProvider)?.profileLastModified; if (currentLastModified == null || lastProfileModified == null) { addCheckIpNumDebounce(); return; } if (currentLastModified <= (lastProfileModified ?? 0)) { addCheckIpNumDebounce(); return; } applyProfileDebounce(); } else { await globalState.handleStop(); await clashCore.resetTraffic(); _ref.read(trafficsProvider.notifier).clear(); _ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(runTimeProvider.notifier).value = null; // tray.updateTrayTitle(null); addCheckIpNumDebounce(); } } updateRunTime() { final startTime = globalState.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; } } updateTraffic() async { final traffic = await clashCore.getTraffic(); _ref.read(trafficsProvider.notifier).addTraffic(traffic); _ref.read(totalTrafficProvider.notifier).value = await clashCore.getTotalTraffic(); } addProfile(Profile profile) async { _ref.read(profilesProvider.notifier).setProfile(profile); if (_ref.read(currentProfileIdProvider) != null) return; _ref.read(currentProfileIdProvider.notifier).value = profile.id; } deleteProfile(String id) async { _ref.read(profilesProvider.notifier).deleteProfileById(id); clearEffect(id); if (globalState.config.currentProfileId == id) { final profiles = globalState.config.profiles; final currentProfileId = _ref.read(currentProfileIdProvider.notifier); if (profiles.isNotEmpty) { final updateId = profiles.first.id; currentProfileId.value = updateId; } else { currentProfileId.value = null; updateStatus(false); } } } updateProviders() async { _ref.read(providersProvider.notifier).value = await clashCore.getExternalProviders(); } updateLocalIp() async { _ref.read(localIpProvider.notifier).value = null; await Future.delayed(commonDuration); _ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress(); } Future updateProfile(Profile profile) async { final newProfile = await profile.update(); _ref .read(profilesProvider.notifier) .setProfile(newProfile.copyWith(isUpdating: false)); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } setProfile(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); } setProfileAndAutoApply(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } setProfiles(List profiles) { _ref.read(profilesProvider.notifier).value = profiles; } addLog(Log log) { _ref.read(logsProvider).add(log); } updateOrAddHotKeyAction(HotKeyAction hotKeyAction) { final hotKeyActions = _ref.read(hotKeyActionsProvider); final index = hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action); if (index == -1) { _ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions) ..add(hotKeyAction); } else { _ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions) ..[index] = hotKeyAction; } _ref.read(hotKeyActionsProvider.notifier).value = index == -1 ? (List.from(hotKeyActions)..add(hotKeyAction)) : (List.from(hotKeyActions)..[index] = hotKeyAction); } List getCurrentGroups() { return _ref.read(currentGroupsStateProvider.select((state) => state.value)); } String getRealTestUrl(String? url) { return _ref.read(getRealTestUrlProvider(url)); } int getProxiesColumns() { return _ref.read(getProxiesColumnsProvider); } addSortNum() { return _ref.read(sortNumProvider.notifier).add(); } getCurrentGroupName() { final currentGroupName = _ref.read(currentProfileProvider.select( (state) => state?.currentGroupName, )); return currentGroupName; } ProxyCardState getProxyCardState(proxyName) { return _ref.read(getProxyCardStateProvider(proxyName)); } getSelectedProxyName(groupName) { return _ref.read(getSelectedProxyNameProvider(groupName)); } updateCurrentGroupName(String groupName) { final profile = _ref.read(currentProfileProvider); if (profile == null || profile.currentGroupName == groupName) { return; } setProfile( profile.copyWith(currentGroupName: groupName), ); } Future updateClashConfig([bool? isPatch]) async { final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { await _updateClashConfig( isPatch, ); }); } Future _updateClashConfig([bool? isPatch]) async { final profile = _ref.watch(currentProfileProvider); await _ref.read(currentProfileProvider)?.checkAndUpdate(); final patchConfig = _ref.read(patchClashConfigProvider); final appSetting = _ref.read(appSettingProvider); bool enableTun = patchConfig.tun.enable; if (enableTun != lastTunEnable && lastTunEnable == false && !Platform.isAndroid) { final code = await system.authorizeCore(); switch (code) { case AuthorizeCode.none: break; case AuthorizeCode.success: lastTunEnable = enableTun; await restartCore(); return; case AuthorizeCode.error: enableTun = false; } } if (appSetting.openLogs) { clashCore.startLog(); } else { clashCore.stopLog(); } final res = await clashCore.updateConfig( globalState.getUpdateConfigParams(isPatch), ); if (res.isNotEmpty) throw res; lastTunEnable = enableTun; lastProfileModified = await profile?.profileLastModified; } Future _applyProfile() async { await clashCore.requestGc(); await updateClashConfig(); await updateGroups(); await updateProviders(); } Future applyProfile({bool silence = false}) async { if (silence) { await _applyProfile(); } else { final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { await _applyProfile(); }); } addCheckIpNumDebounce(); } handleChangeProfile() { _ref.read(delayDataSourceProvider.notifier).value = {}; applyProfile(); } updateBrightness(Brightness brightness) { _ref.read(appBrightnessProvider.notifier).value = brightness; } 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) { _ref.read(logsProvider.notifier).addLog( Log( logLevel: LogLevel.info, payload: e.toString(), ), ); } } } Future updateGroups() async { _ref.read(groupsProvider.notifier).value = await retry( task: () async { return await clashCore.getProxiesGroups(); }, retryIf: (res) => res.isEmpty, ); } updateProfiles() async { for (final profile in _ref.read(profilesProvider)) { if (profile.type == ProfileType.file) { continue; } await updateProfile(profile); } } updateSystemColorSchemes(ColorSchemes colorSchemes) { _ref.read(appSchemesProvider.notifier).value = colorSchemes; } savePreferences() async { commonPrint.log("save preferences"); await preferences.saveConfig(globalState.config); } changeProxy({ required String groupName, required String proxyName, }) async { await clashCore.changeProxy( ChangeProxyParams( groupName: groupName, proxyName: proxyName, ), ); if (_ref.read(appSettingProvider).closeConnections) { clashCore.closeConnections(); } addCheckIpNumDebounce(); } handleBackOrExit() async { if (_ref.read(appSettingProvider).minimizeOnExit) { if (system.isDesktop) { await savePreferencesDebounce(); } await system.back(); } else { await handleExit(); } } handleExit() async { try { await updateStatus(false); await clashCore.shutdown(); await clashService?.destroy(); await proxy?.stopProxy(); await savePreferences(); } finally { system.exit(); } } autoCheckUpdate() async { if (!_ref.read(appSettingProvider).autoCheckUpdate) return; final res = await request.checkForUpdate(); checkUpdateResultHandle(data: res); } checkUpdateResultHandle({ Map? data, bool handleError = false, }) async { if (data != null) { final tagName = data['tag_name']; final body = data['body']; final submits = other.parseReleaseBody(body); final textTheme = context.textTheme; final res = await globalState.showMessage( title: appLocalizations.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: appLocalizations.goDownload, ); if (res != true) { return; } launchUrl( Uri.parse("https://github.com/$repository/releases/latest"), ); } else if (handleError) { globalState.showMessage( title: appLocalizations.checkUpdate, message: TextSpan( text: appLocalizations.checkUpdateError, ), ); } } _handlePreference() async { if (await preferences.isInit) { return; } final res = await globalState.showMessage( title: appLocalizations.tip, message: TextSpan(text: appLocalizations.cacheCorrupt), ); if (res == true) { final file = File(await appPath.sharedPreferencesPath); final isExists = await file.exists(); if (isExists) { await file.delete(); } } await handleExit(); } Future initCore() async { final isInit = await clashCore.isInit; if (!isInit) { await clashCore.setState( globalState.getCoreState(), ); await clashCore.init(); } await applyProfile(); } init() async { await _handlePreference(); await _handlerDisclaimer(); await initCore(); await _initStatus(); updateTray(true); autoLaunch?.updateStatus( _ref.read(appSettingProvider).autoLaunch, ); autoUpdateProfiles(); autoCheckUpdate(); if (!_ref.read(appSettingProvider).silentLaunch) { window?.show(); } else { window?.hide(); } _ref.read(initProvider.notifier).value = true; } _initStatus() async { if (Platform.isAndroid) { await globalState.updateStartTime(); } final status = globalState.isStart == true ? true : _ref.read(appSettingProvider).autoRun; await updateStatus(status); if (!status) { addCheckIpNumDebounce(); } } setDelay(Delay delay) { _ref.read(delayDataSourceProvider.notifier).setDelay(delay); } toPage( int index, { bool hasAnimate = false, }) { final navigations = _ref.read(currentNavigationsStateProvider).value; if (index > navigations.length - 1) { return; } _ref.read(currentPageLabelProvider.notifier).value = navigations[index].label; final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage; final isMobile = _ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile; if (isAnimateToPage && isMobile || hasAnimate) { globalState.pageController?.animateToPage( index, duration: kTabScrollDuration, curve: Curves.easeOut, ); } else { globalState.pageController?.jumpToPage(index); } } toProfiles() { final index = _ref.read(currentNavigationsStateProvider).value.indexWhere( (element) => element.label == PageLabel.profiles, ); if (index != -1) { toPage(index); } } initLink() { linkManager.initAppLinksListen( (url) async { final res = await globalState.showMessage( title: "${appLocalizations.add}${appLocalizations.profile}", message: TextSpan( children: [ TextSpan(text: appLocalizations.doYouWantToPass), TextSpan( text: " $url ", style: TextStyle( color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, decorationColor: Theme.of(context).colorScheme.primary, ), ), TextSpan( text: "${appLocalizations.create}${appLocalizations.profile}"), ], ), ); if (res != true) { return; } addProfileFormURL(url); }, ); } Future showDisclaimer() async { return await globalState.showCommonDialog( dismissible: false, child: AlertDialog( title: Text(appLocalizations.disclaimer), content: Container( width: dialogCommonWidth, constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: SelectableText( appLocalizations.disclaimerDesc, ), ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(appLocalizations.exit), ), TextButton( onPressed: () { _ref.read(appSettingProvider.notifier).updateState( (state) => state.copyWith(disclaimerAccepted: true), ); Navigator.of(context).pop(true); }, child: Text(appLocalizations.agree), ) ], ), ) ?? false; } _handlerDisclaimer() async { if (_ref.read(appSettingProvider).disclaimerAccepted) { return; } final isDisclaimerAccepted = await showDisclaimer(); if (!isDisclaimerAccepted) { await handleExit(); } return; } 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( () async { return await Profile.normal( url: url, ).update(); }, title: "${appLocalizations.add}${appLocalizations.profile}", ); if (profile != null) { await addProfile(profile); } } addProfileFormFile() async { final platformFile = await globalState.safeRun(picker.pickerFile); final bytes = platformFile?.bytes; if (bytes == null) { return null; } 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( () async { await Future.delayed(const Duration(milliseconds: 300)); return await Profile.normal(label: platformFile?.name).saveFile(bytes); }, title: "${appLocalizations.add}${appLocalizations.profile}", ); if (profile != null) { await addProfile(profile); } } addProfileFormQrCode() async { final url = await globalState.safeRun(picker.pickerConfigQRCode); if (url == null) return; addProfileFormURL(url); } updateViewWidth(double width) { WidgetsBinding.instance.addPostFrameCallback((_) { _ref.read(viewWidthProvider.notifier).value = width; }); } setProvider(ExternalProvider? provider) { _ref.read(providersProvider.notifier).setProvider(provider); } List _sortOfName(List proxies) { return List.of(proxies) ..sort( (a, b) => other.sortByChar( other.getPinyin(a.name), other.getPinyin(b.name), ), ); } List _sortOfDelay({ required List proxies, String? testUrl, }) { return List.of(proxies) ..sort( (a, b) { final aDelay = _ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl)); final bDelay = _ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl)); 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 getSortProxies(List proxies, [String? url]) { return switch (_ref.read(proxiesStyleSettingProvider).sortType) { ProxiesSortType.none => proxies, ProxiesSortType.delay => _sortOfDelay( proxies: proxies, testUrl: url, ), ProxiesSortType.name => _sortOfName(proxies), }; } clearEffect(String profileId) async { final profilePath = await appPath.getProfilePath(profileId); final providersPath = await appPath.getProvidersPath(profileId); return await Isolate.run(() async { if (profilePath != null) { await File(profilePath).delete(recursive: true); } if (providersPath != null) { await File(providersPath).delete(recursive: true); } }); } updateTun() { _ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith.tun(enable: !state.tun.enable), ); } updateSystemProxy() { _ref.read(networkSettingProvider.notifier).updateState( (state) => state.copyWith( systemProxy: !state.systemProxy, ), ); } updateStart() { updateStatus(!_ref.read(runTimeProvider.notifier).isStart); } updateCurrentSelectedMap(String groupName, String proxyName) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile != null && currentProfile.selectedMap[groupName] != proxyName) { final SelectedMap selectedMap = Map.from( currentProfile.selectedMap, )..[groupName] = proxyName; _ref.read(profilesProvider.notifier).setProfile( currentProfile.copyWith( selectedMap: selectedMap, ), ); } } updateCurrentUnfoldSet(Set value) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile == null) { return; } _ref.read(profilesProvider.notifier).setProfile( currentProfile.copyWith( unfoldSet: value, ), ); } changeMode(Mode mode) { _ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith(mode: mode), ); // if (mode == Mode.global) { // updateCurrentGroupName(GroupName.GLOBAL.name); // } // addCheckIpNumDebounce(); } updateAutoLaunch() { _ref.read(appSettingProvider.notifier).updateState( (state) => state.copyWith( autoLaunch: !state.autoLaunch, ), ); } updateVisible() async { final visible = await window?.isVisible(); if (visible != null && !visible) { window?.show(); } else { window?.hide(); } } updateMode() { _ref.read(patchClashConfigProvider.notifier).updateState( (state) { final index = Mode.values.indexWhere((item) => item == state.mode); if (index == -1) { return null; } final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; return state.copyWith( mode: Mode.values[nextIndex], ); }, ); } Future exportLogs() async { final logsRaw = _ref.read(logsProvider).list.map( (item) => item.toString(), ); final data = await Isolate.run>(() async { final logsRawString = logsRaw.join("\n"); return utf8.encode(logsRawString); }); return await picker.saveFile( other.logFile, Uint8List.fromList(data), ) != null; } Future> backupData() async { final homeDirPath = await appPath.homeDirPath; final profilesPath = await appPath.profilesPath; final configJson = globalState.config.toJson(); return Isolate.run>(() async { final archive = Archive(); archive.add("config.json", configJson); await archive.addDirectoryToArchive(profilesPath, homeDirPath); final zipEncoder = ZipEncoder(); return zipEncoder.encode(archive) ?? []; }); } updateTray([bool focus = false]) async { tray.update( trayState: _ref.read(trayStateProvider), ); } recoveryData( List data, RecoveryOption recoveryOption, ) async { final archive = await Isolate.run(() { final zipDecoder = ZipDecoder(); return zipDecoder.decodeBytes(data); }); final homeDirPath = await appPath.homeDirPath; 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"); if (configIndex == -1) throw "invalid backup file"; final configFile = configs[configIndex]; var tempConfig = Config.compatibleFromJson( json.decode( utf8.decode(configFile.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); } final clashConfigIndex = configs.indexWhere((config) => config.name == "clashConfig.json"); if (clashConfigIndex != -1) { final clashConfigFile = configs[clashConfigIndex]; tempConfig = tempConfig.copyWith( patchClashConfig: ClashConfig.fromJson( json.decode( utf8.decode( clashConfigFile.content, ), ), ), ); } _recovery( tempConfig, recoveryOption, ); } _recovery(Config config, RecoveryOption recoveryOption) { final profiles = config.profiles; for (final profile in profiles) { _ref.read(profilesProvider.notifier).setProfile(profile); } final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles; if (onlyProfiles) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile != null) { _ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; } return; } _ref.read(patchClashConfigProvider.notifier).value = config.patchClashConfig; _ref.read(appSettingProvider.notifier).value = config.appSetting; _ref.read(currentProfileIdProvider.notifier).value = config.currentProfileId; _ref.read(appDAVSettingProvider.notifier).value = config.dav; _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.proxiesStyle; _ref.read(overrideDnsProvider.notifier).value = config.overrideDns; _ref.read(networkSettingProvider.notifier).value = config.networkProps; _ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions; } }