diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt index d40ae0ba..f2656c25 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt @@ -2,6 +2,7 @@ package com.hiddify.hiddify import android.util.Log import com.hiddify.hiddify.bg.BoxService +import com.hiddify.hiddify.constant.Alert import com.hiddify.hiddify.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -24,6 +25,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin, enum class Trigger(val method: String) { ParseConfig("parse_config"), ChangeConfigOptions("change_config_options"), + GenerateConfig("generate_config"), Start("start"), Stop("stop"), Restart("restart"), @@ -70,6 +72,21 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin, } } + Trigger.GenerateConfig.method -> { + scope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + val path = args["path"] as String + val options = Settings.configOptions + if (options.isBlank() || path.isBlank()) { + error("blank properties") + } + val config = BoxService.buildConfig(path, options) + success(config) + } + } + } + Trigger.Start.method -> { scope.launch { result.runCatching { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt index 41fa05e1..6268b739 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt @@ -72,6 +72,10 @@ class BoxService( } } + fun buildConfig(path: String, options: String):String { + return Mobile.buildConfig(path, options) + } + fun start() { val intent = runBlocking { withContext(Dispatchers.IO) { diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index ad62901c..54bc6b2a 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "Failed to update profile", "successMsg": "Profile updated successfully" }, + "share": { + "buttonText": "Share", + "exportConfigToClipboard": "Export configuration to clipboard", + "exportConfigToClipboardSuccess": "Configuration copied to clipboard" + }, "edit": { "buttonTxt": "Edit", "selectActiveTxt": "Select active profile" diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 6e4f06de..7595fbad 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "در بروزرسانی پروفایل خطایی رخ داد", "successMsg": "پروفایل با موفقیت بروزرسانی شد" }, + "share": { + "buttonText": "اشتراک گذاری", + "exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد", + "exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد" + }, "edit": { "buttonTxt": "ویرایش", "selectActiveTxt": "انتخاب پروفایل فعال" diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index b7d6a63a..21d6b34a 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "Ошибка обновления", "successMsg": "Профиль успешно обновлён" }, + "share": { + "buttonText": "Делиться", + "exportConfigToClipboard": "Экспортировать конфигурацию в буфер обмена", + "exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена." + }, "edit": { "buttonTxt": "Изменить", "selectActiveTxt": "Выберите активный профиль" diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 54e5030d..6e3ced28 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -67,6 +67,11 @@ "failureMsg": "更新配置文件失败", "successMsg": "配置文件更新成功" }, + "share": { + "buttonText": "分享", + "exportConfigToClipboard": "将配置导出到剪贴板", + "exportConfigToClipboardSuccess": "配置已复制到剪贴板" + }, "edit": { "buttonTxt": "编辑", "selectActiveTxt": "选择活动配置文件" diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index c779e61c..d6d88279 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -90,6 +90,27 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } + @override + TaskEither generateConfig( + String fileName, + ) { + return exceptionHandler( + () { + final configPath = filesEditor.configPath(fileName); + final options = configOptions(); + return setup() + .andThen(() => changeConfigOptions(options)) + .andThen( + () => singbox + .generateConfig(configPath) + .mapLeft(CoreServiceFailure.other), + ) + .run(); + }, + CoreServiceFailure.unexpected, + ); + } + @override TaskEither start( String fileName, diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart index 7b63ee9d..dd7a9544 100644 --- a/lib/domain/singbox/singbox_facade.dart +++ b/lib/domain/singbox/singbox_facade.dart @@ -18,6 +18,10 @@ abstract interface class SingboxFacade { ConfigOptions options, ); + TaskEither generateConfig( + String fileName, + ); + TaskEither start( String fileName, bool disableMemoryLimit, diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart index b33f8c2a..8d0ba2ab 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/common/profile_tile.dart @@ -254,6 +254,14 @@ class ProfileActionsMenu extends HookConsumerWidget { initialOnSuccess: () => CustomToast.success(t.profile.update.successMsg).show(context), ); + final exportConfigMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentShortError(err)).show(context); + }, + initialOnSuccess: () => + CustomToast.success(t.profile.share.exportConfigToClipboardSuccess) + .show(context), + ); final deleteProfileMutation = useMutation( initialOnFailure: (err) { CustomAlertDialog.fromErr(t.presentError(err)).show(context); @@ -278,6 +286,25 @@ class ProfileActionsMenu extends HookConsumerWidget { ); }, ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + child: Text(t.profile.share.exportConfigToClipboard), + onPressed: () async { + if (exportConfigMutation.state.isInProgress) { + return; + } + exportConfigMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .exportConfigToClipboard(profile), + ); + }, + ), + ], + leadingIcon: const Icon(Icons.share), + child: Text(t.profile.share.buttonText), + ), MenuItemButton( leadingIcon: const Icon(Icons.edit), child: Text(t.profile.edit.buttonTxt), diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart index 87266d2a..6fb165e5 100644 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ b/lib/features/profiles/notifier/profiles_notifier.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/data/data_providers.dart'; @@ -124,4 +125,16 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { }, ).run(); } + + Future exportConfigToClipboard(Profile profile) async { + await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + (err) { + loggy.warning('error generating config', err); + throw err; + }, + (configJson) async { + await Clipboard.setData(ClipboardData(text: configJson)); + }, + ).run(); + } } diff --git a/lib/gen/singbox_generated_bindings.dart b/lib/gen/singbox_generated_bindings.dart index 25279a7c..6bbdb039 100644 --- a/lib/gen/singbox_generated_bindings.dart +++ b/lib/gen/singbox_generated_bindings.dart @@ -934,6 +934,21 @@ class SingboxNativeLibrary { late final _changeConfigOptions = _changeConfigOptionsPtr .asFunction Function(ffi.Pointer)>(); + ffi.Pointer generateConfig( + ffi.Pointer path, + ) { + return _generateConfig( + path, + ); + } + + late final _generateConfigPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('generateConfig'); + late final _generateConfig = _generateConfigPtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer start( ffi.Pointer configPath, int disableMemoryLimit, diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/services/singbox/ffi_singbox_service.dart index 5d9aa308..87124e55 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/services/singbox/ffi_singbox_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:combine/combine.dart'; +import 'package:dartx/dartx.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; @@ -137,6 +138,28 @@ class FFISingboxService ); } + @override + TaskEither generateConfig( + String path, + ) { + return TaskEither( + () => CombineWorker().execute( + () { + final response = _box + .generateConfig( + path.toNativeUtf8().cast(), + ) + .cast() + .toDartString(); + if (response.startsWith("error")) { + return left(response.removePrefix("error")); + } + return right(response); + }, + ), + ); + } + @override TaskEither start(String configPath, bool disableMemoryLimit) { loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/services/singbox/mobile_singbox_service.dart index 08887bcf..b27ec2ab 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/services/singbox/mobile_singbox_service.dart @@ -73,6 +73,24 @@ class MobileSingboxService ); } + @override + TaskEither generateConfig( + String path, + ) { + return TaskEither( + () async { + final configJson = await _methodChannel.invokeMethod( + "generate_config", + {"path": path}, + ); + if (configJson == null || configJson.isEmpty) { + return left("null response"); + } + return right(configJson); + }, + ); + } + @override TaskEither start(String configPath, bool disableMemoryLimit) { return TaskEither( diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart index 0d89ab88..ac1d4d90 100644 --- a/lib/services/singbox/singbox_service.dart +++ b/lib/services/singbox/singbox_service.dart @@ -33,6 +33,10 @@ abstract interface class SingboxService { TaskEither changeConfigOptions(ConfigOptions options); + TaskEither generateConfig( + String path, + ); + TaskEither start(String configPath, bool disableMemoryLimit); TaskEither stop(); diff --git a/libcore b/libcore index 5c8b283d..953e6a02 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 5c8b283d9cd84a4b1412dd65572ae039a822de78 +Subproject commit 953e6a02d73d0a2894071e289ff209b633470e54