Merge branch 'master' into csf

This commit is contained in:
rustdesk
2022-02-10 02:07:53 +08:00
78 changed files with 8105 additions and 617 deletions

View File

@@ -1,12 +1,10 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_hbb/server_page.dart';
import 'package:tuple/tuple.dart';
import 'dart:io';
import 'main.dart';
typedef F = String Function(String);
typedef FMethod = String Function(String, dynamic);
class Translator {
static F call;
@@ -22,6 +20,8 @@ class MyTheme {
static const Color accent80 = Color(0xAA0071FF);
static const Color canvasColor = Color(0xFF212121);
static const Color border = Color(0xFFCCCCCC);
static const Color idColor = Color(0xFF00B6F0);
static const Color darkGray = Color(0xFFB9BABC);
}
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
@@ -32,41 +32,44 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
),
);
void Function() loadingCancelCallback = null;
void Function() loadingCancelCallback;
void showLoading(String text, BuildContext context) {
if (_hasDialog && context != null) {
Navigator.pop(context);
_hasDialog = false;
}
dismissLoading();
if (Platform.isAndroid) {
if (isAndroid) {
EasyLoading.show(status: text, maskType: EasyLoadingMaskType.black);
return;
}
EasyLoading.showWidget(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: CircularProgressIndicator()),
SizedBox(height: 20),
Center(
child:
Text(Translator.call(text), style: TextStyle(fontSize: 15))),
SizedBox(height: 20),
Center(
child: TextButton(
style: flatButtonStyle,
onPressed: () {
// with out loadingCancelCallback, we can see unexpected input password
// dialog shown in home, no clue why, so use this as workaround
// why no such issue on android?
if (loadingCancelCallback != null) loadingCancelCallback();
Navigator.pop(context);
},
child: Text(Translator.call('Cancel'),
style: TextStyle(color: MyTheme.accent))))
],
),
Container(
constraints: BoxConstraints(maxWidth: 300),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: CircularProgressIndicator()),
SizedBox(height: 20),
Center(
child: Text(Translator.call(text),
style: TextStyle(fontSize: 15))),
SizedBox(height: 20),
Center(
child: TextButton(
style: flatButtonStyle,
onPressed: () {
// with out loadingCancelCallback, we can see unexpected input password
// dialog shown in home, no clue why, so use this as workaround
// why no such issue on android?
if (loadingCancelCallback != null)
loadingCancelCallback();
Navigator.pop(context);
},
child: Text(Translator.call('Cancel'),
style: TextStyle(color: MyTheme.accent))))
],
)),
maskType: EasyLoadingMaskType.black);
}
@@ -126,6 +129,7 @@ void msgbox(String type, String title, String text, BuildContext context,
dismissLoading();
if (_hasDialog) {
Navigator.pop(context);
_hasDialog = false;
}
final buttons = [
Expanded(child: Container()),
@@ -145,18 +149,20 @@ void msgbox(String type, String title, String text, BuildContext context,
}));
}
EasyLoading.showWidget(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(Translator.call(title), style: TextStyle(fontSize: 21)),
SizedBox(height: 20),
Text(Translator.call(text), style: TextStyle(fontSize: 15)),
SizedBox(height: 20),
Row(
children: buttons,
)
],
),
Container(
constraints: BoxConstraints(maxWidth: 300),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(Translator.call(title), style: TextStyle(fontSize: 21)),
SizedBox(height: 20),
Text(Translator.call(text), style: TextStyle(fontSize: 15)),
SizedBox(height: 20),
Row(
children: buttons,
)
],
)),
maskType: EasyLoadingMaskType.black);
}
@@ -211,44 +217,8 @@ Color str2color(String str, [alpha = 0xFF]) {
return Color((hash & 0xFF7FFF) | (alpha << 24));
}
toAndroidChannelInit() {
toAndroidChannel.setMethodCallHandler((call) async {
debugPrint("flutter got android msg");
try {
switch (call.method) {
case "try_start_without_auth":
{
// 可以不需要传递 通过FFI直接去获取 serverModel里面直接封装一个update通过FFI从rust端获取
ServerPage.serverModel.updateClientState();
debugPrint("pre show loginAlert:${ServerPage.serverModel.isFileTransfer.toString()}");
showLoginReqAlert(nowCtx, ServerPage.serverModel.peerID, ServerPage.serverModel.peerName);
debugPrint("from jvm:try_start_without_auth done");
break;
}
case "start_capture":
{
clearLoginReqAlert();
ServerPage.serverModel.updateClientState();
break;
}
case "stop_capture":
{
ServerPage.serverModel.setPeer(false);
break;
}
case "on_permission_changed":
{
var name = call.arguments["name"] as String;
var value = call.arguments["value"] as String == "true";
debugPrint("from jvm:on_permission_changed,$name:$value");
ServerPage.serverModel.changeStatue(name, value);
break;
}
}
} catch (e) {
debugPrint("MethodCallHandler err:$e");
}
return null;
});
}
bool isAndroid = false;
bool isIOS = false;
bool isWeb = false;
bool isDesktop = false;
BuildContext nowCtx;

View File

@@ -1,14 +1,11 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:package_info/package_info.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
import 'common.dart';
import 'main.dart';
import 'model.dart';
import 'remote_page.dart';
import 'dart:io';
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
@@ -22,12 +19,13 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
final _idController = TextEditingController();
var _updateUrl = '';
var _menuPos;
@override
void initState() {
super.initState();
nowCtx = context;
if (Platform.isAndroid) {
if (isAndroid) {
Timer(Duration(seconds: 5), () {
_updateUrl = FFI.getByName('software_update_url');
if (_updateUrl.isNotEmpty) setState(() {});
@@ -45,37 +43,45 @@ class _HomePageState extends State<HomePage> {
appBar: AppBar(
centerTitle: true,
actions: [
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(3000, 70, 3000, 70),
items: [
PopupMenuItem<String>(
child: Text(translate('ID Server')),
value: 'id_server'),
Platform.isAndroid
? PopupMenuItem<String>(
child: Text(translate('Share My Screen')),
value: 'server')
: null,
PopupMenuItem<String>(
child: Text(translate('About') + ' RustDesk'),
value: 'about'),
],
elevation: 8,
);
if (value == 'id_server') {
showServer(context);
} else if (value == 'server') {
Navigator.pushNamed(context, "server_page");
} else if (value == 'about') {
showAbout(context);
}
}();
})
Ink(
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(Icons.more_vert)),
onTapDown: (e) {
var x = e.globalPosition.dx;
var y = e.globalPosition.dy;
this._menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () {
() async {
var value = await showMenu(
context: context,
position: this._menuPos,
items: [
PopupMenuItem<String>(
child: Text(translate('ID Server')),
value: 'server'),
isAndroid
? PopupMenuItem<String>(
child: Text(translate('Share My Screen')),
value: 'server')
: null,
PopupMenuItem<String>(
child: Text(translate('About') + ' RustDesk'),
value: 'about'),
],
elevation: 8,
);
if (value == 'server') {
showServer(context);
} else if (value == 'server') {
Navigator.pushNamed(context, "server_page");
} else if (value == 'about') {
showAbout(context);
}
}();
}))
],
title: Text(widget.title),
),
@@ -104,6 +110,7 @@ class _HomePageState extends State<HomePage> {
color: Colors.white,
fontWeight: FontWeight.bold)))),
getSearchBarUI(),
Container(height: 12),
getPeers(),
]),
));
@@ -136,13 +143,13 @@ class _HomePageState extends State<HomePage> {
if (!FFI.ffiModel.initialized) {
return Container();
}
return Padding(
var w = Padding(
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0),
child: Container(
height: 84,
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Container(
child: Ink(
decoration: BoxDecoration(
color: MyTheme.white,
borderRadius: const BorderRadius.only(
@@ -166,7 +173,7 @@ class _HomePageState extends State<HomePage> {
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
fontSize: 30,
color: Color(0xFF00B6F0),
color: MyTheme.idColor,
),
decoration: InputDecoration(
labelText: translate('Remote ID'),
@@ -175,13 +182,13 @@ class _HomePageState extends State<HomePage> {
helperStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFFB9BABC),
color: MyTheme.darkGray,
),
labelStyle: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.2,
color: Color(0xFFB9BABC),
color: MyTheme.darkGray,
),
),
autofocus: _idController.text.isEmpty,
@@ -194,7 +201,7 @@ class _HomePageState extends State<HomePage> {
height: 60,
child: IconButton(
icon: Icon(Icons.arrow_forward,
color: Color(0xFFB9BABC), size: 45),
color: MyTheme.darkGray, size: 45),
onPressed: onConnect,
autofocus: _idController.text.isNotEmpty,
),
@@ -205,6 +212,8 @@ class _HomePageState extends State<HomePage> {
),
),
);
return Center(
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
}
@override
@@ -225,46 +234,75 @@ class _HomePageState extends State<HomePage> {
if (!FFI.ffiModel.initialized) {
return Container();
}
final size = MediaQuery.of(context).size;
final space = 8.0;
var width = size.width - 2 * space;
final minWidth = 320.0;
if (size.width > minWidth + 2 * space) {
final n = (size.width / (minWidth + 2 * space)).floor();
width = size.width / n - 2 * space;
}
final cards = <Widget>[];
var peers = FFI.peers();
peers.forEach((p) {
cards.add(Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
cards.add(Container(
width: width,
child: Card(
child: GestureDetector(
onTap: () => connect('${p.id}'),
onTap: () => {
if (!isDesktop) {connect('${p.id}')}
},
onDoubleTap: () => {
if (isDesktop) {connect('${p.id}')}
},
onLongPressStart: (details) {
var x = details.globalPosition.dx;
var y = details.globalPosition.dy;
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
PopupMenuItem<String>(
child: Text(translate('Remove')),
value: 'remove'),
],
elevation: 8,
);
if (value == 'remove') {
setState(() => FFI.setByName('remove', '${p.id}'));
() async {
removePreference(p.id);
}();
}
}();
this._menuPos = RelativeRect.fromLTRB(x, y, x, y);
this.showPeerMenu(context, p.id);
},
child: ListTile(
contentPadding: const EdgeInsets.only(left: 12),
subtitle: Text('${p.username}@${p.hostname}'),
title: Text('${p.id}'),
leading: Container(
padding: const EdgeInsets.all(6),
child: getPlatformImage('${p.platform}'),
color: str2color('${p.id}${p.platform}', 0x7f)),
trailing: InkWell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(Icons.more_vert)),
onTapDown: (e) {
var x = e.globalPosition.dx;
var y = e.globalPosition.dy;
this._menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onDoubleTap: () {},
onTap: () {
showPeerMenu(context, p.id);
}),
)))));
});
return Wrap(children: cards);
return Wrap(children: cards, spacing: space, runSpacing: space);
}
void showPeerMenu(BuildContext context, String id) async {
var value = await showMenu(
context: context,
position: this._menuPos,
items: [
PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'remove'),
],
elevation: 8,
);
if (value == 'remove') {
setState(() => FFI.setByName('remove', '$id'));
() async {
removePreference(id);
}();
}
}
}
@@ -332,13 +370,13 @@ void showServer(BuildContext context) {
formKey.currentState.save();
if (id != id0)
FFI.setByName('option',
'{"name": "custom-rendezvous-server", "value": "${id}"}');
'{"name": "custom-rendezvous-server", "value": "$id"}');
if (relay != relay0)
FFI.setByName('option',
'{"name": "relay-server", "value": "${relay}"}');
'{"name": "relay-server", "value": "$relay"}');
if (key != key0)
FFI.setByName(
'option', '{"name": "key", "value": "${key}"}');
'option', '{"name": "key", "value": "$key"}');
Navigator.pop(context);
}
},
@@ -349,13 +387,13 @@ void showServer(BuildContext context) {
}
Future<Null> showAbout(BuildContext context) async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
var version = await FFI.getVersion();
showAlertDialog(
context,
(setState) => Tuple3(
null,
Wrap(direction: Axis.vertical, spacing: 12, children: [
Text('Version: ${packageInfo.version}'),
Text('Version: $version'),
InkWell(
onTap: () async {
const url = 'https://rustdesk.com/';

View File

@@ -1,16 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/server_page.dart';
import 'package:provider/provider.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:firebase_core/firebase_core.dart';
import 'common.dart';
import 'model.dart';
import 'home_page.dart';
const toAndroidChannel = MethodChannel("mChannel");
BuildContext nowCtx;
import 'server_page.dart';
Future<Null> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -20,7 +15,6 @@ Future<Null> main() async {
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
final analytics = FirebaseAnalytics();
@@ -40,7 +34,7 @@ class App extends StatelessWidget {
),
home: HomePage(title: 'RustDesk'),
routes: {
"server_page":(context) => ServerPage(),
"server_page": (context) => ServerPage(),
},
navigatorObservers: [
FirebaseAnalyticsObserver(analytics: analytics),

View File

@@ -1,13 +1,6 @@
import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart';
import 'package:flutter/gestures.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:device_info/device_info.dart';
import 'package:external_path/external_path.dart';
import 'dart:io';
import 'dart:math';
import 'dart:ffi';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' as ui;
@@ -15,17 +8,7 @@ import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
import 'dart:async';
import 'common.dart';
class RgbaFrame extends Struct {
@Uint32()
int len;
Pointer<Uint8> data;
}
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F4 = void Function(Pointer<RgbaFrame>);
typedef F5 = Pointer<RgbaFrame> Function();
import 'native_model.dart' if (dart.library.html) 'web_model.dart';
class FfiModel with ChangeNotifier {
PeerInfo _pi;
@@ -33,6 +16,7 @@ class FfiModel with ChangeNotifier {
var _decoding = false;
bool _waitForImage;
bool _initialized = false;
var _inputBlocked = false;
final _permissions = Map<String, bool>();
bool _secure;
bool _direct;
@@ -48,12 +32,17 @@ class FfiModel with ChangeNotifier {
get direct => _direct;
get pi => _pi;
get inputBlocked => _inputBlocked;
set inputBlocked(v) {
_inputBlocked = v;
}
FfiModel() {
Translator.call = translate;
clear();
() async {
await FFI.init();
await PlatformFFI.init();
_initialized = true;
print("FFI initialized");
notifyListeners();
@@ -66,6 +55,7 @@ class FfiModel with ChangeNotifier {
_permissions[k] = v == 'true';
});
print('$_permissions');
notifyListeners();
}
bool keyboard() => _permissions['keyboard'] != false;
@@ -76,6 +66,7 @@ class FfiModel with ChangeNotifier {
_waitForImage = false;
_secure = null;
_direct = null;
_inputBlocked = false;
clearPermissions();
}
@@ -101,6 +92,7 @@ class FfiModel with ChangeNotifier {
}
void clearPermissions() {
_inputBlocked = false;
_permissions.clear();
}
@@ -140,7 +132,7 @@ class FfiModel with ChangeNotifier {
}
if (pos != null) FFI.cursorModel.updateCursorPosition(pos);
if (!_decoding) {
var rgba = FFI.getRgba();
var rgba = PlatformFFI.getRgba();
if (rgba != null) {
if (_waitForImage) {
_waitForImage = false;
@@ -151,7 +143,7 @@ class FfiModel with ChangeNotifier {
ui.decodeImageFromPixels(
rgba, _display.width, _display.height, ui.PixelFormat.bgra8888,
(image) {
FFI.clearRgbaFrame();
PlatformFFI.clearRgbaFrame();
_decoding = false;
if (FFI.id != pid) return;
try {
@@ -198,7 +190,6 @@ class FfiModel with ChangeNotifier {
}
if (_pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay];
initializeCursorAndCanvas();
}
if (displays.length > 0) {
showLoading(translate('Connected, waiting for image...'), context);
@@ -215,10 +206,15 @@ class ImageModel with ChangeNotifier {
void update(ui.Image image) {
if (_image == null && image != null) {
final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / image.width;
final yscale = size.height / image.height;
FFI.canvasModel.scale = max(xscale, yscale);
if (isDesktop) {
FFI.canvasModel.updateViewStyle();
} else {
final size = MediaQueryData.fromWindow(ui.window).size;
final xscale = size.width / image.width;
final yscale = size.height / image.height;
FFI.canvasModel.scale = max(xscale, yscale);
}
initializeCursorAndCanvas();
}
_image = image;
if (image != null) notifyListeners();
@@ -256,6 +252,29 @@ class CanvasModel with ChangeNotifier {
double get scale => _scale;
void updateViewStyle() {
final s = FFI.getByName('peer_option', 'view-style');
final size = MediaQueryData.fromWindow(ui.window).size;
final s1 = size.width / FFI.ffiModel.display.width;
final s2 = size.height / FFI.ffiModel.display.height;
if (s == 'shrink') {
final s = s1 < s2 ? s1 : s2;
if (s < 1) {
_scale = s;
}
} else if (s == 'stretch') {
final s = s1 > s2 ? s1 : s2;
if (s > 1) {
_scale = s;
}
} else {
_scale = 1;
}
_x = (size.width - FFI.ffiModel.display.width * _scale) / 2;
_y = (size.height - FFI.ffiModel.display.height * _scale) / 2;
notifyListeners();
}
void update(double x, double y, double scale) {
_x = x;
_y = y;
@@ -263,6 +282,26 @@ class CanvasModel with ChangeNotifier {
notifyListeners();
}
void moveDesktopMouse(double x, double y) {
final size = MediaQueryData.fromWindow(ui.window).size;
final dw = FFI.ffiModel.display.width * _scale;
final dh = FFI.ffiModel.display.height * _scale;
var dxOffset = 0;
var dyOffset = 0;
if (dw > size.width) {
dxOffset = (x - dw * (x / size.width) - _x).toInt();
}
if (dh > size.height) {
dyOffset = (y - dh * (y / size.height) - _y).toInt();
}
_x += dxOffset;
_y += dyOffset;
if (dxOffset != 0 || dyOffset != 0) {
notifyListeners();
}
FFI.cursorModel.move(x, y);
}
set scale(v) {
_scale = v;
notifyListeners();
@@ -274,8 +313,12 @@ class CanvasModel with ChangeNotifier {
}
void resetOffset() {
_x = 0;
_y = 0;
if (isDesktop) {
updateViewStyle();
} else {
_x = 0;
_y = 0;
}
notifyListeners();
}
@@ -356,13 +399,17 @@ class CursorModel with ChangeNotifier {
}
void touch(double x, double y, bool right) {
move(x, y);
FFI.moveMouse(_x, _y);
FFI.tap(right);
}
void move(double x, double y) {
final scale = FFI.canvasModel.scale;
final xoffset = FFI.canvasModel.x;
final yoffset = FFI.canvasModel.y;
_x = (x - xoffset) / scale + _displayOriginX;
_y = (y - yoffset) / scale + _displayOriginY;
FFI.moveMouse(_x, _y);
FFI.tap(right);
notifyListeners();
}
@@ -558,7 +605,7 @@ class ServerModel with ChangeNotifier {
bool get inputOk => _inputOk;
// bool get needServerOpen => _needServerOpen;
bool get isPeerStart => _isPeerStart;
bool get isFileTransfer => _isFileTransfer;
@@ -625,13 +672,6 @@ class ServerModel with ChangeNotifier {
class FFI {
static String id = "";
static String _dir = "";
static String _homeDir = "";
static F2 _getByName;
static F3 _setByName;
static F4 _freeRgba;
static F5 _getRgba;
static Pointer<RgbaFrame> _lastRgbaFrame;
static var shift = false;
static var ctrl = false;
static var alt = false;
@@ -640,6 +680,7 @@ class FFI {
static final ffiModel = FfiModel();
static final cursorModel = CursorModel();
static final canvasModel = CanvasModel();
static final serverModel = ServerModel();
static String getId() {
return getByName('remote_id');
@@ -682,7 +723,8 @@ class FFI {
static void inputKey(String name) {
if (!ffiModel.keyboard()) return;
setByName('input_key', json.encode(modify({'name': name})));
setByName(
'input_key', json.encode(modify({'name': name, 'press': 'true'})));
}
static void moveMouse(double x, double y) {
@@ -711,19 +753,6 @@ class FFI {
FFI.id = id;
}
static void clearRgbaFrame() {
if (_lastRgbaFrame != null && _lastRgbaFrame != nullptr)
_freeRgba(_lastRgbaFrame);
}
static Uint8List getRgba() {
if (_getRgba == null) return null;
_lastRgbaFrame = _getRgba();
if (_lastRgbaFrame == null || _lastRgbaFrame == nullptr) return null;
final ref = _lastRgbaFrame.ref;
return Uint8List.sublistView(ref.data.asTypedList(ref.len));
}
static Map<String, dynamic> popEvent() {
var s = getByName('event');
if (s == '') return null;
@@ -746,8 +775,10 @@ class FFI {
}
static void close() {
savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x,
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
if (FFI.imageModel.image != null && !isDesktop) {
savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x,
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
}
id = "";
setByName('close', '');
imageModel.update(null);
@@ -757,63 +788,86 @@ class FFI {
resetModifiers();
}
static void setByName(String name, [String value = '']) {
if (_setByName == null) return;
var a = name.toNativeUtf8();
var b = value.toNativeUtf8();
_setByName(a, b);
calloc.free(a);
calloc.free(b);
}
static String getByName(String name, [String arg = '']) {
if (_getByName == null) return '';
var a = name.toNativeUtf8();
var b = arg.toNativeUtf8();
var p = _getByName(a, b);
assert(p != nullptr && p != null);
var res = p.toDartString();
calloc.free(p);
calloc.free(a);
calloc.free(b);
return res;
return PlatformFFI.getByName(name, arg);
}
static Future<Null> init() async {
final dylib = Platform.isAndroid
? DynamicLibrary.open('librustdesk.so')
: DynamicLibrary.process();
print('initializing FFI');
try {
_getByName = dylib.lookupFunction<F2, F2>('get_by_name');
_setByName =
dylib.lookupFunction<Void Function(Pointer<Utf8>, Pointer<Utf8>), F3>(
'set_by_name');
_freeRgba = dylib
.lookupFunction<Void Function(Pointer<RgbaFrame>), F4>('free_rgba');
_getRgba = dylib.lookupFunction<F5, F5>('get_rgba');
_dir = (await getApplicationDocumentsDirectory()).path;
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
String id = 'NA';
String name = 'Flutter';
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
name = '${androidInfo.brand}-${androidInfo.model}';
id = androidInfo.id.hashCode.toString();
} else {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
name = iosInfo.utsname.machine;
id = iosInfo.identifierForVendor.hashCode.toString();
}
debugPrint("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir");
setByName('info1', id);
setByName('info2', name);
setByName('home_dir',_homeDir);
setByName('init', _dir);
} catch (e) {
print(e);
static void setByName(String name, [String value = '']) {
PlatformFFI.setByName(name, value);
}
static Future<String> getVersion() async {
return await PlatformFFI.getVersion();
}
static handleMouse(Map<String, dynamic> evt) {
var type = '';
var isMove = false;
switch (evt['type']) {
case 'mousedown':
type = 'down';
break;
case 'mouseup':
type = 'up';
break;
case 'mousemove':
isMove = true;
break;
default:
return;
}
evt['type'] = type;
var x = evt['x'];
var y = evt['y'];
if (isMove) {
FFI.canvasModel.moveDesktopMouse(x, y);
}
final d = FFI.ffiModel.display;
x -= FFI.canvasModel.x;
y -= FFI.canvasModel.y;
if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) {
return;
}
x /= FFI.canvasModel.scale;
y /= FFI.canvasModel.scale;
x += d.x;
y += d.y;
if (type != '') {
x = 0;
y = 0;
}
evt['x'] = '$x';
evt['y'] = '$y';
var buttons = '';
switch (evt['buttons']) {
case 1:
buttons = 'left';
break;
case 2:
buttons = 'right';
break;
case 4:
buttons = 'wheel';
break;
}
evt['buttons'] = buttons;
setByName('send_mouse', json.encode(evt));
}
static listenToMouse(bool yesOrNo) {
if (yesOrNo) {
PlatformFFI.startDesktopWebListener(handleMouse);
} else {
PlatformFFI.stopDesktopWebListener();
}
}
static void setMethodCallHandler(FMethod callback) {
PlatformFFI.setMethodCallHandler(callback);
}
static Future<bool> invokeMethod(String method) async {
return await PlatformFFI.invokeMethod(method);
}
}
@@ -861,6 +915,7 @@ void savePreference(String id, double xCursor, double yCursor, double xCanvas,
}
Future<Map<String, dynamic>> getPreference(String id) async {
if (!isDesktop) return null;
SharedPreferences prefs = await SharedPreferences.getInstance();
var p = prefs.getString('peer' + id);
if (p == null) return null;
@@ -894,33 +949,11 @@ void initializeCursorAndCanvas() async {
FFI.canvasModel.update(xCanvas, yCanvas, scale);
}
final locale = Platform.localeName;
final bool isCn =
locale.startsWith('zh') && (locale.endsWith('CN') || locale.endsWith('SG'));
final langs = <String, Map<String, String>>{
'cn': <String, String>{
'Remote ID': '远程ID',
'Paste': '粘贴',
'Are you sure to close the connection?': '是否确认关闭连接?',
'Download new version': '下载新版本',
'Touch mode': '触屏模式',
'Reset canvas': '重置画布',
},
'en': <String, String>{}
};
String translate(String name) {
if (name.startsWith('Failed') && name.contains(':')) {
if (name.startsWith('Failed to') && name.contains(': ')) {
return name.split(': ').map((x) => translate(x)).join(': ');
}
final tmp = isCn ? langs['cn'] : langs['en'];
final v = tmp[name];
if (v == null) {
var a = 'translate';
var b = '{"locale": "${Platform.localeName}", "text": "${name}"}';
return FFI.getByName(a, b);
} else {
return v;
}
var a = 'translate';
var b = '{"locale": "$localeName", "text": "$name"}';
return FFI.getByName(a, b);
}

128
lib/native_model.dart Normal file
View File

@@ -0,0 +1,128 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:device_info/device_info.dart';
import 'package:package_info/package_info.dart';
import 'package:external_path/external_path.dart';
import 'package:flutter/services.dart';
import 'common.dart';
class RgbaFrame extends Struct {
@Uint32()
int len;
Pointer<Uint8> data;
}
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F4 = void Function(Pointer<RgbaFrame>);
typedef F5 = Pointer<RgbaFrame> Function();
class PlatformFFI {
static Pointer<RgbaFrame> _lastRgbaFrame;
static String _dir = '';
static String _homeDir = '';
static F2 _getByName;
static F3 _setByName;
static F4 _freeRgba;
static F5 _getRgba;
static void clearRgbaFrame() {
if (_lastRgbaFrame != null && _lastRgbaFrame != nullptr)
_freeRgba(_lastRgbaFrame);
}
static Uint8List getRgba() {
if (_getRgba == null) return null;
_lastRgbaFrame = _getRgba();
if (_lastRgbaFrame == null || _lastRgbaFrame == nullptr) return null;
final ref = _lastRgbaFrame.ref;
return Uint8List.sublistView(ref.data.asTypedList(ref.len));
}
static Future<String> getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
}
static String getByName(String name, [String arg = '']) {
if (_getByName == null) return '';
var a = name.toNativeUtf8();
var b = arg.toNativeUtf8();
var p = _getByName(a, b);
assert(p != nullptr && p != null);
var res = p.toDartString();
calloc.free(p);
calloc.free(a);
calloc.free(b);
return res;
}
static void setByName(String name, [String value = '']) {
if (_setByName == null) return;
var a = name.toNativeUtf8();
var b = value.toNativeUtf8();
_setByName(a, b);
calloc.free(a);
calloc.free(b);
}
static Future<Null> init() async {
isIOS = Platform.isIOS;
isAndroid = Platform.isAndroid;
final dylib = Platform.isAndroid
? DynamicLibrary.open('librustdesk.so')
: DynamicLibrary.process();
print('initializing FFI');
try {
_getByName = dylib.lookupFunction<F2, F2>('get_by_name');
_setByName =
dylib.lookupFunction<Void Function(Pointer<Utf8>, Pointer<Utf8>), F3>(
'set_by_name');
_freeRgba = dylib
.lookupFunction<Void Function(Pointer<RgbaFrame>), F4>('free_rgba');
_getRgba = dylib.lookupFunction<F5, F5>('get_rgba');
_dir = (await getApplicationDocumentsDirectory()).path;
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
String id = 'NA';
String name = 'Flutter';
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
name = '${androidInfo.brand}-${androidInfo.model}';
id = androidInfo.id.hashCode.toString();
} else {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
name = iosInfo.utsname.machine;
id = iosInfo.identifierForVendor.hashCode.toString();
}
print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir");
setByName('info1', id);
setByName('info2', name);
setByName('home_dir', _homeDir);
setByName('init', _dir);
} catch (e) {
print(e);
}
}
static void startDesktopWebListener(
Function(Map<String, dynamic>) handleMouse) {}
static void stopDesktopWebListener() {}
static void setMethodCallHandler(FMethod callback) {
toAndroidChannel.setMethodCallHandler((call) async {
callback(call.method, call.arguments);
return null;
});
}
static invokeMethod(String method) async {
return await toAndroidChannel.invokeMethod(method);
}
}
final localeName = Platform.localeName;
final toAndroidChannel = MethodChannel("mChannel");

View File

@@ -8,7 +8,6 @@ import 'package:tuple/tuple.dart';
import 'package:wakelock/wakelock.dart';
import 'common.dart';
import 'model.dart';
import 'dart:io';
final initText = '\1' * 1024;
@@ -24,7 +23,7 @@ class RemotePage extends StatefulWidget {
class _RemotePageState extends State<RemotePage> {
Timer _interval;
Timer _timer;
bool _showBar = true;
bool _showBar = !isDesktop;
double _bottom = 0;
String _value = '';
double _xOffset = 0;
@@ -79,7 +78,8 @@ class _RemotePageState extends State<RemotePage> {
return _bottom >= 100;
}
void interval() {
// crash on web before widgit initiated.
void intervalUnsafe() {
var v = MediaQuery.of(context).viewInsets.bottom;
if (v != _bottom) {
resetTool();
@@ -93,6 +93,12 @@ class _RemotePageState extends State<RemotePage> {
FFI.ffiModel.update(widget.id, context, handleMsgbox);
}
void interval() {
try {
intervalUnsafe();
} catch (e) {}
}
void handleMsgbox(Map<String, dynamic> evt, String id) {
var type = evt['type'];
var title = evt['title'];
@@ -103,6 +109,7 @@ class _RemotePageState extends State<RemotePage> {
enterPasswordDialog(id, context);
} else {
var hasRetry = evt['hasRetry'] == 'true';
print(evt);
showMsgBox(type, title, text, hasRetry);
}
}
@@ -124,7 +131,7 @@ class _RemotePageState extends State<RemotePage> {
void handleInput(String newValue) {
var oldValue = _value;
_value = newValue;
if (Platform.isIOS) {
if (isIOS) {
var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {}
var j = oldValue.length - 1;
@@ -236,6 +243,7 @@ class _RemotePageState extends State<RemotePage> {
return false;
},
child: Scaffold(
// resizeToAvoidBottomInset: true,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
@@ -252,175 +260,218 @@ class _RemotePageState extends State<RemotePage> {
}
});
}),
bottomNavigationBar: _showBar && pi.displays != null
? BottomAppBar(
elevation: 10,
color: MyTheme.accent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(children: [
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
close();
},
),
IconButton(
color: Colors.white,
icon: Icon(Icons.keyboard),
onPressed: openKeyboard),
IconButton(
color: Colors.white,
icon: Icon(Icons.tv),
onPressed: () {
setState(() => _showEdit = false);
showOptions(context);
},
),
Container(
color: _mouseTools ? Colors.blue[500] : null,
child: IconButton(
color: Colors.white,
icon: Icon(Icons.mouse),
onPressed: () {
setState(() {
_mouseTools = !_mouseTools;
resetTool();
if (_mouseTools) _drag = true;
});
},
)),
IconButton(
color: Colors.white,
icon: Icon(Icons.more_vert),
onPressed: () {
setState(() => _showEdit = false);
showActions(context);
},
),
]),
IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: () {
setState(() => _showBar = !_showBar);
}),
],
),
)
: null,
bottomNavigationBar:
_showBar && pi.displays != null ? getBottomAppBar() : null,
body: FlutterEasyLoading(
child: Container(
color: Colors.black,
child: SafeArea(
child: GestureDetector(
onLongPress: () {
if (_drag || _scroll) return;
// make right click and real left long click both work
// should add "long press = right click" option?
FFI.sendMouse('down', 'left');
FFI.tap(true);
},
onLongPressUp: () {
FFI.sendMouse('up', 'left');
},
onTapUp: (details) {
if (_drag || _scroll) return;
if (_touchMode) {
FFI.cursorModel.touch(details.localPosition.dx,
details.localPosition.dy, _right);
} else {
FFI.tap(_right);
}
},
onScaleStart: (details) {
_scale = 1;
_xOffset = details.focalPoint.dx;
_yOffset = _yOffset0 = details.focalPoint.dy;
if (_drag) {
FFI.sendMouse('down', 'left');
}
},
onScaleUpdate: (details) {
var scale = details.scale;
if (scale == 1) {
if (!_scroll) {
var x = details.focalPoint.dx;
var y = details.focalPoint.dy;
var dx = x - _xOffset;
var dy = y - _yOffset;
FFI.cursorModel
.updatePan(dx, dy, _touchMode, _drag);
_xOffset = x;
_yOffset = y;
} else {
_xOffset = details.focalPoint.dx;
_yOffset = details.focalPoint.dy;
}
} else if (!_drag && !_scroll) {
FFI.canvasModel.updateScale(scale / _scale);
_scale = scale;
}
},
onScaleEnd: (details) {
if (_drag) {
FFI.sendMouse('up', 'left');
setState(resetMouse);
} else if (_scroll) {
var dy = (_yOffset - _yOffset0) / 10;
if (dy.abs() > 0.1) {
if (dy > 0 && dy < 1) dy = 1;
if (dy < 0 && dy > -1) dy = -1;
FFI.scroll(dy);
}
}
},
child: Container(
color: MyTheme.canvasColor,
child: Stack(children: [
ImagePaint(),
CursorPaint(),
getHelpTools(),
SizedBox(
width: 0,
height: 0,
child: !_showEdit
? Container()
: TextFormField(
textInputAction:
TextInputAction.newline,
autocorrect: false,
enableSuggestions: false,
focusNode: _focusNode,
maxLines: null,
initialValue:
_value, // trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleInput,
),
),
]))))),
child: isDesktop
? getBodyForDesktopWithListener()
: SafeArea(child: getBodyForMobileWithGuesture())),
)),
);
}
Widget getBottomAppBar() {
return BottomAppBar(
elevation: 10,
color: MyTheme.accent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
close();
},
)
] +
(isDesktop
? []
: [
IconButton(
color: Colors.white,
icon: Icon(Icons.keyboard),
onPressed: openKeyboard)
]) +
<Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.tv),
onPressed: () {
setState(() => _showEdit = false);
showOptions(context);
},
)
] +
(isDesktop
? []
: [
Container(
color: _mouseTools ? Colors.blue[500] : null,
child: IconButton(
color: Colors.white,
icon: Icon(Icons.mouse),
onPressed: () {
setState(() {
_mouseTools = !_mouseTools;
resetTool();
if (_mouseTools) _drag = true;
});
},
))
]) +
<Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.more_vert),
onPressed: () {
setState(() => _showEdit = false);
showActions(context);
},
),
]),
IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: () {
setState(() => _showBar = !_showBar);
}),
],
),
);
}
Widget getBodyForMobileWithGuesture() {
return GestureDetector(
onLongPress: () {
if (_drag || _scroll) return;
// make right click and real left long click both work
// should add "long press = right click" option?
FFI.sendMouse('down', 'left');
FFI.tap(true);
},
onLongPressUp: () {
FFI.sendMouse('up', 'left');
},
onTapUp: (details) {
if (_drag || _scroll) return;
if (_touchMode) {
FFI.cursorModel.touch(
details.localPosition.dx, details.localPosition.dy, _right);
} else {
FFI.tap(_right);
}
},
onScaleStart: (details) {
_scale = 1;
_xOffset = details.focalPoint.dx;
_yOffset = _yOffset0 = details.focalPoint.dy;
if (_drag) {
FFI.sendMouse('down', 'left');
}
},
onScaleUpdate: (details) {
var scale = details.scale;
if (scale == 1) {
if (!_scroll) {
var x = details.focalPoint.dx;
var y = details.focalPoint.dy;
var dx = x - _xOffset;
var dy = y - _yOffset;
FFI.cursorModel.updatePan(dx, dy, _touchMode, _drag);
_xOffset = x;
_yOffset = y;
} else {
_xOffset = details.focalPoint.dx;
_yOffset = details.focalPoint.dy;
}
} else if (!_drag && !_scroll) {
FFI.canvasModel.updateScale(scale / _scale);
_scale = scale;
}
},
onScaleEnd: (details) {
if (_drag) {
FFI.sendMouse('up', 'left');
setState(resetMouse);
} else if (_scroll) {
var dy = (_yOffset - _yOffset0) / 10;
if (dy.abs() > 0.1) {
if (dy > 0 && dy < 1) dy = 1;
if (dy < 0 && dy > -1) dy = -1;
FFI.scroll(dy);
}
}
},
child: getBodyForMobile());
}
Widget getBodyForMobile() {
return Container(
color: MyTheme.canvasColor,
child: Stack(children: [
ImagePaint(),
CursorPaint(),
getHelpTools(),
SizedBox(
width: 0,
height: 0,
child: !_showEdit
? Container()
: TextFormField(
textInputAction: TextInputAction.newline,
autocorrect: false,
enableSuggestions: false,
focusNode: _focusNode,
maxLines: null,
initialValue:
_value, // trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleInput,
),
),
]));
}
Widget getBodyForDesktopWithListener() {
final keyboard = FFI.ffiModel.permissions['keyboard'] != false;
var paints = <Widget>[ImagePaint()];
if (keyboard ||
FFI.getByName('toggle-option', 'show-remote-cursor') == 'true') {
paints.add(CursorPaint());
}
return MouseRegion(
cursor: keyboard
? SystemMouseCursors.none
: null, // still laggy, set cursor directly for web is better
onEnter: (event) {
print('enter');
FFI.listenToMouse(true);
},
onExit: (event) {
print('exit');
FFI.listenToMouse(false);
},
child: Container(
color: MyTheme.canvasColor, child: Stack(children: paints)));
}
void showActions(BuildContext context) {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
final more = <PopupMenuItem<String>>[];
if (FFI.ffiModel.pi.version.isNotEmpty) {
final pi = FFI.ffiModel.pi;
final perms = FFI.ffiModel.permissions;
if (pi.version.isNotEmpty) {
more.add(PopupMenuItem<String>(
child: Text(translate('Refresh')), value: 'refresh'));
}
if (FFI.ffiModel.permissions['keyboard'] != false &&
FFI.ffiModel.permissions['clipboard'] != false) {
more.add(PopupMenuItem<String>(
child: Text(translate('Paste')), value: 'paste'));
}
more.add(PopupMenuItem<String>(
child: Row(
children: ([
@@ -435,38 +486,57 @@ class _RemotePageState extends State<RemotePage> {
)
])),
value: 'enter_os_password'));
more.add(PopupMenuItem<String>(
child: Row(
children: ([
Container(width: 100.0, child: Text(translate('Touch mode'))),
Padding(padding: EdgeInsets.symmetric(horizontal: 16.0)),
Icon(
_touchMode
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: MyTheme.accent)
])),
value: 'touch_mode'));
more.add(PopupMenuItem<String>(
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
if (!isDesktop) {
if (perms['keyboard'] != false && perms['clipboard'] != false) {
more.add(PopupMenuItem<String>(
child: Text(translate('Paste')), value: 'paste'));
}
more.add(PopupMenuItem<String>(
child: Row(
children: ([
Container(width: 100.0, child: Text(translate('Touch mode'))),
Padding(padding: EdgeInsets.symmetric(horizontal: 16.0)),
Icon(
_touchMode
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: MyTheme.accent)
])),
value: 'touch_mode'));
more.add(PopupMenuItem<String>(
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
}
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
more.add(PopupMenuItem<String>(
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
value: 'cad'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'));
if (pi.platform == 'Windows' &&
FFI.getByName('toggle_option', 'privacy-mode') != 'true') {
more.add(PopupMenuItem<String>(
child: Text(translate(
(FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
value: 'block-input'));
}
}
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
PopupMenuItem<String>(
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
value: 'cad'),
PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'),
] +
more,
items: more,
elevation: 8,
);
if (value == 'cad') {
FFI.setByName('ctrl_alt_del');
} else if (value == 'lock') {
FFI.setByName('lock_screen');
} else if (value == 'block-input') {
FFI.setByName('toggle_option',
(FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-inpu');
FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked;
} else if (value == 'refresh') {
FFI.setByName('refresh');
} else if (value == 'paste') {
@@ -486,7 +556,7 @@ class _RemotePageState extends State<RemotePage> {
} else if (value == 'touch_mode') {
_touchMode = !_touchMode;
final v = _touchMode ? 'Y' : '';
FFI.setByName('peer_option', '{"name": "touch-mode", "value": "${v}"}');
FFI.setByName('peer_option', '{"name": "touch-mode", "value": "$v"}');
} else if (value == 'reset_canvas') {
FFI.cursorModel.reset();
}
@@ -790,9 +860,36 @@ void wrongPasswordDialog(String id, BuildContext context) {
]));
}
CheckboxListTile getToggle(
void Function(void Function()) setState, option, name) {
return CheckboxListTile(
value: FFI.getByName('toggle_option', option) == 'true',
onChanged: (v) {
setState(() {
FFI.setByName('toggle_option', option);
});
},
dense: true,
title: Text(translate(name)));
}
RadioListTile<String> getRadio(String name, String toValue, String curValue,
void Function(String) onChange) {
return RadioListTile<String>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate(name)),
value: toValue,
groupValue: curValue,
onChanged: onChange,
dense: true,
);
}
void showOptions(BuildContext context) {
String quality = FFI.getByName('image_quality');
if (quality == '') quality = 'balanced';
String viewStyle = FFI.getByName('peer_option', 'view-style');
if (viewStyle == '') viewStyle = 'original';
var displays = <Widget>[];
final pi = FFI.ffiModel.pi;
final image = FFI.ffiModel.getConnectionImage();
@@ -829,82 +926,57 @@ void showOptions(BuildContext context) {
if (displays.isNotEmpty) {
displays.add(Divider(color: MyTheme.border));
}
final perms = FFI.ffiModel.permissions;
showAlertDialog(context, (setState) {
final more = <Widget>[];
if (FFI.ffiModel.permissions['audio'] != false) {
more.add(CheckboxListTile(
value: FFI.getByName('toggle_option', 'disable-audio') == 'true',
onChanged: (v) {
setState(() {
FFI.setByName('toggle_option', 'disable-audio');
});
},
title: Text(translate('Mute'))));
if (perms['audio'] != false) {
more.add(getToggle(setState, 'disable-audio', 'Mute'));
}
if (FFI.ffiModel.permissions['keyboard'] != false) {
more.add(CheckboxListTile(
value: FFI.getByName('toggle_option', 'lock-after-session-end') ==
'true',
onChanged: (v) {
setState(() {
FFI.setByName('toggle_option', 'lock-after-session-end');
});
},
title: Text(translate('Lock after session end'))));
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false)
more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard'));
more.add(getToggle(
setState, 'lock-after-session-end', 'Lock after session end'));
if (pi.platform == 'Windows') {
more.add(getToggle(setState, 'privacy-mode', 'Privacy mode'));
}
}
var setQuality = (String value) {
setState(() {
quality = value;
FFI.setByName('image_quality', value);
});
};
var setViewStyle = (String value) {
setState(() {
viewStyle = value;
FFI.setByName(
'peer_option', '{"name": "view-style", "value": "$value"}');
FFI.canvasModel.updateViewStyle();
});
};
return Tuple3(
null,
Column(
mainAxisSize: MainAxisSize.min,
children: displays +
(isDesktop
? <Widget>[
getRadio(
'Original', 'original', viewStyle, setViewStyle),
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
Divider(color: MyTheme.border),
]
: {}) +
<Widget>[
RadioListTile<String>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate('Good image quality')),
value: 'best',
groupValue: quality,
onChanged: (String value) {
setState(() {
quality = value;
FFI.setByName('image_quality', value);
});
},
),
RadioListTile<String>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate('Balanced')),
value: 'balanced',
groupValue: quality,
onChanged: (String value) {
setState(() {
quality = value;
FFI.setByName('image_quality', value);
});
},
),
RadioListTile<String>(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate('Optimize reaction time')),
value: 'low',
groupValue: quality,
onChanged: (String value) {
setState(() {
quality = value;
FFI.setByName('image_quality', value);
});
},
),
getRadio('Good image quality', 'best', quality, setQuality),
getRadio('Balanced', 'balanced', quality, setQuality),
getRadio(
'Optimize reaction time', 'low', quality, setQuality),
Divider(color: MyTheme.border),
CheckboxListTile(
value: FFI.getByName(
'toggle_option', 'show-remote-cursor') ==
'true',
onChanged: (v) {
setState(() {
FFI.setByName('toggle_option', 'show-remote-cursor');
});
},
title: Text(translate('Show remote cursor'))),
getToggle(
setState, 'show-remote-cursor', 'Show remote cursor'),
] +
more),
null);
@@ -948,7 +1020,7 @@ void showSetOSPassword(BuildContext context, bool login) {
onPressed: () {
var text = controller.text.trim();
FFI.setByName('peer_option',
'{"name": "os-password", "value": "${text}"}');
'{"name": "os-password", "value": "$text"}');
FFI.setByName('peer_option',
'{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}');
if (text != "" && login) {

View File

@@ -4,16 +4,14 @@ import 'package:flutter_hbb/model.dart';
import 'package:provider/provider.dart';
import 'common.dart';
import 'main.dart';
import 'model.dart';
class ServerPage extends StatelessWidget {
static final serverModel = ServerModel();
@override
Widget build(BuildContext context) {
checkService();
return ChangeNotifierProvider.value(
value: serverModel,
value: FFI.serverModel,
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
@@ -57,8 +55,8 @@ class ServerPage extends StatelessWidget {
void checkService() {
// 检测当前服务状态,若已存在服务则异步更新数据回来
toAndroidChannel.invokeMethod("check_service"); // jvm
ServerPage.serverModel.updateClientState();
FFI.invokeMethod("check_service"); // jvm
FFI.serverModel.updateClientState();
}
class ServerInfo extends StatefulWidget {
@@ -74,14 +72,13 @@ class _ServerInfoState extends State<ServerInfo> {
var _serverPasswd = TextEditingController(text: "");
static const _emptyIdShow = "正在获取ID...";
@override
void initState() {
super.initState();
var id = FFI.getByName("server_id");
_serverId.text = id==""?_emptyIdShow:id;
_serverId.text = id == "" ? _emptyIdShow : id;
_serverPasswd.text = FFI.getByName("server_password");
if(_serverId.text == _emptyIdShow || _serverPasswd.text == ""){
if (_serverId.text == _emptyIdShow || _serverPasswd.text == "") {
fetchConfigAgain();
}
}
@@ -132,19 +129,21 @@ class _ServerInfoState extends State<ServerInfo> {
],
));
}
fetchConfigAgain()async{
fetchConfigAgain() async {
FFI.setByName("start_service");
var count = 0;
const maxCount = 10;
while(count<maxCount){
if(_serverId.text!=_emptyIdShow && _serverPasswd.text!=""){
while (count < maxCount) {
if (_serverId.text != _emptyIdShow && _serverPasswd.text != "") {
break;
}
await Future.delayed(Duration(seconds: 2));
var id = FFI.getByName("server_id");
_serverId.text = id==""?_emptyIdShow:id;
_serverId.text = id == "" ? _emptyIdShow : id;
_serverPasswd.text = FFI.getByName("server_password");
debugPrint("fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
debugPrint(
"fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
count++;
}
FFI.setByName("stop_service");
@@ -191,7 +190,7 @@ class _PermissionCheckerState extends State<PermissionChecker> {
BuildContext loginReqAlertCtx;
void showLoginReqAlert(BuildContext context, String peerID, String name)async {
void showLoginReqAlert(BuildContext context, String peerID, String name) async {
debugPrint("got try_start_without_auth");
await showDialog(
context: context,
@@ -205,10 +204,10 @@ void showLoginReqAlert(BuildContext context, String peerID, String name)async {
child: Text("接受"),
onPressed: () {
FFI.setByName("login_res", "true");
if (!ServerPage.serverModel.isFileTransfer) {
if (!FFI.serverModel.isFileTransfer) {
_toAndroidStartCapture();
}
ServerPage.serverModel.setPeer(true);
FFI.serverModel.setPeer(true);
Navigator.of(alertContext).pop();
}),
TextButton(
@@ -224,10 +223,10 @@ void showLoginReqAlert(BuildContext context, String peerID, String name)async {
loginReqAlertCtx = null;
}
clearLoginReqAlert(){
if (loginReqAlertCtx!=null){
clearLoginReqAlert() {
if (loginReqAlertCtx != null) {
Navigator.of(loginReqAlertCtx).pop();
ServerPage.serverModel.updateClientState();
FFI.serverModel.updateClientState();
}
}
@@ -321,31 +320,73 @@ Widget myCard(Widget child) {
}
Future<Null> _toAndroidInitService() async {
bool res = await toAndroidChannel.invokeMethod("init_service");
bool res = await FFI.invokeMethod("init_service");
FFI.setByName("start_service");
debugPrint("_toAndroidInitService:$res");
}
Future<Null> _toAndroidStartCapture() async {
bool res = await toAndroidChannel.invokeMethod("start_capture");
bool res = await FFI.invokeMethod("start_capture");
debugPrint("_toAndroidStartCapture:$res");
}
// Future<Null> _toAndroidStopCapture() async {
// bool res = await toAndroidChannel.invokeMethod("stop_capture");
// bool res = await FFI.invokeMethod("stop_capture");
// debugPrint("_toAndroidStopCapture:$res");
// }
Future<Null> _toAndroidStopService() async {
FFI.setByName("close_conn");
ServerPage.serverModel.setPeer(false);
FFI.serverModel.setPeer(false);
bool res = await toAndroidChannel.invokeMethod("stop_service");
bool res = await FFI.invokeMethod("stop_service");
FFI.setByName("stop_service");
debugPrint("_toAndroidStopSer:$res");
}
Future<Null> _toAndroidInitInput() async {
bool res = await toAndroidChannel.invokeMethod("init_input");
bool res = await FFI.invokeMethod("init_input");
debugPrint("_toAndroidInitInput:$res");
}
void toAndroidChannelInit() {
FFI.setMethodCallHandler((method, arguments) {
debugPrint("flutter got android msg");
try {
switch (method) {
case "try_start_without_auth":
{
FFI.serverModel.updateClientState();
debugPrint(
"pre show loginAlert:${FFI.serverModel.isFileTransfer.toString()}");
showLoginReqAlert(
nowCtx, FFI.serverModel.peerID, FFI.serverModel.peerName);
debugPrint("from jvm:try_start_without_auth done");
break;
}
case "start_capture":
{
clearLoginReqAlert();
FFI.serverModel.updateClientState();
break;
}
case "stop_capture":
{
FFI.serverModel.setPeer(false);
break;
}
case "on_permission_changed":
{
var name = arguments["name"] as String;
var value = arguments["value"] as String == "true";
debugPrint("from jvm:on_permission_changed,$name:$value");
FFI.serverModel.changeStatue(name, value);
break;
}
}
} catch (e) {
debugPrint("MethodCallHandler err:$e");
}
return "";
});
}

175
lib/web_model.dart Normal file
View File

@@ -0,0 +1,175 @@
import 'dart:typed_data';
import 'dart:js' as js;
import 'package:flutter/cupertino.dart';
import 'dart:convert';
import 'common.dart';
import 'dart:html';
import 'dart:async';
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
int lastMouseDownButtons = 0;
bool mouseIn = false;
class PlatformFFI {
static void clearRgbaFrame() {}
static Uint8List getRgba() {
return js.context.callMethod('getRgba');
}
static Future<String> getVersion() async {
return getByName('version');
}
static String getByName(String name, [String arg = '']) {
return js.context.callMethod('getByName', [name, arg]);
}
static void setByName(String name, [String value = '']) {
js.context.callMethod('setByName', [name, value]);
}
static Future<Null> init() async {
isWeb = true;
isDesktop = !js.context.callMethod('isMobile');
js.context.callMethod('init');
}
// MouseRegion onHover not work for mouse move when right button down
static void startDesktopWebListener(
Function(Map<String, dynamic>) handleMouse) {
mouseIn = true;
lastMouseDownButtons = 0;
// document.body.getElementsByTagName('flt-glass-pane')[0].style.cursor = 'none';
mouseListeners
.add(window.document.onMouseEnter.listen((evt) => mouseIn = true));
mouseListeners
.add(window.document.onMouseLeave.listen((evt) => mouseIn = false));
mouseListeners.add(window.document.onMouseMove
.listen((evt) => handleMouse(getEvent(evt))));
mouseListeners.add(window.document.onMouseDown
.listen((evt) => handleMouse(getEvent(evt))));
mouseListeners.add(
window.document.onMouseUp.listen((evt) => handleMouse(getEvent(evt))));
mouseListeners.add(window.document.onMouseWheel.listen((evt) {
var dx = evt.deltaX;
var dy = evt.deltaY;
if (dx > 0)
dx = -1;
else if (dx < 0) dx = 1;
if (dy > 0)
dy = -1;
else if (dy < 0) dy = 1;
setByName('send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}');
}));
mouseListeners.add(
window.document.onContextMenu.listen((evt) => evt.preventDefault()));
keyListeners
.add(window.document.onKeyDown.listen((evt) => handleKey(evt, true)));
keyListeners
.add(window.document.onKeyUp.listen((evt) => handleKey(evt, false)));
}
static void stopDesktopWebListener() {
mouseIn = true;
mouseListeners.forEach((l) {
l.cancel();
});
mouseListeners.clear();
keyListeners.forEach((l) {
l.cancel();
});
keyListeners.clear();
}
static void setMethodCallHandler(FMethod callback) {}
static Future<bool> invokeMethod(String method) async {
return true;
}
}
Map<String, dynamic> getEvent(MouseEvent evt) {
// https://github.com/novnc/noVNC/blob/679b45fa3b453c7cf32f4b4455f4814818ecf161/core/rfb.js
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/mousedown_event
final Map<String, dynamic> out = {};
out['type'] = evt.type;
out['x'] = evt.client.x;
out['y'] = evt.client.y;
if (evt.altKey) out['alt'] = 'true';
if (evt.shiftKey) out['shift'] = 'true';
if (evt.ctrlKey) out['ctrl'] = 'true';
if (evt.metaKey) out['command'] = 'true';
out['buttons'] = evt
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
if (evt.buttons != 0) {
lastMouseDownButtons = evt.buttons;
} else {
out['buttons'] = lastMouseDownButtons;
}
return out;
}
void handleKey(KeyboardEvent evt, bool down) {
if (!mouseIn) return;
evt.stopPropagation();
evt.preventDefault();
evt.stopImmediatePropagation();
print('${evt.code} ${evt.key} ${evt.location}');
final out = {};
var name = ctrlKeyMap[evt.code];
if (name == null) {
if (evt.code == evt.key) {
name = evt.code;
} else {
name = evt.key;
if (name.toLowerCase() != name.toUpperCase() &&
name == name.toUpperCase()) {
if (!evt.shiftKey) out['shift'] = 'true';
}
}
}
out['name'] = name;
if (evt.altKey) out['alt'] = 'true';
if (evt.shiftKey) out['shift'] = 'true';
if (evt.ctrlKey) out['ctrl'] = 'true';
if (evt.metaKey) out['command'] = 'true';
if (down) out['down'] = 'true';
PlatformFFI.setByName('input_key', json.encode(out));
}
final localeName = window.navigator.language;
final ctrlKeyMap = {
'AltLeft': 'Alt',
'AltRight': 'RAlt',
'ShiftLeft': 'Shift',
'ShiftRight': 'RShift',
'ControlLeft': 'Control',
'ControlRight': 'RControl',
'MetaLeft': 'Meta',
'MetaRight': 'RWin',
'ContextMenu': 'Apps',
'ArrowUp': 'UpArrow',
'ArrowDown': 'DownArrow',
'ArrowLeft': 'LeftArrow',
'ArrowRight': 'RightArrow',
'NumpadDecimal': 'Decimal',
'NumpadDivide': 'Divide',
'NumpadMultiply': 'Multiply',
'NumpadSubtract': 'Subtract',
'NumpadAdd': 'Add',
'NumpadEnter': 'NumpadEnter',
'Enter': 'Return',
'Space': 'Space',
'NumpadClear': 'Clear',
'NumpadBackspace': 'Backspace',
'PrintScreen': 'Snapshot',
'HangulMode': 'Hangul',
'HanjaMode': 'Hanja',
'KanaMode': 'Kana',
'JunjaMode': 'Junja',
'KanjiMode': 'Hanja',
};