diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db102505..1f01aef9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ on: env: IS_GITHUB_ACTIONS: 1 CHANNEL: "${{ inputs.channel }}" - FLUTTER_VERSION: '3.19.x' + FLUTTER_VERSION: '3.22.x' NDK_VERSION: r26b UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" TAG_NAME: "${{ inputs.tag-name }}" diff --git a/.vscode/launch.json b/.vscode/launch.json index fa9c6ea9..373f8b4c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,16 @@ { "version": "0.2.0", "configurations": [ + { + "name": "go Package", + "type": "go", + "request": "launch", + "mode": "auto", + "cwd": "./libcore", + "program": "./libcore/cli/main.go", + "args": ["build","-c","a.txt","-d","b.txt","--full-config"] , + "buildFlags": "-tags with_clash_api,with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server" + }, { "name": "Hiddify Dev", "request": "launch", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bedb24e9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "dart.lineLength": 250, + "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code", + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.tabSize": 2, + "editor.rulers": [ + 250 + ], + "editor.detectIndentation": false, + "editor.selectionHighlight": false, + "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.suggestSelection": "first", + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": "off" + }, + + "html.format.wrapLineLength": 250, + +} \ No newline at end of file diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index e4f24d7e..830c2ff1 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -78,8 +78,10 @@ "permissionRequest": "Permission to camera to scan QR Code" }, "manually": "Manual Entry", + "addWarp": "Add Warp", "addingProfileMsg": "Adding Profile", "failureMsg": "Failed to Add Profile" + }, "update": { "buttonTxt": "Update", @@ -162,8 +164,8 @@ "requiresRestartMsg": "For this to take effect restart the app", "experimental": "Experimental", "experimentalMsg": "Features with Experimental flag are still in development and might cause issues.", - "exportOptions": "Export Options to Clipboard", - "exportAllOptions": "Export Options to Clipboard (Debug)", + "exportOptions": "Copy Anonymous Options to Clipboard", + "exportAllOptions": "Copy All Options to Clipboard", "importOptions": "Import Options From Clipboard", "importOptionsMsg": "This will rewrite all config options with provided values. Are you sure?", "general": { diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 4c17d07b..60dd17c9 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -162,8 +162,8 @@ "requiresRestartMsg": "برای اعمال این تنظیم، برنامه را دوباره راه‌اندازی کنید", "experimental": "آزمایشی", "experimentalMsg": "تنظیماتی که عنوان آزمایشی دارند، هم‌چنان در دست توسعه هستند و فعال‌سازی آن‌ها می‌تواند باعث بروز مشکلاتی شود. ", - "exportOptions": "صادر کردن تنظیمات به کلیپ‌بورد", - "exportAllOptions": "صادر کردن تنظیمات به کلیپ‌بورد (اشکال‌زدایی)", + "exportOptions": "کپی تنظیمات ساده به کلیپ‌بورد", + "exportAllOptions": "کپی همه تنظیمات به کلیپ‌بورد", "importOptions": "وارد کردن تنظیمات از کلیپ‌بورد", "importOptionsMsg": "این اقدام همه‌ی تنظیمات پیکربندی را با مقادیر اولیه بازنویسی می‌کند. آیا مطمئن هستید؟", "general": { diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 85ffe63b..6189c587 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -162,8 +162,7 @@ "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение.", "experimental": "Экспериментальный", "experimentalMsg": "Функции с флагом «Экспериментально» все еще находятся в разработке и могут вызвать проблемы.", - "exportOptions": "Экспорт параметров в буфер обмена", - "exportAllOptions": "Экспорт параметров в буфер обмена (отладка)", + "importOptions": "Импорт параметров из буфера обмена", "importOptionsMsg": "Это перезапишет все параметры конфига предоставленными значениями. Вы уверены?", "general": { diff --git a/assets/translations/strings_zh-CN.i18n.json b/assets/translations/strings_zh-CN.i18n.json index 4db410c4..1e36a4a0 100644 --- a/assets/translations/strings_zh-CN.i18n.json +++ b/assets/translations/strings_zh-CN.i18n.json @@ -162,8 +162,7 @@ "requiresRestartMsg": "要使其生效,请重新启动应用程序", "experimental": "实验性选项", "experimentalMsg": "带有实验标志的功能仍在开发中,可能会出现问题。", - "exportOptions": "导出选项到剪切板", - "exportAllOptions": "导出选项到剪切板(用于调试)", + "importOptions": "从剪切板导入选项", "importOptionsMsg": "这将使用提供的值重写所有配置选项。您确定吗?", "general": { diff --git a/lib/core/analytics/analytics_controller.dart b/lib/core/analytics/analytics_controller.dart index ad0f7649..a27e6498 100644 --- a/lib/core/analytics/analytics_controller.dart +++ b/lib/core/analytics/analytics_controller.dart @@ -23,8 +23,7 @@ class AnalyticsController extends _$AnalyticsController with AppLogger { return _preferences.getBool(enableAnalyticsPrefKey) ?? true; } - SharedPreferences get _preferences => - ref.read(sharedPreferencesProvider).requireValue; + SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue; Future enableAnalytics() async { if (state case AsyncData(value: final enabled)) { diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index 14f5d6cd..bc61a9c3 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -51,8 +51,7 @@ abstract class ConfigOptions { validator: (value) => value.isNotBlank, ); - static final remoteDnsDomainStrategy = - PreferencesNotifier.create( + static final remoteDnsDomainStrategy = PreferencesNotifier.create( "remote-dns-domain-strategy", DomainStrategy.auto, mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value), @@ -65,8 +64,7 @@ abstract class ConfigOptions { validator: (value) => value.isNotBlank, ); - static final directDnsDomainStrategy = - PreferencesNotifier.create( + static final directDnsDomainStrategy = PreferencesNotifier.create( "direct-dns-domain-strategy", DomainStrategy.auto, mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value), @@ -91,8 +89,7 @@ abstract class ConfigOptions { validator: (value) => isPort(value.toString()), ); - static final tunImplementation = - PreferencesNotifier.create( + static final tunImplementation = PreferencesNotifier.create( "tun-implementation", TunImplementation.mixed, mapFrom: TunImplementation.values.byName, @@ -101,8 +98,7 @@ abstract class ConfigOptions { static final mtu = PreferencesNotifier.create("mtu", 9000); - static final strictRoute = - PreferencesNotifier.create("strict-route", true); + static final strictRoute = PreferencesNotifier.create("strict-route", true); static final connectionTestUrl = PreferencesNotifier.create( "connection-test-url", @@ -128,8 +124,7 @@ abstract class ConfigOptions { validator: (value) => isPort(value.toString()), ); - static final bypassLan = - PreferencesNotifier.create("bypass-lan", false); + static final bypassLan = PreferencesNotifier.create("bypass-lan", false); static final allowConnectionFromLan = PreferencesNotifier.create( "allow-connection-from-lan", @@ -156,16 +151,14 @@ abstract class ConfigOptions { false, ); - static final tlsFragmentSize = - PreferencesNotifier.create( + static final tlsFragmentSize = PreferencesNotifier.create( "tls-fragment-size", const OptionalRange(min: 1, max: 500), mapFrom: OptionalRange.parse, mapTo: const OptionalRangeJsonConverter().toJson, ); - static final tlsFragmentSleep = - PreferencesNotifier.create( + static final tlsFragmentSleep = PreferencesNotifier.create( "tls-fragment-sleep", const OptionalRange(min: 0, max: 500), mapFrom: OptionalRange.parse, @@ -182,8 +175,7 @@ abstract class ConfigOptions { false, ); - static final tlsPaddingSize = - PreferencesNotifier.create( + static final tlsPaddingSize = PreferencesNotifier.create( "tls-padding-size", const OptionalRange(min: 1, max: 1500), mapFrom: OptionalRange.parse, @@ -218,8 +210,7 @@ abstract class ConfigOptions { false, ); - static final warpDetourMode = - PreferencesNotifier.create( + static final warpDetourMode = PreferencesNotifier.create( "warp-detour-mode", WarpDetourMode.proxyOverWarp, mapFrom: WarpDetourMode.values.byName, @@ -230,16 +221,28 @@ abstract class ConfigOptions { "warp-license-key", "", ); + static final warp2LicenseKey = PreferencesNotifier.create( + "warp2s-license-key", + "", + ); static final warpAccountId = PreferencesNotifier.create( "warp-account-id", "", ); + static final warp2AccountId = PreferencesNotifier.create( + "warp2-account-id", + "", + ); static final warpAccessToken = PreferencesNotifier.create( "warp-access-token", "", ); + static final warp2AccessToken = PreferencesNotifier.create( + "warp2-access-token", + "", + ); static final warpCleanIp = PreferencesNotifier.create( "warp-clean-ip", @@ -259,8 +262,7 @@ abstract class ConfigOptions { mapTo: const OptionalRangeJsonConverter().toJson, ); - static final warpNoiseDelay = - PreferencesNotifier.create( + static final warpNoiseDelay = PreferencesNotifier.create( "warp-noise-delay", const OptionalRange(min: 20, max: 200), mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true), @@ -271,6 +273,10 @@ abstract class ConfigOptions { "warp-wireguard-config", "", ); + static final warp2WireguardConfig = PreferencesNotifier.create( + "warp2-wireguard-config", + "", + ); static final hasExperimentalFeatures = Provider.autoDispose( (ref) { @@ -278,13 +284,7 @@ abstract class ConfigOptions { if (PlatformUtils.isDesktop && mode == ServiceMode.tun) { return true; } - if (ref.watch(enableTlsFragment) || - ref.watch(enableTlsMixedSniCase) || - ref.watch(enableTlsPadding) || - ref.watch(enableMux) || - ref.watch(enableWarp) || - ref.watch(bypassLan) || - ref.watch(allowConnectionFromLan)) { + if (ref.watch(enableTlsFragment) || ref.watch(enableTlsMixedSniCase) || ref.watch(enableTlsPadding) || ref.watch(enableMux) || ref.watch(enableWarp) || ref.watch(bypassLan) || ref.watch(allowConnectionFromLan)) { return true; } @@ -298,10 +298,13 @@ abstract class ConfigOptions { "warp.access-token", "warp.account-id", "warp.wireguard-config", + "warp2.license-key", + "warp2.access-token", + "warp2.account-id", + "warp2.wireguard-config", }; - static final Map> - preferences = { + static final Map> preferences = { "service-mode": serviceMode, "log-level": logLevel, "resolve-destination": resolveDestination, @@ -348,6 +351,10 @@ abstract class ConfigOptions { "warp.noise": warpNoise, "warp.noise-delay": warpNoiseDelay, "warp.wireguard-config": warpWireguardConfig, + "warp2.license-key": warp2LicenseKey, + "warp2.account-id": warp2AccountId, + "warp2.access-token": warp2AccessToken, + "warp2.wireguard-config": warp2WireguardConfig, }; static final singboxConfigOptions = FutureProvider( @@ -386,8 +393,7 @@ abstract class ConfigOptions { }; final geoAssetsRepo = await ref.watch(geoAssetRepositoryProvider.future); - final geoAssets = - await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run(); + final geoAssets = await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run(); final mode = ref.watch(serviceMode); return SingboxConfigOption( @@ -443,6 +449,18 @@ abstract class ConfigOptions { noise: ref.watch(warpNoise), noiseDelay: ref.watch(warpNoiseDelay), ), + warp2: SingboxWarpOption( + enable: ref.watch(enableWarp), + mode: ref.watch(warpDetourMode), + wireguardConfig: ref.watch(warp2WireguardConfig), + licenseKey: ref.watch(warp2LicenseKey), + accountId: ref.watch(warp2AccountId), + accessToken: ref.watch(warp2AccessToken), + cleanIp: ref.watch(warpCleanIp), + cleanPort: ref.watch(warpPort), + noise: ref.watch(warpNoise), + noiseDelay: ref.watch(warpNoiseDelay), + ), geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, @@ -470,8 +488,7 @@ class ConfigOptionRepository with ExceptionHandler, InfraLogger { final GeoAssetRepository geoAssetRepository; final GeoAssetPathResolver geoAssetPathResolver; - TaskEither - getFullSingboxConfigOption() { + TaskEither getFullSingboxConfigOption() { return exceptionHandler( () async { return right(await getConfigOptions()); diff --git a/lib/features/config_option/notifier/warp_option_notifier.dart b/lib/features/config_option/notifier/warp_option_notifier.dart index 5dcdc2ed..b1a32810 100644 --- a/lib/features/config_option/notifier/warp_option_notifier.dart +++ b/lib/features/config_option/notifier/warp_option_notifier.dart @@ -26,20 +26,14 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger { return WarpOptions( consentGiven: consent, - configGeneration: hasWarpConfig - ? const AsyncValue.data("") - : AsyncError(const MissingWarpConfigFailure(), StackTrace.current), + configGeneration: hasWarpConfig ? const AsyncValue.data("") : AsyncError(const MissingWarpConfigFailure(), StackTrace.current), ); } - SharedPreferences get _prefs => - ref.read(sharedPreferencesProvider).requireValue; + SharedPreferences get _prefs => ref.read(sharedPreferencesProvider).requireValue; Future agree() async { - await ref - .read(sharedPreferencesProvider) - .requireValue - .setBool(warpConsentGiven, true); + await ref.read(sharedPreferencesProvider).requireValue.setBool(warpConsentGiven, true); state = state.copyWith(consentGiven: true); await generateWarpConfig(); } @@ -59,15 +53,33 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger { .getOrElse((l) => throw l) .run(); - await ref - .read(ConfigOptions.warpAccountId.notifier) - .update(warp.accountId); - await ref - .read(ConfigOptions.warpAccessToken.notifier) - .update(warp.accessToken); - await ref - .read(ConfigOptions.warpWireguardConfig.notifier) - .update(warp.wireguardConfig); + await ref.read(ConfigOptions.warpAccountId.notifier).update(warp.accountId); + await ref.read(ConfigOptions.warpAccessToken.notifier).update(warp.accessToken); + await ref.read(ConfigOptions.warpWireguardConfig.notifier).update(warp.wireguardConfig); + return warp.log; + }); + + state = state.copyWith(configGeneration: result); + } + + Future generateWarp2Config() async { + if (state.configGeneration.isLoading) return; + state = state.copyWith(configGeneration: const AsyncLoading()); + + final result = await AsyncValue.guard(() async { + final warp = await ref + .read(singboxServiceProvider) + .generateWarpConfig( + licenseKey: ref.read(ConfigOptions.warpLicenseKey), + previousAccountId: ref.read(ConfigOptions.warp2AccountId), + previousAccessToken: ref.read(ConfigOptions.warp2AccessToken), + ) + .getOrElse((l) => throw l) + .run(); + + await ref.read(ConfigOptions.warp2AccountId.notifier).update(warp.accountId); + await ref.read(ConfigOptions.warp2AccessToken.notifier).update(warp.accessToken); + await ref.read(ConfigOptions.warp2WireguardConfig.notifier).update(warp.wireguardConfig); return warp.log; }); diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index c9de0956..3ef2a948 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -23,17 +23,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; enum ConfigOptionSection { - warp; + warp, + fragment; static final _warpKey = GlobalKey(debugLabel: "warp-section-key"); + static final _fragmentKey = GlobalKey(debugLabel: "fragment-section-key"); - GlobalKey get key => switch (this) { _ => _warpKey }; + GlobalKey get key => switch (this) { + ConfigOptionSection.warp => _warpKey, + ConfigOptionSection.fragment => _fragmentKey, + }; } class ConfigOptionsPage extends HookConsumerWidget { - ConfigOptionsPage({super.key, String? section}) - : section = - section != null ? ConfigOptionSection.values.byName(section) : null; + ConfigOptionsPage({super.key, String? section}) : section = section != null ? ConfigOptionSection.values.byName(section) : null; final ConfigOptionSection? section; @@ -47,14 +50,10 @@ class ConfigOptionsPage extends HookConsumerWidget { if (section != null) { WidgetsBinding.instance.addPostFrameCallback( (_) { - final box = - section!.key.currentContext?.findRenderObject() as RenderBox?; + final box = section!.key.currentContext?.findRenderObject() as RenderBox?; final offset = box?.localToGlobal(Offset.zero); if (offset == null) return; - final height = scrollController.offset + - offset.dy - - MediaQueryData.fromView(View.of(context)).padding.top - - kToolbarHeight; + final height = scrollController.offset + offset.dy - MediaQueryData.fromView(View.of(context)).padding.top - kToolbarHeight; scrollController.animateTo( height, duration: const Duration(milliseconds: 500), @@ -83,36 +82,26 @@ class ConfigOptionsPage extends HookConsumerWidget { itemBuilder: (context) { return [ PopupMenuItem( - onTap: () async => ref - .read(configOptionNotifierProvider.notifier) - .exportJsonToClipboard() - .then((success) { + onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard().then((success) { if (success) { - ref - .read(inAppNotificationControllerProvider) - .showSuccessToast( + ref.read(inAppNotificationControllerProvider).showSuccessToast( t.general.clipboardExportSuccessMsg, ); } }), child: Text(t.settings.exportOptions), ), - if (ref.watch(debugModeNotifierProvider)) - PopupMenuItem( - onTap: () async => ref - .read(configOptionNotifierProvider.notifier) - .exportJsonToClipboard(excludePrivate: false) - .then((success) { - if (success) { - ref - .read(inAppNotificationControllerProvider) - .showSuccessToast( - t.general.clipboardExportSuccessMsg, - ); - } - }), - child: Text(t.settings.exportAllOptions), - ), + // if (ref.watch(debugModeNotifierProvider)) + PopupMenuItem( + onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard(excludePrivate: false).then((success) { + if (success) { + ref.read(inAppNotificationControllerProvider).showSuccessToast( + t.general.clipboardExportSuccessMsg, + ); + } + }), + child: Text(t.settings.exportAllOptions), + ), PopupMenuItem( onTap: () async { final shouldImport = await showConfirmationDialog( @@ -121,9 +110,7 @@ class ConfigOptionsPage extends HookConsumerWidget { message: t.settings.importOptionsMsg, ); if (shouldImport) { - await ref - .read(configOptionNotifierProvider.notifier) - .importFromClipboard(); + await ref.read(configOptionNotifierProvider.notifier).importFromClipboard(); } }, child: Text(t.settings.importOptions), @@ -131,9 +118,7 @@ class ConfigOptionsPage extends HookConsumerWidget { PopupMenuItem( child: Text(t.config.resetBtn), onTap: () async { - await ref - .read(configOptionNotifierProvider.notifier) - .resetOption(); + await ref.read(configOptionNotifierProvider.notifier).resetOption(); }, ), ]; @@ -158,15 +143,12 @@ class ConfigOptionsPage extends HookConsumerWidget { SwitchListTile( title: Text(experimental(t.config.bypassLan)), value: ref.watch(ConfigOptions.bypassLan), - onChanged: - ref.watch(ConfigOptions.bypassLan.notifier).update, + onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update, ), SwitchListTile( title: Text(t.config.resolveDestination), value: ref.watch(ConfigOptions.resolveDestination), - onChanged: ref - .watch(ConfigOptions.resolveDestination.notifier) - .update, + onChanged: ref.watch(ConfigOptions.resolveDestination.notifier).update, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.ipv6Mode), @@ -179,28 +161,24 @@ class ConfigOptionsPage extends HookConsumerWidget { SettingsSection(t.config.section.dns), ValuePreferenceWidget( value: ref.watch(ConfigOptions.remoteDnsAddress), - preferences: - ref.watch(ConfigOptions.remoteDnsAddress.notifier), + preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier), title: t.config.remoteDnsAddress, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy), - preferences: ref - .watch(ConfigOptions.remoteDnsDomainStrategy.notifier), + preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier), choices: DomainStrategy.values, title: t.config.remoteDnsDomainStrategy, presentChoice: (value) => value.displayName, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.directDnsAddress), - preferences: - ref.watch(ConfigOptions.directDnsAddress.notifier), + preferences: ref.watch(ConfigOptions.directDnsAddress.notifier), title: t.config.directDnsAddress, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.directDnsDomainStrategy), - preferences: ref - .watch(ConfigOptions.directDnsDomainStrategy.notifier), + preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier), choices: DomainStrategy.values, title: t.config.directDnsDomainStrategy, presentChoice: (value) => value.displayName, @@ -208,9 +186,7 @@ class ConfigOptionsPage extends HookConsumerWidget { SwitchListTile( title: Text(t.config.enableDnsRouting), value: ref.watch(ConfigOptions.enableDnsRouting), - onChanged: ref - .watch(ConfigOptions.enableDnsRouting.notifier) - .update, + onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update, ), // const SettingsDivider(), // SettingsSection(experimental(t.config.section.mux)), @@ -247,13 +223,11 @@ class ConfigOptionsPage extends HookConsumerWidget { SwitchListTile( title: Text(t.config.strictRoute), value: ref.watch(ConfigOptions.strictRoute), - onChanged: - ref.watch(ConfigOptions.strictRoute.notifier).update, + onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.tunImplementation), - preferences: - ref.watch(ConfigOptions.tunImplementation.notifier), + preferences: ref.watch(ConfigOptions.tunImplementation.notifier), choices: TunImplementation.values, title: t.config.tunImplementation, presentChoice: (value) => value.name, @@ -287,25 +261,21 @@ class ConfigOptionsPage extends HookConsumerWidget { experimental(t.config.allowConnectionFromLan), ), value: ref.watch(ConfigOptions.allowConnectionFromLan), - onChanged: ref - .read(ConfigOptions.allowConnectionFromLan.notifier) - .update, + onChanged: ref.read(ConfigOptions.allowConnectionFromLan.notifier).update, ), const SettingsDivider(), SettingsSection( experimental(t.config.section.tlsTricks), + key: ConfigOptionSection._fragmentKey, ), SwitchListTile( title: Text(t.config.enableTlsFragment), value: ref.watch(ConfigOptions.enableTlsFragment), - onChanged: ref - .watch(ConfigOptions.enableTlsFragment.notifier) - .update, + onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.tlsFragmentSize), - preferences: - ref.watch(ConfigOptions.tlsFragmentSize.notifier), + preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier), title: t.config.tlsFragmentSize, inputToValue: OptionalRange.tryParse, presentValue: (value) => value.present(t), @@ -313,8 +283,7 @@ class ConfigOptionsPage extends HookConsumerWidget { ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.tlsFragmentSleep), - preferences: - ref.watch(ConfigOptions.tlsFragmentSleep.notifier), + preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier), title: t.config.tlsFragmentSleep, inputToValue: OptionalRange.tryParse, presentValue: (value) => value.present(t), @@ -323,21 +292,16 @@ class ConfigOptionsPage extends HookConsumerWidget { SwitchListTile( title: Text(t.config.enableTlsMixedSniCase), value: ref.watch(ConfigOptions.enableTlsMixedSniCase), - onChanged: ref - .watch(ConfigOptions.enableTlsMixedSniCase.notifier) - .update, + onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update, ), SwitchListTile( title: Text(t.config.enableTlsPadding), value: ref.watch(ConfigOptions.enableTlsPadding), - onChanged: ref - .watch(ConfigOptions.enableTlsPadding.notifier) - .update, + onChanged: ref.watch(ConfigOptions.enableTlsPadding.notifier).update, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.tlsPaddingSize), - preferences: - ref.watch(ConfigOptions.tlsPaddingSize.notifier), + preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier), title: t.config.tlsPaddingSize, inputToValue: OptionalRange.tryParse, presentValue: (value) => value.format(), @@ -350,38 +314,26 @@ class ConfigOptionsPage extends HookConsumerWidget { SettingsSection(t.config.section.misc), ValuePreferenceWidget( value: ref.watch(ConfigOptions.connectionTestUrl), - preferences: - ref.watch(ConfigOptions.connectionTestUrl.notifier), + preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier), title: t.config.connectionTestUrl, ), ListTile( title: Text(t.config.urlTestInterval), subtitle: Text( - ref - .watch(ConfigOptions.urlTestInterval) - .toApproximateTime(isRelativeToNow: false), + ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false), ), onTap: () async { final urlTestInterval = await SettingsSliderDialog( title: t.config.urlTestInterval, - initialValue: ref - .watch(ConfigOptions.urlTestInterval) - .inMinutes - .coerceIn(0, 60) - .toDouble(), - onReset: ref - .read(ConfigOptions.urlTestInterval.notifier) - .reset, + initialValue: ref.watch(ConfigOptions.urlTestInterval).inMinutes.coerceIn(0, 60).toDouble(), + onReset: ref.read(ConfigOptions.urlTestInterval.notifier).reset, min: 1, max: 60, divisions: 60, - labelGen: (value) => Duration(minutes: value.toInt()) - .toApproximateTime(isRelativeToNow: false), + labelGen: (value) => Duration(minutes: value.toInt()).toApproximateTime(isRelativeToNow: false), ).show(context); if (urlTestInterval == null) return; - await ref - .read(ConfigOptions.urlTestInterval.notifier) - .update(Duration(minutes: urlTestInterval.toInt())); + await ref.read(ConfigOptions.urlTestInterval.notifier).update(Duration(minutes: urlTestInterval.toInt())); }, ), ValuePreferenceWidget( diff --git a/lib/features/config_option/overview/warp_options_widgets.dart b/lib/features/config_option/overview/warp_options_widgets.dart index 112487e4..ba02bf00 100644 --- a/lib/features/config_option/overview/warp_options_widgets.dart +++ b/lib/features/config_option/overview/warp_options_widgets.dart @@ -63,17 +63,15 @@ class WarpOptionsTiles extends HookConsumerWidget { AsyncLoading() => const LinearProgressIndicator(), AsyncError() => Text( t.config.missingWarpConfig, - style: - TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), _ => null, } : null, enabled: canChangeOptions, onTap: () async { - await ref - .read(warpOptionNotifierProvider.notifier) - .generateWarpConfig(); + await ref.read(warpOptionNotifierProvider.notifier).generateWarpConfig(); + await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config(); }, ), ChoicePreferenceWidget( @@ -111,8 +109,7 @@ class WarpOptionsTiles extends HookConsumerWidget { preferences: ref.watch(ConfigOptions.warpNoise.notifier), enabled: canChangeOptions, title: t.config.warpNoise, - inputToValue: (input) => - OptionalRange.tryParse(input, allowEmpty: true), + inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true), presentValue: (value) => value.present(t), formatInputValue: (value) => value.format(), ), @@ -121,8 +118,7 @@ class WarpOptionsTiles extends HookConsumerWidget { preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier), enabled: canChangeOptions, title: t.config.warpNoiseDelay, - inputToValue: (input) => - OptionalRange.tryParse(input, allowEmpty: true), + inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true), presentValue: (value) => value.present(t), formatInputValue: (value) => value.format(), ), diff --git a/lib/features/config_option/widget/quick_settings_modal.dart b/lib/features/config_option/widget/quick_settings_modal.dart index e5acea73..f3ffb578 100644 --- a/lib/features/config_option/widget/quick_settings_modal.dart +++ b/lib/features/config_option/widget/quick_settings_modal.dart @@ -16,8 +16,7 @@ class QuickSettingsModal extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final warpPrefaceCompleted = - ref.watch(warpOptionNotifierProvider).consentGiven; + final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider).consentGiven; return SingleChildScrollView( child: Column( @@ -33,37 +32,41 @@ class QuickSettingsModal extends HookConsumerWidget { e.presentShort(t), overflow: TextOverflow.ellipsis, ), - tooltip: - e.isExperimental ? t.settings.experimental : null, + tooltip: e.isExperimental ? t.settings.experimental : null, ), ) .toList(), selected: {ref.watch(ConfigOptions.serviceMode)}, - onSelectionChanged: (newSet) => ref - .read(ConfigOptions.serviceMode.notifier) - .update(newSet.first), + onSelectionChanged: (newSet) => ref.read(ConfigOptions.serviceMode.notifier).update(newSet.first), ), ), const Gap(8), if (warpPrefaceCompleted) - SwitchListTile( - value: ref.watch(ConfigOptions.enableWarp), - onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update, - title: Text(t.config.enableWarp), + GestureDetector( + onLongPress: () { + ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context); + }, + child: SwitchListTile( + value: ref.watch(ConfigOptions.enableWarp), + onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update, + title: Text(t.config.enableWarp), + ), ) else ListTile( title: Text(t.config.setupWarp), trailing: const Icon(FluentIcons.chevron_right_24_regular), - onTap: () => - ConfigOptionsRoute(section: ConfigOptionSection.warp.name) - .go(context), + onTap: () => ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context), + ), + GestureDetector( + onLongPress: () { + ConfigOptionsRoute(section: ConfigOptionSection.fragment.name).go(context); + }, + child: SwitchListTile( + value: ref.watch(ConfigOptions.enableTlsFragment), + onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, + title: Text(t.config.enableTlsFragment), ), - SwitchListTile( - value: ref.watch(ConfigOptions.enableTlsFragment), - onChanged: - ref.watch(ConfigOptions.enableTlsFragment.notifier).update, - title: Text(t.config.enableTlsFragment), ), // SwitchListTile( // value: ref.watch(ConfigOptions.enableMux), diff --git a/lib/features/profile/add/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart index 5752d532..7e5989ad 100644 --- a/lib/features/profile/add/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -5,11 +5,16 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; +import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart'; +import 'package:hiddify/features/config_option/overview/config_options_page.dart'; +import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart'; import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class AddProfileModal extends HookConsumerWidget { const AddProfileModal({ @@ -17,7 +22,7 @@ class AddProfileModal extends HookConsumerWidget { this.url, this.scrollController, }); - + static const warpConsentGiven = "warp_consent_given"; final String? url; final ScrollController? scrollController; @@ -58,8 +63,7 @@ class AddProfileModal extends HookConsumerWidget { child: LayoutBuilder( builder: (context, constraints) { // temporary solution, aspect ratio widget relies on height and in a row there no height! - final buttonWidth = - constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2)); + final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2)); return AnimatedCrossFade( firstChild: SizedBox( @@ -93,8 +97,7 @@ class AddProfileModal extends HookConsumerWidget { secondChild: Column( children: [ Padding( - padding: - const EdgeInsets.symmetric(horizontal: buttonsPadding), + padding: const EdgeInsets.symmetric(horizontal: buttonsPadding), child: Row( children: [ _Button( @@ -103,13 +106,9 @@ class AddProfileModal extends HookConsumerWidget { icon: FluentIcons.clipboard_paste_24_regular, size: buttonWidth, onTap: () async { - final captureResult = - await Clipboard.getData(Clipboard.kTextPlain) - .then((value) => value?.text ?? ''); + final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? ''); if (addProfileState.isLoading) return; - ref - .read(addProfileProvider.notifier) - .add(captureResult); + ref.read(addProfileProvider.notifier).add(captureResult); }, ), const Gap(buttonsGap), @@ -120,8 +119,7 @@ class AddProfileModal extends HookConsumerWidget { icon: FluentIcons.qr_code_24_regular, size: buttonWidth, onTap: () async { - final cr = - await QRCodeScannerScreen().open(context); + final cr = await QRCodeScannerScreen().open(context); if (cr == null) return; if (addProfileState.isLoading) return; @@ -142,56 +140,118 @@ class AddProfileModal extends HookConsumerWidget { ], ), ), - if (!PlatformUtils.isDesktop) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: buttonsPadding, - vertical: 16, - ), - child: Semantics( - button: true, - child: SizedBox( - height: 36, - child: Material( - key: const ValueKey("add_manually_button"), - elevation: 8, - color: theme.colorScheme.surface, - surfaceTintColor: theme.colorScheme.surfaceTint, - shadowColor: Colors.transparent, - borderRadius: BorderRadius.circular(8), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () async { - context.pop(); - await const NewProfileRoute().push(context); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - FluentIcons.add_24_regular, - color: theme.colorScheme.primary, - ), - const Gap(8), - Text( - t.profile.add.manually, - style: theme.textTheme.labelLarge?.copyWith( + Padding( + padding: const EdgeInsets.symmetric( + horizontal: buttonsPadding, + vertical: 16, + ), + child: Column( + children: [ + Semantics( + button: true, + child: SizedBox( + height: 36, + child: Material( + key: const ValueKey("add_warp_button"), + elevation: 8, + color: theme.colorScheme.surface, + surfaceTintColor: theme.colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + Future.microtask(() async { + context.pop(); + final _prefs = ref.read(sharedPreferencesProvider).requireValue; + final consent = _prefs.getBool(warpConsentGiven) ?? false; + if (!consent) { + final agreed = await showDialog( + context: context, + builder: (context) => const WarpLicenseAgreementModal(), + ); + + if (agreed ?? false) { + await ref.read(warpOptionNotifierProvider.notifier).agree(); + } + } + + final accountId = _prefs.getString("warp2-account-id"); + final accessToken = _prefs.getString("warp2-access-token"); + final hasWarp2Config = accountId != null && accessToken != null; + + if (!hasWarp2Config) { + await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config(); + } + await ref.read(addProfileProvider.notifier).add("#profile-title: Hiddify WARP\nwarp://p2@auto#Remote&&detour=warp://p1@auto#Local"); // + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + FluentIcons.add_24_regular, color: theme.colorScheme.primary, ), - ), - ], + const SizedBox(width: 8), + Text( + t.profile.add.addWarp, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), ), ), ), ), - ), + if (!PlatformUtils.isDesktop) const SizedBox(height: 16), // Spacing between the buttons + if (!PlatformUtils.isDesktop) + Semantics( + button: true, + child: SizedBox( + height: 36, + child: Material( + key: const ValueKey("add_manually_button"), + elevation: 8, + color: theme.colorScheme.surface, + surfaceTintColor: theme.colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + context.pop(); + await const NewProfileRoute().push(context); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + FluentIcons.add_24_regular, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + t.profile.add.manually, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), + ), + ], ), + ), const Gap(24), ], ), - crossFadeState: addProfileState.isLoading - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, + crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 250), ); }, diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart index 98c1368e..d351d999 100644 --- a/lib/features/profile/notifier/profile_notifier.dart +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -47,8 +47,7 @@ class AddProfile extends _$AddProfile with AppLogger { return const AsyncData(null); } - ProfileRepository get _profilesRepo => - ref.read(profileRepositoryProvider).requireValue; + ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue; CancelToken? _cancelToken; Future add(String rawInput) async { @@ -57,8 +56,7 @@ class AddProfile extends _$AddProfile with AppLogger { state = await AsyncValue.guard( () async { final activeProfile = await ref.read(activeProfileProvider.future); - final markAsActive = - activeProfile == null || ref.read(Preferences.markNewProfileActive); + final markAsActive = activeProfile == null || ref.read(Preferences.markNewProfileActive); final TaskEither task; if (LinkParser.parse(rawInput) case (final link)?) { loggy.debug("adding profile, url: [${link.url}]"); @@ -70,7 +68,11 @@ class AddProfile extends _$AddProfile with AppLogger { } else if (LinkParser.protocol(rawInput) case (final parsed)?) { loggy.debug("adding profile, content"); var name = parsed.name; - + var oldItem = await _profilesRepo.getByName(name); + if (name == "Hiddify WARP" && oldItem != null) { + _profilesRepo.setAsActive(oldItem.id).run(); + return unit; + } while (await _profilesRepo.getByName(name) != null) { name += '${randomInt(0, 9).run()}'; } @@ -122,8 +124,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger { return const AsyncData(null); } - ProfileRepository get _profilesRepo => - ref.read(profileRepositoryProvider).requireValue; + ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue; Future updateProfile(RemoteProfileEntity profile) async { if (state.isLoading) return; @@ -143,9 +144,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger { await ref.read(activeProfileProvider.future).then((active) async { if (active != null && active.id == profile.id) { - await ref - .read(connectionNotifierProvider.notifier) - .reconnect(profile); + await ref.read(connectionNotifierProvider.notifier).reconnect(profile); } }); return unit; diff --git a/lib/features/proxy/overview/proxies_overview_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart index deb27f38..d4f96ee8 100644 --- a/lib/features/proxy/overview/proxies_overview_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:combine/combine.dart'; import 'package:dartx/dartx.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; @@ -85,46 +85,41 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger { List proxies, ProxiesSort sortBy, ) async { - return CombineWorker().execute( - () { - final groupWithSelected = { - for (final o in proxies) o.tag: o.selected, - }; - final sortedProxies = []; - for (final group in proxies) { - final sortedItems = switch (sortBy) { - ProxiesSort.name => group.items.sortedWith((a, b) { - if (a.type.isGroup && !b.type.isGroup) return -1; - if (!a.type.isGroup && b.type.isGroup) return 1; - return a.tag.compareTo(b.tag); - }), - ProxiesSort.delay => group.items.sortedWith((a, b) { - if (a.type.isGroup && !b.type.isGroup) return -1; - if (!a.type.isGroup && b.type.isGroup) return 1; + final groupWithSelected = { + for (final o in proxies) o.tag: o.selected, + }; + final sortedProxies = []; + for (final group in proxies) { + final sortedItems = switch (sortBy) { + ProxiesSort.name => group.items.sortedWith((a, b) { + if (a.type.isGroup && !b.type.isGroup) return -1; + if (!a.type.isGroup && b.type.isGroup) return 1; + return a.tag.compareTo(b.tag); + }), + ProxiesSort.delay => group.items.sortedWith((a, b) { + if (a.type.isGroup && !b.type.isGroup) return -1; + if (!a.type.isGroup && b.type.isGroup) return 1; - final ai = a.urlTestDelay; - final bi = b.urlTestDelay; - if (ai == 0 && bi == 0) return -1; - if (ai == 0 && bi > 0) return 1; - if (ai > 0 && bi == 0) return -1; - return ai.compareTo(bi); - }), - ProxiesSort.unsorted => group.items, - }; - final items = []; - for (final item in sortedItems) { - if (groupWithSelected.keys.contains(item.tag)) { - items - .add(item.copyWith(selectedTag: groupWithSelected[item.tag])); - } else { - items.add(item); - } - } - sortedProxies.add(group.copyWith(items: items)); + final ai = a.urlTestDelay; + final bi = b.urlTestDelay; + if (ai == 0 && bi == 0) return -1; + if (ai == 0 && bi > 0) return 1; + if (ai > 0 && bi == 0) return -1; + return ai.compareTo(bi); + }), + ProxiesSort.unsorted => group.items, + }; + final items = []; + for (final item in sortedItems) { + if (groupWithSelected.keys.contains(item.tag)) { + items.add(item.copyWith(selectedTag: groupWithSelected[item.tag])); + } else { + items.add(item); } - return sortedProxies; - }, - ); + } + sortedProxies.add(group.copyWith(items: items)); + } + return sortedProxies; } Future changeProxy(String groupTag, String outboundTag) async { diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart index ef4871c0..3bcd2798 100644 --- a/lib/singbox/model/singbox_config_option.dart +++ b/lib/singbox/model/singbox_config_option.dart @@ -48,6 +48,7 @@ class SingboxConfigOption with _$SingboxConfigOption { required SingboxMuxOption mux, required SingboxTlsTricks tlsTricks, required SingboxWarpOption warp, + required SingboxWarpOption warp2, }) = _SingboxConfigOption; String format() { @@ -55,8 +56,7 @@ class SingboxConfigOption with _$SingboxConfigOption { return encoder.convert(toJson()); } - factory SingboxConfigOption.fromJson(Map json) => - _$SingboxConfigOptionFromJson(json); + factory SingboxConfigOption.fromJson(Map json) => _$SingboxConfigOptionFromJson(json); } @freezed @@ -75,8 +75,7 @@ class SingboxWarpOption with _$SingboxWarpOption { @OptionalRangeJsonConverter() required OptionalRange noiseDelay, }) = _SingboxWarpOption; - factory SingboxWarpOption.fromJson(Map json) => - _$SingboxWarpOptionFromJson(json); + factory SingboxWarpOption.fromJson(Map json) => _$SingboxWarpOptionFromJson(json); } @freezed @@ -89,8 +88,7 @@ class SingboxMuxOption with _$SingboxMuxOption { required MuxProtocol protocol, }) = _SingboxMuxOption; - factory SingboxMuxOption.fromJson(Map json) => - _$SingboxMuxOptionFromJson(json); + factory SingboxMuxOption.fromJson(Map json) => _$SingboxMuxOptionFromJson(json); } @freezed @@ -105,6 +103,5 @@ class SingboxTlsTricks with _$SingboxTlsTricks { @OptionalRangeJsonConverter() required OptionalRange paddingSize, }) = _SingboxTlsTricks; - factory SingboxTlsTricks.fromJson(Map json) => - _$SingboxTlsTricksFromJson(json); + factory SingboxTlsTricks.fromJson(Map json) => _$SingboxTlsTricksFromJson(json); } diff --git a/lib/singbox/service/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart index 83583a0f..889fa91c 100644 --- a/lib/singbox/service/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -4,7 +4,6 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:combine/combine.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/model/directories.dart'; @@ -52,10 +51,7 @@ class FFISingboxService with InfraLogger implements SingboxService { Future init() async { loggy.debug("initializing"); _statusReceiver = ReceivePort('service status receiver'); - final source = _statusReceiver - .asBroadcastStream() - .map((event) => jsonDecode(event as String)) - .map(SingboxStatus.fromEvent); + final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent); _status = ValueConnectableStream.seeded( source, const SingboxStopped(), @@ -69,8 +65,8 @@ class FFISingboxService with InfraLogger implements SingboxService { ) { final port = _statusReceiver.sendPort.nativePort; return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { _box.setupOnce(NativeApi.initializeApiDLData); final err = _box .setup( @@ -98,8 +94,8 @@ class FFISingboxService with InfraLogger implements SingboxService { bool debug, ) { return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final err = _box .parse( path.toNativeUtf8().cast(), @@ -120,13 +116,10 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final json = jsonEncode(options.toJson()); - final err = _box - .changeConfigOptions(json.toNativeUtf8().cast()) - .cast() - .toDartString(); + final err = _box.changeConfigOptions(json.toNativeUtf8().cast()).cast().toDartString(); if (err.isNotEmpty) { return left(err); } @@ -141,8 +134,8 @@ class FFISingboxService with InfraLogger implements SingboxService { String path, ) { return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final response = _box .generateConfig( path.toNativeUtf8().cast(), @@ -166,8 +159,8 @@ class FFISingboxService with InfraLogger implements SingboxService { ) { loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final err = _box .start( configPath.toNativeUtf8().cast(), @@ -187,8 +180,8 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither stop() { return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final err = _box.stop().cast().toDartString(); if (err.isNotEmpty) { return left(err); @@ -207,8 +200,8 @@ class FFISingboxService with InfraLogger implements SingboxService { ) { loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]"); return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final err = _box .restart( configPath.toNativeUtf8().cast(), @@ -265,10 +258,7 @@ class FFISingboxService with InfraLogger implements SingboxService { }, ); - final err = _box - .startCommandClient(1, receiver.sendPort.nativePort) - .cast() - .toDartString(); + final err = _box.startCommandClient(1, receiver.sendPort.nativePort).cast().toDartString(); if (err.isNotEmpty) { loggy.error("error starting status command: $err"); throw err; @@ -310,10 +300,7 @@ class FFISingboxService with InfraLogger implements SingboxService { ); try { - final err = _box - .startCommandClient(5, receiver.sendPort.nativePort) - .cast() - .toDartString(); + final err = _box.startCommandClient(5, receiver.sendPort.nativePort).cast().toDartString(); if (err.isNotEmpty) { logger.error("error starting group command: $err"); throw err; @@ -357,10 +344,7 @@ class FFISingboxService with InfraLogger implements SingboxService { ); try { - final err = _box - .startCommandClient(13, receiver.sendPort.nativePort) - .cast() - .toDartString(); + final err = _box.startCommandClient(13, receiver.sendPort.nativePort).cast().toDartString(); if (err.isNotEmpty) { logger.error("error starting: $err"); throw err; @@ -376,8 +360,8 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither selectOutbound(String groupTag, String outboundTag) { return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final err = _box .selectOutbound( groupTag.toNativeUtf8().cast(), @@ -397,12 +381,9 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither urlTest(String groupTag) { return TaskEither( - () => CombineWorker().execute( - () { - final err = _box - .urlTest(groupTag.toNativeUtf8().cast()) - .cast() - .toDartString(); + () => Future.microtask( + () async { + final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast().toDartString(); if (err.isNotEmpty) { return left(err); } @@ -418,9 +399,7 @@ class FFISingboxService with InfraLogger implements SingboxService { @override Stream> watchLogs(String path) async* { yield await _readLogFile(File(path)); - yield* Watcher(path, pollingDelay: const Duration(seconds: 1)) - .events - .asyncMap((event) async { + yield* Watcher(path, pollingDelay: const Duration(seconds: 1)).events.asyncMap((event) async { if (event.type == ChangeType.MODIFY) { await _readLogFile(File(path)); } @@ -431,17 +410,18 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither clearLogs() { return TaskEither( - () async { - _logBuffer.clear(); - return right(unit); - }, + () => Future.microtask( + () async { + _logBuffer.clear(); + return right(unit); + }, + ), ); } Future> _readLogFile(File file) async { if (_logFilePosition == 0 && file.lengthSync() == 0) return []; - final content = - await file.openRead(_logFilePosition).transform(utf8.decoder).join(); + final content = await file.openRead(_logFilePosition).transform(utf8.decoder).join(); _logFilePosition = file.lengthSync(); final lines = const LineSplitter().convert(content); if (lines.length > 300) { @@ -464,8 +444,8 @@ class FFISingboxService with InfraLogger implements SingboxService { }) { loggy.debug("generating warp config"); return TaskEither( - () => CombineWorker().execute( - () { + () => Future.microtask( + () async { final response = _box .generateWarpConfig( licenseKey.toNativeUtf8().cast(), diff --git a/libcore b/libcore index b2658863..bc48ec07 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit b26588630694cf99f66292d2703476f9deee79d7 +Subproject commit bc48ec07a8cb85c43a8794b8780c7f732af010d1 diff --git a/pubspec.lock b/pubspec.lock index 9a19197a..c447c967 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,14 +218,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - combine: - dependency: "direct main" - description: - name: combine - sha256: "8b52083c822a614a448fdd307e78c05266080e9747604b61fca5ddfe736a6c1e" - url: "https://pub.dev" - source: hosted - version: "0.5.7-0.1.pre" convert: dependency: transitive description: @@ -801,11 +793,12 @@ packages: humanizer: dependency: "direct main" description: - name: humanizer - sha256: "08728a4b6d62accd7d09e668bd54e81e6e09a82c8cfda30553224b3eb868d4f2" - url: "https://pub.dev" - source: hosted - version: "2.2.0" + path: "." + ref: up-version + resolved-ref: "8ae61d68357fae197be7ee71d67ccb9498b9d5c7" + url: "https://github.com/alex-relov/humanizer" + source: git + version: "2.3.0" iconsax_flutter: dependency: transitive description: @@ -850,10 +843,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -922,26 +915,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lint: dependency: "direct dev" description: @@ -1002,10 +995,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1703,26 +1696,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" time: dependency: transitive description: @@ -1935,10 +1928,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3a857efc..b7f29620 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: flutter_localizations: sdk: flutter cupertino_icons: ^1.0.6 - intl: ^0.18.1 + intl: ^0.19.0 slang: ^3.30.1 slang_flutter: ^3.30.0 fpdart: ^1.1.0 @@ -40,7 +40,7 @@ dependencies: launch_at_startup: ^0.2.2 sentry_flutter: ^7.16.1 sentry_dart_plugin: ^1.7.1 - combine: ^0.5.7-0.1.pre + path: ^1.8.3 loggy: ^2.0.3 flutter_loggy: ^2.0.2 @@ -58,7 +58,11 @@ dependencies: percent_indicator: ^4.2.3 sliver_tools: ^0.2.12 flutter_adaptive_scaffold: ^0.1.8 - humanizer: ^2.2.0 + # humanizer: ^2.2.0 + humanizer: + git: + url: https://github.com/alex-relov/humanizer + ref: up-version upgrader: ^9.0.0 toastification: ^1.2.1 version: ^3.0.2