mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-09 00:34:17 +08:00
Support remote device management
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"preference": "Preferences",
|
||||
"feedback": "Feedback",
|
||||
"about": "About",
|
||||
"filter": "Filter",
|
||||
"filter": "Proxy Filter",
|
||||
"script": "Script",
|
||||
"share": "Share",
|
||||
"port": "Port: ",
|
||||
@@ -26,6 +26,8 @@
|
||||
"setting": "Settings",
|
||||
"mobileConnect": "Mobile Connect",
|
||||
"connectRemote": "Connect Remote",
|
||||
"remoteDevice": "Remote Device",
|
||||
"remoteDeviceList": "Remote Device List",
|
||||
"myQRCode": "My QR Code",
|
||||
|
||||
"theme": "Theme",
|
||||
@@ -227,12 +229,14 @@
|
||||
"appExitTips": "Press again to exit the program",
|
||||
"remoteConnectDisconnect": "Check remote connection failed, disconnected",
|
||||
"reconnect": "Reconnect",
|
||||
"remoteConnected": "Connected {os},Mobile packet capture is turned off",
|
||||
"remoteConnected": "Connected {os}, traffic will be forwarded to {os}",
|
||||
"remoteConnectForward": "Remote connection, forwarding requests to other terminals",
|
||||
"connectSuccess": "Connect successful",
|
||||
"connectedRemote": "Connected to remote",
|
||||
"connected": "Connected",
|
||||
"notConnected": "Not connected",
|
||||
"disconnect": "Disconnect",
|
||||
"inputAddress": "Input Address",
|
||||
"syncConfig": "Sync configuration",
|
||||
"pullConfigFail": "Failed to pull configuration, please check the network connection",
|
||||
"sync": "Sync",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preference": "偏好设置",
|
||||
"feedback": "反馈",
|
||||
"about": "关于",
|
||||
"filter": "过滤",
|
||||
"filter": "代理过滤",
|
||||
"script": "脚本",
|
||||
"share": "分享",
|
||||
"port": "端口号: ",
|
||||
@@ -26,6 +26,8 @@
|
||||
"setting": "设置",
|
||||
"mobileConnect": "手机连接",
|
||||
"connectRemote": "连接终端",
|
||||
"remoteDevice": "远程设备",
|
||||
"remoteDeviceList": "远程设备列表",
|
||||
"myQRCode": "我的二维码",
|
||||
|
||||
"theme": "主题",
|
||||
@@ -227,11 +229,13 @@
|
||||
"appExitTips": "再按一次退出程序",
|
||||
"remoteConnectDisconnect": "检查远程连接失败,已断开",
|
||||
"reconnect": "重新连接",
|
||||
"remoteConnected": "已连接{os},手机抓包已关闭",
|
||||
"remoteConnectForward": "远程连接,将请求转发到其他终端",
|
||||
"remoteConnected": "已连接{os},流量将转发到{os}",
|
||||
"remoteConnectForward": "远程连接,将其他设备流量转发到当前设备",
|
||||
"connectSuccess": "连接成功",
|
||||
"connectedRemote": "已连接远程",
|
||||
"connected": "已连接",
|
||||
"notConnected": "未连接",
|
||||
"inputAddress": "输入地址",
|
||||
"disconnect": "断开连接",
|
||||
"syncConfig": "同步配置",
|
||||
"pullConfigFail": "拉取配置失败, 请检查网络连接",
|
||||
|
||||
@@ -82,7 +82,7 @@ class _PhoneConnectState extends State<PhoneConnect> {
|
||||
items: widget.hosts
|
||||
.map((it) => DropdownMenuItem(
|
||||
value: it,
|
||||
child: Text('$it:$port'),
|
||||
child: SelectableText('$it:$port'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (String? value) {
|
||||
|
||||
@@ -116,10 +116,11 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primaryColor = Theme.of(context).primaryColor;
|
||||
return IconButton(
|
||||
tooltip: started ? localizations.stop : localizations.start,
|
||||
icon: Icon(started ? Icons.stop : Icons.play_arrow_sharp,
|
||||
color: started ? Colors.red : Colors.green, size: widget.size.toDouble()),
|
||||
color: started ? Colors.red : primaryColor, size: widget.size.toDouble()),
|
||||
onPressed: () async {
|
||||
if (started) {
|
||||
if (!widget.serverLaunch) {
|
||||
|
||||
@@ -16,31 +16,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:easy_permission/easy_permission.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:network_proxy/native/vpn.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
import 'package:network_proxy/network/http_client.dart';
|
||||
import 'package:network_proxy/network/util/logger.dart';
|
||||
import 'package:network_proxy/ui/mobile/mobile.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/app_filter.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/ssl.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/highlight.dart';
|
||||
import 'package:network_proxy/utils/ip.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:qrscan/qrscan.dart' as scanner;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/remote_device.dart';
|
||||
|
||||
/// +号菜单
|
||||
class MoreMenu extends StatelessWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final ValueNotifier<RemoteModel> desktop;
|
||||
final ValueNotifier<RemoteModel> remoteDevice;
|
||||
|
||||
const MoreMenu({super.key, required this.proxyServer, required this.desktop});
|
||||
const MoreMenu({super.key, required this.proxyServer, required this.remoteDevice});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -74,25 +64,11 @@ class MoreMenu extends StatelessWidget {
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.qr_code_scanner_outlined),
|
||||
title: Text(localizations.connectRemote),
|
||||
leading: const Icon(Icons.devices),
|
||||
title: Text(localizations.remoteDevice),
|
||||
onTap: () {
|
||||
Navigator.maybePop(context);
|
||||
connectRemote(context);
|
||||
},
|
||||
)),
|
||||
PopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.phone_iphone_outlined),
|
||||
title: Text(localizations.myQRCode),
|
||||
onTap: () async {
|
||||
Navigator.maybePop(context);
|
||||
var ip = await localIp(readCache: false);
|
||||
if (context.mounted) {
|
||||
connectQrCode(context, ip, proxyServer.port);
|
||||
}
|
||||
navigator(context, RemoteDevicePage(proxyServer: proxyServer, remoteDevice: remoteDevice));
|
||||
},
|
||||
)),
|
||||
const PopupMenuDivider(height: 0),
|
||||
@@ -143,108 +119,4 @@ class MoreMenu extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///扫码连接
|
||||
connectRemote(BuildContext context) async {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
|
||||
String scanRes;
|
||||
if (Platform.isAndroid) {
|
||||
await EasyPermission.requestPermissions([PermissionType.CAMERA]);
|
||||
scanRes = await scanner.scan() ?? "-1";
|
||||
} else {
|
||||
scanRes = await FlutterBarcodeScanner.scanBarcode("#ff6666", localizations.cancel, true, ScanMode.QR);
|
||||
}
|
||||
if (scanRes == "-1") return;
|
||||
if (scanRes.startsWith("http")) {
|
||||
launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scanRes.startsWith("proxypin://connect")) {
|
||||
Uri uri = Uri.parse(scanRes);
|
||||
var host = uri.queryParameters['host'];
|
||||
var port = uri.queryParameters['port'];
|
||||
|
||||
try {
|
||||
var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 1));
|
||||
if (response.bodyAsString == "pong") {
|
||||
desktop.value = RemoteModel(
|
||||
connect: true,
|
||||
host: host,
|
||||
port: int.parse(port!),
|
||||
os: response.headers.get("os"),
|
||||
hostname: response.headers.get("hostname"));
|
||||
|
||||
if (context.mounted && Navigator.canPop(context)) {
|
||||
FlutterToastr.show(
|
||||
"${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}",
|
||||
context,
|
||||
duration: 3);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
if (context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(content: Text(localizations.remoteConnectFail));
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
FlutterToastr.show(localizations.invalidQRCode, context);
|
||||
}
|
||||
}
|
||||
|
||||
///连接二维码
|
||||
connectQrCode(BuildContext context, String host, int port) {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.only(top: 5),
|
||||
actionsPadding: const EdgeInsets.only(bottom: 5),
|
||||
title: Text(localizations.remoteConnectForward, style: const TextStyle(fontSize: 16)),
|
||||
content: SizedBox(
|
||||
height: 260,
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
QrImageView(
|
||||
backgroundColor: Colors.white,
|
||||
data: "proxypin://connect?host=$host&port=${proxyServer.port}",
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('${localizations.localIP}:'),
|
||||
const SizedBox(width: 5),
|
||||
SelectableText('$host:$port'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(localizations.mobileScan),
|
||||
],
|
||||
)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(localizations.cancel)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ import 'package:network_proxy/ui/mobile/menu/me.dart';
|
||||
import 'package:network_proxy/ui/mobile/menu/menu.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/list.dart';
|
||||
import 'package:network_proxy/ui/mobile/request/search.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/pip.dart';
|
||||
import 'package:network_proxy/ui/mobile/widgets/remote_device.dart';
|
||||
import 'package:network_proxy/utils/ip.dart';
|
||||
import 'package:network_proxy/utils/lang.dart';
|
||||
import 'package:network_proxy/utils/listenable_list.dart';
|
||||
@@ -177,7 +177,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
body: LazyIndexedStack(index: index, children: navigationView),
|
||||
bottomNavigationBar: widget.appConfiguration.bottomNavigation
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxHeight: 72),
|
||||
constraints: const BoxConstraints(maxHeight: 80),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(splashColor: Colors.transparent),
|
||||
child: BottomNavigationBar(
|
||||
@@ -249,7 +249,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
String content = isCN
|
||||
? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n'
|
||||
'1. 手机端增加底部导航,可在设置中切换;\n'
|
||||
'2. 外部代理支持身份验证;\n'
|
||||
'2. 增加远程设备管理,可快速连接设备;\n'
|
||||
'3. 双击列表tab滚动到顶部;\n'
|
||||
'4. 修复部分p12证书导入失败的问题;\n'
|
||||
'5. 脚本增加rawBody原始字节参数, body支持字节数组修改;\n'
|
||||
@@ -261,7 +261,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
: 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
|
||||
'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n'
|
||||
'1. Mobile: Add bottom navigation bar,which can be switched in settings;\n'
|
||||
'2. External proxy support authentication;\n'
|
||||
'2. Support remote device management to quickly connect to devices;\n'
|
||||
'3. Double-click the list tab to scroll to the top;\n'
|
||||
'4. Fix the issue of partial p12 certificate import failure;\n'
|
||||
'5. The script add rawBody raw byte parameter, body supports byte array modification;\n'
|
||||
@@ -310,7 +310,7 @@ class RequestPage extends StatefulWidget {
|
||||
|
||||
class RequestPageState extends State<RequestPage> {
|
||||
/// 远程连接
|
||||
final ValueNotifier<RemoteModel> desktop = ValueNotifier(RemoteModel(connect: false));
|
||||
final ValueNotifier<RemoteModel> remoteDevice = ValueNotifier(RemoteModel(connect: false));
|
||||
|
||||
late ProxyServer proxyServer;
|
||||
|
||||
@@ -322,9 +322,9 @@ class RequestPageState extends State<RequestPage> {
|
||||
proxyServer = widget.proxyServer;
|
||||
|
||||
//远程连接
|
||||
desktop.addListener(() {
|
||||
if (desktop.value.connect) {
|
||||
proxyServer.configuration.remoteHost = "http://${desktop.value.host}:${desktop.value.port}";
|
||||
remoteDevice.addListener(() {
|
||||
if (remoteDevice.value.connect) {
|
||||
proxyServer.configuration.remoteHost = "http://${remoteDevice.value.host}:${remoteDevice.value.port}";
|
||||
checkConnectTask(context);
|
||||
} else {
|
||||
proxyServer.configuration.remoteHost = null;
|
||||
@@ -334,7 +334,7 @@ class RequestPageState extends State<RequestPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
desktop.dispose();
|
||||
remoteDevice.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -343,13 +343,13 @@ class RequestPageState extends State<RequestPage> {
|
||||
return Scaffold(
|
||||
floatingActionButton: PictureInPictureIcon(proxyServer),
|
||||
body: Scaffold(
|
||||
appBar: _MobileAppBar(widget.appConfiguration, proxyServer, desktop: desktop),
|
||||
appBar: _MobileAppBar(widget.appConfiguration, proxyServer, remoteDevice: remoteDevice),
|
||||
drawer: widget.appConfiguration.bottomNavigation
|
||||
? null
|
||||
: DrawerWidget(proxyServer: proxyServer, container: MobileApp.container),
|
||||
floatingActionButton: _launchActionButton(),
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: desktop,
|
||||
valueListenable: remoteDevice,
|
||||
builder: (context, value, _) {
|
||||
return Column(children: [
|
||||
value.connect ? remoteConnect(value) : const SizedBox(),
|
||||
@@ -387,13 +387,16 @@ class RequestPageState extends State<RequestPage> {
|
||||
Widget remoteConnect(RemoteModel value) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 5, bottom: 5),
|
||||
height: 55,
|
||||
height: 56,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ConnectRemote(desktop: desktop, proxyServer: proxyServer);
|
||||
return RemoteDevicePage(remoteDevice: remoteDevice, proxyServer: proxyServer);
|
||||
})),
|
||||
child: Text(localizations.remoteConnected(desktop.value.os ?? ''),
|
||||
child: Text(localizations.remoteConnected(remoteDevice.value.os ?? ', ${remoteDevice.value.hostname}'),
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
));
|
||||
}
|
||||
@@ -401,14 +404,14 @@ class RequestPageState extends State<RequestPage> {
|
||||
/// 检查远程连接
|
||||
checkConnectTask(BuildContext context) async {
|
||||
int retry = 0;
|
||||
Timer.periodic(const Duration(milliseconds: 3000), (timer) async {
|
||||
if (desktop.value.connect == false) {
|
||||
Timer.periodic(const Duration(milliseconds: 10000), (timer) async {
|
||||
if (remoteDevice.value.connect == false) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await HttpClients.get("http://${desktop.value.host}:${desktop.value.port}/ping")
|
||||
var response = await HttpClients.get("http://${remoteDevice.value.host}:${remoteDevice.value.port}/ping")
|
||||
.timeout(const Duration(seconds: 1));
|
||||
if (response.bodyAsString == "pong") {
|
||||
retry = 0;
|
||||
@@ -419,13 +422,16 @@ class RequestPageState extends State<RequestPage> {
|
||||
}
|
||||
|
||||
if (retry > 5) {
|
||||
timer.cancel();
|
||||
desktop.value = RemoteModel(connect: false);
|
||||
retry = 0;
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(localizations.remoteConnectDisconnect),
|
||||
action: SnackBarAction(
|
||||
label: localizations.reconnect, onPressed: () => desktop.value = RemoteModel(connect: true))));
|
||||
label: localizations.disconnect,
|
||||
onPressed: () {
|
||||
timer.cancel();
|
||||
remoteDevice.value = RemoteModel(connect: false);
|
||||
})));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -436,9 +442,9 @@ class RequestPageState extends State<RequestPage> {
|
||||
class _MobileAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final AppConfiguration appConfiguration;
|
||||
final ProxyServer proxyServer;
|
||||
final ValueNotifier<RemoteModel> desktop;
|
||||
final ValueNotifier<RemoteModel> remoteDevice;
|
||||
|
||||
const _MobileAppBar(this.appConfiguration, this.proxyServer, {required this.desktop});
|
||||
const _MobileAppBar(this.appConfiguration, this.proxyServer, {required this.remoteDevice});
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(42);
|
||||
@@ -458,7 +464,7 @@ class _MobileAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
icon: const Icon(Icons.cleaning_services_outlined),
|
||||
onPressed: () => MobileApp.requestStateKey.currentState?.clean()),
|
||||
const SizedBox(width: 2),
|
||||
MoreMenu(proxyServer: proxyServer, desktop: desktop),
|
||||
MoreMenu(proxyServer: proxyServer, remoteDevice: remoteDevice),
|
||||
const SizedBox(width: 10),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Hongen Wang
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:network_proxy/network/bin/configuration.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
import 'package:network_proxy/network/components/host_filter.dart';
|
||||
import 'package:network_proxy/network/components/request_rewrite_manager.dart';
|
||||
import 'package:network_proxy/network/components/script_manager.dart';
|
||||
import 'package:network_proxy/network/http_client.dart';
|
||||
import 'package:network_proxy/network/util/logger.dart';
|
||||
|
||||
class RemoteModel {
|
||||
final bool connect;
|
||||
final String? host;
|
||||
final int? port;
|
||||
final String? os;
|
||||
final String? hostname;
|
||||
|
||||
RemoteModel({
|
||||
required this.connect,
|
||||
this.host,
|
||||
this.port,
|
||||
this.os,
|
||||
this.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
class ConnectRemote extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final ValueNotifier<RemoteModel> desktop;
|
||||
|
||||
const ConnectRemote({super.key, required this.desktop, required this.proxyServer});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ConnectRemoteState();
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectRemoteState extends State<ConnectRemote> {
|
||||
bool syncConfig = false;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(localizations.connectedRemote, style: const TextStyle(fontSize: 16))),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('${localizations.connected}:${widget.desktop.value.hostname}',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 10),
|
||||
OutlinedButton(
|
||||
child: Text(localizations.disconnect),
|
||||
onPressed: () {
|
||||
widget.desktop.value = RemoteModel(connect: false);
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
OutlinedButton(
|
||||
child: Text(localizations.syncConfig),
|
||||
onPressed: () {
|
||||
pullConfig();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//拉取桌面配置
|
||||
pullConfig() {
|
||||
var desktopModel = widget.desktop.value;
|
||||
HttpClients.get('http://${desktopModel.host}:${desktopModel.port}/config').then((response) {
|
||||
if (response.status.isSuccessful()) {
|
||||
var config = jsonDecode(response.bodyAsString);
|
||||
syncConfig = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ConfigSyncWidget(configuration: widget.proxyServer.configuration, config: config);
|
||||
});
|
||||
}
|
||||
}).onError((error, stackTrace) {
|
||||
logger.e('拉取配置失败', error: error, stackTrace: stackTrace);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(localizations.pullConfigFail)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigSyncWidget extends StatefulWidget {
|
||||
final Configuration configuration;
|
||||
final Map<String, dynamic> config;
|
||||
|
||||
const ConfigSyncWidget({super.key, required this.configuration, required this.config});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ConfigSyncState();
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigSyncState extends State<ConfigSyncWidget> {
|
||||
bool syncWhiteList = true;
|
||||
bool syncBlackList = true;
|
||||
bool syncRewrite = true;
|
||||
bool syncScript = true;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(localizations.syncConfig, style: const TextStyle(fontSize: 16)),
|
||||
content: SizedBox(
|
||||
height: 260,
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
subtitle: Text("${localizations.sync}${localizations.domainWhitelist}"),
|
||||
value: syncWhiteList,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncWhiteList = val;
|
||||
});
|
||||
}),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
subtitle: Text("${localizations.sync}${localizations.domainBlacklist}"),
|
||||
value: syncBlackList,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncBlackList = val;
|
||||
});
|
||||
}),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
subtitle: Text("${localizations.sync}${localizations.requestRewrite}"),
|
||||
value: syncRewrite,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncRewrite = val;
|
||||
});
|
||||
}),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
subtitle: Text("${localizations.sync}${localizations.script}"),
|
||||
value: syncScript,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncScript = val;
|
||||
});
|
||||
}),
|
||||
],
|
||||
)),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(localizations.cancel),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
TextButton(
|
||||
child: Text('${localizations.start}${localizations.sync}'),
|
||||
onPressed: () async {
|
||||
if (syncWhiteList) {
|
||||
HostFilter.whitelist.load(widget.config['whitelist']);
|
||||
}
|
||||
if (syncBlackList) {
|
||||
HostFilter.blacklist.load(widget.config['blacklist']);
|
||||
}
|
||||
widget.configuration.flushConfig();
|
||||
|
||||
if (syncRewrite) {
|
||||
var requestRewrites = await RequestRewrites.instance;
|
||||
await requestRewrites.syncConfig(widget.config['requestRewrites']);
|
||||
}
|
||||
|
||||
if (syncScript) {
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
await scriptManager.clean();
|
||||
scriptManager.list.clear();
|
||||
for (var item in widget.config['scripts']) {
|
||||
await scriptManager.addScript(ScriptItem.fromJson(item), item['script']);
|
||||
}
|
||||
await scriptManager.flushConfig();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(this.context);
|
||||
ScaffoldMessenger.of(this.context)
|
||||
.showSnackBar(SnackBar(content: Text('${localizations.sync}${localizations.success}')));
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 Hongen Wang
|
||||
* Copyright 2023 Hongen Wang All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
468
lib/ui/mobile/widgets/remote_device.dart
Normal file
468
lib/ui/mobile/widgets/remote_device.dart
Normal file
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* Copyright 2024 Hongen Wang All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_permission/easy_permission.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:network_proxy/native/vpn.dart';
|
||||
import 'package:network_proxy/network/bin/configuration.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
import 'package:network_proxy/network/components/host_filter.dart';
|
||||
import 'package:network_proxy/network/components/request_rewrite_manager.dart';
|
||||
import 'package:network_proxy/network/components/script_manager.dart';
|
||||
import 'package:network_proxy/network/http_client.dart';
|
||||
import 'package:network_proxy/network/util/logger.dart';
|
||||
import 'package:network_proxy/ui/component/utils.dart';
|
||||
import 'package:network_proxy/ui/component/widgets.dart';
|
||||
import 'package:network_proxy/utils/ip.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:qrscan/qrscan.dart' as scanner;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
///远程设备
|
||||
///Remote device
|
||||
///@author Hongen Wang
|
||||
class RemoteModel {
|
||||
final bool connect;
|
||||
final String? host;
|
||||
final int? port;
|
||||
final String? os;
|
||||
final String? hostname;
|
||||
|
||||
RemoteModel({
|
||||
required this.connect,
|
||||
this.host,
|
||||
this.port,
|
||||
this.os,
|
||||
this.hostname,
|
||||
});
|
||||
|
||||
factory RemoteModel.fromJson(Map<String, dynamic> json) {
|
||||
return RemoteModel(
|
||||
connect: json['connect'],
|
||||
host: json['host'],
|
||||
port: json['port'],
|
||||
os: json['os'],
|
||||
hostname: json['hostname'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'connect': connect,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'os': os,
|
||||
'hostname': hostname,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteDevicePage extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final ValueNotifier<RemoteModel> remoteDevice;
|
||||
|
||||
const RemoteDevicePage({super.key, required this.proxyServer, required this.remoteDevice});
|
||||
|
||||
@override
|
||||
State<RemoteDevicePage> createState() => _RemoteDevicePageState();
|
||||
}
|
||||
|
||||
class _RemoteDevicePageState extends State<RemoteDevicePage> {
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
bool syncConfig = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(localizations.remoteDevice, style: const TextStyle(fontSize: 16)),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuEntry>[
|
||||
CustomPopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner_outlined),
|
||||
dense: true,
|
||||
title: Text(localizations.scanCode),
|
||||
onTap: () => connectRemote(context))),
|
||||
// CustomPopupMenuItem(
|
||||
// height: 32,
|
||||
// child: ListTile(
|
||||
// leading: const Icon(Icons.edit_rounded),
|
||||
// dense: true,
|
||||
// title: Text(localizations.inputAddress),
|
||||
// onTap: () {})),
|
||||
PopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.phone_android),
|
||||
title: Text(localizations.myQRCode),
|
||||
onTap: () async {
|
||||
Navigator.maybePop(context);
|
||||
var ip = await localIp(readCache: false);
|
||||
if (context.mounted) {
|
||||
qrCode(context, ip, widget.proxyServer.port);
|
||||
}
|
||||
},
|
||||
)),
|
||||
];
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
remoteDeviceStatus(), //远程设备状态
|
||||
const SizedBox(height: 20),
|
||||
Text(localizations.remoteDeviceList, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(child: futureWidget(SharedPreferences.getInstance(), rows)), //远程设备列表
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget rows(SharedPreferences prefs) {
|
||||
var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? [];
|
||||
|
||||
return ListView(
|
||||
children: remoteDeviceList.map((it) {
|
||||
var remoteDevice = RemoteModel.fromJson(jsonDecode(it));
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
title: Text(remoteDevice.hostname ?? ''),
|
||||
subtitle: Text('${remoteDevice.host}:${remoteDevice.port}'),
|
||||
trailing: getIcon(remoteDevice.os!),
|
||||
onTap: () {
|
||||
doConnect(remoteDevice.host!, remoteDevice.port!);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Icon getIcon(String os) {
|
||||
if (os.contains("windows")) {
|
||||
return const Icon(Icons.window_sharp, size: 30);
|
||||
} else if (os.contains("linux")) {
|
||||
return const Icon(Icons.desktop_windows, size: 30);
|
||||
} else if (os.contains("macos") || os.contains("ios")) {
|
||||
return const Icon(Icons.apple, size: 30);
|
||||
} else if (os == 'android') {
|
||||
return const Icon(Icons.android, size: 30);
|
||||
} else {
|
||||
return const Icon(Icons.devices, size: 30);
|
||||
}
|
||||
}
|
||||
|
||||
///远程设备状态
|
||||
Widget remoteDeviceStatus() {
|
||||
if (widget.remoteDevice.value.connect) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline_outlined, size: 55, color: Colors.green),
|
||||
Text('${localizations.connected}:${widget.remoteDevice.value.hostname}',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 6),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
|
||||
TextButton.icon(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)))),
|
||||
onPressed: pullConfig,
|
||||
icon: const Icon(Icons.sync),
|
||||
label: Text(localizations.syncConfig),
|
||||
),
|
||||
TextButton.icon(
|
||||
label: Text(localizations.disconnect),
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0))),
|
||||
),
|
||||
icon: const Icon(Icons.cancel_outlined),
|
||||
onPressed: () {
|
||||
widget.remoteDevice.value = RemoteModel(connect: false);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
])
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(children: [
|
||||
const Icon(Icons.cancel_outlined, size: 55, color: Colors.red),
|
||||
const SizedBox(height: 6),
|
||||
Text(localizations.notConnected, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
]));
|
||||
}
|
||||
|
||||
///扫码连接
|
||||
connectRemote(BuildContext context) async {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
|
||||
String scanRes;
|
||||
if (Platform.isAndroid) {
|
||||
await EasyPermission.requestPermissions([PermissionType.CAMERA]);
|
||||
scanRes = await scanner.scan() ?? "-1";
|
||||
} else {
|
||||
scanRes = await FlutterBarcodeScanner.scanBarcode("#ff6666", localizations.cancel, true, ScanMode.QR);
|
||||
}
|
||||
if (scanRes == "-1") return;
|
||||
if (scanRes.startsWith("http")) {
|
||||
launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scanRes.startsWith("proxypin://connect")) {
|
||||
Uri uri = Uri.parse(scanRes);
|
||||
var host = uri.queryParameters['host'];
|
||||
var port = uri.queryParameters['port'];
|
||||
|
||||
doConnect(host!, int.parse(port!));
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
FlutterToastr.show(localizations.invalidQRCode, context);
|
||||
}
|
||||
}
|
||||
|
||||
doConnect(String host, int port) async {
|
||||
try {
|
||||
var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 3));
|
||||
if (response.bodyAsString == "pong") {
|
||||
widget.remoteDevice.value = RemoteModel(
|
||||
connect: true,
|
||||
host: host,
|
||||
port: port,
|
||||
os: response.headers.get("os"),
|
||||
hostname: response.headers.get("hostname"));
|
||||
|
||||
//去重记录5条连接记录
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? [];
|
||||
var value = jsonEncode(widget.remoteDevice.value.toJson());
|
||||
remoteDeviceList.remove(value);
|
||||
remoteDeviceList.insert(0, value);
|
||||
prefs.setStringList('remoteDeviceList', remoteDeviceList).then((value) {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
FlutterToastr.show(
|
||||
"${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}",
|
||||
context,
|
||||
duration: 3);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(content: Text(localizations.remoteConnectFail));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///连接二维码
|
||||
qrCode(BuildContext context, String host, int port) {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(15),
|
||||
actionsPadding: const EdgeInsets.only(bottom: 10, right: 10),
|
||||
title: Text(localizations.remoteConnectForward, style: const TextStyle(fontSize: 16)),
|
||||
content: SizedBox(
|
||||
height: 280,
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
QrImageView(
|
||||
backgroundColor: Colors.white,
|
||||
data: "proxypin://connect?host=$host&port=${widget.proxyServer.port}",
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('${localizations.localIP}:'),
|
||||
const SizedBox(width: 5),
|
||||
SelectableText('$host:$port'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(localizations.mobileScan),
|
||||
],
|
||||
)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(localizations.cancel)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//拉取桌面配置
|
||||
pullConfig() {
|
||||
var desktopModel = widget.remoteDevice.value;
|
||||
HttpClients.get('http://${desktopModel.host}:${desktopModel.port}/config').then((response) {
|
||||
if (response.status.isSuccessful()) {
|
||||
var config = jsonDecode(response.bodyAsString);
|
||||
syncConfig = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ConfigSyncWidget(configuration: widget.proxyServer.configuration, config: config);
|
||||
});
|
||||
}
|
||||
}).onError((error, stackTrace) {
|
||||
logger.e('拉取配置失败', error: error, stackTrace: stackTrace);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(localizations.pullConfigFail)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigSyncWidget extends StatefulWidget {
|
||||
final Configuration configuration;
|
||||
final Map<String, dynamic> config;
|
||||
|
||||
const ConfigSyncWidget({super.key, required this.configuration, required this.config});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ConfigSyncState();
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigSyncState extends State<ConfigSyncWidget> {
|
||||
bool syncWhiteList = true;
|
||||
bool syncBlackList = true;
|
||||
bool syncRewrite = true;
|
||||
bool syncScript = true;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(localizations.syncConfig, style: const TextStyle(fontSize: 16)),
|
||||
content: Wrap(children: [
|
||||
SwitchWidget(
|
||||
title: "${localizations.sync}${localizations.domainWhitelist}",
|
||||
value: syncWhiteList,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncWhiteList = val;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 5),
|
||||
SwitchWidget(
|
||||
title: "${localizations.sync}${localizations.domainBlacklist}",
|
||||
value: syncBlackList,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncBlackList = val;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 5),
|
||||
SwitchWidget(
|
||||
title: "${localizations.sync}${localizations.requestRewrite}",
|
||||
value: syncRewrite,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncRewrite = val;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 5),
|
||||
SwitchWidget(
|
||||
title: "${localizations.sync}${localizations.script}",
|
||||
value: syncScript,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
syncScript = val;
|
||||
});
|
||||
}),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(localizations.cancel),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
TextButton(
|
||||
child: Text('${localizations.start}${localizations.sync}'),
|
||||
onPressed: () async {
|
||||
if (syncWhiteList) {
|
||||
HostFilter.whitelist.load(widget.config['whitelist']);
|
||||
}
|
||||
if (syncBlackList) {
|
||||
HostFilter.blacklist.load(widget.config['blacklist']);
|
||||
}
|
||||
widget.configuration.flushConfig();
|
||||
|
||||
if (syncRewrite) {
|
||||
var requestRewrites = await RequestRewrites.instance;
|
||||
await requestRewrites.syncConfig(widget.config['requestRewrites']);
|
||||
}
|
||||
|
||||
if (syncScript) {
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
await scriptManager.clean();
|
||||
scriptManager.list.clear();
|
||||
for (var item in widget.config['scripts']) {
|
||||
await scriptManager.addScript(ScriptItem.fromJson(item), item['script']);
|
||||
}
|
||||
await scriptManager.flushConfig();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(this.context);
|
||||
ScaffoldMessenger.of(this.context)
|
||||
.showSnackBar(SnackBar(content: Text('${localizations.sync}${localizations.success}')));
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user