mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge remote-tracking branch 'rustdesk/master' into flutter_desktop
# Conflicts: # .github/workflows/ci.yml # Cargo.lock # Cargo.toml # flutter/lib/common.dart # flutter/lib/mobile/pages/remote_page.dart # flutter/lib/mobile/pages/server_page.dart # flutter/lib/mobile/pages/settings_page.dart # flutter/lib/mobile/widgets/dialog.dart # flutter/lib/models/model.dart # flutter/lib/models/server_model.dart # src/client.rs # src/common.rs # src/ipc.rs # src/mobile_ffi.rs # src/rendezvous_mediator.rs # src/ui.rs
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
package="com.carriez.flutter_hbb">
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode|navigation"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
|
||||
@@ -8,14 +8,14 @@ package com.carriez.flutter_hbb
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.content.Context
|
||||
import android.graphics.Path
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
const val LIFT_DOWN = 9
|
||||
const val LIFT_MOVE = 8
|
||||
@@ -49,28 +49,40 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private val wheelActionsQueue = LinkedList<GestureDescription>()
|
||||
private var isWheelActionsPolling = false
|
||||
private var isWaitingLongPress = false
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||
val x = if (_x < 0) {
|
||||
0
|
||||
} else {
|
||||
_x
|
||||
}
|
||||
|
||||
val y = if (_y < 0) {
|
||||
0
|
||||
} else {
|
||||
_y
|
||||
}
|
||||
val x = max(0, _x)
|
||||
val y = max(0, _y)
|
||||
|
||||
if (mask == 0 || mask == LIFT_MOVE) {
|
||||
val oldX = mouseX
|
||||
val oldY = mouseY
|
||||
mouseX = x * SCREEN_INFO.scale
|
||||
mouseY = y * SCREEN_INFO.scale
|
||||
if (isWaitingLongPress) {
|
||||
val delta = abs(oldX - mouseX) + abs(oldY - mouseY)
|
||||
Log.d(logTag,"delta:$delta")
|
||||
if (delta > 8) {
|
||||
isWaitingLongPress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// left button down ,was up
|
||||
if (mask == LIFT_DOWN) {
|
||||
isWaitingLongPress = true
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
if (isWaitingLongPress) {
|
||||
isWaitingLongPress = false
|
||||
leftIsDown = false
|
||||
endGesture(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
}, LONG_TAP_DELAY * 4)
|
||||
|
||||
leftIsDown = true
|
||||
startGesture(mouseX, mouseY)
|
||||
return
|
||||
@@ -83,9 +95,12 @@ class InputService : AccessibilityService() {
|
||||
|
||||
// left up ,was down
|
||||
if (mask == LIFT_UP) {
|
||||
leftIsDown = false
|
||||
endGesture(mouseX, mouseY)
|
||||
return
|
||||
if (leftIsDown) {
|
||||
leftIsDown = false
|
||||
isWaitingLongPress = false
|
||||
endGesture(mouseX, mouseY)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (mask == RIGHT_UP) {
|
||||
|
||||
@@ -192,7 +192,6 @@ class MainActivity : FlutterActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val inputPer = InputService.isOpen
|
||||
Log.d(logTag, "onResume inputPer:$inputPer")
|
||||
activity.runOnUiThread {
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"on_state_changed",
|
||||
|
||||
@@ -2,20 +2,26 @@ package com.carriez.flutter_hbb
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioRecord
|
||||
import android.media.AudioRecord.READ_BLOCKING
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.hjq.permissions.Permission
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
val LOCAL_NAME = Locale.getDefault().toString()
|
||||
val SCREEN_INFO = Info(0, 0, 1, 200)
|
||||
@@ -38,8 +44,31 @@ fun testVP9Support(): Boolean {
|
||||
return res != null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun requestPermission(context: Context, type: String) {
|
||||
val permission = when (type) {
|
||||
"ignore_battery_optimizations" -> {
|
||||
try {
|
||||
context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:" + context.packageName)
|
||||
})
|
||||
} catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return
|
||||
}
|
||||
"application_details_settings" -> {
|
||||
try {
|
||||
context.startActivity(Intent().apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
action = "android.settings.APPLICATION_DETAILS_SETTINGS"
|
||||
data = Uri.parse("package:" + context.packageName)
|
||||
})
|
||||
} catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return
|
||||
}
|
||||
"audio" -> {
|
||||
Permission.RECORD_AUDIO
|
||||
}
|
||||
@@ -52,7 +81,7 @@ fun requestPermission(context: Context, type: String) {
|
||||
}
|
||||
XXPermissions.with(context)
|
||||
.permission(permission)
|
||||
.request { permissions, all ->
|
||||
.request { _, all ->
|
||||
if (all) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel.invokeMethod(
|
||||
@@ -64,8 +93,13 @@ fun requestPermission(context: Context, type: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun checkPermission(context: Context, type: String): Boolean {
|
||||
val permission = when (type) {
|
||||
"ignore_battery_optimizations" -> {
|
||||
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return pw.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
"audio" -> {
|
||||
Permission.RECORD_AUDIO
|
||||
}
|
||||
|
||||
@@ -282,7 +282,12 @@ class PermissionManager {
|
||||
static Timer? _timer;
|
||||
static var _current = "";
|
||||
|
||||
static final permissions = ["audio", "file"];
|
||||
static final permissions = [
|
||||
"audio",
|
||||
"file",
|
||||
"ignore_battery_optimizations",
|
||||
"application_details_settings"
|
||||
];
|
||||
|
||||
static bool isWaitingFile() {
|
||||
if (_completer != null) {
|
||||
@@ -301,6 +306,10 @@ class PermissionManager {
|
||||
if (!permissions.contains(type))
|
||||
return Future.error("Wrong permission!$type");
|
||||
|
||||
FFI.invokeMethod("request_permission", type);
|
||||
if (type == "ignore_battery_optimizations") {
|
||||
return Future.value(false);
|
||||
}
|
||||
_current = type;
|
||||
_completer = Completer<bool>();
|
||||
gFFI.invokeMethod("request_permission", type);
|
||||
@@ -328,6 +337,18 @@ class PermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
RadioListTile<T> getRadio<T>(
|
||||
String name, T toValue, T curValue, void Function(T?) onChange) {
|
||||
return RadioListTile<T>(
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
title: Text(translate(name)),
|
||||
value: toValue,
|
||||
groupValue: curValue,
|
||||
onChanged: onChange,
|
||||
dense: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// find ffi, tag is Remote ID
|
||||
/// for session specific usage
|
||||
FFI ffi(String? tag) {
|
||||
|
||||
@@ -264,7 +264,6 @@ class _RemotePageState extends State<RemotePage> {
|
||||
: SafeArea(child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
if (_currentOrientation != orientation) {
|
||||
debugPrint("on orientation changed");
|
||||
Timer(Duration(milliseconds: 200), () {
|
||||
resetMobileActionsOverlay();
|
||||
_currentOrientation = orientation;
|
||||
@@ -345,9 +344,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
onKey: (data, e) {
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (e.repeat) {
|
||||
if (e.repeat &&
|
||||
!e.isAltPressed &&
|
||||
!e.isControlPressed &&
|
||||
!e.isShiftPressed &&
|
||||
!e.isMetaPressed) {
|
||||
sendRawKey(e, press: true);
|
||||
} else {
|
||||
sendRawKey(e, down: true);
|
||||
if (e.isAltPressed && !gFFI.alt) {
|
||||
gFFI.alt = true;
|
||||
} else if (e.isControlPressed && !gFFI.ctrl) {
|
||||
@@ -357,7 +361,6 @@ class _RemotePageState extends State<RemotePage> {
|
||||
} else if (e.isMetaPressed && !gFFI.command) {
|
||||
gFFI.command = true;
|
||||
}
|
||||
sendRawKey(e, down: true);
|
||||
}
|
||||
}
|
||||
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
|
||||
@@ -483,6 +486,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
/// DoubleFiner -> right click
|
||||
/// HoldDrag -> left drag
|
||||
|
||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||
Widget getBodyForMobileWithGesture() {
|
||||
final touchMode = gFFI.ffiModel.touchMode;
|
||||
return getMixinGestureDetector(
|
||||
@@ -507,10 +511,15 @@ class _RemotePageState extends State<RemotePage> {
|
||||
onLongPressDown: (d) {
|
||||
if (touchMode) {
|
||||
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_cacheLongPressPosition = d.localPosition;
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
gFFI.tap(MouseButtons.right);
|
||||
if (touchMode) {
|
||||
FFI.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
}
|
||||
FFI.tap(MouseButtons.right);
|
||||
},
|
||||
onDoubleFinerTap: (d) {
|
||||
if (!touchMode) {
|
||||
@@ -536,6 +545,15 @@ class _RemotePageState extends State<RemotePage> {
|
||||
if (touchMode) {
|
||||
gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
gFFI.sendMouse('down', MouseButtons.left);
|
||||
} else {
|
||||
final cursorX = FFI.cursorModel.x;
|
||||
final cursorY = FFI.cursorModel.y;
|
||||
final visible =
|
||||
FFI.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
||||
FFI.cursorModel.move(size.width / 2, size.height / 2);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOneFingerPanUpdate: (d) {
|
||||
@@ -946,18 +964,6 @@ CheckboxListTile getToggle(void Function(void Function()) setState, option, name
|
||||
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() {
|
||||
String quality = gFFI.getByName('image_quality');
|
||||
if (quality == '') quality = 'balanced';
|
||||
@@ -1045,6 +1051,8 @@ void showOptions() {
|
||||
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
||||
Divider(color: MyTheme.border),
|
||||
getToggle(setState, 'show-remote-cursor', 'Show remote cursor'),
|
||||
getToggle(
|
||||
setState, 'show-quality-monitor', 'Show quality monitor'),
|
||||
] +
|
||||
more),
|
||||
actions: [],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
@@ -24,36 +26,84 @@ class ServerPage extends StatelessWidget implements PageShape {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Change ID")),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "changeID",
|
||||
enabled: false,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Set your own password")),
|
||||
value: "changePW",
|
||||
enabled: gFFI.serverModel.isStart,
|
||||
child: Text(translate("Set permanent password")),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "setPermanentPassword",
|
||||
enabled:
|
||||
gFFI.serverModel.verificationMethod != kUseTemporaryPassword,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Refresh random password")),
|
||||
value: "refreshPW",
|
||||
enabled: gFFI.serverModel.isStart,
|
||||
)
|
||||
child: Text(translate("Set temporary password length")),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "setTemporaryPasswordLength",
|
||||
enabled:
|
||||
gFFI.serverModel.verificationMethod != kUsePermanentPassword,
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUseTemporaryPassword,
|
||||
child: Container(
|
||||
child: ListTile(
|
||||
title: Text(translate("Use temporary password")),
|
||||
trailing: Icon(
|
||||
Icons.check,
|
||||
color: gFFI.serverModel.verificationMethod ==
|
||||
kUseTemporaryPassword
|
||||
? null
|
||||
: Color(0xFFFFFFFF),
|
||||
))),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUsePermanentPassword,
|
||||
child: ListTile(
|
||||
title: Text(translate("Use permanent password")),
|
||||
trailing: Icon(
|
||||
Icons.check,
|
||||
color: gFFI.serverModel.verificationMethod ==
|
||||
kUsePermanentPassword
|
||||
? null
|
||||
: Color(0xFFFFFFFF),
|
||||
)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUseBothPasswords,
|
||||
child: ListTile(
|
||||
title: Text(translate("Use both passwords")),
|
||||
trailing: Icon(
|
||||
Icons.check,
|
||||
color: gFFI.serverModel.verificationMethod !=
|
||||
kUseTemporaryPassword &&
|
||||
gFFI.serverModel.verificationMethod !=
|
||||
kUsePermanentPassword
|
||||
? null
|
||||
: Color(0xFFFFFFFF),
|
||||
)),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == "changeID") {
|
||||
// TODO
|
||||
} else if (value == "changePW") {
|
||||
updatePasswordDialog();
|
||||
} else if (value == "refreshPW") {
|
||||
() async {
|
||||
showLoading(translate("Waiting"));
|
||||
if (await gFFI.serverModel.updatePassword("")) {
|
||||
showSuccess();
|
||||
} else {
|
||||
showError();
|
||||
}
|
||||
debugPrint("end updatePassword");
|
||||
}();
|
||||
} else if (value == "setPermanentPassword") {
|
||||
setPermanentPasswordDialog();
|
||||
} else if (value == "setTemporaryPasswordLength") {
|
||||
setTemporaryPasswordLengthDialog();
|
||||
} else if (value == kUsePermanentPassword ||
|
||||
value == kUseTemporaryPassword ||
|
||||
value == kUseBothPasswords) {
|
||||
Map<String, String> msg = Map()
|
||||
..["name"] = "verification-method"
|
||||
..["value"] = value;
|
||||
gFFI.setByName('option', jsonEncode(msg));
|
||||
gFFI.serverModel.updatePasswordModel();
|
||||
}
|
||||
})
|
||||
];
|
||||
@@ -90,17 +140,13 @@ void checkService() async {
|
||||
}
|
||||
}
|
||||
|
||||
class ServerInfo extends StatefulWidget {
|
||||
@override
|
||||
_ServerInfoState createState() => _ServerInfoState();
|
||||
}
|
||||
|
||||
class _ServerInfoState extends State<ServerInfo> {
|
||||
class ServerInfo extends StatelessWidget {
|
||||
final model = gFFI.serverModel;
|
||||
var _passwdShow = false;
|
||||
final emptyController = TextEditingController(text: "-");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPermanent = model.verificationMethod == kUsePermanentPassword;
|
||||
return model.isStart
|
||||
? PaddingCard(
|
||||
child: Column(
|
||||
@@ -123,24 +169,23 @@ class _ServerInfoState extends State<ServerInfo> {
|
||||
),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
obscureText: !_passwdShow,
|
||||
style: TextStyle(
|
||||
fontSize: 25.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: MyTheme.accent),
|
||||
controller: model.serverPasswd,
|
||||
controller: isPermanent ? emptyController : model.serverPasswd,
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(Icons.lock),
|
||||
labelText: translate("Password"),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: MyTheme.accent50),
|
||||
suffix: IconButton(
|
||||
icon: Icon(Icons.visibility),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwdShow = !_passwdShow;
|
||||
});
|
||||
})),
|
||||
suffix: isPermanent
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
gFFI.setByName("temporary_password");
|
||||
})),
|
||||
onSaved: (String? value) {},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -26,13 +32,100 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
||||
_SettingsState createState() => _SettingsState();
|
||||
}
|
||||
|
||||
class _SettingsState extends State<SettingsPage> {
|
||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
static const url = 'https://rustdesk.com/';
|
||||
final _hasIgnoreBattery = androidVersion >= 26;
|
||||
var _ignoreBatteryOpt = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
if (_hasIgnoreBattery) {
|
||||
updateIgnoreBatteryStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
updateIgnoreBatteryStatus();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateIgnoreBatteryStatus() async {
|
||||
final res = await PermissionManager.check("ignore_battery_optimizations");
|
||||
if (_ignoreBatteryOpt != res) {
|
||||
setState(() {
|
||||
_ignoreBatteryOpt = res;
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final username = getUsername();
|
||||
final enableAbr = FFI.getByName("option", "enable-abr") != 'N';
|
||||
final enhancementsTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Adaptive Bitrate') + '(beta)'),
|
||||
initialValue: enableAbr,
|
||||
onToggle: (v) {
|
||||
final msg = Map()
|
||||
..["name"] = "enable-abr"
|
||||
..["value"] = "";
|
||||
if (!v) {
|
||||
msg["value"] = "N";
|
||||
}
|
||||
FFI.setByName("option", json.encode(msg));
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
];
|
||||
if (_hasIgnoreBattery) {
|
||||
enhancementsTiles.insert(
|
||||
0,
|
||||
SettingsTile.switchTile(
|
||||
initialValue: _ignoreBatteryOpt,
|
||||
title: Text(translate('Keep RustDesk background service')),
|
||||
description:
|
||||
Text('* ${translate('Ignore Battery Optimizations')}'),
|
||||
onToggle: (v) async {
|
||||
if (v) {
|
||||
PermissionManager.request("ignore_battery_optimizations");
|
||||
} else {
|
||||
final res = await DialogManager.show<bool>(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("Open System Setting")),
|
||||
content: Text(translate(
|
||||
"android_open_battery_optimizations_tip")),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: () => close(true),
|
||||
child:
|
||||
Text(translate("Open System Setting"))),
|
||||
],
|
||||
));
|
||||
if (res == true) {
|
||||
PermissionManager.request("application_details_settings");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return SettingsList(
|
||||
sections: [
|
||||
SettingsSection(
|
||||
@@ -53,17 +146,17 @@ class _SettingsState extends State<SettingsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate("Settings")),
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||
SettingsTile.navigation(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings();
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
]),
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate("About")),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@@ -21,9 +22,10 @@ void showError({Duration duration = SEC1}) {
|
||||
showToast(translate("Error"), duration: SEC1);
|
||||
}
|
||||
|
||||
void updatePasswordDialog() {
|
||||
final p0 = TextEditingController();
|
||||
final p1 = TextEditingController();
|
||||
void setPermanentPasswordDialog() {
|
||||
final pw = FFI.getByName("permanent_password");
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
var validateLength = false;
|
||||
var validateSame = false;
|
||||
DialogManager.show((setState, close) {
|
||||
@@ -87,7 +89,7 @@ void updatePasswordDialog() {
|
||||
? () async {
|
||||
close();
|
||||
showLoading(translate("Waiting"));
|
||||
if (await gFFI.serverModel.updatePassword(p0.text)) {
|
||||
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
||||
showSuccess();
|
||||
} else {
|
||||
showError();
|
||||
@@ -101,6 +103,41 @@ void updatePasswordDialog() {
|
||||
});
|
||||
}
|
||||
|
||||
void setTemporaryPasswordLengthDialog() {
|
||||
List<String> lengths = ['6', '8', '10'];
|
||||
String length = FFI.getByName('option', 'temporary-password-length');
|
||||
var index = lengths.indexOf(length);
|
||||
if (index < 0) index = 0;
|
||||
length = lengths[index];
|
||||
DialogManager.show((setState, close) {
|
||||
final setLength = (newValue) {
|
||||
final oldValue = length;
|
||||
if (oldValue == newValue) return;
|
||||
setState(() {
|
||||
length = newValue;
|
||||
});
|
||||
Map<String, String> msg = Map()
|
||||
..["name"] = "temporary-password-length"
|
||||
..["value"] = newValue;
|
||||
FFI.setByName("option", jsonEncode(msg));
|
||||
FFI.setByName("temporary_password");
|
||||
Future.delayed(Duration(milliseconds: 200), () {
|
||||
close();
|
||||
showSuccess();
|
||||
});
|
||||
};
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set temporary password length")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
lengths.map((e) => getRadio(e, e, length, setLength)).toList()),
|
||||
actions: [],
|
||||
contentPadding: 14,
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
void enterPasswordDialog(String id) {
|
||||
final controller = TextEditingController();
|
||||
var remember = gFFI.getByName('remember', id) == 'true';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum CustomTouchGestureState {
|
||||
enum GestureState {
|
||||
none,
|
||||
oneFingerPan,
|
||||
twoFingerScale,
|
||||
@@ -35,64 +35,41 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
|
||||
GestureDragEndCallback? onThreeFingerVerticalDragEnd;
|
||||
|
||||
var _currentState = CustomTouchGestureState.none;
|
||||
Timer? _startEventDebounceTimer;
|
||||
var _currentState = GestureState.none;
|
||||
Timer? _debounceTimer;
|
||||
|
||||
void _init() {
|
||||
debugPrint("CustomTouchGestureRecognizer init");
|
||||
onStart = (d) {
|
||||
_startEventDebounceTimer?.cancel();
|
||||
if (d.pointerCount == 1) {
|
||||
_currentState = CustomTouchGestureState.oneFingerPan;
|
||||
if (onOneFingerPanStart != null) {
|
||||
onOneFingerPanStart!(DragStartDetails(
|
||||
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
|
||||
}
|
||||
debugPrint("start oneFingerPan");
|
||||
} else if (d.pointerCount == 2) {
|
||||
if (_currentState == CustomTouchGestureState.threeFingerVerticalDrag) {
|
||||
// 3 -> 2 debounce
|
||||
_startEventDebounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
_currentState = CustomTouchGestureState.twoFingerScale;
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint,
|
||||
focalPoint: d.focalPoint));
|
||||
}
|
||||
debugPrint("debounce start twoFingerScale success");
|
||||
});
|
||||
}
|
||||
_currentState = CustomTouchGestureState.twoFingerScale;
|
||||
// startWatchTimer();
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
|
||||
}
|
||||
debugPrint("start twoFingerScale");
|
||||
} else if (d.pointerCount == 3) {
|
||||
_currentState = CustomTouchGestureState.threeFingerVerticalDrag;
|
||||
// onStart = (d) {};
|
||||
onUpdate = (d) {
|
||||
_debounceTimer?.cancel();
|
||||
if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
|
||||
onOneFingerStartDebounce(d);
|
||||
} else if (d.pointerCount == 2 &&
|
||||
_currentState != GestureState.twoFingerScale) {
|
||||
onTwoFingerStartDebounce(d);
|
||||
} else if (d.pointerCount == 3 &&
|
||||
_currentState != GestureState.threeFingerVerticalDrag) {
|
||||
_currentState = GestureState.threeFingerVerticalDrag;
|
||||
if (onThreeFingerVerticalDragStart != null) {
|
||||
onThreeFingerVerticalDragStart!(
|
||||
DragStartDetails(globalPosition: d.localFocalPoint));
|
||||
}
|
||||
debugPrint("start threeFingerScale");
|
||||
// _reset();
|
||||
}
|
||||
};
|
||||
onUpdate = (d) {
|
||||
if (_currentState != CustomTouchGestureState.none) {
|
||||
if (_currentState != GestureState.none) {
|
||||
switch (_currentState) {
|
||||
case CustomTouchGestureState.oneFingerPan:
|
||||
case GestureState.oneFingerPan:
|
||||
if (onOneFingerPanUpdate != null) {
|
||||
onOneFingerPanUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.twoFingerScale:
|
||||
case GestureState.twoFingerScale:
|
||||
if (onTwoFingerScaleUpdate != null) {
|
||||
onTwoFingerScaleUpdate!(d);
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.threeFingerVerticalDrag:
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
if (onThreeFingerVerticalDragUpdate != null) {
|
||||
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
@@ -105,21 +82,22 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
};
|
||||
onEnd = (d) {
|
||||
debugPrint("ScaleGestureRecognizer onEnd");
|
||||
_debounceTimer?.cancel();
|
||||
// end
|
||||
switch (_currentState) {
|
||||
case CustomTouchGestureState.oneFingerPan:
|
||||
case GestureState.oneFingerPan:
|
||||
debugPrint("TwoFingerState.pan onEnd");
|
||||
if (onOneFingerPanEnd != null) {
|
||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.twoFingerScale:
|
||||
case GestureState.twoFingerScale:
|
||||
debugPrint("TwoFingerState.scale onEnd");
|
||||
if (onTwoFingerScaleEnd != null) {
|
||||
onTwoFingerScaleEnd!(d);
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.threeFingerVerticalDrag:
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
debugPrint("ThreeFingerState.vertical onEnd");
|
||||
if (onThreeFingerVerticalDragEnd != null) {
|
||||
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
|
||||
@@ -128,10 +106,50 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_currentState = CustomTouchGestureState.none;
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
_currentState = GestureState.none;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||
final start = (ScaleUpdateDetails d) {
|
||||
_currentState = GestureState.oneFingerPan;
|
||||
if (onOneFingerPanStart != null) {
|
||||
onOneFingerPanStart!(DragStartDetails(
|
||||
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
|
||||
}
|
||||
};
|
||||
if (_currentState != GestureState.none) {
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
start(d);
|
||||
debugPrint("debounce start oneFingerPan");
|
||||
});
|
||||
} else {
|
||||
start(d);
|
||||
debugPrint("start oneFingerPan");
|
||||
}
|
||||
}
|
||||
|
||||
void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
|
||||
final start = (ScaleUpdateDetails d) {
|
||||
_currentState = GestureState.twoFingerScale;
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
|
||||
}
|
||||
};
|
||||
if (_currentState == GestureState.threeFingerVerticalDrag) {
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
start(d);
|
||||
debugPrint("debounce start twoFingerScale");
|
||||
});
|
||||
} else {
|
||||
start(d);
|
||||
debugPrint("start twoFingerScale");
|
||||
}
|
||||
}
|
||||
|
||||
DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) =>
|
||||
DragUpdateDetails(
|
||||
globalPosition: d.focalPoint,
|
||||
|
||||
@@ -396,19 +396,19 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
double get maxScale {
|
||||
if (_image == null) return 1.0;
|
||||
if (_image == null) return 1.5;
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final xscale = size.width / _image!.width;
|
||||
final yscale = size.height / _image!.height;
|
||||
return max(1.0, max(xscale, yscale));
|
||||
return max(1.5, max(xscale, yscale));
|
||||
}
|
||||
|
||||
double get minScale {
|
||||
if (_image == null) return 1.0;
|
||||
if (_image == null) return 1.5;
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final xscale = size.width / _image!.width;
|
||||
final yscale = size.height / _image!.height;
|
||||
return min(xscale, yscale);
|
||||
return min(xscale, yscale) / 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import 'model.dart';
|
||||
|
||||
const loginDialogTag = "LOGIN";
|
||||
|
||||
const kUseTemporaryPassword = "use-temporary-password";
|
||||
const kUsePermanentPassword = "use-permanent-password";
|
||||
const kUseBothPasswords = "use-both-passwords";
|
||||
|
||||
class ServerModel with ChangeNotifier {
|
||||
bool _isStart = false; // Android MainService status
|
||||
bool _mediaOk = false;
|
||||
@@ -18,6 +22,7 @@ class ServerModel with ChangeNotifier {
|
||||
bool _audioOk = false;
|
||||
bool _fileOk = false;
|
||||
int _connectStatus = 0; // Rendezvous Server status
|
||||
String _verificationMethod = "";
|
||||
|
||||
late String _emptyIdShow;
|
||||
late final TextEditingController _serverId;
|
||||
@@ -37,6 +42,8 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
int get connectStatus => _connectStatus;
|
||||
|
||||
String get verificationMethod => _verificationMethod;
|
||||
|
||||
TextEditingController get serverId => _serverId;
|
||||
|
||||
TextEditingController get serverPasswd => _serverPasswd;
|
||||
@@ -112,9 +119,29 @@ class ServerModel with ChangeNotifier {
|
||||
debugPrint("clients not match!");
|
||||
updateClientState(res);
|
||||
}
|
||||
|
||||
updatePasswordModel();
|
||||
});
|
||||
}
|
||||
|
||||
updatePasswordModel() {
|
||||
var update = false;
|
||||
final temporaryPassword = FFI.getByName("temporary_password");
|
||||
final verificationMethod = FFI.getByName("option", "verification-method");
|
||||
if (_serverPasswd.text != temporaryPassword) {
|
||||
_serverPasswd.text = temporaryPassword;
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (_verificationMethod != verificationMethod) {
|
||||
_verificationMethod = verificationMethod;
|
||||
update = true;
|
||||
}
|
||||
if (update) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
toggleAudio() async {
|
||||
if (!_audioOk && !await PermissionManager.check("audio")) {
|
||||
final res = await PermissionManager.request("audio");
|
||||
@@ -216,7 +243,7 @@ class ServerModel with ChangeNotifier {
|
||||
parent.target?.ffiModel.updateEventListener("");
|
||||
await parent.target?.invokeMethod("init_service");
|
||||
parent.target?.setByName("start_service");
|
||||
getIDPasswd();
|
||||
_fetchID();
|
||||
updateClientState();
|
||||
if (!Platform.isLinux) {
|
||||
// current linux is not supported
|
||||
@@ -242,54 +269,33 @@ class ServerModel with ChangeNotifier {
|
||||
await parent.target?.invokeMethod("init_input");
|
||||
}
|
||||
|
||||
Future<bool> updatePassword(String pw) async {
|
||||
final oldPasswd = _serverPasswd.text;
|
||||
parent.target?.setByName("update_password", pw);
|
||||
Future<bool> setPermanentPassword(String newPW) async {
|
||||
parent.target?.setByName("permanent_password", newPW);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
await getIDPasswd(force: true);
|
||||
|
||||
// check result
|
||||
if (pw == "") {
|
||||
if (_serverPasswd.text.isNotEmpty && _serverPasswd.text != oldPasswd) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
final pw = parent.target?.getByName("permanent_password", newPW);
|
||||
if (newPW == pw) {
|
||||
return true;
|
||||
} else {
|
||||
if (_serverPasswd.text == pw) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getIDPasswd({bool force = false}) async {
|
||||
if (!force && _serverId.text != _emptyIdShow && _serverPasswd.text != "") {
|
||||
return;
|
||||
}
|
||||
_fetchID() async {
|
||||
final old = _serverId.text;
|
||||
var count = 0;
|
||||
const maxCount = 10;
|
||||
while (count < maxCount) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
final id = parent.target?.getByName("server_id") ?? "";
|
||||
final passwd = parent.target?.getByName("server_password") ?? "";
|
||||
final id = parent.target?.getByName("server_id");
|
||||
if (id.isEmpty) {
|
||||
continue;
|
||||
} else {
|
||||
_serverId.text = id;
|
||||
}
|
||||
|
||||
if (passwd.isEmpty) {
|
||||
continue;
|
||||
} else {
|
||||
_serverPasswd.text = passwd;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
|
||||
debugPrint("fetch id again at $count:id:${_serverId.text}");
|
||||
count++;
|
||||
if (_serverId.text != _emptyIdShow && _serverPasswd.text.isNotEmpty) {
|
||||
if (_serverId.text != old) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user