diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index aa023c82c..70231d603 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -44,15 +45,23 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { Provider.of(context); if (_idController.text.isEmpty) _idController.text = gFFI.getId(); - return SingleChildScrollView( + return Container( + decoration: BoxDecoration( + color: MyTheme.grayBg + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ getUpdateUI(), - getSearchBarUI(), + Row( + children: [ + getSearchBarUI(), + ], + ).marginOnly(top: 16.0, left: 16.0), SizedBox(height: 12), + Divider(thickness: 1,), getPeers(), ]), ); @@ -106,104 +115,102 @@ class _ConnectionPageState extends State { /// UI for the search bar. /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { - var w = Padding( - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0), - child: Container( - child: Padding( - padding: const EdgeInsets.only(top: 16, bottom: 16), - child: Ink( - decoration: BoxDecoration( - color: MyTheme.white, - borderRadius: const BorderRadius.all(Radius.circular(13)), - ), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 16, right: 16), - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - // color: MyTheme.idColor, - ), - decoration: InputDecoration( - labelText: translate('Control Remote Desktop'), - // hintText: 'Enter your remote ID', - // border: InputBorder., - border: OutlineInputBorder( - borderRadius: BorderRadius.zero), - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: MyTheme.dark, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 26, - letterSpacing: 0.2, - color: MyTheme.dark, - ), - ), - controller: _idController, + var w = Container( + width: 500, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), + decoration: BoxDecoration( + color: MyTheme.white, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Ink( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Container( + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + // color: MyTheme.idColor, + ), + decoration: InputDecoration( + labelText: translate('Control Remote Desktop'), + // hintText: 'Enter your remote ID', + // border: InputBorder., + border: OutlineInputBorder( + borderRadius: BorderRadius.zero), + helperStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.dark, + ), + labelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 26, + letterSpacing: 0.2, + color: MyTheme.dark, ), ), + controller: _idController, + onSubmitted: (s) { + onConnect(); + }, ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () { - onConnect(isFileTransfer: true); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 8.0), - child: Text( - translate( - "Transfer File", - ), - style: TextStyle(color: MyTheme.dark), - ), - ), - ), - SizedBox( - width: 30, - ), - OutlinedButton( - onPressed: onConnect, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - translate( - "Connection", - ), - style: TextStyle(color: MyTheme.white), - ), - ), - style: OutlinedButton.styleFrom( - backgroundColor: Colors.blueAccent, - ), - ), - ], ), - ) + ), ], ), - ), + Padding( + padding: const EdgeInsets.only( + top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () { + onConnect(isFileTransfer: true); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 8.0), + child: Text( + translate( + "Transfer File", + ), + style: TextStyle(color: MyTheme.dark), + ), + ), + ), + SizedBox( + width: 30, + ), + OutlinedButton( + onPressed: onConnect, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16.0), + child: Text( + translate( + "Connection", + ), + style: TextStyle(color: MyTheme.white), + ), + ), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.blueAccent, + ), + ), + ], + ), + ) + ], ), ), ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1e7006628..305155f0e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,10 +1,12 @@ import 'dart:io'; import 'package:flutter/material.dart' hide MenuItem; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -42,9 +44,6 @@ class _DesktopHomePageState extends State with TrayListener { child: buildServerInfo(context), flex: 1, ), - SizedBox( - width: 16.0, - ), Flexible( child: buildServerBoard(context), flex: 4, @@ -76,12 +75,8 @@ class _DesktopHomePageState extends State with TrayListener { buildServerBoard(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, children: [ - // buildControlPanel(context), - // buildRecentSession(context), - Expanded(child: ConnectionPage()) + Expanded(child: ConnectionPage()), ], ); } @@ -105,9 +100,35 @@ class _DesktopHomePageState extends State with TrayListener { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - translate("ID"), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translate("ID"), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + PopupMenuButton( + padding: EdgeInsets.all(4.0), + itemBuilder: (context) => [ + genEnablePopupMenuItem(translate("Enable Keyboard/Mouse"), 'enable-keyboard',), + genEnablePopupMenuItem(translate("Enable Clipboard"), 'enable-clipboard',), + genEnablePopupMenuItem(translate("Enable File Transfer"), 'enable-file-transfer',), + genEnablePopupMenuItem(translate("Enable TCP Tunneling"), 'enable-tunnel',), + genAudioInputPopupMenuItem(), + // TODO: Audio Input + PopupMenuItem(child: Text(translate("ID/Relay Server")), value: 'custom-server',), + PopupMenuItem(child: Text(translate("IP Whitelisting")), value: 'whitelist',), + PopupMenuItem(child: Text(translate("Socks5 Proxy")), value: 'Socks5 Proxy',), + // sep + genEnablePopupMenuItem(translate("Enable Service"), 'stop-service',), + // TODO: direct server + genEnablePopupMenuItem(translate("Always connected via relay"),'allow-always-relay',), + genEnablePopupMenuItem(translate("Start ID/relay service"),'stop-rendezvous-service',), + PopupMenuItem(child: Text(translate("Change ID")), value: 'change-id',), + genEnablePopupMenuItem(translate("Dark Theme"), 'allow-darktheme',), + PopupMenuItem(child: Text(translate("About")), value: 'about',), + ], onSelected: onSelectMenu,) + ], ), TextFormField( controller: model.serverId, @@ -194,7 +215,9 @@ class _DesktopHomePageState extends State with TrayListener { children: [ TextFormField( controller: TextEditingController(), - inputFormatters: [], + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) + ], ) ], )) @@ -232,4 +255,152 @@ class _DesktopHomePageState extends State with TrayListener { trayManager.removeListener(this); super.dispose(); } + + void onSelectMenu(String value) { + if (value.startsWith('enable-')) { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "N" ? "" : "N"); + } else if (value.startsWith('allow-')) { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "Y" ? "" : "Y"); + } else if (value == "stop-service") { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "Y" ? "" : "Y"); + } else if (value == "change-id") { + changeId(); + } + } + + PopupMenuItem genEnablePopupMenuItem(String label, String value) { + final isEnable = + label.startsWith('enable-') ? gFFI.getOption(value) != "N" : gFFI.getOption(value) != "Y"; + return PopupMenuItem(child: Row( + children: [ + Offstage(offstage: !isEnable, child: Icon(Icons.check)), + Text(label, style: genTextStyle(isEnable),), + ], + ), value: value,); + } + + TextStyle genTextStyle(bool isPositive) { + return isPositive ? TextStyle() : TextStyle( + color: Colors.redAccent, + decoration: TextDecoration.lineThrough + ); + } + + PopupMenuItem genAudioInputPopupMenuItem() { + final _enabledInput = gFFI.getOption('enable-audio'); + var defaultInput = gFFI.getDefaultAudioInput().obs; + var enabled = (_enabledInput != "N").obs; + return PopupMenuItem(child: FutureBuilder>( + future: gFFI.getAudioInputs(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final inputs = snapshot.data!; + if (Platform.isWindows) { + inputs.insert(0, translate("System Sound")); + } + var inputList = inputs.map((e) => PopupMenuItem( + child: Row( + children: [ + Obx(()=> Offstage(offstage: defaultInput.value != e, child: Icon(Icons.check))), + Expanded(child: Tooltip( + message: e, + child: Text("$e",maxLines: 1, overflow: TextOverflow.ellipsis,))), + ], + ), + value: e, + )).toList(); + inputList.insert(0, PopupMenuItem( + child: Row( + children: [ + Obx(()=> Offstage(offstage: enabled.value, child: Icon(Icons.check))), + Expanded(child: Text(translate("Mute"))), + ], + ), + value: "Mute", + )); + return PopupMenuButton( + padding: EdgeInsets.zero, + child: Container( + alignment: Alignment.centerLeft, + child: Text(translate("Audio Input"))), + itemBuilder: (context) => inputList, + onSelected: (dev) { + if (dev == "Mute") { + gFFI.setOption('enable-audio', _enabledInput == 'N' ? '': 'N'); + enabled.value = gFFI.getOption('enable-audio') != 'N'; + } else if (dev != gFFI.getDefaultAudioInput()) { + gFFI.setDefaultAudioInput(dev); + defaultInput.value = dev; + } + }, + ); + } else { + return Text("..."); + } + }, + ), value: 'audio-input',); + } + + /// change local ID + void changeId() { + var newId = ""; + var msg = ""; + var isInProgress = false; + DialogManager.show( (setState, close) { + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + Offstage( + offstage: msg.isEmpty, + child: Text(msg, style: TextStyle(color: Colors.grey),)).marginOnly(bottom: 4.0), + TextField( + onChanged: (s) { + newId = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder() + ), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + ), + SizedBox(height: 4.0,), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ], + ), actions: [ + TextButton(onPressed: (){ + close(); + }, child: Text("取消")), + TextButton(onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + gFFI.bind.mainChangeId(newId: newId); + }); + + var status = await gFFI.bind.mainGetAsyncStatus(); + while (status == " "){ + await Future.delayed(Duration(milliseconds: 100)); + status = await gFFI.bind.mainGetAsyncStatus(); + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + + }, child: Text("确定")), + ], + ); + }); + } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 45a5bc696..9b0b7930a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -990,6 +991,17 @@ class FFI { ffiModel.platformFFI.setByName(name, value); } + String getOption(String name) { + return ffiModel.platformFFI.getByName("option", name); + } + + void setOption(String name, String value) { + Map res = Map() + ..["name"] = name + ..["value"] = value; + return ffiModel.platformFFI.setByName('option', jsonEncode(res)); + } + RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; handleMouse(Map evt) { @@ -1062,6 +1074,22 @@ class FFI { Future invokeMethod(String method, [dynamic arguments]) async { return await ffiModel.platformFFI.invokeMethod(method, arguments); } + + Future> getAudioInputs() async { + return await bind.mainGetSoundInputs(); + } + + String getDefaultAudioInput() { + final input = getOption('audio-input'); + if (input.isEmpty && Platform.isWindows) { + return "System Sound"; + } + return input; + } + + void setDefaultAudioInput(String input){ + setOption('audio-input', input); + } } class Peer { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b34076310..a798799f1 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -187,8 +187,8 @@ packages: dependency: "direct main" description: path: "." - ref: c7d97cb6615f2def34f8bad4def01af9e0077beb - resolved-ref: c7d97cb6615f2def34f8bad4def01af9e0077beb + ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" + resolved-ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.0.1" diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f2bef5716..60d8fd4b5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,6 +19,7 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; +use crate::ui_interface::{change_id, get_async_job_status, get_sound_inputs, is_ok_change_id}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -378,6 +379,18 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } } +pub fn main_get_sound_inputs() -> Vec { + get_sound_inputs() +} + +pub fn main_change_id(new_id: String) { + change_id(new_id) +} + +pub fn main_get_async_status() -> String { + get_async_job_status() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. ///