Merge pull request #979 from Kingtous/flutter_desktop

feat: main menu implementation for flutter desktop
This commit is contained in:
RustDesk 2022-07-15 17:14:44 +08:00 committed by GitHub
commit fc06762a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 328 additions and 109 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -44,15 +45,23 @@ class _ConnectionPageState extends State<ConnectionPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
if (_idController.text.isEmpty) _idController.text = gFFI.getId(); if (_idController.text.isEmpty) _idController.text = gFFI.getId();
return SingleChildScrollView( return Container(
decoration: BoxDecoration(
color: MyTheme.grayBg
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
getUpdateUI(), getUpdateUI(),
getSearchBarUI(), Row(
children: [
getSearchBarUI(),
],
).marginOnly(top: 16.0, left: 16.0),
SizedBox(height: 12), SizedBox(height: 12),
Divider(thickness: 1,),
getPeers(), getPeers(),
]), ]),
); );
@ -106,104 +115,102 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// UI for the search bar. /// UI for the search bar.
/// Search for a peer and connect to it if the id exists. /// Search for a peer and connect to it if the id exists.
Widget getSearchBarUI() { Widget getSearchBarUI() {
var w = Padding( var w = Container(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0), width: 500,
child: Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Padding( decoration: BoxDecoration(
padding: const EdgeInsets.only(top: 16, bottom: 16), color: MyTheme.white,
child: Ink( borderRadius: const BorderRadius.all(Radius.circular(13)),
decoration: BoxDecoration( ),
color: MyTheme.white, child: Ink(
borderRadius: const BorderRadius.all(Radius.circular(13)), child: Column(
), children: [
child: Column( Row(
children: [ children: <Widget>[
Row( Expanded(
children: <Widget>[ child: Container(
Expanded( child: TextField(
child: Container( autocorrect: false,
padding: const EdgeInsets.only(left: 16, right: 16), enableSuggestions: false,
child: TextField( keyboardType: TextInputType.visiblePassword,
autocorrect: false, // keyboardType: TextInputType.number,
enableSuggestions: false, style: TextStyle(
keyboardType: TextInputType.visiblePassword, fontFamily: 'WorkSans',
// keyboardType: TextInputType.number, fontWeight: FontWeight.bold,
style: TextStyle( fontSize: 30,
fontFamily: 'WorkSans', // color: MyTheme.idColor,
fontWeight: FontWeight.bold, ),
fontSize: 30, decoration: InputDecoration(
// color: MyTheme.idColor, labelText: translate('Control Remote Desktop'),
), // hintText: 'Enter your remote ID',
decoration: InputDecoration( // border: InputBorder.,
labelText: translate('Control Remote Desktop'), border: OutlineInputBorder(
// hintText: 'Enter your remote ID', borderRadius: BorderRadius.zero),
// border: InputBorder., helperStyle: TextStyle(
border: OutlineInputBorder( fontWeight: FontWeight.bold,
borderRadius: BorderRadius.zero), fontSize: 16,
helperStyle: TextStyle( color: MyTheme.dark,
fontWeight: FontWeight.bold, ),
fontSize: 16, labelStyle: TextStyle(
color: MyTheme.dark, fontWeight: FontWeight.w600,
), fontSize: 26,
labelStyle: TextStyle( letterSpacing: 0.2,
fontWeight: FontWeight.w600, color: MyTheme.dark,
fontSize: 26,
letterSpacing: 0.2,
color: MyTheme.dark,
),
),
controller: _idController,
), ),
), ),
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,
),
),
],
),
)
],
), ),
), ),
); );

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/material.dart' hide MenuItem;
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
@ -42,9 +44,6 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
child: buildServerInfo(context), child: buildServerInfo(context),
flex: 1, flex: 1,
), ),
SizedBox(
width: 16.0,
),
Flexible( Flexible(
child: buildServerBoard(context), child: buildServerBoard(context),
flex: 4, flex: 4,
@ -76,12 +75,8 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
buildServerBoard(BuildContext context) { buildServerBoard(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// buildControlPanel(context), Expanded(child: ConnectionPage()),
// buildRecentSession(context),
Expanded(child: ConnectionPage())
], ],
); );
} }
@ -105,9 +100,35 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
translate("ID"), mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), 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( TextFormField(
controller: model.serverId, controller: model.serverId,
@ -194,7 +215,9 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
children: [ children: [
TextFormField( TextFormField(
controller: TextEditingController(), controller: TextEditingController(),
inputFormatters: [], inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r"[0-9]"))
],
) )
], ],
)) ))
@ -232,4 +255,152 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
trayManager.removeListener(this); trayManager.removeListener(this);
super.dispose(); 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<String> 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<String> genAudioInputPopupMenuItem() {
final _enabledInput = gFFI.getOption('enable-audio');
var defaultInput = gFFI.getDefaultAudioInput().obs;
var enabled = (_enabledInput != "N").obs;
return PopupMenuItem(child: FutureBuilder<List<String>>(
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<String>(
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("确定")),
],
);
});
}
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@ -990,6 +991,17 @@ class FFI {
ffiModel.platformFFI.setByName(name, value); ffiModel.platformFFI.setByName(name, value);
} }
String getOption(String name) {
return ffiModel.platformFFI.getByName("option", name);
}
void setOption(String name, String value) {
Map<String, String> res = Map()
..["name"] = name
..["value"] = value;
return ffiModel.platformFFI.setByName('option', jsonEncode(res));
}
RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; RustdeskImpl get bind => ffiModel.platformFFI.ffiBind;
handleMouse(Map<String, dynamic> evt) { handleMouse(Map<String, dynamic> evt) {
@ -1062,6 +1074,22 @@ class FFI {
Future<bool> invokeMethod(String method, [dynamic arguments]) async { Future<bool> invokeMethod(String method, [dynamic arguments]) async {
return await ffiModel.platformFFI.invokeMethod(method, arguments); return await ffiModel.platformFFI.invokeMethod(method, arguments);
} }
Future<List<String>> 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 { class Peer {

View File

@ -187,8 +187,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: c7d97cb6615f2def34f8bad4def01af9e0077beb ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45"
resolved-ref: c7d97cb6615f2def34f8bad4def01af9e0077beb resolved-ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45"
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@ -19,6 +19,7 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s
use crate::flutter::{self, Session, SESSIONS}; use crate::flutter::{self, Session, SESSIONS};
use crate::start_server; use crate::start_server;
use crate::ui_interface; 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) { fn initialize(app_dir: &str) {
*config::APP_DIR.write().unwrap() = app_dir.to_owned(); *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<String> {
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. /// FFI for **get** commands which are idempotent.
/// Return result in c string. /// Return result in c string.
/// ///