mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master' into csf
This commit is contained in:
134
lib/common.dart
134
lib/common.dart
@@ -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;
|
||||
|
||||
@@ -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/';
|
||||
|
||||
@@ -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),
|
||||
|
||||
299
lib/model.dart
299
lib/model.dart
@@ -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
128
lib/native_model.dart
Normal 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");
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
175
lib/web_model.dart
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user