Add export config to clipboard
This commit is contained in:
@@ -2,6 +2,7 @@ package com.hiddify.hiddify
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.hiddify.hiddify.bg.BoxService
|
import com.hiddify.hiddify.bg.BoxService
|
||||||
|
import com.hiddify.hiddify.constant.Alert
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.hiddify.hiddify.constant.Status
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
@@ -24,6 +25,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
|
|||||||
enum class Trigger(val method: String) {
|
enum class Trigger(val method: String) {
|
||||||
ParseConfig("parse_config"),
|
ParseConfig("parse_config"),
|
||||||
ChangeConfigOptions("change_config_options"),
|
ChangeConfigOptions("change_config_options"),
|
||||||
|
GenerateConfig("generate_config"),
|
||||||
Start("start"),
|
Start("start"),
|
||||||
Stop("stop"),
|
Stop("stop"),
|
||||||
Restart("restart"),
|
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 -> {
|
Trigger.Start.method -> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
result.runCatching {
|
result.runCatching {
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ class BoxService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildConfig(path: String, options: String):String {
|
||||||
|
return Mobile.buildConfig(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val intent = runBlocking {
|
val intent = runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"failureMsg": "Failed to update profile",
|
"failureMsg": "Failed to update profile",
|
||||||
"successMsg": "Profile updated successfully"
|
"successMsg": "Profile updated successfully"
|
||||||
},
|
},
|
||||||
|
"share": {
|
||||||
|
"buttonText": "Share",
|
||||||
|
"exportConfigToClipboard": "Export configuration to clipboard",
|
||||||
|
"exportConfigToClipboardSuccess": "Configuration copied to clipboard"
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"buttonTxt": "Edit",
|
"buttonTxt": "Edit",
|
||||||
"selectActiveTxt": "Select active profile"
|
"selectActiveTxt": "Select active profile"
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"failureMsg": "در بروزرسانی پروفایل خطایی رخ داد",
|
"failureMsg": "در بروزرسانی پروفایل خطایی رخ داد",
|
||||||
"successMsg": "پروفایل با موفقیت بروزرسانی شد"
|
"successMsg": "پروفایل با موفقیت بروزرسانی شد"
|
||||||
},
|
},
|
||||||
|
"share": {
|
||||||
|
"buttonText": "اشتراک گذاری",
|
||||||
|
"exportConfigToClipboard": "افزودن پیکربندی به کلیپ بورد",
|
||||||
|
"exportConfigToClipboardSuccess": "پیکربندی در کلیپ بورد کپی شد"
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"buttonTxt": "ویرایش",
|
"buttonTxt": "ویرایش",
|
||||||
"selectActiveTxt": "انتخاب پروفایل فعال"
|
"selectActiveTxt": "انتخاب پروفایل فعال"
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"failureMsg": "Ошибка обновления",
|
"failureMsg": "Ошибка обновления",
|
||||||
"successMsg": "Профиль успешно обновлён"
|
"successMsg": "Профиль успешно обновлён"
|
||||||
},
|
},
|
||||||
|
"share": {
|
||||||
|
"buttonText": "Делиться",
|
||||||
|
"exportConfigToClipboard": "Экспортировать конфигурацию в буфер обмена",
|
||||||
|
"exportConfigToClipboardSuccess": "Конфигурация скопирована в буфер обмена."
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"buttonTxt": "Изменить",
|
"buttonTxt": "Изменить",
|
||||||
"selectActiveTxt": "Выберите активный профиль"
|
"selectActiveTxt": "Выберите активный профиль"
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"failureMsg": "更新配置文件失败",
|
"failureMsg": "更新配置文件失败",
|
||||||
"successMsg": "配置文件更新成功"
|
"successMsg": "配置文件更新成功"
|
||||||
},
|
},
|
||||||
|
"share": {
|
||||||
|
"buttonText": "分享",
|
||||||
|
"exportConfigToClipboard": "将配置导出到剪贴板",
|
||||||
|
"exportConfigToClipboardSuccess": "配置已复制到剪贴板"
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"buttonTxt": "编辑",
|
"buttonTxt": "编辑",
|
||||||
"selectActiveTxt": "选择活动配置文件"
|
"selectActiveTxt": "选择活动配置文件"
|
||||||
|
|||||||
@@ -90,6 +90,27 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<CoreServiceFailure, String> 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
|
@override
|
||||||
TaskEither<CoreServiceFailure, Unit> start(
|
TaskEither<CoreServiceFailure, Unit> start(
|
||||||
String fileName,
|
String fileName,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ abstract interface class SingboxFacade {
|
|||||||
ConfigOptions options,
|
ConfigOptions options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TaskEither<CoreServiceFailure, String> generateConfig(
|
||||||
|
String fileName,
|
||||||
|
);
|
||||||
|
|
||||||
TaskEither<CoreServiceFailure, Unit> start(
|
TaskEither<CoreServiceFailure, Unit> start(
|
||||||
String fileName,
|
String fileName,
|
||||||
bool disableMemoryLimit,
|
bool disableMemoryLimit,
|
||||||
|
|||||||
@@ -254,6 +254,14 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
|||||||
initialOnSuccess: () =>
|
initialOnSuccess: () =>
|
||||||
CustomToast.success(t.profile.update.successMsg).show(context),
|
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(
|
final deleteProfileMutation = useMutation(
|
||||||
initialOnFailure: (err) {
|
initialOnFailure: (err) {
|
||||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
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(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(Icons.edit),
|
leadingIcon: const Icon(Icons.edit),
|
||||||
child: Text(t.profile.edit.buttonTxt),
|
child: Text(t.profile.edit.buttonTxt),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:hiddify/core/prefs/prefs.dart';
|
import 'package:hiddify/core/prefs/prefs.dart';
|
||||||
import 'package:hiddify/data/data_providers.dart';
|
import 'package:hiddify/data/data_providers.dart';
|
||||||
@@ -124,4 +125,16 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger {
|
|||||||
},
|
},
|
||||||
).run();
|
).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -934,6 +934,21 @@ class SingboxNativeLibrary {
|
|||||||
late final _changeConfigOptions = _changeConfigOptionsPtr
|
late final _changeConfigOptions = _changeConfigOptionsPtr
|
||||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
|
ffi.Pointer<ffi.Char> generateConfig(
|
||||||
|
ffi.Pointer<ffi.Char> path,
|
||||||
|
) {
|
||||||
|
return _generateConfig(
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _generateConfigPtr = _lookup<
|
||||||
|
ffi.NativeFunction<
|
||||||
|
ffi.Pointer<ffi.Char> Function(
|
||||||
|
ffi.Pointer<ffi.Char>)>>('generateConfig');
|
||||||
|
late final _generateConfig = _generateConfigPtr
|
||||||
|
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> start(
|
ffi.Pointer<ffi.Char> start(
|
||||||
ffi.Pointer<ffi.Char> configPath,
|
ffi.Pointer<ffi.Char> configPath,
|
||||||
int disableMemoryLimit,
|
int disableMemoryLimit,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:combine/combine.dart';
|
import 'package:combine/combine.dart';
|
||||||
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||||
@@ -137,6 +138,28 @@ class FFISingboxService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<String, String> generateConfig(
|
||||||
|
String path,
|
||||||
|
) {
|
||||||
|
return TaskEither(
|
||||||
|
() => CombineWorker().execute(
|
||||||
|
() {
|
||||||
|
final response = _box
|
||||||
|
.generateConfig(
|
||||||
|
path.toNativeUtf8().cast(),
|
||||||
|
)
|
||||||
|
.cast<Utf8>()
|
||||||
|
.toDartString();
|
||||||
|
if (response.startsWith("error")) {
|
||||||
|
return left(response.removePrefix("error"));
|
||||||
|
}
|
||||||
|
return right(response);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit) {
|
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit) {
|
||||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||||
|
|||||||
@@ -73,6 +73,24 @@ class MobileSingboxService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<String, String> generateConfig(
|
||||||
|
String path,
|
||||||
|
) {
|
||||||
|
return TaskEither(
|
||||||
|
() async {
|
||||||
|
final configJson = await _methodChannel.invokeMethod<String>(
|
||||||
|
"generate_config",
|
||||||
|
{"path": path},
|
||||||
|
);
|
||||||
|
if (configJson == null || configJson.isEmpty) {
|
||||||
|
return left("null response");
|
||||||
|
}
|
||||||
|
return right(configJson);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit) {
|
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ abstract interface class SingboxService {
|
|||||||
|
|
||||||
TaskEither<String, Unit> changeConfigOptions(ConfigOptions options);
|
TaskEither<String, Unit> changeConfigOptions(ConfigOptions options);
|
||||||
|
|
||||||
|
TaskEither<String, String> generateConfig(
|
||||||
|
String path,
|
||||||
|
);
|
||||||
|
|
||||||
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit);
|
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit);
|
||||||
|
|
||||||
TaskEither<String, Unit> stop();
|
TaskEither<String, Unit> stop();
|
||||||
|
|||||||
2
libcore
2
libcore
Submodule libcore updated: 5c8b283d9c...953e6a02d7
Reference in New Issue
Block a user