mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
fix conflicts
This commit is contained in:
@@ -46,7 +46,7 @@ var isWebDesktop = false;
|
||||
var version = "";
|
||||
int androidVersion = 0;
|
||||
|
||||
/// only avaliable for Windows target
|
||||
/// only available for Windows target
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
|
||||
@@ -99,22 +99,28 @@ class IconFont {
|
||||
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
const ColorThemeExtension({
|
||||
required this.border,
|
||||
required this.highlight,
|
||||
});
|
||||
|
||||
final Color? border;
|
||||
final Color? highlight;
|
||||
|
||||
static const light = ColorThemeExtension(
|
||||
border: Color(0xFFCCCCCC),
|
||||
highlight: Color(0xFFE5E5E5),
|
||||
);
|
||||
|
||||
static const dark = ColorThemeExtension(
|
||||
border: Color(0xFF555555),
|
||||
highlight: Color(0xFF3F3F3F),
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) {
|
||||
ThemeExtension<ColorThemeExtension> copyWith(
|
||||
{Color? border, Color? highlight}) {
|
||||
return ColorThemeExtension(
|
||||
border: border ?? this.border,
|
||||
highlight: highlight ?? this.highlight,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
}
|
||||
return ColorThemeExtension(
|
||||
border: Color.lerp(border, other.border, t),
|
||||
highlight: Color.lerp(highlight, other.highlight, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -223,7 +230,7 @@ class MyTheme {
|
||||
bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||
}
|
||||
bind.mainChangeTheme(dark: currentThemeMode().toShortString());
|
||||
bind.mainChangeTheme(dark: mode.toShortString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,11 +977,13 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
Widget getPlatformImage(String platform, {double size = 50}) {
|
||||
platform = platform.toLowerCase();
|
||||
if (platform == 'mac os') {
|
||||
if (platform == kPeerPlatformMacOS) {
|
||||
platform = 'mac';
|
||||
} else if (platform != 'linux' && platform != 'android') {
|
||||
} else if (platform != kPeerPlatformLinux &&
|
||||
platform != kPeerPlatformAndroid) {
|
||||
platform = 'win';
|
||||
} else {
|
||||
platform = platform.toLowerCase();
|
||||
}
|
||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||
}
|
||||
@@ -1360,13 +1369,13 @@ connect(BuildContext context, String id,
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getHttpHeaders() async {
|
||||
Map<String, String> getHttpHeaders() {
|
||||
return {
|
||||
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
|
||||
};
|
||||
}
|
||||
|
||||
// Simple wrapper of built-in types for refrence use.
|
||||
// Simple wrapper of built-in types for reference use.
|
||||
class SimpleWrapper<T> {
|
||||
T value;
|
||||
SimpleWrapper(this.value);
|
||||
@@ -1402,7 +1411,7 @@ Future<void> reloadAllWindows() async {
|
||||
/// Indicate the flutter app is running in portable mode.
|
||||
///
|
||||
/// [Note]
|
||||
/// Portable build is only avaliable on Windows.
|
||||
/// Portable build is only available on Windows.
|
||||
bool isRunningInPortableMode() {
|
||||
if (!Platform.isWindows) {
|
||||
return false;
|
||||
@@ -1411,7 +1420,7 @@ bool isRunningInPortableMode() {
|
||||
}
|
||||
|
||||
/// Window status callback
|
||||
void onActiveWindowChanged() async {
|
||||
Future<void> onActiveWindowChanged() async {
|
||||
print(
|
||||
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
|
||||
if (rustDeskWinManager.getActiveWindows().isEmpty) {
|
||||
@@ -1503,3 +1512,53 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
|
||||
bool get kUseCompatibleUiMode =>
|
||||
Platform.isWindows &&
|
||||
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
|
||||
|
||||
class ServerConfig {
|
||||
late String idServer;
|
||||
late String relayServer;
|
||||
late String apiServer;
|
||||
late String key;
|
||||
|
||||
ServerConfig(
|
||||
{String? idServer, String? relayServer, String? apiServer, String? key}) {
|
||||
this.idServer = idServer?.trim() ?? '';
|
||||
this.relayServer = relayServer?.trim() ?? '';
|
||||
this.apiServer = apiServer?.trim() ?? '';
|
||||
this.key = key?.trim() ?? '';
|
||||
}
|
||||
|
||||
/// decode from shared string (from user shared or rustdesk-server generated)
|
||||
/// also see [encode]
|
||||
/// throw when decoding failure
|
||||
ServerConfig.decode(String msg) {
|
||||
final input = msg.split('').reversed.join('');
|
||||
final bytes = base64Decode(base64.normalize(input));
|
||||
final json = jsonDecode(utf8.decode(bytes));
|
||||
|
||||
idServer = json['host'] ?? '';
|
||||
relayServer = json['relay'] ?? '';
|
||||
apiServer = json['api'] ?? '';
|
||||
key = json['key'] ?? '';
|
||||
}
|
||||
|
||||
/// encode to shared string
|
||||
/// also see [ServerConfig.decode]
|
||||
String encode() {
|
||||
Map<String, String> config = {};
|
||||
config['host'] = idServer.trim();
|
||||
config['relay'] = relayServer.trim();
|
||||
config['api'] = apiServer.trim();
|
||||
config['key'] = key.trim();
|
||||
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
|
||||
.split('')
|
||||
.reversed
|
||||
.join();
|
||||
}
|
||||
|
||||
/// from local options
|
||||
ServerConfig.fromOptions(Map<String, dynamic> options)
|
||||
: idServer = options['custom-rendezvous-server'] ?? "",
|
||||
relayServer = options['relay-server'] ?? "",
|
||||
apiServer = options['api-server'] ?? "",
|
||||
key = options['key'] ?? "";
|
||||
}
|
||||
|
||||
119
flutter/lib/common/hbbs/hbbs.dart
Normal file
119
flutter/lib/common/hbbs/hbbs.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
class HttpType {
|
||||
static const kAuthReqTypeAccount = "account";
|
||||
static const kAuthReqTypeMobile = "mobile";
|
||||
static const kAuthReqTypeSMSCode = "sms_code";
|
||||
static const kAuthReqTypeEmailCode = "email_code";
|
||||
|
||||
static const kAuthResTypeToken = "access_token";
|
||||
static const kAuthResTypeEmailCheck = "email_check";
|
||||
}
|
||||
|
||||
class UserPayload {
|
||||
String name = '';
|
||||
String email = '';
|
||||
String note = '';
|
||||
int? status;
|
||||
String grp = '';
|
||||
bool isAdmin = false;
|
||||
|
||||
UserPayload.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'] ?? '',
|
||||
email = json['email'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
status = json['status'],
|
||||
grp = json['grp'] ?? '',
|
||||
isAdmin = json['is_admin'] == true;
|
||||
}
|
||||
|
||||
class PeerPayload {
|
||||
String id = '';
|
||||
String info = '';
|
||||
int? status;
|
||||
String user = '';
|
||||
String user_name = '';
|
||||
String note = '';
|
||||
|
||||
PeerPayload.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'] ?? '',
|
||||
info = json['info'] ?? '',
|
||||
status = json['status'],
|
||||
user = json['user'] ?? '',
|
||||
user_name = json['user_name'] ?? '',
|
||||
note = json['note'] ?? '';
|
||||
|
||||
static Peer toPeer(PeerPayload p) {
|
||||
return Peer.fromJson({"id": p.id});
|
||||
}
|
||||
}
|
||||
|
||||
class LoginRequest {
|
||||
String? username;
|
||||
String? password;
|
||||
String? id;
|
||||
String? uuid;
|
||||
bool? autoLogin;
|
||||
String? type;
|
||||
String? verificationCode;
|
||||
String? deviceInfo;
|
||||
|
||||
LoginRequest(
|
||||
{this.username,
|
||||
this.password,
|
||||
this.id,
|
||||
this.uuid,
|
||||
this.autoLogin,
|
||||
this.type,
|
||||
this.verificationCode,
|
||||
this.deviceInfo});
|
||||
|
||||
LoginRequest.fromJson(Map<String, dynamic> json) {
|
||||
username = json['username'];
|
||||
password = json['password'];
|
||||
id = json['id'];
|
||||
uuid = json['uuid'];
|
||||
autoLogin = json['autoLogin'];
|
||||
type = json['type'];
|
||||
verificationCode = json['verificationCode'];
|
||||
deviceInfo = json['deviceInfo'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['username'] = username ?? '';
|
||||
data['password'] = password ?? '';
|
||||
data['id'] = id ?? '';
|
||||
data['uuid'] = uuid ?? '';
|
||||
data['autoLogin'] = autoLogin ?? '';
|
||||
data['type'] = type ?? '';
|
||||
data['verificationCode'] = verificationCode ?? '';
|
||||
data['deviceInfo'] = deviceInfo ?? '';
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class LoginResponse {
|
||||
String? access_token;
|
||||
String? type;
|
||||
UserPayload? user;
|
||||
|
||||
LoginResponse({this.access_token, this.type, this.user});
|
||||
|
||||
LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
access_token = json['access_token'];
|
||||
type = json['type'];
|
||||
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestException implements Exception {
|
||||
int statusCode;
|
||||
String cause;
|
||||
RequestException(this.statusCode, this.cause);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "RequestException, statusCode: $statusCode, error: $cause";
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/login.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../desktop/pages/desktop_home_page.dart';
|
||||
import '../../mobile/pages/settings_page.dart';
|
||||
import 'login.dart';
|
||||
|
||||
class AddressBook extends StatefulWidget {
|
||||
final EdgeInsets? menuPadding;
|
||||
@@ -28,7 +26,6 @@ class _AddressBookState extends State<AddressBook> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -42,25 +39,12 @@ class _AddressBookState extends State<AddressBook> {
|
||||
}
|
||||
});
|
||||
|
||||
handleLogin() {
|
||||
// TODO refactor login dialog for desktop and mobile
|
||||
if (isDesktop) {
|
||||
loginDialog().then((success) {
|
||||
if (success) {
|
||||
gFFI.abModel.pullAb();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showLogin(gFFI.dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Widget> buildBody(BuildContext context) async {
|
||||
return Obx(() {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
return Center(
|
||||
child: InkWell(
|
||||
onTap: handleLogin,
|
||||
onTap: loginDialog,
|
||||
child: Text(
|
||||
translate("Login"),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
|
||||
676
flutter/lib/common/widgets/login.dart
Normal file
676
flutter/lib/common/widgets/login.dart
Normal file
@@ -0,0 +1,676 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
class _IconOP extends StatelessWidget {
|
||||
final String icon;
|
||||
final double iconWidth;
|
||||
final EdgeInsets margin;
|
||||
const _IconOP(
|
||||
{Key? key,
|
||||
required this.icon,
|
||||
required this.iconWidth,
|
||||
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: margin,
|
||||
child: SvgPicture.asset(
|
||||
'assets/$icon.svg',
|
||||
width: iconWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonOP extends StatelessWidget {
|
||||
final String op;
|
||||
final RxString curOP;
|
||||
final double iconWidth;
|
||||
final Color primaryColor;
|
||||
final double height;
|
||||
final Function() onTap;
|
||||
|
||||
const ButtonOP({
|
||||
Key? key,
|
||||
required this.op,
|
||||
required this.curOP,
|
||||
required this.iconWidth,
|
||||
required this.primaryColor,
|
||||
required this.height,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
Container(
|
||||
height: height,
|
||||
width: 200,
|
||||
child: Obx(() => ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: curOP.value.isEmpty || curOP.value == op
|
||||
? primaryColor
|
||||
: Colors.grey,
|
||||
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
|
||||
onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: _IconOP(
|
||||
icon: op,
|
||||
iconWidth: iconWidth,
|
||||
margin: EdgeInsets.only(right: 5),
|
||||
)),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Text('${translate("Continue with")} $op')))),
|
||||
],
|
||||
))),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigOP {
|
||||
final String op;
|
||||
final double iconWidth;
|
||||
ConfigOP({required this.op, required this.iconWidth});
|
||||
}
|
||||
|
||||
class WidgetOP extends StatefulWidget {
|
||||
final ConfigOP config;
|
||||
final RxString curOP;
|
||||
final Function(String) cbLogin;
|
||||
const WidgetOP({
|
||||
Key? key,
|
||||
required this.config,
|
||||
required this.curOP,
|
||||
required this.cbLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _WidgetOPState();
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetOPState extends State<WidgetOP> {
|
||||
Timer? _updateTimer;
|
||||
String _stateMsg = '';
|
||||
String _failedMsg = '';
|
||||
String _url = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_updateTimer?.cancel();
|
||||
}
|
||||
|
||||
_beginQueryState() {
|
||||
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
_updateState();
|
||||
});
|
||||
}
|
||||
|
||||
_updateState() {
|
||||
bind.mainAccountAuthResult().then((result) {
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final resultMap = jsonDecode(result);
|
||||
if (resultMap == null) {
|
||||
return;
|
||||
}
|
||||
final String stateMsg = resultMap['state_msg'];
|
||||
String failedMsg = resultMap['failed_msg'];
|
||||
final String? url = resultMap['url'];
|
||||
final authBody = resultMap['auth_body'];
|
||||
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
|
||||
if (_url.isEmpty && url != null && url.isNotEmpty) {
|
||||
launchUrl(Uri.parse(url));
|
||||
_url = url;
|
||||
}
|
||||
if (authBody != null) {
|
||||
_updateTimer?.cancel();
|
||||
final String username = authBody['user']['name'];
|
||||
widget.curOP.value = '';
|
||||
widget.cbLogin(username);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_stateMsg = stateMsg;
|
||||
_failedMsg = failedMsg;
|
||||
if (failedMsg.isNotEmpty) {
|
||||
widget.curOP.value = '';
|
||||
_updateTimer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_resetState() {
|
||||
_stateMsg = '';
|
||||
_failedMsg = '';
|
||||
_url = '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ButtonOP(
|
||||
op: widget.config.op,
|
||||
curOP: widget.curOP,
|
||||
iconWidth: widget.config.iconWidth,
|
||||
primaryColor: str2color(widget.config.op, 0x7f),
|
||||
height: 36,
|
||||
onTap: () async {
|
||||
_resetState();
|
||||
widget.curOP.value = widget.config.op;
|
||||
await bind.mainAccountAuth(op: widget.config.op);
|
||||
_beginQueryState();
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
if (widget.curOP.isNotEmpty &&
|
||||
widget.curOP.value != widget.config.op) {
|
||||
_failedMsg = '';
|
||||
}
|
||||
return Offstage(
|
||||
offstage:
|
||||
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_stateMsg,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
_failedMsg,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: widget.curOP.value != widget.config.op,
|
||||
child: const SizedBox(
|
||||
height: 5.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: widget.curOP.value != widget.config.op,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 20),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
widget.curOP.value = '';
|
||||
_updateTimer?.cancel();
|
||||
_resetState();
|
||||
bind.mainAccountAuthCancel();
|
||||
},
|
||||
child: Text(
|
||||
translate('Cancel'),
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginWidgetOP extends StatelessWidget {
|
||||
final List<ConfigOP> ops;
|
||||
final RxString curOP;
|
||||
final Function(String) cbLogin;
|
||||
|
||||
LoginWidgetOP({
|
||||
Key? key,
|
||||
required this.ops,
|
||||
required this.curOP,
|
||||
required this.cbLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var children = ops
|
||||
.map((op) => [
|
||||
WidgetOP(
|
||||
config: op,
|
||||
curOP: curOP,
|
||||
cbLogin: cbLogin,
|
||||
),
|
||||
const Divider(
|
||||
indent: 5,
|
||||
endIndent: 5,
|
||||
)
|
||||
])
|
||||
.expand((i) => i)
|
||||
.toList();
|
||||
if (children.isNotEmpty) {
|
||||
children.removeLast();
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class LoginWidgetUserPass extends StatelessWidget {
|
||||
final TextEditingController username;
|
||||
final TextEditingController pass;
|
||||
final String? usernameMsg;
|
||||
final String? passMsg;
|
||||
final bool isInProgress;
|
||||
final RxString curOP;
|
||||
final RxBool autoLogin;
|
||||
final Function() onLogin;
|
||||
final FocusNode? userFocusNode;
|
||||
const LoginWidgetUserPass({
|
||||
Key? key,
|
||||
this.userFocusNode,
|
||||
required this.username,
|
||||
required this.pass,
|
||||
required this.usernameMsg,
|
||||
required this.passMsg,
|
||||
required this.isInProgress,
|
||||
required this.curOP,
|
||||
required this.autoLogin,
|
||||
required this.onLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 8.0),
|
||||
DialogTextField(
|
||||
title: '${translate("Username")}:',
|
||||
controller: username,
|
||||
focusNode: userFocusNode,
|
||||
prefixIcon: Icon(Icons.account_circle_outlined),
|
||||
errorText: usernameMsg),
|
||||
DialogTextField(
|
||||
title: '${translate("Password")}:',
|
||||
obscureText: true,
|
||||
controller: pass,
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
errorText: passMsg),
|
||||
Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Remember me"),
|
||||
),
|
||||
value: autoLogin.value,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
autoLogin.value = v;
|
||||
},
|
||||
)),
|
||||
Offstage(
|
||||
offstage: !isInProgress,
|
||||
child: const LinearProgressIndicator()),
|
||||
const SizedBox(height: 12.0),
|
||||
FittedBox(
|
||||
child:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Container(
|
||||
height: 38,
|
||||
width: 200,
|
||||
child: Obx(() => ElevatedButton(
|
||||
child: Text(
|
||||
translate('Login'),
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed:
|
||||
curOP.value.isEmpty || curOP.value == 'rustdesk'
|
||||
? () {
|
||||
onLogin();
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
),
|
||||
])),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class DialogTextField extends StatelessWidget {
|
||||
final String title;
|
||||
final bool obscureText;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final Widget? prefixIcon;
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
DialogTextField(
|
||||
{Key? key,
|
||||
this.focusNode,
|
||||
this.obscureText = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.prefixIcon,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: prefixIcon,
|
||||
helperText: helperText,
|
||||
helperMaxLines: 8,
|
||||
errorText: errorText),
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// common login dialog for desktop
|
||||
/// call this directly
|
||||
Future<bool?> loginDialog() async {
|
||||
var username = TextEditingController();
|
||||
var password = TextEditingController();
|
||||
final userFocusNode = FocusNode()..requestFocus();
|
||||
Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
|
||||
|
||||
String? usernameMsg;
|
||||
String? passwordMsg;
|
||||
var isInProgress = false;
|
||||
final autoLogin = true.obs;
|
||||
final RxString curOP = ''.obs;
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
username.addListener(() {
|
||||
if (usernameMsg != null) {
|
||||
setState(() => usernameMsg = null);
|
||||
}
|
||||
});
|
||||
|
||||
password.addListener(() {
|
||||
if (passwordMsg != null) {
|
||||
setState(() => passwordMsg = null);
|
||||
}
|
||||
});
|
||||
|
||||
onDialogCancel() {
|
||||
isInProgress = false;
|
||||
close(false);
|
||||
}
|
||||
|
||||
onLogin() async {
|
||||
// validate
|
||||
if (username.text.isEmpty) {
|
||||
setState(() => usernameMsg = translate('Username missed'));
|
||||
return;
|
||||
}
|
||||
if (password.text.isEmpty) {
|
||||
setState(() => passwordMsg = translate('Password missed'));
|
||||
return;
|
||||
}
|
||||
curOP.value = 'rustdesk';
|
||||
setState(() => isInProgress = true);
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(LoginRequest(
|
||||
username: username.text,
|
||||
password: password.text,
|
||||
id: await bind.mainGetMyId(),
|
||||
uuid: await bind.mainGetUuid(),
|
||||
autoLogin: autoLogin.value,
|
||||
type: HttpType.kAuthReqTypeAccount));
|
||||
|
||||
switch (resp.type) {
|
||||
case HttpType.kAuthResTypeToken:
|
||||
if (resp.access_token != null) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'access_token', value: resp.access_token!);
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case HttpType.kAuthResTypeEmailCheck:
|
||||
setState(() => isInProgress = false);
|
||||
final res = await verificationCodeDialog(resp.user);
|
||||
if (res == true) {
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
passwordMsg = "Failed, bad response from server";
|
||||
break;
|
||||
}
|
||||
} on RequestException catch (err) {
|
||||
passwordMsg = translate(err.cause);
|
||||
debugPrintStack(label: err.toString());
|
||||
} catch (err) {
|
||||
passwordMsg = "Unknown Error: $err";
|
||||
debugPrintStack(label: err.toString());
|
||||
}
|
||||
curOP.value = '';
|
||||
setState(() => isInProgress = false);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Login')),
|
||||
contentBoxConstraints: BoxConstraints(minWidth: 400),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
LoginWidgetUserPass(
|
||||
username: username,
|
||||
pass: password,
|
||||
usernameMsg: usernameMsg,
|
||||
passMsg: passwordMsg,
|
||||
isInProgress: isInProgress,
|
||||
curOP: curOP,
|
||||
autoLogin: autoLogin,
|
||||
onLogin: onLogin,
|
||||
userFocusNode: userFocusNode,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
translate('or'),
|
||||
style: TextStyle(fontSize: 16),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
LoginWidgetOP(
|
||||
ops: [
|
||||
ConfigOP(op: 'GitHub', iconWidth: 20),
|
||||
ConfigOP(op: 'Google', iconWidth: 20),
|
||||
ConfigOP(op: 'Okta', iconWidth: 38),
|
||||
],
|
||||
curOP: curOP,
|
||||
cbLogin: (String username) {
|
||||
gFFI.userModel.userName.value = username;
|
||||
close(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [msgBoxButton(translate('Close'), onDialogCancel)],
|
||||
onCancel: onDialogCancel,
|
||||
);
|
||||
});
|
||||
|
||||
if (res != null) {
|
||||
// update ab and group status
|
||||
await gFFI.abModel.pullAb();
|
||||
await gFFI.groupModel.pull();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool?> verificationCodeDialog(UserPayload? user) async {
|
||||
var autoLogin = true;
|
||||
var isInProgress = false;
|
||||
String? errorText;
|
||||
|
||||
final code = TextEditingController();
|
||||
final focusNode = FocusNode()..requestFocus();
|
||||
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
bool validate() {
|
||||
return code.text.length >= 6;
|
||||
}
|
||||
|
||||
code.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() => errorText = null);
|
||||
}
|
||||
});
|
||||
|
||||
void onVerify() async {
|
||||
if (!validate()) {
|
||||
setState(
|
||||
() => errorText = translate('Too short, at least 6 characters.'));
|
||||
return;
|
||||
}
|
||||
setState(() => isInProgress = true);
|
||||
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(LoginRequest(
|
||||
verificationCode: code.text,
|
||||
username: user?.name,
|
||||
id: await bind.mainGetMyId(),
|
||||
uuid: await bind.mainGetUuid(),
|
||||
autoLogin: autoLogin,
|
||||
type: HttpType.kAuthReqTypeEmailCode));
|
||||
|
||||
switch (resp.type) {
|
||||
case HttpType.kAuthResTypeToken:
|
||||
if (resp.access_token != null) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'access_token', value: resp.access_token!);
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
errorText = "Failed, bad response from server";
|
||||
break;
|
||||
}
|
||||
} on RequestException catch (err) {
|
||||
errorText = translate(err.cause);
|
||||
debugPrintStack(label: err.toString());
|
||||
} catch (err) {
|
||||
errorText = "Unknown Error: $err";
|
||||
debugPrintStack(label: err.toString());
|
||||
}
|
||||
|
||||
setState(() => isInProgress = false);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Verification code")),
|
||||
contentBoxConstraints: BoxConstraints(maxWidth: 300),
|
||||
content: Column(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: user?.email == null,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Email",
|
||||
prefixIcon: Icon(Icons.email),
|
||||
border: InputBorder.none),
|
||||
readOnly: true,
|
||||
controller: TextEditingController(text: user?.email),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
DialogTextField(
|
||||
title: '${translate("Verification code")}:',
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
focusNode: focusNode,
|
||||
helperText: translate('verification_tip'),
|
||||
),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Row(children: [
|
||||
Expanded(child: Text(translate("Trust this device")))
|
||||
]),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = !autoLogin);
|
||||
},
|
||||
),
|
||||
Offstage(
|
||||
offstage: !isInProgress,
|
||||
child: const LinearProgressIndicator()),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: onVerify, child: Text(translate("Verify"))),
|
||||
]);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
225
flutter/lib/common/widgets/my_group.dart
Normal file
225
flutter/lib/common/widgets/my_group.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
class MyGroup extends StatefulWidget {
|
||||
final EdgeInsets? menuPadding;
|
||||
const MyGroup({Key? key, this.menuPadding}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MyGroupState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MyGroupState extends State<MyGroup> {
|
||||
static final RxString selectedUser = ''.obs;
|
||||
static final RxString searchUserText = ''.obs;
|
||||
static TextEditingController searchUserController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FutureBuilder<Widget>(
|
||||
future: buildBody(context),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
return const Offstage();
|
||||
}
|
||||
});
|
||||
|
||||
Future<Widget> buildBody(BuildContext context) async {
|
||||
return Obx(() {
|
||||
if (gFFI.groupModel.userLoading.value) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (gFFI.groupModel.userLoadError.isNotEmpty) {
|
||||
return _buildShowError(gFFI.groupModel.userLoadError.value);
|
||||
}
|
||||
if (isDesktop) {
|
||||
return _buildDesktop();
|
||||
} else {
|
||||
return _buildMobile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildShowError(String error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(translate(error)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
gFFI.groupModel.pull();
|
||||
},
|
||||
child: Text(translate("Retry")))
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
return Obx(
|
||||
() => Row(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).scaffoldBackgroundColor)),
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildLeftHeader(),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration:
|
||||
BoxDecoration(borderRadius: BorderRadius.circular(2)),
|
||||
child: _buildUserContacts(),
|
||||
).marginSymmetric(vertical: 8.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
).marginOnly(right: 8.0),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
initPeers: gFFI.groupModel.peersShow.value)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
return Obx(
|
||||
() => Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).scaffoldBackgroundColor)),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLeftHeader(),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration:
|
||||
BoxDecoration(borderRadius: BorderRadius.circular(4)),
|
||||
child: _buildUserContacts(),
|
||||
).marginSymmetric(vertical: 8.0)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
initPeers: gFFI.groupModel.peersShow.value)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeftHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchUserController,
|
||||
onChanged: (value) {
|
||||
searchUserText.value = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
Icons.search_rounded,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
hintText: translate("Search"),
|
||||
hintStyle:
|
||||
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserContacts() {
|
||||
return Obx(() {
|
||||
return Column(
|
||||
children: gFFI.groupModel.users
|
||||
.where((p0) {
|
||||
if (searchUserText.isNotEmpty) {
|
||||
return p0.name.contains(searchUserText.value);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((e) => _buildUserItem(e.name))
|
||||
.toList());
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildUserItem(String username) {
|
||||
return InkWell(onTap: () {
|
||||
if (selectedUser.value != username) {
|
||||
selectedUser.value = username;
|
||||
gFFI.groupModel.pullUserPeers(username);
|
||||
}
|
||||
}, child: Obx(
|
||||
() {
|
||||
bool selected = selectedUser.value == username;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? MyTheme.color(context).highlight : null,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1))),
|
||||
),
|
||||
child: Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16)
|
||||
.marginOnly(right: 4),
|
||||
Expanded(child: Text(username)),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4),
|
||||
),
|
||||
);
|
||||
},
|
||||
)).marginSymmetric(horizontal: 12);
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,7 @@ enum CardType {
|
||||
fav,
|
||||
lan,
|
||||
ab,
|
||||
grp,
|
||||
}
|
||||
|
||||
abstract class BasePeerCard extends StatelessWidget {
|
||||
@@ -463,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Only avaliable on Windows.
|
||||
/// Only available on Windows.
|
||||
@protected
|
||||
MenuEntryBase<String> _createShortCutAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
@@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
case CardType.ab:
|
||||
gFFI.abModel.pullAb();
|
||||
break;
|
||||
case CardType.grp:
|
||||
gFFI.groupModel.pull();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
}
|
||||
}
|
||||
|
||||
class MyGroupPeerCard extends BasePeerCard {
|
||||
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
|
||||
: super(
|
||||
peer: peer,
|
||||
cardType: CardType.grp,
|
||||
menuPadding: menuPadding,
|
||||
key: key);
|
||||
|
||||
@override
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
];
|
||||
if (isDesktop && peer.platform != 'Android') {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
}
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
menuItems.add(_wolAction(peer.id));
|
||||
if (Platform.isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
}
|
||||
|
||||
void _rdpDialog(String id, CardType card) async {
|
||||
String port, username;
|
||||
if (card == CardType.ab) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/address_book.dart';
|
||||
import 'package:flutter_hbb/common/widgets/my_group.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@@ -16,6 +16,101 @@ import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
|
||||
const int groupTabIndex = 4;
|
||||
const String defaultGroupTabname = 'Group';
|
||||
|
||||
class StatePeerTab {
|
||||
final RxInt currentTab = 0.obs;
|
||||
final RxInt tabHiddenFlag = 0.obs;
|
||||
final RxList<String> tabNames = [
|
||||
'Recent Sessions',
|
||||
'Favorites',
|
||||
'Discovered',
|
||||
'Address Book',
|
||||
defaultGroupTabname,
|
||||
].obs;
|
||||
|
||||
StatePeerTab._() {
|
||||
tabHiddenFlag.value = (int.tryParse(
|
||||
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
|
||||
radix: 2) ??
|
||||
0);
|
||||
var tabs = _notHiddenTabs();
|
||||
currentTab.value =
|
||||
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
|
||||
if (!tabs.contains(currentTab.value)) {
|
||||
currentTab.value = 0;
|
||||
}
|
||||
}
|
||||
static final StatePeerTab instance = StatePeerTab._();
|
||||
|
||||
check() {
|
||||
var tabs = _notHiddenTabs();
|
||||
if (filterGroupCard()) {
|
||||
if (currentTab.value == groupTabIndex) {
|
||||
currentTab.value =
|
||||
tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
|
||||
bind.setLocalFlutterConfig(
|
||||
k: 'peer-tab-index', v: currentTab.value.toString());
|
||||
}
|
||||
} else {
|
||||
if (gFFI.userModel.isAdmin.isFalse &&
|
||||
gFFI.userModel.groupName.isNotEmpty) {
|
||||
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
|
||||
} else {
|
||||
tabNames[groupTabIndex] = defaultGroupTabname;
|
||||
}
|
||||
if (tabs.contains(groupTabIndex) &&
|
||||
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
|
||||
groupTabIndex) {
|
||||
currentTab.value = groupTabIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> currentTabs() {
|
||||
var v = List<int>.empty(growable: true);
|
||||
for (int i = 0; i < tabNames.length; i++) {
|
||||
if (!_isTabHidden(i) && !_isTabFilter(i)) {
|
||||
v.add(i);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
bool filterGroupCard() {
|
||||
if (gFFI.groupModel.users.isEmpty ||
|
||||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTabHidden(int tabindex) {
|
||||
return tabHiddenFlag & (1 << tabindex) != 0;
|
||||
}
|
||||
|
||||
bool _isTabFilter(int tabIndex) {
|
||||
if (tabIndex == groupTabIndex) {
|
||||
return filterGroupCard();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<int> _notHiddenTabs() {
|
||||
var v = List<int>.empty(growable: true);
|
||||
for (int i = 0; i < tabNames.length; i++) {
|
||||
if (!_isTabHidden(i)) {
|
||||
v.add(i);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
final statePeerTab = StatePeerTab.instance;
|
||||
|
||||
class PeerTabPage extends StatefulWidget {
|
||||
const PeerTabPage({Key? key}) : super(key: key);
|
||||
@override
|
||||
@@ -23,10 +118,9 @@ class PeerTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TabEntry {
|
||||
final String name;
|
||||
final Widget widget;
|
||||
final Function() load;
|
||||
_TabEntry(this.name, this.widget, this.load);
|
||||
_TabEntry(this.widget, this.load);
|
||||
}
|
||||
|
||||
EdgeInsets? _menuPadding() {
|
||||
@@ -35,65 +129,36 @@ EdgeInsets? _menuPadding() {
|
||||
|
||||
class _PeerTabPageState extends State<PeerTabPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final RxInt tabHiddenFlag;
|
||||
late final RxString currentTab;
|
||||
late final RxList<String> visibleOrderedTabs;
|
||||
final List<_TabEntry> entries = [
|
||||
_TabEntry(
|
||||
'Recent Sessions',
|
||||
RecentPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainLoadRecentPeers),
|
||||
_TabEntry(
|
||||
'Favorites',
|
||||
FavoritePeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainLoadFavPeers),
|
||||
_TabEntry(
|
||||
'Discovered',
|
||||
DiscoveredPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainDiscover),
|
||||
_TabEntry(
|
||||
'Address Book',
|
||||
AddressBook(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
() => {}),
|
||||
_TabEntry(
|
||||
MyGroup(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
() => {}),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
tabHiddenFlag = (int.tryParse(
|
||||
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
|
||||
radix: 2) ??
|
||||
0)
|
||||
.obs;
|
||||
currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs;
|
||||
visibleOrderedTabs = entries
|
||||
.where((e) => !isTabHidden(e.name))
|
||||
.map((e) => e.name)
|
||||
.toList()
|
||||
.obs;
|
||||
try {
|
||||
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
|
||||
if (conf.isNotEmpty) {
|
||||
final json = jsonDecode(conf);
|
||||
if (json is List) {
|
||||
final List<String> list = json.map((e) => e.toString()).toList();
|
||||
if (list.length == visibleOrderedTabs.length &&
|
||||
visibleOrderedTabs.every((e) => list.contains(e))) {
|
||||
visibleOrderedTabs.value = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrintStack(label: '$e');
|
||||
}
|
||||
|
||||
adjustTab();
|
||||
|
||||
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
|
||||
@@ -105,10 +170,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> handleTabSelection(String tabName) async {
|
||||
currentTab.value = tabName;
|
||||
await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName);
|
||||
entries.firstWhereOrNull((e) => e.name == tabName)?.load();
|
||||
Future<void> handleTabSelection(int tabIndex) async {
|
||||
if (tabIndex < entries.length) {
|
||||
statePeerTab.currentTab.value = tabIndex;
|
||||
entries[tabIndex].load();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -149,65 +215,80 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget _createSwitchBar(BuildContext context) {
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
return Obx(() {
|
||||
int indexCounter = -1;
|
||||
return ReorderableListView(
|
||||
buildDefaultDragHandles: false,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
var list = visibleOrderedTabs.toList();
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final String item = list.removeAt(oldIndex);
|
||||
list.insert(newIndex, item);
|
||||
bind.setLocalFlutterConfig(
|
||||
k: 'peer-tab-order', v: jsonEncode(list));
|
||||
visibleOrderedTabs.value = list;
|
||||
},
|
||||
var tabs = statePeerTab.currentTabs();
|
||||
return ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
scrollController: ScrollController(),
|
||||
children: visibleOrderedTabs.map((t) {
|
||||
indexCounter++;
|
||||
return ReorderableDragStartListener(
|
||||
key: ValueKey(t),
|
||||
index: indexCounter,
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTab.value == t
|
||||
? Theme.of(context).backgroundColor
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
controller: ScrollController(),
|
||||
children: tabs.map((t) {
|
||||
return InkWell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: statePeerTab.currentTab.value == t
|
||||
? Theme.of(context).backgroundColor
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
translatedTabname(t),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: statePeerTab.currentTab.value == t
|
||||
? textColor
|
||||
: textColor
|
||||
?..withOpacity(0.5)),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
translate(t),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: currentTab.value == t ? textColor : textColor
|
||||
?..withOpacity(0.5)),
|
||||
),
|
||||
)),
|
||||
onTap: () async => await handleTabSelection(t),
|
||||
),
|
||||
)),
|
||||
onTap: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'peer-tab-index', v: t.toString());
|
||||
},
|
||||
);
|
||||
}).toList());
|
||||
});
|
||||
}
|
||||
|
||||
translatedTabname(int index) {
|
||||
if (index < statePeerTab.tabNames.length) {
|
||||
final name = statePeerTab.tabNames[index];
|
||||
if (index == groupTabIndex) {
|
||||
if (name == defaultGroupTabname) {
|
||||
return translate(name);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
} else {
|
||||
return translate(name);
|
||||
}
|
||||
}
|
||||
assert(false);
|
||||
return index.toString();
|
||||
}
|
||||
|
||||
Widget _createPeersView() {
|
||||
final verticalMargin = isDesktop ? 12.0 : 6.0;
|
||||
return Expanded(
|
||||
child: Obx(() =>
|
||||
entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ??
|
||||
visibleContextMenuListener(Center(
|
||||
child: Text(translate('Right click to select tabs')),
|
||||
))).marginSymmetric(vertical: verticalMargin),
|
||||
);
|
||||
child: Obx(() {
|
||||
var tabs = statePeerTab.currentTabs();
|
||||
if (tabs.isEmpty) {
|
||||
return visibleContextMenuListener(Center(
|
||||
child: Text(translate('Right click to select tabs')),
|
||||
));
|
||||
} else {
|
||||
if (tabs.contains(statePeerTab.currentTab.value)) {
|
||||
return entries[statePeerTab.currentTab.value].widget;
|
||||
} else {
|
||||
statePeerTab.currentTab.value = tabs[0];
|
||||
return entries[statePeerTab.currentTab.value].widget;
|
||||
}
|
||||
}
|
||||
}).marginSymmetric(vertical: verticalMargin));
|
||||
}
|
||||
|
||||
Widget _createPeerViewTypeSwitch(BuildContext context) {
|
||||
@@ -240,22 +321,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
bool isTabHidden(String name) {
|
||||
int index = entries.indexWhere((e) => e.name == name);
|
||||
if (index >= 0) {
|
||||
return tabHiddenFlag & (1 << index) != 0;
|
||||
}
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
adjustTab() {
|
||||
if (visibleOrderedTabs.isNotEmpty) {
|
||||
if (!visibleOrderedTabs.contains(currentTab.value)) {
|
||||
handleTabSelection(visibleOrderedTabs[0]);
|
||||
}
|
||||
} else {
|
||||
currentTab.value = '';
|
||||
var tabs = statePeerTab.currentTabs();
|
||||
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
|
||||
statePeerTab.currentTab.value = tabs[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,47 +347,44 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
Widget visibleContextMenu(CancelFunc cancelFunc) {
|
||||
final List<MenuEntryBase> menu = entries.asMap().entries.map((e) {
|
||||
int bitMask = 1 << e.key;
|
||||
return MenuEntrySwitch(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate(e.value.name),
|
||||
getter: () async {
|
||||
return tabHiddenFlag.value & bitMask == 0;
|
||||
},
|
||||
setter: (show) async {
|
||||
if (show) {
|
||||
tabHiddenFlag.value &= ~bitMask;
|
||||
} else {
|
||||
tabHiddenFlag.value |= bitMask;
|
||||
}
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2));
|
||||
visibleOrderedTabs.removeWhere((e) => isTabHidden(e));
|
||||
visibleOrderedTabs.addAll(entries
|
||||
.where((e) =>
|
||||
!visibleOrderedTabs.contains(e.name) &&
|
||||
!isTabHidden(e.name))
|
||||
.map((e) => e.name)
|
||||
.toList());
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs));
|
||||
cancelFunc();
|
||||
adjustTab();
|
||||
});
|
||||
}).toList();
|
||||
return mod_menu.PopupMenu(
|
||||
items: menu
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: MyTheme.accent,
|
||||
height: 20.0,
|
||||
dividerHeight: 12.0,
|
||||
)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
);
|
||||
return Obx(() {
|
||||
final List<MenuEntryBase> menu = List.empty(growable: true);
|
||||
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
|
||||
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
|
||||
continue;
|
||||
}
|
||||
int bitMask = 1 << i;
|
||||
menu.add(MenuEntrySwitch(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translatedTabname(i),
|
||||
getter: () async {
|
||||
return statePeerTab.tabHiddenFlag & bitMask == 0;
|
||||
},
|
||||
setter: (show) async {
|
||||
if (show) {
|
||||
statePeerTab.tabHiddenFlag.value &= ~bitMask;
|
||||
} else {
|
||||
statePeerTab.tabHiddenFlag.value |= bitMask;
|
||||
}
|
||||
await bind.setLocalFlutterConfig(
|
||||
k: 'hidden-peer-card',
|
||||
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
|
||||
cancelFunc();
|
||||
adjustTab();
|
||||
}));
|
||||
}
|
||||
return mod_menu.PopupMenu(
|
||||
items: menu
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: MyTheme.accent,
|
||||
height: 20.0,
|
||||
dividerHeight: 12.0,
|
||||
)))
|
||||
.expand((i) => i)
|
||||
.toList());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class MyGroupPeerView extends BasePeersView {
|
||||
MyGroupPeerView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required List<Peer> initPeers})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'my group peer',
|
||||
loadEvent: 'load_my_group_peers',
|
||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: initPeers,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../models/input_model.dart';
|
||||
|
||||
@@ -9,11 +10,12 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
final InputModel inputModel;
|
||||
final Widget child;
|
||||
|
||||
RawKeyFocusScope(
|
||||
{this.focusNode,
|
||||
this.onFocusChange,
|
||||
required this.inputModel,
|
||||
required this.child});
|
||||
RawKeyFocusScope({
|
||||
this.focusNode,
|
||||
this.onFocusChange,
|
||||
required this.inputModel,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -24,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
canRequestFocus: true,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
onKey: inputModel.handleRawKeyEvent,
|
||||
onKey:
|
||||
stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null,
|
||||
child: child));
|
||||
}
|
||||
}
|
||||
@@ -35,11 +38,15 @@ class RawPointerMouseRegion extends StatelessWidget {
|
||||
final MouseCursor? cursor;
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
final PointerDownEventListener? onPointerDown;
|
||||
final PointerUpEventListener? onPointerUp;
|
||||
|
||||
RawPointerMouseRegion(
|
||||
{this.onEnter,
|
||||
this.onExit,
|
||||
this.cursor,
|
||||
this.onPointerDown,
|
||||
this.onPointerUp,
|
||||
required this.inputModel,
|
||||
required this.child});
|
||||
|
||||
@@ -47,8 +54,14 @@ class RawPointerMouseRegion extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerHover: inputModel.onPointHoverImage,
|
||||
onPointerDown: inputModel.onPointDownImage,
|
||||
onPointerUp: inputModel.onPointUpImage,
|
||||
onPointerDown: (evt) {
|
||||
onPointerDown?.call(evt);
|
||||
inputModel.onPointDownImage(evt);
|
||||
},
|
||||
onPointerUp: (evt) {
|
||||
onPointerUp?.call(evt);
|
||||
inputModel.onPointUpImage(evt);
|
||||
},
|
||||
onPointerMove: inputModel.onPointMoveImage,
|
||||
onPointerSignal: inputModel.onPointerSignalImage,
|
||||
/*
|
||||
|
||||
@@ -5,6 +5,11 @@ import 'package:flutter_hbb/common.dart';
|
||||
|
||||
const double kDesktopRemoteTabBarHeight = 28.0;
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
const String kPeerPlatformAndroid = "Android";
|
||||
|
||||
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
|
||||
const String kAppTypeMain = "main";
|
||||
const String kAppTypeDesktopRemote = "remote";
|
||||
@@ -100,6 +105,8 @@ const kRemoteImageQualityLow = 'low';
|
||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||
const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||
/// see [LogicalKeyboardKey.keyLabel]
|
||||
const Map<int, String> logicalKeyMap = <int, String>{
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/address_book.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/peers_view.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
@@ -46,7 +44,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
var svcStatusCode = 0.obs;
|
||||
var svcIsUsingPublicServer = true.obs;
|
||||
|
||||
bool isWindowMinisized = false;
|
||||
bool isWindowMinimized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -82,13 +80,13 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void onWindowEvent(String eventName) {
|
||||
super.onWindowEvent(eventName);
|
||||
if (eventName == 'minimize') {
|
||||
isWindowMinisized = true;
|
||||
isWindowMinimized = true;
|
||||
} else if (eventName == 'maximize' || eventName == 'restore') {
|
||||
if (isWindowMinisized && Platform.isWindows) {
|
||||
// windows can't update when minisized.
|
||||
if (isWindowMinimized && Platform.isWindows) {
|
||||
// windows can't update when minimized.
|
||||
Get.forceAppUpdate();
|
||||
}
|
||||
isWindowMinisized = false;
|
||||
isWindowMinimized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +170,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
maxLength: 90,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
@@ -179,12 +178,13 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1,
|
||||
height: 1.25,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
|
||||
@@ -42,6 +42,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
var svcStopped = false.obs;
|
||||
var watchIsCanScreenRecording = false;
|
||||
var watchIsProcessTrust = false;
|
||||
var watchIsInputMonitoring = false;
|
||||
Timer? _updateTimer;
|
||||
|
||||
@override
|
||||
@@ -334,6 +335,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
bind.mainIsProcessTrusted(prompt: true);
|
||||
watchIsProcessTrust = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
|
||||
return buildInstallCard("Permissions", "config_input", "Configure",
|
||||
() async {
|
||||
bind.mainIsCanInputMonitoring(prompt: true);
|
||||
watchIsInputMonitoring = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!svcStopped.value &&
|
||||
bind.mainIsInstalled() &&
|
||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||
@@ -438,7 +445,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bind.mainStartGrabKeyboard();
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
await gFFI.serverModel.fetchID();
|
||||
final url = await bind.mainGetSoftwareUpdateUrl();
|
||||
@@ -468,6 +474,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
if (watchIsInputMonitoring) {
|
||||
if (bind.mainIsCanInputMonitoring(prompt: false)) {
|
||||
watchIsInputMonitoring = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
||||
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
||||
@@ -501,9 +513,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventShow) {
|
||||
rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
|
||||
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
|
||||
} else if (call.method == kWindowEventHide) {
|
||||
rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
|
||||
await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
|
||||
} else if (call.method == kWindowConnect) {
|
||||
await connectMainDesktop(
|
||||
call.arguments['id'],
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/login.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
|
||||
const double _kTabWidth = 235;
|
||||
const double _kTabHeight = 42;
|
||||
@@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
scrollController: controller,
|
||||
child: PageView(
|
||||
controller: controller,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
_General(),
|
||||
_Safety(),
|
||||
@@ -273,6 +274,15 @@ class _GeneralState extends State<_General> {
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
'enable-confirm-closing-tabs'),
|
||||
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
|
||||
if (Platform.isLinux)
|
||||
Tooltip(
|
||||
message: translate('software_render_tip'),
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
"Always use software rendering",
|
||||
'allow-always-software-render',
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -932,6 +942,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
|
||||
if (old.isNotEmpty && old != idServer) {
|
||||
await gFFI.userModel.logOut();
|
||||
}
|
||||
// should set one by one
|
||||
await bind.mainSetOption(
|
||||
key: 'custom-rendezvous-server', value: idServer);
|
||||
@@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
import() {
|
||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||
TextEditingController mytext = TextEditingController();
|
||||
String? aNullableString = '';
|
||||
aNullableString = value?.text;
|
||||
mytext.text = aNullableString.toString();
|
||||
if (mytext.text.isNotEmpty) {
|
||||
final text = value?.text;
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
Map<String, dynamic> config = jsonDecode(mytext.text);
|
||||
if (config.containsKey('IdServer')) {
|
||||
String id = config['IdServer'] ?? '';
|
||||
String relay = config['RelayServer'] ?? '';
|
||||
String api = config['ApiServer'] ?? '';
|
||||
String key = config['Key'] ?? '';
|
||||
idController.text = id;
|
||||
relayController.text = relay;
|
||||
apiController.text = api;
|
||||
keyController.text = key;
|
||||
Future<bool> success = set(id, relay, api, key);
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
idController.text = sc.idServer;
|
||||
relayController.text = sc.relayServer;
|
||||
apiController.text = sc.apiServer;
|
||||
keyController.text = sc.key;
|
||||
Future<bool> success =
|
||||
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
|
||||
success.then((value) {
|
||||
if (value) {
|
||||
showToast(
|
||||
@@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
|
||||
export() {
|
||||
Map<String, String> config = {};
|
||||
config['IdServer'] = idController.text.trim();
|
||||
config['RelayServer'] = relayController.text.trim();
|
||||
config['ApiServer'] = apiController.text.trim();
|
||||
config['Key'] = keyController.text.trim();
|
||||
Clipboard.setData(ClipboardData(text: jsonEncode(config)));
|
||||
final text = ServerConfig(
|
||||
idServer: idController.text,
|
||||
relayServer: relayController.text,
|
||||
apiServer: apiController.text,
|
||||
key: keyController.text)
|
||||
.encode();
|
||||
debugPrint("ServerConfig export: $text");
|
||||
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
showToast(translate('Export server configuration successfully'));
|
||||
}
|
||||
|
||||
@@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> {
|
||||
}
|
||||
|
||||
Widget accountAction() {
|
||||
return _futureBuilder(future: () async {
|
||||
return await gFFI.userModel.getUserName();
|
||||
}(), hasData: (_) {
|
||||
return Obx(() => _Button(
|
||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
||||
() => {
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? loginDialog().then((success) {
|
||||
if (success) {
|
||||
gFFI.abModel.pullAb();
|
||||
}
|
||||
})
|
||||
: gFFI.userModel.logOut()
|
||||
}));
|
||||
});
|
||||
return Obx(() => _Button(
|
||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
||||
() => {
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? loginDialog()
|
||||
: gFFI.userModel.logOut()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> {
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
child: _Card(title: 'About RustDesk', children: [
|
||||
child: _Card(title: '${translate('About')} RustDesk', children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Text('Version: $version').marginSymmetric(vertical: 4.0),
|
||||
Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0),
|
||||
Text('${translate('Version')}: $version')
|
||||
.marginSymmetric(vertical: 4.0),
|
||||
Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy');
|
||||
},
|
||||
child: const Text(
|
||||
'Privacy Statement',
|
||||
child: Text(
|
||||
translate('Privacy Statement'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com');
|
||||
},
|
||||
child: const Text(
|
||||
'Website',
|
||||
child: Text(
|
||||
translate('Website'),
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
Container(
|
||||
@@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> {
|
||||
'Copyright © 2022 Purslane Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
const Text(
|
||||
'Made with heart in this chaotic world!',
|
||||
Text(
|
||||
translate('Slogan_tip'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white),
|
||||
@@ -1227,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
||||
ref.value = option;
|
||||
if (reverse) option = !option;
|
||||
String value = bool2option(key, option);
|
||||
bind.mainSetOption(key: key, value: value);
|
||||
await bind.mainSetOption(key: key, value: value);
|
||||
update?.call();
|
||||
}
|
||||
}
|
||||
@@ -1431,7 +1436,7 @@ Widget _lock(
|
||||
|
||||
_LabeledTextField(
|
||||
BuildContext context,
|
||||
String lable,
|
||||
String label,
|
||||
TextEditingController controller,
|
||||
String errorText,
|
||||
bool enabled,
|
||||
@@ -1442,7 +1447,7 @@ _LabeledTextField(
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
'${translate(lable)}:',
|
||||
'${translate(label)}:',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(color: _disabledTextColor(context, enabled)),
|
||||
),
|
||||
@@ -1455,6 +1460,8 @@ _LabeledTextField(
|
||||
enabled: enabled,
|
||||
obscureText: secure,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
errorText: errorText.isNotEmpty ? errorText : null),
|
||||
style: TextStyle(
|
||||
color: _disabledTextColor(context, enabled),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -32,6 +33,18 @@ enum LocationStatus {
|
||||
fileSearchBar
|
||||
}
|
||||
|
||||
/// The status of currently focused scope of the mouse
|
||||
enum MouseFocusScope {
|
||||
/// Mouse is in local field.
|
||||
local,
|
||||
|
||||
/// Mouse is in remote field.
|
||||
remote,
|
||||
|
||||
/// Mouse is not in local field, remote neither.
|
||||
none
|
||||
}
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
const FileManagerPage({Key? key, required this.id}) : super(key: key);
|
||||
final String id;
|
||||
@@ -55,6 +68,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
final _searchTextRemote = "".obs;
|
||||
final _breadCrumbScrollerLocal = ScrollController();
|
||||
final _breadCrumbScrollerRemote = ScrollController();
|
||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||
final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal");
|
||||
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
|
||||
final _listSearchBufferLocal = TimeoutStringBuffer();
|
||||
final _listSearchBufferRemote = TimeoutStringBuffer();
|
||||
|
||||
/// [_lastClickTime], [_lastClickEntry] help to handle double click
|
||||
int _lastClickTime =
|
||||
@@ -197,6 +215,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
|
||||
Widget body({bool isLocal = false}) {
|
||||
final scrollController = ScrollController();
|
||||
return Container(
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
@@ -217,8 +236,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ScrollController(),
|
||||
child: _buildDataTable(context, isLocal),
|
||||
controller: scrollController,
|
||||
child: _buildDataTable(context, isLocal, scrollController),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -228,7 +247,9 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataTable(BuildContext context, bool isLocal) {
|
||||
Widget _buildDataTable(
|
||||
BuildContext context, bool isLocal, ScrollController scrollController) {
|
||||
const rowHeight = 25.0;
|
||||
final fd = model.getCurrentDir(isLocal);
|
||||
final entries = fd.entries;
|
||||
final sortIndex = (SortBy style) {
|
||||
@@ -246,130 +267,219 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
final sortAscending =
|
||||
isLocal ? model.localSortAscending : model.remoteSortAscending;
|
||||
|
||||
return ObxValue<RxString>(
|
||||
(searchText) {
|
||||
final filteredEntries = searchText.isNotEmpty
|
||||
? entries.where((element) {
|
||||
return element.name.contains(searchText.value);
|
||||
}).toList(growable: false)
|
||||
: entries;
|
||||
return DataTable(
|
||||
key: ValueKey(isLocal ? 0 : 1),
|
||||
showCheckboxColumn: false,
|
||||
dataRowHeight: 25,
|
||||
headingRowHeight: 30,
|
||||
horizontalMargin: 8,
|
||||
columnSpacing: 8,
|
||||
showBottomBorder: true,
|
||||
sortColumnIndex: sortIndex,
|
||||
sortAscending: sortAscending,
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Name"),
|
||||
).marginSymmetric(horizontal: 4),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.name,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Modified"),
|
||||
),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.modified,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(translate("Size")),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.size,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
],
|
||||
rows: filteredEntries.map((entry) {
|
||||
final sizeStr =
|
||||
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
return DataRow(
|
||||
key: ValueKey(entry.name),
|
||||
onSelectChanged: (s) {
|
||||
_onSelectedChanged(getSelectedItems(isLocal), filteredEntries,
|
||||
entry, isLocal);
|
||||
},
|
||||
selected: getSelectedItems(isLocal).contains(entry),
|
||||
cells: [
|
||||
DataCell(
|
||||
Container(
|
||||
width: 200,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: entry.name,
|
||||
child: Row(children: [
|
||||
entry.isDrive
|
||||
? Image(
|
||||
image: iconHardDrive,
|
||||
fit: BoxFit.scaleDown,
|
||||
return MouseRegion(
|
||||
onEnter: (evt) {
|
||||
_mouseFocusScope.value =
|
||||
isLocal ? MouseFocusScope.local : MouseFocusScope.remote;
|
||||
if (isLocal) {
|
||||
_keyboardNodeLocal.requestFocus();
|
||||
} else {
|
||||
_keyboardNodeRemote.requestFocus();
|
||||
}
|
||||
},
|
||||
onExit: (evt) {
|
||||
_mouseFocusScope.value = MouseFocusScope.none;
|
||||
},
|
||||
child: ListSearchActionListener(
|
||||
node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote,
|
||||
buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote,
|
||||
onNext: (buffer) {
|
||||
debugPrint("searching next for $buffer");
|
||||
assert(buffer.length == 1);
|
||||
final selectedEntries = getSelectedItems(isLocal);
|
||||
assert(selectedEntries.length <= 1);
|
||||
var skipCount = 0;
|
||||
if (selectedEntries.items.isNotEmpty) {
|
||||
final index = entries.indexOf(selectedEntries.items.first);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
skipCount = index + 1;
|
||||
}
|
||||
var searchResult = entries
|
||||
.skip(skipCount)
|
||||
.where((element) => element.name.startsWith(buffer));
|
||||
if (searchResult.isEmpty) {
|
||||
// cannot find next, lets restart search from head
|
||||
searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
}
|
||||
if (searchResult.isEmpty) {
|
||||
setState(() {
|
||||
getSelectedItems(isLocal).clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
_jumpToEntry(
|
||||
isLocal, searchResult.first, scrollController, rowHeight, buffer);
|
||||
},
|
||||
onSearch: (buffer) {
|
||||
debugPrint("searching for $buffer");
|
||||
final selectedEntries = getSelectedItems(isLocal);
|
||||
final searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
selectedEntries.clear();
|
||||
if (searchResult.isEmpty) {
|
||||
setState(() {
|
||||
getSelectedItems(isLocal).clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
_jumpToEntry(
|
||||
isLocal, searchResult.first, scrollController, rowHeight, buffer);
|
||||
},
|
||||
child: ObxValue<RxString>(
|
||||
(searchText) {
|
||||
final filteredEntries = searchText.isNotEmpty
|
||||
? entries.where((element) {
|
||||
return element.name.contains(searchText.value);
|
||||
}).toList(growable: false)
|
||||
: entries;
|
||||
return DataTable(
|
||||
key: ValueKey(isLocal ? 0 : 1),
|
||||
showCheckboxColumn: false,
|
||||
dataRowHeight: rowHeight,
|
||||
headingRowHeight: 30,
|
||||
horizontalMargin: 8,
|
||||
columnSpacing: 8,
|
||||
showBottomBorder: true,
|
||||
sortColumnIndex: sortIndex,
|
||||
sortAscending: sortAscending,
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Name"),
|
||||
).marginSymmetric(horizontal: 4),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.name,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
translate("Modified"),
|
||||
),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.modified,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
DataColumn(
|
||||
label: Text(translate("Size")),
|
||||
onSort: (columnIndex, ascending) {
|
||||
model.changeSortStyle(SortBy.size,
|
||||
isLocal: isLocal, ascending: ascending);
|
||||
}),
|
||||
],
|
||||
rows: filteredEntries.map((entry) {
|
||||
final sizeStr =
|
||||
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
return DataRow(
|
||||
key: ValueKey(entry.name),
|
||||
onSelectChanged: (s) {
|
||||
_onSelectedChanged(getSelectedItems(isLocal),
|
||||
filteredEntries, entry, isLocal);
|
||||
},
|
||||
selected: getSelectedItems(isLocal).contains(entry),
|
||||
cells: [
|
||||
DataCell(
|
||||
Container(
|
||||
width: 200,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: entry.name,
|
||||
child: Row(children: [
|
||||
entry.isDrive
|
||||
? Image(
|
||||
image: iconHardDrive,
|
||||
fit: BoxFit.scaleDown,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withOpacity(0.7))
|
||||
.paddingAll(4)
|
||||
: Icon(
|
||||
entry.isFile
|
||||
? Icons.feed_outlined
|
||||
: Icons.folder,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withOpacity(0.7))
|
||||
.paddingAll(4)
|
||||
: Icon(
|
||||
entry.isFile
|
||||
? Icons.feed_outlined
|
||||
: Icons.folder,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withOpacity(0.7),
|
||||
).marginSymmetric(horizontal: 2),
|
||||
Expanded(
|
||||
child: Text(entry.name,
|
||||
overflow: TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
onTap: () {
|
||||
final items = getSelectedItems(isLocal);
|
||||
?.withOpacity(0.7),
|
||||
).marginSymmetric(horizontal: 2),
|
||||
Expanded(
|
||||
child: Text(entry.name,
|
||||
overflow: TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
onTap: () {
|
||||
final items = getSelectedItems(isLocal);
|
||||
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
openDirectory(entry.path, isLocal: isLocal);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
),
|
||||
DataCell(FittedBox(
|
||||
child: Tooltip(
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
openDirectory(entry.path, isLocal: isLocal);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
),
|
||||
DataCell(FittedBox(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: lastModifiedStr,
|
||||
child: Text(
|
||||
lastModifiedStr,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: MyTheme.darkGray),
|
||||
)))),
|
||||
DataCell(Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: lastModifiedStr,
|
||||
message: sizeStr,
|
||||
child: Text(
|
||||
lastModifiedStr,
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: MyTheme.darkGray),
|
||||
)))),
|
||||
DataCell(Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: sizeStr,
|
||||
child: Text(
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 10, color: MyTheme.darkGray),
|
||||
))),
|
||||
]);
|
||||
}).toList(growable: false),
|
||||
);
|
||||
},
|
||||
isLocal ? _searchTextLocal : _searchTextRemote,
|
||||
fontSize: 10, color: MyTheme.darkGray),
|
||||
))),
|
||||
]);
|
||||
}).toList(growable: false),
|
||||
);
|
||||
},
|
||||
isLocal ? _searchTextLocal : _searchTextRemote,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _jumpToEntry(bool isLocal, Entry entry,
|
||||
ScrollController scrollController, double rowHeight, String buffer) {
|
||||
final entries = model.getCurrentDir(isLocal).entries;
|
||||
final index = entries.indexOf(entry);
|
||||
if (index == -1) {
|
||||
debugPrint("entry is not valid: ${entry.path}");
|
||||
}
|
||||
final selectedEntries = getSelectedItems(isLocal);
|
||||
final searchResult =
|
||||
entries.where((element) => element.name.startsWith(buffer));
|
||||
selectedEntries.clear();
|
||||
if (searchResult.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final offset = min(
|
||||
max(scrollController.position.minScrollExtent,
|
||||
entries.indexOf(searchResult.first) * rowHeight),
|
||||
scrollController.position.maxScrollExtent);
|
||||
scrollController.jumpTo(offset);
|
||||
setState(() {
|
||||
selectedEntries.add(isLocal, searchResult.first);
|
||||
debugPrint("focused on ${searchResult.first.name}");
|
||||
});
|
||||
}
|
||||
|
||||
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
|
||||
Entry entry, bool isLocal) {
|
||||
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||
@@ -872,6 +982,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
},
|
||||
dismissOnClicked: true));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("buildBread fetchDirectory err=$e");
|
||||
} finally {
|
||||
if (!isLocal) {
|
||||
_ffi.dialogManager.dismissByTag(loadingTag);
|
||||
@@ -1015,4 +1127,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
model.sendFiles(items, isRemote: false);
|
||||
}
|
||||
|
||||
void refocusKeyboardListener(bool isLocal) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (isLocal) {
|
||||
_keyboardNodeLocal.requestFocus();
|
||||
} else {
|
||||
_keyboardNodeRemote.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,8 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
}
|
||||
|
||||
buildTunnel(BuildContext context) {
|
||||
text(String lable) => Expanded(
|
||||
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
|
||||
text(String label) => Expanded(
|
||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
@@ -241,8 +241,8 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
}
|
||||
|
||||
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
|
||||
text(String lable) => Expanded(
|
||||
child: Text(lable, style: const TextStyle(fontSize: 20))
|
||||
text(String label) => Expanded(
|
||||
child: Text(label, style: const TextStyle(fontSize: 20))
|
||||
.marginOnly(left: _kTextLeftMargin));
|
||||
|
||||
return Container(
|
||||
@@ -285,11 +285,11 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
}
|
||||
|
||||
buildRdp(BuildContext context) {
|
||||
text1(String lable) => Expanded(
|
||||
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
|
||||
text2(String lable) => Expanded(
|
||||
text1(String label) => Expanded(
|
||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||
text2(String label) => Expanded(
|
||||
child: Text(
|
||||
lable,
|
||||
label,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
).marginOnly(left: _kTextLeftMargin));
|
||||
return Theme(
|
||||
|
||||
@@ -2,9 +2,12 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_custom_cursor/cursor_manager.dart'
|
||||
as custom_cursor_manager;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
@@ -20,6 +23,7 @@ import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
import '../widgets/remote_menubar.dart';
|
||||
import '../widgets/kb_layout_type_chooser.dart';
|
||||
|
||||
bool _isCustomCursorInited = false;
|
||||
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||
@@ -46,17 +50,17 @@ class RemotePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
||||
Timer? _timer;
|
||||
String keyboardMode = "legacy";
|
||||
bool _isWindowBlur = false;
|
||||
final _cursorOverImage = false.obs;
|
||||
late RxBool _showRemoteCursor;
|
||||
late RxBool _zoomCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode();
|
||||
var _imageFocused = false;
|
||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||
|
||||
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
||||
|
||||
@@ -92,6 +96,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI();
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
});
|
||||
_ffi.start(widget.id);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
@@ -101,7 +109,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
_ffi.ffiModel.updateEventListener(widget.id);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
@@ -109,22 +116,59 @@ class _RemotePageState extends State<RemotePage>
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value =
|
||||
bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor');
|
||||
if (!_isCustomCursorInited) {
|
||||
customCursorController.registerNeedUpdateCursorCallback(
|
||||
(String? lastKey, String? currentKey) async {
|
||||
if (_firstEnterImage.value) {
|
||||
_firstEnterImage.value = false;
|
||||
return true;
|
||||
}
|
||||
return lastKey == null || lastKey != currentKey;
|
||||
});
|
||||
_isCustomCursorInited = true;
|
||||
DesktopMultiWindow.addListener(this);
|
||||
// if (!_isCustomCursorInited) {
|
||||
// customCursorController.registerNeedUpdateCursorCallback(
|
||||
// (String? lastKey, String? currentKey) async {
|
||||
// if (_firstEnterImage.value) {
|
||||
// _firstEnterImage.value = false;
|
||||
// return true;
|
||||
// }
|
||||
// return lastKey == null || lastKey != currentKey;
|
||||
// });
|
||||
// _isCustomCursorInited = true;
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
super.onWindowBlur();
|
||||
// On windows, we use `focus` way to handle keyboard better.
|
||||
// Now on Linux, there's some rdev issues which will break the input.
|
||||
// We disable the `focus` way for non-Windows temporarily.
|
||||
if (Platform.isWindows) {
|
||||
_isWindowBlur = true;
|
||||
// unfocus the primary-focus when the whole window is lost focus,
|
||||
// and let OS to handle events instead.
|
||||
_rawKeyFocusNode.unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
super.onWindowFocus();
|
||||
// See [onWindowBlur].
|
||||
if (Platform.isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
super.onWindowRestore();
|
||||
// On windows, we use `onWindowRestore` way to handle window restore from
|
||||
// a minimized state.
|
||||
if (Platform.isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint("REMOTE PAGE dispose ${widget.id}");
|
||||
// ensure we leave this session, this is a double check
|
||||
bind.sessionEnterOrLeave(id: widget.id, enter: false);
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.recordingModel.onClose();
|
||||
_rawKeyFocusNode.dispose();
|
||||
@@ -153,8 +197,23 @@ class _RemotePageState extends State<RemotePage>
|
||||
color: Colors.black,
|
||||
child: RawKeyFocusScope(
|
||||
focusNode: _rawKeyFocusNode,
|
||||
onFocusChange: (bool v) {
|
||||
_imageFocused = v;
|
||||
onFocusChange: (bool imageFocused) {
|
||||
debugPrint(
|
||||
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
|
||||
// See [onWindowBlur].
|
||||
if (Platform.isWindows) {
|
||||
if (_isWindowBlur) {
|
||||
imageFocused = false;
|
||||
Future.delayed(Duration.zero, () {
|
||||
_rawKeyFocusNode.unfocus();
|
||||
});
|
||||
}
|
||||
if (imageFocused) {
|
||||
_ffi.inputModel.enterOrLeave(true);
|
||||
} else {
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
inputModel: _ffi.inputModel,
|
||||
child: getBodyForDesktop(context)));
|
||||
@@ -181,9 +240,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void enterView(PointerEnterEvent evt) {
|
||||
if (!_imageFocused) {
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
}
|
||||
_cursorOverImage.value = true;
|
||||
_firstEnterImage.value = true;
|
||||
if (_onEnterOrLeaveImage4Menubar != null) {
|
||||
@@ -193,7 +249,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
//
|
||||
}
|
||||
}
|
||||
_ffi.inputModel.enterOrLeave(true);
|
||||
// See [onWindowBlur].
|
||||
if (!Platform.isWindows) {
|
||||
if (!_rawKeyFocusNode.hasFocus) {
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
}
|
||||
bind.sessionEnterOrLeave(id: widget.id, enter: true);
|
||||
}
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
@@ -206,7 +268,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
//
|
||||
}
|
||||
}
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
// See [onWindowBlur].
|
||||
if (!Platform.isWindows) {
|
||||
bind.sessionEnterOrLeave(id: widget.id, enter: false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
@@ -228,6 +293,21 @@ class _RemotePageState extends State<RemotePage>
|
||||
listenerBuilder: (child) => RawPointerMouseRegion(
|
||||
onEnter: enterView,
|
||||
onExit: leaveView,
|
||||
onPointerDown: (event) {
|
||||
// A double check for blur status.
|
||||
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
|
||||
// Sometimes the system does not send the necessary focus event to flutter. We should manually
|
||||
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
|
||||
// ensure the grab-key thread is running when our users are clicking the remote canvas.
|
||||
if (_isWindowBlur) {
|
||||
debugPrint(
|
||||
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!_rawKeyFocusNode.hasFocus) {
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
inputModel: _ffi.inputModel,
|
||||
child: child,
|
||||
),
|
||||
@@ -235,9 +315,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
}))
|
||||
];
|
||||
|
||||
if (!_ffi.canvasModel.cursorEmbeded) {
|
||||
paints.add(Obx(() => Visibility(
|
||||
visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
|
||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||
paints.add(Obx(() => Offstage(
|
||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
@@ -302,7 +382,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
mouseRegion({child}) => Obx(() => MouseRegion(
|
||||
cursor: cursorOverImage.isTrue
|
||||
? c.cursorEmbeded
|
||||
? c.cursorEmbedded
|
||||
? SystemMouseCursors.none
|
||||
: keyboardEnabled.isTrue
|
||||
? (() {
|
||||
@@ -322,35 +402,36 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child));
|
||||
|
||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = c.getDisplayWidth() * s;
|
||||
final imageHeight = c.getDisplayHeight() * s;
|
||||
final imageSize = Size(imageWidth, imageHeight);
|
||||
final imageWidget = CustomPaint(
|
||||
size: Size(imageWidth, imageHeight),
|
||||
size: imageSize,
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
);
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.hasClients
|
||||
? _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter)
|
||||
: 0.0;
|
||||
final percentY = _vertical.hasClients
|
||||
? _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter)
|
||||
: 0.0;
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: mouseRegion(
|
||||
child: _buildCrossScrollbar(context, _buildListener(imageWidget),
|
||||
Size(imageWidth, imageHeight))),
|
||||
);
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.hasClients
|
||||
? _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter)
|
||||
: 0.0;
|
||||
final percentY = _vertical.hasClients
|
||||
? _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter)
|
||||
: 0.0;
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: mouseRegion(
|
||||
child: Obx(() => _buildCrossScrollbarFromLayout(
|
||||
context, _buildListener(imageWidget), c.size, imageSize)),
|
||||
));
|
||||
} else {
|
||||
final imageWidget = CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
@@ -366,15 +447,23 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
return MouseCursor.defer;
|
||||
} else {
|
||||
final key = cache.updateGetKey(scale, zoomCursor.value);
|
||||
cursor.addKey(key);
|
||||
return FlutterCustomMemoryImageCursor(
|
||||
pixbuf: cache.data,
|
||||
key: key,
|
||||
hotx: cache.hotx,
|
||||
hoty: cache.hoty,
|
||||
imageWidth: (cache.width * cache.scale).toInt(),
|
||||
imageHeight: (cache.height * cache.scale).toInt(),
|
||||
);
|
||||
if (!cursor.cachedKeys.contains(key)) {
|
||||
debugPrint("Register custom cursor with key $key");
|
||||
// [Safety]
|
||||
// It's ok to call async registerCursor in current synchronous context,
|
||||
// because activating the cursor is also an async call and will always
|
||||
// be executed after this.
|
||||
custom_cursor_manager.CursorManager.instance
|
||||
.registerCursor(custom_cursor_manager.CursorData()
|
||||
..buffer = cache.data!
|
||||
..height = (cache.height * cache.scale).toInt()
|
||||
..width = (cache.width * cache.scale).toInt()
|
||||
..hotX = cache.hotx
|
||||
..hotY = cache.hoty
|
||||
..name = key);
|
||||
cursor.addKey(key);
|
||||
}
|
||||
return FlutterCustomMemoryImageCursor(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,24 +566,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) {
|
||||
var layoutSize = MediaQuery.of(context).size;
|
||||
// If minimized, w or h may be negative here.
|
||||
final w = layoutSize.width - kWindowBorderWidth * 2;
|
||||
final h =
|
||||
layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight;
|
||||
layoutSize = Size(
|
||||
w < 0 ? 0 : w,
|
||||
h < 0 ? 0 : h,
|
||||
);
|
||||
bool overflow =
|
||||
layoutSize.width < size.width || layoutSize.height < size.height;
|
||||
return overflow
|
||||
? Obx(() =>
|
||||
_buildCrossScrollbarFromLayout(context, child, layoutSize, size))
|
||||
: _buildCrossScrollbarFromLayout(context, child, layoutSize, size);
|
||||
}
|
||||
|
||||
Widget _buildListener(Widget child) {
|
||||
if (listenerBuilder != null) {
|
||||
return listenerBuilder!(child);
|
||||
@@ -529,7 +600,8 @@ class CursorPaint extends StatelessWidget {
|
||||
|
||||
double cx = c.x;
|
||||
double cy = c.y;
|
||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
|
||||
c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final d = c.parent.target!.ffiModel.display;
|
||||
final imageWidth = d.width * c.scale;
|
||||
final imageHeight = d.height * c.scale;
|
||||
@@ -538,7 +610,7 @@ class CursorPaint extends StatelessWidget {
|
||||
}
|
||||
|
||||
double x = (m.x - hotx) * c.scale + cx;
|
||||
double y = (m.y - hoty) * c.scale + cx;
|
||||
double y = (m.y - hoty) * c.scale + cy;
|
||||
double scale = 1.0;
|
||||
if (zoomCursor.isTrue) {
|
||||
x = m.x - hotx + cx / c.scale;
|
||||
|
||||
@@ -257,7 +257,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
),
|
||||
]);
|
||||
|
||||
if (!ffi.canvasModel.cursorEmbeded) {
|
||||
if (!ffi.canvasModel.cursorEmbedded) {
|
||||
menu.add(MenuEntryDivider<String>());
|
||||
menu.add(() {
|
||||
final state = ShowRemoteCursorState.find(key);
|
||||
@@ -308,7 +308,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
'${translate("Insert")} Ctrl + Alt + Del',
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// multi-tab desktop remote screen
|
||||
class DesktopRemoteScreen extends StatelessWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key);
|
||||
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
|
||||
if (!bind.mainStartGrabKeyboard()) {
|
||||
stateGlobal.grabKeyboard = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
225
flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
Normal file
225
flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
typedef KBChosenCallback = Future<bool> Function(String);
|
||||
|
||||
const double _kImageMarginVertical = 6.0;
|
||||
const double _kImageMarginHorizontal = 10.0;
|
||||
const double _kImageBoarderWidth = 4.0;
|
||||
const double _kImagePaddingWidth = 4.0;
|
||||
const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2);
|
||||
const double _kBorderRadius = 6.0;
|
||||
const String _kKBLayoutTypeISO = 'ISO';
|
||||
const String _kKBLayoutTypeNotISO = 'Not ISO';
|
||||
|
||||
const _kKBLayoutImageMap = {
|
||||
_kKBLayoutTypeISO: 'kb_layout_iso',
|
||||
_kKBLayoutTypeNotISO: 'kb_layout_not_iso',
|
||||
};
|
||||
|
||||
class _KBImage extends StatelessWidget {
|
||||
final String kbLayoutType;
|
||||
final double imageWidth;
|
||||
final RxString chosenType;
|
||||
const _KBImage({
|
||||
Key? key,
|
||||
required this.kbLayoutType,
|
||||
required this.imageWidth,
|
||||
required this.chosenType,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_kBorderRadius),
|
||||
border: Border.all(
|
||||
color: chosenType.value == kbLayoutType
|
||||
? _kImageBorderColor
|
||||
: Colors.transparent,
|
||||
width: _kImageBoarderWidth,
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: _kImageMarginHorizontal,
|
||||
vertical: _kImageMarginVertical,
|
||||
),
|
||||
padding: EdgeInsets.all(_kImagePaddingWidth),
|
||||
child: SvgPicture.asset(
|
||||
'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg',
|
||||
width: imageWidth -
|
||||
_kImageMarginHorizontal * 2 -
|
||||
_kImagePaddingWidth * 2 -
|
||||
_kImageBoarderWidth * 2,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _KBChooser extends StatelessWidget {
|
||||
final String kbLayoutType;
|
||||
final double imageWidth;
|
||||
final RxString chosenType;
|
||||
final KBChosenCallback cb;
|
||||
const _KBChooser({
|
||||
Key? key,
|
||||
required this.kbLayoutType,
|
||||
required this.imageWidth,
|
||||
required this.chosenType,
|
||||
required this.cb,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
onChanged(String? v) async {
|
||||
if (v != null) {
|
||||
if (await cb(v)) {
|
||||
chosenType.value = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onChanged(kbLayoutType);
|
||||
},
|
||||
child: _KBImage(
|
||||
kbLayoutType: kbLayoutType,
|
||||
imageWidth: imageWidth,
|
||||
chosenType: chosenType,
|
||||
),
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||
),
|
||||
TextButton(
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(() => Radio(
|
||||
splashRadius: 0,
|
||||
value: kbLayoutType,
|
||||
groupValue: chosenType.value,
|
||||
onChanged: onChanged,
|
||||
)),
|
||||
Text(kbLayoutType),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
onChanged(kbLayoutType);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KBLayoutTypeChooser extends StatelessWidget {
|
||||
final RxString chosenType;
|
||||
final double width;
|
||||
final double height;
|
||||
final double dividerWidth;
|
||||
final KBChosenCallback cb;
|
||||
KBLayoutTypeChooser({
|
||||
Key? key,
|
||||
required this.chosenType,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.dividerWidth,
|
||||
required this.cb,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageWidth = width / 2 - dividerWidth;
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Center(
|
||||
child: Row(
|
||||
children: [
|
||||
_KBChooser(
|
||||
kbLayoutType: _kKBLayoutTypeISO,
|
||||
imageWidth: imageWidth,
|
||||
chosenType: chosenType,
|
||||
cb: cb,
|
||||
),
|
||||
VerticalDivider(
|
||||
width: dividerWidth * 2,
|
||||
),
|
||||
_KBChooser(
|
||||
kbLayoutType: _kKBLayoutTypeNotISO,
|
||||
imageWidth: imageWidth,
|
||||
chosenType: chosenType,
|
||||
cb: cb,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RxString KBLayoutType = ''.obs;
|
||||
|
||||
String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||
String localPlatform = '';
|
||||
if (peerPlatform != kPeerPlatformMacOS) {
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
localPlatform = kPeerPlatformWindows;
|
||||
} else if (Platform.isLinux) {
|
||||
localPlatform = kPeerPlatformLinux;
|
||||
}
|
||||
// to-do: web desktop support ?
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
String peerPlatform,
|
||||
OverlayDialogManager dialogManager,
|
||||
) async {
|
||||
final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform);
|
||||
if (localPlatform == '') {
|
||||
return;
|
||||
}
|
||||
KBLayoutType.value = bind.getLocalKbLayoutType();
|
||||
if (KBLayoutType.value == _kKBLayoutTypeISO ||
|
||||
KBLayoutType.value == _kKBLayoutTypeNotISO) {
|
||||
return;
|
||||
}
|
||||
showKBLayoutTypeChooser(localPlatform, dialogManager);
|
||||
}
|
||||
|
||||
showKBLayoutTypeChooser(
|
||||
String localPlatform,
|
||||
OverlayDialogManager dialogManager,
|
||||
) {
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Text('${translate('Select local keyboard type')} ($localPlatform)'),
|
||||
content: KBLayoutTypeChooser(
|
||||
chosenType: KBLayoutType,
|
||||
width: 360,
|
||||
height: 200,
|
||||
dividerWidth: 4.0,
|
||||
cb: (String v) async {
|
||||
await bind.setLocalKbLayoutType(kbLayoutType: v);
|
||||
KBLayoutType.value = bind.getLocalKbLayoutType();
|
||||
return v == KBLayoutType.value;
|
||||
}),
|
||||
actions: [msgBoxButton(translate('Close'), close)],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
75
flutter/lib/desktop/widgets/list_search_action_listener.dart
Normal file
75
flutter/lib/desktop/widgets/list_search_action_listener.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListSearchActionListener extends StatelessWidget {
|
||||
final FocusNode node;
|
||||
final TimeoutStringBuffer buffer;
|
||||
final Widget child;
|
||||
final Function(String) onNext;
|
||||
final Function(String) onSearch;
|
||||
|
||||
const ListSearchActionListener(
|
||||
{super.key,
|
||||
required this.node,
|
||||
required this.buffer,
|
||||
required this.child,
|
||||
required this.onNext,
|
||||
required this.onSearch});
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeyboardListener(
|
||||
autofocus: true,
|
||||
onKeyEvent: (kv) {
|
||||
final ch = kv.character;
|
||||
if (ch == null) {
|
||||
return;
|
||||
}
|
||||
final action = buffer.input(ch);
|
||||
switch (action) {
|
||||
case ListSearchAction.search:
|
||||
onSearch(buffer.buffer);
|
||||
break;
|
||||
case ListSearchAction.next:
|
||||
onNext(buffer.buffer);
|
||||
break;
|
||||
}
|
||||
},
|
||||
focusNode: node,
|
||||
child: child);
|
||||
}
|
||||
}
|
||||
|
||||
enum ListSearchAction { search, next }
|
||||
|
||||
class TimeoutStringBuffer {
|
||||
var _buffer = "";
|
||||
late DateTime _duration;
|
||||
|
||||
static int timeoutMilliSec = 1500;
|
||||
|
||||
String get buffer => _buffer;
|
||||
|
||||
TimeoutStringBuffer() {
|
||||
_duration = DateTime.now();
|
||||
}
|
||||
|
||||
ListSearchAction input(String ch) {
|
||||
final curr = DateTime.now();
|
||||
try {
|
||||
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
|
||||
_buffer = ch;
|
||||
return ListSearchAction.search;
|
||||
} else {
|
||||
if (ch == _buffer) {
|
||||
return ListSearchAction.next;
|
||||
} else {
|
||||
_buffer += ch;
|
||||
return ListSearchAction.search;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_duration = curr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
|
||||
|
||||
class _IconOP extends StatelessWidget {
|
||||
final String icon;
|
||||
final double iconWidth;
|
||||
const _IconOP({Key? key, required this.icon, required this.iconWidth})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: SvgPicture.asset(
|
||||
'assets/$icon.svg',
|
||||
width: iconWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonOP extends StatelessWidget {
|
||||
final String op;
|
||||
final RxString curOP;
|
||||
final double iconWidth;
|
||||
final Color primaryColor;
|
||||
final double height;
|
||||
final Function() onTap;
|
||||
|
||||
const ButtonOP({
|
||||
Key? key,
|
||||
required this.op,
|
||||
required this.curOP,
|
||||
required this.iconWidth,
|
||||
required this.primaryColor,
|
||||
required this.height,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: height,
|
||||
padding: kMidButtonPadding,
|
||||
child: Obx(() => ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: curOP.value.isEmpty || curOP.value == op
|
||||
? primaryColor
|
||||
: Colors.grey,
|
||||
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
|
||||
onPressed:
|
||||
curOP.value.isEmpty || curOP.value == op ? onTap : null,
|
||||
child: Stack(children: [
|
||||
Center(child: Text('${translate("Continue with")} $op')),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
child: _IconOP(
|
||||
icon: op,
|
||||
iconWidth: iconWidth,
|
||||
)),
|
||||
),
|
||||
]),
|
||||
)),
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigOP {
|
||||
final String op;
|
||||
final double iconWidth;
|
||||
ConfigOP({required this.op, required this.iconWidth});
|
||||
}
|
||||
|
||||
class WidgetOP extends StatefulWidget {
|
||||
final ConfigOP config;
|
||||
final RxString curOP;
|
||||
final Function(String) cbLogin;
|
||||
const WidgetOP({
|
||||
Key? key,
|
||||
required this.config,
|
||||
required this.curOP,
|
||||
required this.cbLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _WidgetOPState();
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetOPState extends State<WidgetOP> {
|
||||
Timer? _updateTimer;
|
||||
String _stateMsg = '';
|
||||
String _FailedMsg = '';
|
||||
String _url = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_updateTimer?.cancel();
|
||||
}
|
||||
|
||||
_beginQueryState() {
|
||||
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
_updateState();
|
||||
});
|
||||
}
|
||||
|
||||
_updateState() {
|
||||
bind.mainAccountAuthResult().then((result) {
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final resultMap = jsonDecode(result);
|
||||
if (resultMap == null) {
|
||||
return;
|
||||
}
|
||||
final String stateMsg = resultMap['state_msg'];
|
||||
String failedMsg = resultMap['failed_msg'];
|
||||
final String? url = resultMap['url'];
|
||||
final authBody = resultMap['auth_body'];
|
||||
if (_stateMsg != stateMsg || _FailedMsg != failedMsg) {
|
||||
if (_url.isEmpty && url != null && url.isNotEmpty) {
|
||||
launchUrl(Uri.parse(url));
|
||||
_url = url;
|
||||
}
|
||||
if (authBody != null) {
|
||||
_updateTimer?.cancel();
|
||||
final String username = authBody['user']['name'];
|
||||
widget.curOP.value = '';
|
||||
widget.cbLogin(username);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_stateMsg = stateMsg;
|
||||
_FailedMsg = failedMsg;
|
||||
if (failedMsg.isNotEmpty) {
|
||||
widget.curOP.value = '';
|
||||
_updateTimer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_resetState() {
|
||||
_stateMsg = '';
|
||||
_FailedMsg = '';
|
||||
_url = '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ButtonOP(
|
||||
op: widget.config.op,
|
||||
curOP: widget.curOP,
|
||||
iconWidth: widget.config.iconWidth,
|
||||
primaryColor: str2color(widget.config.op, 0x7f),
|
||||
height: 36,
|
||||
onTap: () async {
|
||||
_resetState();
|
||||
widget.curOP.value = widget.config.op;
|
||||
await bind.mainAccountAuth(op: widget.config.op);
|
||||
_beginQueryState();
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
if (widget.curOP.isNotEmpty &&
|
||||
widget.curOP.value != widget.config.op) {
|
||||
_FailedMsg = '';
|
||||
}
|
||||
return Offstage(
|
||||
offstage:
|
||||
_FailedMsg.isEmpty && widget.curOP.value != widget.config.op,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_stateMsg,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
_FailedMsg,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: widget.curOP.value != widget.config.op,
|
||||
child: const SizedBox(
|
||||
height: 5.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: widget.curOP.value != widget.config.op,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 20),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
widget.curOP.value = '';
|
||||
_updateTimer?.cancel();
|
||||
_resetState();
|
||||
bind.mainAccountAuthCancel();
|
||||
},
|
||||
child: Text(
|
||||
translate('Cancel'),
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginWidgetOP extends StatelessWidget {
|
||||
final List<ConfigOP> ops;
|
||||
final RxString curOP;
|
||||
final Function(String) cbLogin;
|
||||
|
||||
LoginWidgetOP({
|
||||
Key? key,
|
||||
required this.ops,
|
||||
required this.curOP,
|
||||
required this.cbLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var children = ops
|
||||
.map((op) => [
|
||||
WidgetOP(
|
||||
config: op,
|
||||
curOP: curOP,
|
||||
cbLogin: cbLogin,
|
||||
),
|
||||
const Divider(
|
||||
indent: 5,
|
||||
endIndent: 5,
|
||||
)
|
||||
])
|
||||
.expand((i) => i)
|
||||
.toList();
|
||||
if (children.isNotEmpty) {
|
||||
children.removeLast();
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class LoginWidgetUserPass extends StatelessWidget {
|
||||
final String username;
|
||||
final String pass;
|
||||
final String usernameMsg;
|
||||
final String passMsg;
|
||||
final bool isInProgress;
|
||||
final RxString curOP;
|
||||
final Function(String, String) onLogin;
|
||||
const LoginWidgetUserPass({
|
||||
Key? key,
|
||||
required this.username,
|
||||
required this.pass,
|
||||
required this.usernameMsg,
|
||||
required this.passMsg,
|
||||
required this.isInProgress,
|
||||
required this.curOP,
|
||||
required this.onLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var userController = TextEditingController(text: username);
|
||||
var pwdController = TextEditingController(text: pass);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Container(
|
||||
padding: kMidButtonPadding,
|
||||
child: Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
'${translate("Username")}:',
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: usernameMsg.isNotEmpty ? usernameMsg : null),
|
||||
controller: userController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Container(
|
||||
padding: kMidButtonPadding,
|
||||
child: Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text('${translate("Password")}:')
|
||||
.marginOnly(bottom: 16.0)),
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||
controller: pwdController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator()),
|
||||
const SizedBox(
|
||||
height: 12.0,
|
||||
),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 38,
|
||||
padding: kMidButtonPadding,
|
||||
child: Obx(() => ElevatedButton(
|
||||
style: curOP.value.isEmpty || curOP.value == 'rustdesk'
|
||||
? null
|
||||
: ElevatedButton.styleFrom(
|
||||
primary: Colors.grey,
|
||||
),
|
||||
child: Text(
|
||||
translate('Login'),
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk'
|
||||
? () {
|
||||
onLogin(userController.text, pwdController.text);
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// common login dialog for desktop
|
||||
/// call this directly
|
||||
Future<bool> loginDialog() async {
|
||||
String username = '';
|
||||
var usernameMsg = '';
|
||||
String pass = '';
|
||||
var passMsg = '';
|
||||
var isInProgress = false;
|
||||
var completer = Completer<bool>();
|
||||
final RxString curOP = ''.obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
isInProgress = false;
|
||||
completer.complete(false);
|
||||
close();
|
||||
}
|
||||
|
||||
onLogin(String username0, String pass0) async {
|
||||
setState(() {
|
||||
usernameMsg = '';
|
||||
passMsg = '';
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
curOP.value = '';
|
||||
if (isInProgress) {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
curOP.value = 'rustdesk';
|
||||
username = username0;
|
||||
pass = pass0;
|
||||
if (username.isEmpty) {
|
||||
usernameMsg = translate('Username missed');
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (pass.isEmpty) {
|
||||
passMsg = translate('Password missed');
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(username, pass);
|
||||
if (resp.containsKey('error')) {
|
||||
passMsg = resp['error'];
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
|
||||
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
|
||||
debugPrint('$resp');
|
||||
completer.complete(true);
|
||||
} catch (err) {
|
||||
debugPrintStack(label: err.toString());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Login')),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
LoginWidgetUserPass(
|
||||
username: username,
|
||||
pass: pass,
|
||||
usernameMsg: usernameMsg,
|
||||
passMsg: passMsg,
|
||||
isInProgress: isInProgress,
|
||||
curOP: curOP,
|
||||
onLogin: onLogin,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
translate('or'),
|
||||
style: TextStyle(fontSize: 16),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
LoginWidgetOP(
|
||||
ops: [
|
||||
ConfigOP(op: 'Github', iconWidth: 20),
|
||||
ConfigOP(op: 'Google', iconWidth: 20),
|
||||
ConfigOP(op: 'Okta', iconWidth: 38),
|
||||
],
|
||||
curOP: curOP,
|
||||
cbLogin: (String username) {
|
||||
gFFI.userModel.userName.value = username;
|
||||
completer.complete(true);
|
||||
close();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [msgBoxButton(translate('Close'), cancel)],
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
@@ -118,6 +118,15 @@ abstract class MenuEntryBase<T> {
|
||||
this.enabled,
|
||||
});
|
||||
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
||||
|
||||
enabledStyle(BuildContext context) => TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
disabledStyle() => TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
}
|
||||
|
||||
class MenuEntryDivider<T> extends MenuEntryBase<T> {
|
||||
@@ -189,54 +198,76 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
|
||||
mod_menu.PopupMenuEntry<T> _buildMenuItem(
|
||||
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
|
||||
Widget getTextChild() {
|
||||
final enabledTextChild = Text(
|
||||
opt.text,
|
||||
style: enabledStyle(context),
|
||||
);
|
||||
final disabledTextChild = Text(
|
||||
opt.text,
|
||||
style: disabledStyle(),
|
||||
);
|
||||
if (opt.enabled == null) {
|
||||
return enabledTextChild;
|
||||
} else {
|
||||
return Obx(
|
||||
() => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild);
|
||||
}
|
||||
}
|
||||
|
||||
final child = Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints:
|
||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||
child: Row(
|
||||
children: [
|
||||
getTextChild(),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: MenuConfig.iconScale,
|
||||
child: Obx(() => opt.value == curOption.value
|
||||
? IconButton(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0),
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.check,
|
||||
color: (opt.enabled ?? true.obs).isTrue
|
||||
? conf.commonColor
|
||||
: Colors.grey,
|
||||
))
|
||||
: const SizedBox.shrink()),
|
||||
))),
|
||||
],
|
||||
),
|
||||
);
|
||||
onPressed() {
|
||||
if (opt.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
setOption(opt.value);
|
||||
}
|
||||
|
||||
return mod_menu.PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
height: conf.height,
|
||||
child: Container(
|
||||
width: conf.boxWidth,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: conf.height, maxHeight: conf.height),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: MenuConfig.iconScale,
|
||||
child: Obx(() => opt.value == curOption.value
|
||||
? IconButton(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
8.0, 0.0, 8.0, 0.0),
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.check,
|
||||
color: conf.commonColor,
|
||||
))
|
||||
: const SizedBox.shrink()),
|
||||
))),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (opt.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
setOption(opt.value);
|
||||
},
|
||||
)),
|
||||
width: conf.boxWidth,
|
||||
child: opt.enabled == null
|
||||
? TextButton(
|
||||
child: child,
|
||||
onPressed: onPressed,
|
||||
)
|
||||
: Obx(() => TextButton(
|
||||
child: child,
|
||||
onPressed: opt.enabled!.isTrue ? onPressed : null,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -567,12 +598,9 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Obx(() => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: super.enabled!.value
|
||||
? Theme.of(context).textTheme.titleLarge?.color
|
||||
: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
style: super.enabled!.value
|
||||
? enabledStyle(context)
|
||||
: disabledStyle(),
|
||||
)),
|
||||
Expanded(
|
||||
child: Align(
|
||||
@@ -605,14 +633,6 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
);
|
||||
|
||||
Widget _buildChild(BuildContext context, MenuConfig conf) {
|
||||
final enabledStyle = TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
const disabledStyle = TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
super.enabled ??= true.obs;
|
||||
return Obx(() => Container(
|
||||
width: conf.boxWidth,
|
||||
@@ -631,7 +651,7 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
constraints:
|
||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||
child: childBuilder(
|
||||
super.enabled!.value ? enabledStyle : disabledStyle),
|
||||
super.enabled!.value ? enabledStyle(context) : disabledStyle()),
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
import './popup_menu.dart';
|
||||
import './material_mod_popup_menu.dart' as mod_menu;
|
||||
import './kb_layout_type_chooser.dart';
|
||||
|
||||
class MenubarState {
|
||||
final kStoreKey = 'remoteMenubarState';
|
||||
@@ -171,6 +172,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// No need to use future builder here.
|
||||
_updateScreen();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Obx(() => show.value
|
||||
@@ -362,8 +365,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
RxInt display = CurrentDisplayState.find(widget.id);
|
||||
if (display.value != i) {
|
||||
bind.sessionSwitchDisplay(id: widget.id, value: i);
|
||||
pi.currentDisplay = i;
|
||||
display.value = i;
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -569,7 +570,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
),
|
||||
]);
|
||||
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
|
||||
final auditServer = bind.sessionGetAuditServerSync(id: widget.id);
|
||||
final auditServer =
|
||||
bind.sessionGetAuditServerSync(id: widget.id, typ: "conn");
|
||||
if (auditServer.isNotEmpty) {
|
||||
displayMenu.add(
|
||||
MenuEntryButton<String>(
|
||||
@@ -587,7 +589,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
displayMenu.add(MenuEntryDivider());
|
||||
if (perms['keyboard'] != false) {
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
'${translate("Insert")} Ctrl + Alt + Del',
|
||||
@@ -602,9 +604,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
}
|
||||
if (perms['restart'] != false &&
|
||||
(pi.platform == 'Linux' ||
|
||||
pi.platform == 'Windows' ||
|
||||
pi.platform == 'Mac OS')) {
|
||||
(pi.platform == kPeerPlatformLinux ||
|
||||
pi.platform == kPeerPlatformWindows ||
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Restart Remote Device'),
|
||||
@@ -631,7 +633,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
|
||||
if (pi.platform == 'Windows') {
|
||||
if (pi.platform == kPeerPlatformWindows) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||
translate(
|
||||
@@ -697,12 +699,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
if (_screen == null) {
|
||||
return false;
|
||||
}
|
||||
double scale = _screen!.scaleFactor;
|
||||
double selfWidth = _screen!.frame.width;
|
||||
double selfHeight = _screen!.frame.height;
|
||||
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
|
||||
double selfWidth = _screen!.visibleFrame.width;
|
||||
double selfHeight = _screen!.visibleFrame.height;
|
||||
if (isFullscreen) {
|
||||
selfWidth = _screen!.visibleFrame.width;
|
||||
selfHeight = _screen!.visibleFrame.height;
|
||||
selfWidth = _screen!.frame.width;
|
||||
selfHeight = _screen!.frame.height;
|
||||
}
|
||||
|
||||
final canvasModel = widget.ffi.canvasModel;
|
||||
@@ -827,7 +829,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
qualityInitValue = qualityMaxValue;
|
||||
}
|
||||
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
||||
final debouncerQuanlity = Debouncer<double>(
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setCustomValues(quality: v);
|
||||
@@ -843,7 +845,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
divisions: 90,
|
||||
onChanged: (double value) {
|
||||
qualitySliderValue.value = value;
|
||||
debouncerQuanlity.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
@@ -934,11 +936,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
text: translate('ScrollAuto'),
|
||||
value: kRemoteScrollStyleAuto,
|
||||
dismissOnClicked: true,
|
||||
enabled: widget.ffi.canvasModel.imageOverflow,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scrollbar'),
|
||||
value: kRemoteScrollStyleBar,
|
||||
dismissOnClicked: true,
|
||||
enabled: widget.ffi.canvasModel.imageOverflow,
|
||||
),
|
||||
],
|
||||
curOptionGetter: () async =>
|
||||
@@ -952,75 +956,77 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
displayMenu.insert(3, MenuEntryDivider<String>());
|
||||
}
|
||||
|
||||
if (_isWindowCanBeAdjusted(remoteCount)) {
|
||||
displayMenu.insert(
|
||||
0,
|
||||
MenuEntryDivider<String>(),
|
||||
);
|
||||
displayMenu.insert(
|
||||
0,
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
child: Text(
|
||||
translate('Adjust Window'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
() async {
|
||||
await _updateScreen();
|
||||
if (_screen != null) {
|
||||
_setFullscreen(false);
|
||||
double scale = _screen!.scaleFactor;
|
||||
final wndRect =
|
||||
await WindowController.fromWindowId(windowId).getFrame();
|
||||
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
|
||||
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
|
||||
// https://stackoverflow.com/a/7561083
|
||||
double magicWidth =
|
||||
wndRect.right - wndRect.left - mediaSize.width * scale;
|
||||
double magicHeight =
|
||||
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
||||
if (_isWindowCanBeAdjusted(remoteCount)) {
|
||||
displayMenu.insert(
|
||||
0,
|
||||
MenuEntryDivider<String>(),
|
||||
);
|
||||
displayMenu.insert(
|
||||
0,
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
child: Text(
|
||||
translate('Adjust Window'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
() async {
|
||||
await _updateScreen();
|
||||
if (_screen != null) {
|
||||
_setFullscreen(false);
|
||||
double scale = _screen!.scaleFactor;
|
||||
final wndRect =
|
||||
await WindowController.fromWindowId(windowId).getFrame();
|
||||
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
|
||||
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
|
||||
// https://stackoverflow.com/a/7561083
|
||||
double magicWidth =
|
||||
wndRect.right - wndRect.left - mediaSize.width * scale;
|
||||
double magicHeight =
|
||||
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
||||
|
||||
final canvasModel = widget.ffi.canvasModel;
|
||||
final width = (canvasModel.getDisplayWidth() +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
scale +
|
||||
magicWidth;
|
||||
final height = (canvasModel.getDisplayHeight() +
|
||||
canvasModel.tabBarHeight +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
scale +
|
||||
magicHeight;
|
||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||
double top = wndRect.top + (wndRect.height - height) / 2;
|
||||
final canvasModel = widget.ffi.canvasModel;
|
||||
final width =
|
||||
(canvasModel.getDisplayWidth() * canvasModel.scale +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
scale +
|
||||
magicWidth;
|
||||
final height =
|
||||
(canvasModel.getDisplayHeight() * canvasModel.scale +
|
||||
canvasModel.tabBarHeight +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
scale +
|
||||
magicHeight;
|
||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||
double top = wndRect.top + (wndRect.height - height) / 2;
|
||||
|
||||
Rect frameRect = _screen!.frame;
|
||||
if (!isFullscreen) {
|
||||
frameRect = _screen!.visibleFrame;
|
||||
Rect frameRect = _screen!.frame;
|
||||
if (!isFullscreen) {
|
||||
frameRect = _screen!.visibleFrame;
|
||||
}
|
||||
if (left < frameRect.left) {
|
||||
left = frameRect.left;
|
||||
}
|
||||
if (top < frameRect.top) {
|
||||
top = frameRect.top;
|
||||
}
|
||||
if ((left + width) > frameRect.right) {
|
||||
left = frameRect.right - width;
|
||||
}
|
||||
if ((top + height) > frameRect.bottom) {
|
||||
top = frameRect.bottom - height;
|
||||
}
|
||||
await WindowController.fromWindowId(windowId)
|
||||
.setFrame(Rect.fromLTWH(left, top, width, height));
|
||||
}
|
||||
if (left < frameRect.left) {
|
||||
left = frameRect.left;
|
||||
}
|
||||
if (top < frameRect.top) {
|
||||
top = frameRect.top;
|
||||
}
|
||||
if ((left + width) > frameRect.right) {
|
||||
left = frameRect.right - width;
|
||||
}
|
||||
if ((top + height) > frameRect.bottom) {
|
||||
top = frameRect.bottom - height;
|
||||
}
|
||||
await WindowController.fromWindowId(windowId)
|
||||
.setFrame(Rect.fromLTWH(left, top, width, height));
|
||||
}
|
||||
}();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
);
|
||||
}();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show Codec Preference
|
||||
@@ -1032,7 +1038,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} finally {}
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
|
||||
displayMenu.add(MenuEntryRadios<String>(
|
||||
text: translate('Codec Preference'),
|
||||
@@ -1082,7 +1090,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
|
||||
/// Show remote cursor
|
||||
if (!widget.ffi.canvasModel.cursorEmbeded) {
|
||||
if (!widget.ffi.canvasModel.cursorEmbedded) {
|
||||
displayMenu.add(() {
|
||||
final state = ShowRemoteCursorState.find(widget.id);
|
||||
return MenuEntrySwitch2<String>(
|
||||
@@ -1149,7 +1157,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
|
||||
if (Platform.isWindows &&
|
||||
pi.platform == 'Windows' &&
|
||||
pi.platform == kPeerPlatformWindows &&
|
||||
perms['file'] != false) {
|
||||
displayMenu.add(_createSwitchMenuEntry(
|
||||
'Allow file copy and paste', 'enable-file-transfer', padding, true));
|
||||
@@ -1182,7 +1190,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getKeyboardMenu() {
|
||||
final keyboardMenu = [
|
||||
final List<MenuEntryBase<String>> keyboardMenu = [
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () {
|
||||
@@ -1209,11 +1217,58 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
},
|
||||
)
|
||||
];
|
||||
|
||||
final localPlatform =
|
||||
getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
|
||||
if (localPlatform != '') {
|
||||
keyboardMenu.add(MenuEntryDivider());
|
||||
keyboardMenu.add(
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
height: _MenubarTheme.height,
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(() => RichText(
|
||||
text: TextSpan(
|
||||
text: '${translate('Local keyboard type')}: ',
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: KBLayoutType.value,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
showKBLayoutTypeChooser(
|
||||
localPlatform, widget.ffi.dialogManager);
|
||||
},
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
)),
|
||||
proc: () {},
|
||||
padding: EdgeInsets.zero,
|
||||
dismissOnClicked: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return keyboardMenu;
|
||||
}
|
||||
|
||||
@@ -1373,10 +1428,10 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_DraggableShowHide> createState() => __DraggableShowHideState();
|
||||
State<_DraggableShowHide> createState() => _DraggableShowHideState();
|
||||
}
|
||||
|
||||
class __DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
Offset position = Offset.zero;
|
||||
Size size = Size.zero;
|
||||
|
||||
@@ -1385,7 +1440,8 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
axis: Axis.horizontal,
|
||||
child: Icon(
|
||||
Icons.drag_indicator,
|
||||
size: 15,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
feedback: widget,
|
||||
onDragStarted: (() {
|
||||
@@ -1428,7 +1484,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
}),
|
||||
child: Obx((() => Icon(
|
||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||
size: 15,
|
||||
size: 20,
|
||||
))),
|
||||
),
|
||||
],
|
||||
@@ -1441,7 +1497,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
border: Border.all(color: MyTheme.border),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
height: 20,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget {
|
||||
return _buildBlock(
|
||||
child: Obx(() => PageView(
|
||||
controller: state.value.pageController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: state.value.tabs
|
||||
.map((tab) => tab.page)
|
||||
.toList(growable: false))));
|
||||
@@ -526,13 +527,19 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
void onWindowClose() async {
|
||||
// hide window on close
|
||||
if (widget.isMainWindow) {
|
||||
await rustDeskWinManager.unregisterActiveWindow(0);
|
||||
// `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
|
||||
// flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
|
||||
// e.g.: saving window position.
|
||||
await windowManager.hide();
|
||||
rustDeskWinManager.unregisterActiveWindow(0);
|
||||
} else {
|
||||
widget.onClose?.call();
|
||||
// it's safe to hide the subwindow
|
||||
await WindowController.fromWindowId(windowId!).hide();
|
||||
rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": windowId!});
|
||||
await Future.wait([
|
||||
rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": windowId!}),
|
||||
widget.onClose?.call() ?? Future.microtask(() => null)
|
||||
]);
|
||||
}
|
||||
super.onWindowClose();
|
||||
}
|
||||
@@ -899,7 +906,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
children: [
|
||||
_buildTabContent(),
|
||||
Obx((() => _CloseButton(
|
||||
visiable: hover.value && widget.closable,
|
||||
visible: hover.value && widget.closable,
|
||||
tabSelected: isSelected,
|
||||
onClose: () => widget.onClose(),
|
||||
)))
|
||||
@@ -931,13 +938,13 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
}
|
||||
|
||||
class _CloseButton extends StatelessWidget {
|
||||
final bool visiable;
|
||||
final bool visible;
|
||||
final bool tabSelected;
|
||||
final Function onClose;
|
||||
|
||||
const _CloseButton({
|
||||
Key? key,
|
||||
required this.visiable,
|
||||
required this.visible,
|
||||
required this.tabSelected,
|
||||
required this.onClose,
|
||||
}) : super(key: key);
|
||||
@@ -947,7 +954,7 @@ class _CloseButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
width: _kIconSize,
|
||||
child: Offstage(
|
||||
offstage: !visiable,
|
||||
offstage: !visible,
|
||||
child: InkWell(
|
||||
customBorder: const RoundedRectangleBorder(),
|
||||
onTap: () => onClose(),
|
||||
|
||||
@@ -61,6 +61,8 @@ Future<void> main(List<String> args) async {
|
||||
kAppTypeDesktopRemote,
|
||||
'RustDesk - Remote Desktop',
|
||||
);
|
||||
WindowController.fromWindowId(windowId!)
|
||||
.setTitle('RustDesk - Remote Desktop');
|
||||
break;
|
||||
case WindowType.FileTransfer:
|
||||
desktopType = DesktopType.fileTransfer;
|
||||
@@ -69,6 +71,8 @@ Future<void> main(List<String> args) async {
|
||||
kAppTypeDesktopFileTransfer,
|
||||
'RustDesk - File Transfer',
|
||||
);
|
||||
WindowController.fromWindowId(windowId!)
|
||||
.setTitle('RustDesk - File Transfer');
|
||||
break;
|
||||
case WindowType.PortForward:
|
||||
desktopType = DesktopType.portForward;
|
||||
@@ -117,6 +121,7 @@ void runMainApp(bool startService) async {
|
||||
// await windowManager.ensureInitialized();
|
||||
gFFI.serverModel.startService();
|
||||
}
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
// restore the location of the main window before window hide or show
|
||||
await restoreWindowPosition(WindowType.Main);
|
||||
@@ -134,6 +139,7 @@ void runMainApp(bool startService) async {
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
windowManager.setOpacity(1);
|
||||
});
|
||||
windowManager.setTitle("RustDesk");
|
||||
}
|
||||
|
||||
void runMobileApp() async {
|
||||
@@ -195,6 +201,9 @@ void runMultiWindow(
|
||||
// no such appType
|
||||
exit(0);
|
||||
}
|
||||
// show window from hidden status
|
||||
WindowController.fromWindowId(windowId!).show();
|
||||
WindowController.fromWindowId(windowId!).setTitle(title);
|
||||
}
|
||||
|
||||
void runConnectionManagerScreen(bool hide) async {
|
||||
|
||||
@@ -7,9 +7,8 @@ import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/address_book.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/peers_view.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
showLogin(gFFI.dialogManager);
|
||||
loginDialog();
|
||||
} else {
|
||||
gFFI.userModel.logOut();
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
),
|
||||
),
|
||||
];
|
||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
||||
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||
paints.add(CursorPaint());
|
||||
}
|
||||
return paints;
|
||||
@@ -527,7 +527,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
|
||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
||||
var paints = <Widget>[ImagePaint()];
|
||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
||||
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
if (keyboard || cursor) {
|
||||
@@ -574,14 +574,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Physical Keyboard Input Mode')),
|
||||
value: 'input-mode'));
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
if (pi.platform == kPeerPlatformLinux || 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' &&
|
||||
if (pi.platform == kPeerPlatformWindows &&
|
||||
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
||||
true) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
@@ -591,9 +591,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
}
|
||||
if (perms["restart"] != false &&
|
||||
(pi.platform == "Linux" ||
|
||||
pi.platform == "Windows" ||
|
||||
pi.platform == "Mac OS")) {
|
||||
(pi.platform == kPeerPlatformLinux ||
|
||||
pi.platform == kPeerPlatformWindows ||
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Restart Remote Device')), value: 'restart'));
|
||||
}
|
||||
@@ -696,7 +696,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
void setMode(String? v) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "keyboard-mode", value: v ?? "");
|
||||
id: widget.id, name: "keyboard-mode", value: v ?? "");
|
||||
setState(() => current = v ?? '');
|
||||
Future.delayed(Duration(milliseconds: 300), close);
|
||||
}
|
||||
@@ -740,7 +740,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
|
||||
final pi = gFFI.ffiModel.pi;
|
||||
final isMac = pi.platform == "Mac OS";
|
||||
final isMac = pi.platform == kPeerPlatformMacOS;
|
||||
final modifiers = <Widget>[
|
||||
wrap('Ctrl ', () {
|
||||
setState(() => inputModel.ctrl = !inputModel.ctrl);
|
||||
@@ -978,7 +978,9 @@ void showOptions(
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} finally {}
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
}
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
@@ -993,7 +995,7 @@ void showOptions(
|
||||
}
|
||||
more.add(getToggle(
|
||||
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
||||
if (pi.platform == 'Windows') {
|
||||
if (pi.platform == kPeerPlatformWindows) {
|
||||
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
|
||||
}
|
||||
}
|
||||
@@ -1056,7 +1058,7 @@ void showOptions(
|
||||
final toggles = [
|
||||
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
|
||||
];
|
||||
if (!gFFI.canvasModel.cursorEmbeded) {
|
||||
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||
toggles.insert(0,
|
||||
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
@override
|
||||
_ScanPageState createState() => _ScanPageState();
|
||||
State<ScanPage> createState() => _ScanPageState();
|
||||
}
|
||||
|
||||
class _ScanPageState extends State<ScanPage> {
|
||||
@@ -42,9 +41,9 @@ class _ScanPageState extends State<ScanPage> {
|
||||
icon: Icon(Icons.image_search),
|
||||
iconSize: 32.0,
|
||||
onPressed: () async {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? file =
|
||||
await _picker.pickImage(source: ImageSource.gallery);
|
||||
await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
var image = img.decodeNamedImage(
|
||||
File(file.path).readAsBytesSync(), file.path)!;
|
||||
@@ -139,155 +138,12 @@ class _ScanPageState extends State<ScanPage> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, dynamic> values = json.decode(data.substring(7));
|
||||
var host = values['host'] != null ? values['host'] as String : '';
|
||||
var key = values['key'] != null ? values['key'] as String : '';
|
||||
var api = values['api'] != null ? values['api'] as String : '';
|
||||
final sc = ServerConfig.decode(data.substring(7));
|
||||
Timer(Duration(milliseconds: 60), () {
|
||||
showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager);
|
||||
showServerSettingsWithValue(sc, gFFI.dialogManager);
|
||||
});
|
||||
} catch (e) {
|
||||
showToast('Invalid QR code');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(String id, String relay, String key,
|
||||
String api, OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
String id0 = oldOptions['custom-rendezvous-server'] ?? "";
|
||||
String relay0 = oldOptions['relay-server'] ?? "";
|
||||
String api0 = oldOptions['api-server'] ?? "";
|
||||
String key0 = oldOptions['key'] ?? "";
|
||||
var isInProgress = false;
|
||||
final idController = TextEditingController(text: id);
|
||||
final relayController = TextEditingController(text: relay);
|
||||
final apiController = TextEditingController(text: api);
|
||||
|
||||
String? idServerMsg;
|
||||
String? relayServerMsg;
|
||||
String? apiServerMsg;
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
Future<bool> validate() async {
|
||||
if (idController.text != id) {
|
||||
final res = await validateAsync(idController.text);
|
||||
setState(() => idServerMsg = res);
|
||||
if (idServerMsg != null) return false;
|
||||
id = idController.text;
|
||||
}
|
||||
if (relayController.text != relay) {
|
||||
relayServerMsg = await validateAsync(relayController.text);
|
||||
if (relayServerMsg != null) return false;
|
||||
relay = relayController.text;
|
||||
}
|
||||
if (apiController.text != relay) {
|
||||
apiServerMsg = await validateAsync(apiController.text);
|
||||
if (apiServerMsg != null) return false;
|
||||
api = apiController.text;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
content: Form(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: idController,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('ID Server'),
|
||||
errorText: idServerMsg),
|
||||
)
|
||||
] +
|
||||
(isAndroid
|
||||
? [
|
||||
TextFormField(
|
||||
controller: relayController,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg),
|
||||
)
|
||||
]
|
||||
: []) +
|
||||
[
|
||||
TextFormField(
|
||||
controller: apiController,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('API Server'),
|
||||
),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (v) {
|
||||
if (v != null && v.length > 0) {
|
||||
if (!(v.startsWith('http://') ||
|
||||
v.startsWith("https://"))) {
|
||||
return translate("invalid_http");
|
||||
}
|
||||
}
|
||||
return apiServerMsg;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: key,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Key',
|
||||
),
|
||||
onChanged: (String? value) {
|
||||
if (value != null) key = value.trim();
|
||||
},
|
||||
),
|
||||
Offstage(
|
||||
offstage: !isInProgress,
|
||||
child: LinearProgressIndicator())
|
||||
])),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
idServerMsg = null;
|
||||
relayServerMsg = null;
|
||||
apiServerMsg = null;
|
||||
isInProgress = true;
|
||||
});
|
||||
if (await validate()) {
|
||||
if (id != id0) {
|
||||
bind.mainSetOption(key: "custom-rendezvous-server", value: id);
|
||||
}
|
||||
if (relay != relay0) {
|
||||
bind.mainSetOption(key: "relay-server", value: relay);
|
||||
}
|
||||
if (key != key0) bind.mainSetOption(key: "key", value: key);
|
||||
if (api != api0) {
|
||||
bind.mainSetOption(key: "api-server", value: api);
|
||||
}
|
||||
close();
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final res = await bind.mainTestIfValidServer(server: value);
|
||||
return res.isEmpty ? null : res;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
}
|
||||
|
||||
class _ServerPageState extends State<ServerPage> {
|
||||
Timer? _updateTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
|
||||
await gFFI.serverModel.fetchID();
|
||||
});
|
||||
gFFI.serverModel.checkAndroidPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_updateTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkService();
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
@@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
leading: Icon(Icons.person),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
showLogin(gFFI.dialogManager);
|
||||
loginDialog();
|
||||
} else {
|
||||
gFFI.userModel.logOut();
|
||||
}
|
||||
@@ -391,17 +392,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
||||
String id = options['custom-rendezvous-server'] ?? "";
|
||||
String relay = options['relay-server'] ?? "";
|
||||
String api = options['api-server'] ?? "";
|
||||
String key = options['key'] ?? "";
|
||||
showServerSettingsWithValue(id, relay, key, api, dialogManager);
|
||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||
}
|
||||
|
||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
try {
|
||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||
var lang = await bind.mainGetLocalOption(key: "lang");
|
||||
var lang = bind.mainGetLocalOption(key: "lang");
|
||||
dialogManager.show((setState, close) {
|
||||
setLang(v) {
|
||||
if (lang != v) {
|
||||
@@ -486,78 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}
|
||||
|
||||
void showLogin(OverlayDialogManager dialogManager) {
|
||||
final passwordController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
var loading = false;
|
||||
var error = '';
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Login')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextField(
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Username'),
|
||||
),
|
||||
controller: nameController,
|
||||
),
|
||||
PasswordWidget(controller: passwordController, autoFocus: false),
|
||||
]),
|
||||
actions: (loading
|
||||
? <Widget>[CircularProgressIndicator()]
|
||||
: (error != ""
|
||||
? <Widget>[
|
||||
Text(translate(error),
|
||||
style: TextStyle(color: Colors.red))
|
||||
]
|
||||
: <Widget>[])) +
|
||||
<Widget>[
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: loading
|
||||
? null
|
||||
: () {
|
||||
close();
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: loading
|
||||
? null
|
||||
: () async {
|
||||
final name = nameController.text.trim();
|
||||
final pass = passwordController.text.trim();
|
||||
if (name != "" && pass != "") {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
final resp = await gFFI.userModel.login(name, pass);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
if (resp.containsKey('error')) {
|
||||
error = resp['error'];
|
||||
return;
|
||||
}
|
||||
gFFI.abModel.pullAb();
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class ScanButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -236,6 +237,145 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) {
|
||||
]));
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
final oldCfg = ServerConfig.fromOptions(oldOptions);
|
||||
|
||||
var isInProgress = false;
|
||||
final idCtrl = TextEditingController(text: serverConfig.idServer);
|
||||
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
|
||||
final apiCtrl = TextEditingController(text: serverConfig.apiServer);
|
||||
final keyCtrl = TextEditingController(text: serverConfig.key);
|
||||
|
||||
String? idServerMsg;
|
||||
String? relayServerMsg;
|
||||
String? apiServerMsg;
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
Future<bool> validate() async {
|
||||
if (idCtrl.text != oldCfg.idServer) {
|
||||
final res = await validateAsync(idCtrl.text);
|
||||
setState(() => idServerMsg = res);
|
||||
if (idServerMsg != null) return false;
|
||||
}
|
||||
if (relayCtrl.text != oldCfg.relayServer) {
|
||||
relayServerMsg = await validateAsync(relayCtrl.text);
|
||||
if (relayServerMsg != null) return false;
|
||||
}
|
||||
if (apiCtrl.text != oldCfg.apiServer) {
|
||||
if (apiServerMsg != null) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
content: Form(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: idCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('ID Server'),
|
||||
errorText: idServerMsg),
|
||||
)
|
||||
] +
|
||||
(isAndroid
|
||||
? [
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg),
|
||||
)
|
||||
]
|
||||
: []) +
|
||||
[
|
||||
TextFormField(
|
||||
controller: apiCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('API Server'),
|
||||
),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (v) {
|
||||
if (v != null && v.isNotEmpty) {
|
||||
if (!(v.startsWith('http://') ||
|
||||
v.startsWith("https://"))) {
|
||||
return translate("invalid_http");
|
||||
}
|
||||
}
|
||||
return apiServerMsg;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: keyCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Key',
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !isInProgress,
|
||||
child: LinearProgressIndicator())
|
||||
])),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
idServerMsg = null;
|
||||
relayServerMsg = null;
|
||||
apiServerMsg = null;
|
||||
isInProgress = true;
|
||||
});
|
||||
if (await validate()) {
|
||||
if (idCtrl.text != oldCfg.idServer) {
|
||||
if (oldCfg.idServer.isNotEmpty) {
|
||||
await gFFI.userModel.logOut();
|
||||
}
|
||||
bind.mainSetOption(
|
||||
key: "custom-rendezvous-server", value: idCtrl.text);
|
||||
}
|
||||
if (relayCtrl.text != oldCfg.relayServer) {
|
||||
bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
|
||||
}
|
||||
if (keyCtrl.text != oldCfg.key) {
|
||||
bind.mainSetOption(key: "key", value: keyCtrl.text);
|
||||
}
|
||||
if (apiCtrl.text != oldCfg.apiServer) {
|
||||
bind.mainSetOption(key: "api-server", value: apiCtrl.text);
|
||||
}
|
||||
close();
|
||||
showToast(translate('Successful'));
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final res = await bind.mainTestIfValidServer(server: value);
|
||||
return res.isEmpty ? null : res;
|
||||
}
|
||||
|
||||
class PasswordWidget extends StatefulWidget {
|
||||
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
|
||||
: super(key: key);
|
||||
@@ -285,7 +425,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
),
|
||||
onPressed: () {
|
||||
// Update the state i.e. toogle the state of passwordVisible variable
|
||||
// Update the state i.e. toggle the state of passwordVisible variable
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
|
||||
@@ -21,16 +21,13 @@ class AbModel {
|
||||
|
||||
AbModel(this.parent);
|
||||
|
||||
FFI? get _ffi => parent.target;
|
||||
|
||||
Future<dynamic> pullAb() async {
|
||||
if (_ffi!.userModel.userName.isEmpty) return;
|
||||
if (gFFI.userModel.userName.isEmpty) return;
|
||||
abLoading.value = true;
|
||||
abError.value = "";
|
||||
final api = "${await bind.mainGetApiServer()}/api/ab/get";
|
||||
try {
|
||||
final resp =
|
||||
await http.post(Uri.parse(api), headers: await getHttpHeaders());
|
||||
final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
|
||||
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
||||
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||
if (json.containsKey('error')) {
|
||||
@@ -63,7 +60,8 @@ class AbModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
Future<void> reset() async {
|
||||
await bind.mainSetLocalOption(key: "selected-tags", value: '');
|
||||
tags.clear();
|
||||
peers.clear();
|
||||
}
|
||||
@@ -103,7 +101,7 @@ class AbModel {
|
||||
Future<void> pushAb() async {
|
||||
abLoading.value = true;
|
||||
final api = "${await bind.mainGetApiServer()}/api/ab";
|
||||
var authHeaders = await getHttpHeaders();
|
||||
var authHeaders = getHttpHeaders();
|
||||
authHeaders['Content-Type'] = "application/json";
|
||||
final peersJsonData = peers.map((e) => e.toJson()).toList();
|
||||
final body = jsonEncode({
|
||||
@@ -188,9 +186,4 @@ class AbModel {
|
||||
await pushAb();
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
peers.clear();
|
||||
tags.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
@@ -213,7 +214,6 @@ class FileModel extends ChangeNotifier {
|
||||
}
|
||||
|
||||
receiveFileDir(Map<String, dynamic> evt) {
|
||||
debugPrint("recv file dir:$evt");
|
||||
if (evt['is_local'] == "false") {
|
||||
// init remote home, the connection will automatic read remote home when established,
|
||||
try {
|
||||
@@ -237,7 +237,9 @@ class FileModel extends ChangeNotifier {
|
||||
debugPrint("init remote home:${fd.path}");
|
||||
_currentRemoteDir = fd;
|
||||
}
|
||||
} finally {}
|
||||
} catch (e) {
|
||||
debugPrint("receiveFileDir err=$e");
|
||||
}
|
||||
}
|
||||
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||
notifyListeners();
|
||||
@@ -346,7 +348,7 @@ class FileModel extends ChangeNotifier {
|
||||
id: parent.target?.id ?? "", name: "remote_show_hidden"))
|
||||
.isNotEmpty;
|
||||
_remoteOption.isWindows =
|
||||
parent.target?.ffiModel.pi.platform.toLowerCase() == "windows";
|
||||
parent.target?.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
|
||||
|
||||
140
flutter/lib/models/group_model.dart
Normal file
140
flutter/lib/models/group_model.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class GroupModel {
|
||||
final RxBool userLoading = false.obs;
|
||||
final RxString userLoadError = "".obs;
|
||||
final RxBool peerLoading = false.obs; //to-do: not used
|
||||
final RxString peerLoadError = "".obs;
|
||||
final RxList<UserPayload> users = RxList.empty(growable: true);
|
||||
final RxList<PeerPayload> peerPayloads = RxList.empty(growable: true);
|
||||
final RxList<Peer> peersShow = RxList.empty(growable: true);
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
GroupModel(this.parent);
|
||||
|
||||
Future<void> reset() async {
|
||||
userLoading.value = false;
|
||||
userLoadError.value = "";
|
||||
peerLoading.value = false;
|
||||
peerLoadError.value = "";
|
||||
users.clear();
|
||||
peerPayloads.clear();
|
||||
peersShow.clear();
|
||||
}
|
||||
|
||||
Future<void> pull() async {
|
||||
await reset();
|
||||
if (gFFI.userModel.userName.isEmpty ||
|
||||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
|
||||
statePeerTab.check();
|
||||
return;
|
||||
}
|
||||
userLoading.value = true;
|
||||
userLoadError.value = "";
|
||||
final api = "${await bind.mainGetApiServer()}/api/users";
|
||||
try {
|
||||
var uri0 = Uri.parse(api);
|
||||
final pageSize = 20;
|
||||
var total = 0;
|
||||
int current = 0;
|
||||
do {
|
||||
current += 1;
|
||||
var uri = Uri(
|
||||
scheme: uri0.scheme,
|
||||
host: uri0.host,
|
||||
path: uri0.path,
|
||||
port: uri0.port,
|
||||
queryParameters: {
|
||||
'current': current.toString(),
|
||||
'pageSize': pageSize.toString(),
|
||||
if (gFFI.userModel.isAdmin.isFalse)
|
||||
'grp': gFFI.userModel.groupName.value,
|
||||
});
|
||||
final resp = await http.get(uri, headers: getHttpHeaders());
|
||||
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
||||
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||
if (json.containsKey('error')) {
|
||||
throw json['error'];
|
||||
} else {
|
||||
if (total == 0) total = json['total'];
|
||||
if (json.containsKey('data')) {
|
||||
final data = json['data'];
|
||||
if (data is List) {
|
||||
for (final user in data) {
|
||||
users.add(UserPayload.fromJson(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (current * pageSize < total);
|
||||
} catch (err) {
|
||||
debugPrint('$err');
|
||||
userLoadError.value = err.toString();
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
statePeerTab.check();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pullUserPeers(String username) async {
|
||||
peerPayloads.clear();
|
||||
peersShow.clear();
|
||||
peerLoading.value = true;
|
||||
peerLoadError.value = "";
|
||||
final api = "${await bind.mainGetApiServer()}/api/peers";
|
||||
try {
|
||||
var uri0 = Uri.parse(api);
|
||||
final pageSize = 20;
|
||||
var total = 0;
|
||||
int current = 0;
|
||||
do {
|
||||
current += 1;
|
||||
var uri = Uri(
|
||||
scheme: uri0.scheme,
|
||||
host: uri0.host,
|
||||
path: uri0.path,
|
||||
port: uri0.port,
|
||||
queryParameters: {
|
||||
'current': current.toString(),
|
||||
'pageSize': pageSize.toString(),
|
||||
'grp': gFFI.userModel.groupName.value,
|
||||
'target_user': username
|
||||
});
|
||||
final resp = await http.get(uri, headers: getHttpHeaders());
|
||||
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
||||
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||
if (json.containsKey('error')) {
|
||||
throw json['error'];
|
||||
} else {
|
||||
if (total == 0) total = json['total'];
|
||||
if (json.containsKey('data')) {
|
||||
final data = json['data'];
|
||||
if (data is List) {
|
||||
for (final p in data) {
|
||||
final peer = PeerPayload.fromJson(p);
|
||||
peerPayloads.add(peer);
|
||||
peersShow.add(PeerPayload.toPeer(peer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (current * pageSize < total);
|
||||
} catch (err) {
|
||||
debugPrint('$err');
|
||||
peerLoadError.value = err.toString();
|
||||
} finally {
|
||||
peerLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ import './state_model.dart';
|
||||
/// Mouse button enum.
|
||||
enum MouseButtons { left, right, wheel }
|
||||
|
||||
const _kMouseEventDown = 'mousedown';
|
||||
const _kMouseEventUp = 'mouseup';
|
||||
const _kMouseEventMove = 'mousemove';
|
||||
|
||||
extension ToString on MouseButtons {
|
||||
String get value {
|
||||
switch (this) {
|
||||
@@ -46,7 +50,7 @@ class InputModel {
|
||||
|
||||
// mouse
|
||||
final isPhysicalMouse = false.obs;
|
||||
int _lastMouseDownButtons = 0;
|
||||
int _lastButtons = 0;
|
||||
Offset lastMousePos = Offset.zero;
|
||||
|
||||
get id => parent.target?.id ?? "";
|
||||
@@ -115,8 +119,11 @@ class InputModel {
|
||||
keyCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataLinux) {
|
||||
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
|
||||
scanCode = newData.scanCode;
|
||||
keyCode = newData.keyCode;
|
||||
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
|
||||
// 1. scanCode means keycode
|
||||
// 2. keyCode means keysym
|
||||
scanCode = 0;
|
||||
keyCode = newData.scanCode;
|
||||
} else if (e.data is RawKeyEventDataAndroid) {
|
||||
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
|
||||
scanCode = newData.scanCode + 8;
|
||||
@@ -131,16 +138,33 @@ class InputModel {
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(e.character ?? "", keyCode, scanCode, down);
|
||||
inputRawKey(e.character ?? '', keyCode, scanCode, down);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int keyCode, int scanCode, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
id: id,
|
||||
name: name,
|
||||
keycode: keyCode,
|
||||
scancode: scanCode,
|
||||
lockModes: lockModes,
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
@@ -183,20 +207,42 @@ class InputModel {
|
||||
|
||||
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
out['type'] = type;
|
||||
out['x'] = evt.position.dx;
|
||||
out['y'] = evt.position.dy;
|
||||
if (alt) out['alt'] = 'true';
|
||||
if (shift) out['shift'] = 'true';
|
||||
if (ctrl) out['ctrl'] = 'true';
|
||||
if (command) 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;
|
||||
|
||||
// Check update event type and set buttons to be sent.
|
||||
int buttons = _lastButtons;
|
||||
if (type == _kMouseEventMove) {
|
||||
// flutter may emit move event if one button is pressed and another button
|
||||
// is pressing or releasing.
|
||||
if (evt.buttons != _lastButtons) {
|
||||
// For simplicity
|
||||
// Just consider 3 - 1 ((Left + Right buttons) - Left button)
|
||||
// Do not consider 2 - 1 (Right button - Left button)
|
||||
// or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons))
|
||||
// and so on
|
||||
buttons = evt.buttons - _lastButtons;
|
||||
if (buttons > 0) {
|
||||
type = _kMouseEventDown;
|
||||
} else {
|
||||
type = _kMouseEventUp;
|
||||
buttons = -buttons;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out['buttons'] = _lastMouseDownButtons;
|
||||
if (evt.buttons != 0) {
|
||||
buttons = evt.buttons;
|
||||
}
|
||||
}
|
||||
_lastButtons = evt.buttons;
|
||||
|
||||
out['buttons'] = buttons;
|
||||
out['type'] = type;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -260,7 +306,7 @@ class InputModel {
|
||||
isPhysicalMouse.value = true;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(getEvent(e, 'mousemove'));
|
||||
handleMouse(getEvent(e, _kMouseEventMove));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,21 +371,21 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(getEvent(e, 'mousedown'));
|
||||
handleMouse(getEvent(e, _kMouseEventDown));
|
||||
}
|
||||
}
|
||||
|
||||
void onPointUpImage(PointerUpEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(getEvent(e, 'mouseup'));
|
||||
handleMouse(getEvent(e, _kMouseEventUp));
|
||||
}
|
||||
}
|
||||
|
||||
void onPointMoveImage(PointerMoveEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(getEvent(e, 'mousemove'));
|
||||
handleMouse(getEvent(e, _kMouseEventMove));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,13 +434,13 @@ class InputModel {
|
||||
var type = '';
|
||||
var isMove = false;
|
||||
switch (evt['type']) {
|
||||
case 'mousedown':
|
||||
case _kMouseEventDown:
|
||||
type = 'down';
|
||||
break;
|
||||
case 'mouseup':
|
||||
case _kMouseEventUp:
|
||||
type = 'up';
|
||||
break;
|
||||
case 'mousemove':
|
||||
case _kMouseEventMove:
|
||||
isMove = true;
|
||||
break;
|
||||
default:
|
||||
@@ -440,15 +486,21 @@ class InputModel {
|
||||
evt['y'] = '${y.round()}';
|
||||
var buttons = '';
|
||||
switch (evt['buttons']) {
|
||||
case 1:
|
||||
case kPrimaryMouseButton:
|
||||
buttons = 'left';
|
||||
break;
|
||||
case 2:
|
||||
case kSecondaryMouseButton:
|
||||
buttons = 'right';
|
||||
break;
|
||||
case 4:
|
||||
case kMiddleMouseButton:
|
||||
buttons = 'wheel';
|
||||
break;
|
||||
case kBackMouseButton:
|
||||
buttons = 'back';
|
||||
break;
|
||||
case kForwardMouseButton:
|
||||
buttons = 'forward';
|
||||
break;
|
||||
}
|
||||
evt['buttons'] = buttons;
|
||||
bind.sessionSendMouse(id: id, msg: json.encode(evt));
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/group_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -19,8 +20,9 @@ import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:image/image.dart' as img2;
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
import 'package:flutter_custom_cursor/cursor_manager.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../common/shared_state.dart';
|
||||
@@ -59,7 +61,7 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
bool get touchMode => _touchMode;
|
||||
|
||||
bool get isPeerAndroid => _pi.platform == 'Android';
|
||||
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
|
||||
|
||||
set inputBlocked(v) {
|
||||
_inputBlocked = v;
|
||||
@@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier {
|
||||
setConnectionType(
|
||||
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||
} else if (name == 'switch_display') {
|
||||
handleSwitchDisplay(evt);
|
||||
handleSwitchDisplay(evt, peerId);
|
||||
} else if (name == 'cursor_data') {
|
||||
await parent.target?.cursorModel.updateCursorData(evt);
|
||||
} else if (name == 'cursor_id') {
|
||||
@@ -213,7 +215,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchDisplay(Map<String, dynamic> evt) {
|
||||
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
|
||||
final oldOrientation = _display.width > _display.height;
|
||||
var old = _pi.currentDisplay;
|
||||
_pi.currentDisplay = int.parse(evt['display']);
|
||||
@@ -221,15 +223,26 @@ class FfiModel with ChangeNotifier {
|
||||
_display.y = double.parse(evt['y']);
|
||||
_display.width = int.parse(evt['width']);
|
||||
_display.height = int.parse(evt['height']);
|
||||
_display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1;
|
||||
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
|
||||
if (old != _pi.currentDisplay) {
|
||||
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||
}
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
// remote is mobile, and orientation changed
|
||||
if ((_display.width > _display.height) != oldOrientation) {
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
}
|
||||
if (_pi.platform == kPeerPlatformLinux ||
|
||||
_pi.platform == kPeerPlatformWindows ||
|
||||
_pi.platform == kPeerPlatformMacOS) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
}
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -331,7 +344,7 @@ class FfiModel with ChangeNotifier {
|
||||
d.y = d0['y'].toDouble();
|
||||
d.width = d0['width'];
|
||||
d.height = d0['height'];
|
||||
d.cursorEmbeded = d0['cursor_embeded'] == 1;
|
||||
d.cursorEmbedded = d0['cursor_embedded'] == 1;
|
||||
_pi.displays.add(d);
|
||||
}
|
||||
if (_pi.currentDisplay < _pi.displays.length) {
|
||||
@@ -380,12 +393,22 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
final List<Function(String)> _callbacksOnFirstImage = [];
|
||||
|
||||
ImageModel(this.parent);
|
||||
|
||||
addCallbackOnFirstImage(Function(String) cb) =>
|
||||
_callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(Uint8List rgba) {
|
||||
if (_waitForImage[id]!) {
|
||||
_waitForImage[id] = false;
|
||||
parent.target?.dialogManager.dismissAll();
|
||||
if (isDesktop) {
|
||||
for (final cb in _callbacksOnFirstImage) {
|
||||
cb(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
final pid = parent.target?.id;
|
||||
ui.decodeImageFromPixels(
|
||||
@@ -495,7 +518,7 @@ class ViewStyle {
|
||||
|
||||
double get scale {
|
||||
double s = 1.0;
|
||||
if (style == 'adaptive') {
|
||||
if (style == kRemoteViewStyleAdaptive) {
|
||||
final s1 = width / displayWidth;
|
||||
final s2 = height / displayHeight;
|
||||
s = s1 < s2 ? s1 : s2;
|
||||
@@ -511,6 +534,7 @@ class CanvasModel with ChangeNotifier {
|
||||
double _y = 0;
|
||||
// image scale
|
||||
double _scale = 1.0;
|
||||
Size _size = Size.zero;
|
||||
// the tabbar over the image
|
||||
// double tabBarHeight = 0.0;
|
||||
// the window border's width
|
||||
@@ -524,6 +548,8 @@ class CanvasModel with ChangeNotifier {
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
ViewStyle _lastViewStyle = ViewStyle();
|
||||
|
||||
final _imageOverflow = false.obs;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
CanvasModel(this.parent);
|
||||
@@ -531,7 +557,12 @@ class CanvasModel with ChangeNotifier {
|
||||
double get x => _x;
|
||||
double get y => _y;
|
||||
double get scale => _scale;
|
||||
Size get size => _size;
|
||||
ScrollStyle get scrollStyle => _scrollStyle;
|
||||
ViewStyle get viewStyle => _lastViewStyle;
|
||||
RxBool get imageOverflow => _imageOverflow;
|
||||
|
||||
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||
|
||||
setScrollPercent(double x, double y) {
|
||||
_scrollX = x;
|
||||
@@ -542,28 +573,44 @@ class CanvasModel with ChangeNotifier {
|
||||
double get scrollY => _scrollY;
|
||||
|
||||
updateViewStyle() async {
|
||||
Size getSize() {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
// If minimized, w or h may be negative here.
|
||||
double w = size.width - windowBorderWidth * 2;
|
||||
double h = size.height - tabBarHeight - windowBorderWidth * 2;
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
}
|
||||
|
||||
final style = await bind.sessionGetViewStyle(id: id);
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
final sizeWidth = size.width;
|
||||
final sizeHeight = size.height;
|
||||
|
||||
_size = getSize();
|
||||
final displayWidth = getDisplayWidth();
|
||||
final displayHeight = getDisplayHeight();
|
||||
final viewStyle = ViewStyle(
|
||||
style: style,
|
||||
width: sizeWidth,
|
||||
height: sizeHeight,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
displayWidth: displayWidth,
|
||||
displayHeight: displayHeight,
|
||||
);
|
||||
if (_lastViewStyle == viewStyle) {
|
||||
return;
|
||||
}
|
||||
if (_lastViewStyle.style != viewStyle.style) {
|
||||
_resetScroll();
|
||||
}
|
||||
_lastViewStyle = viewStyle;
|
||||
_scale = viewStyle.scale;
|
||||
_x = (sizeWidth - displayWidth * _scale) / 2;
|
||||
_y = (sizeHeight - displayHeight * _scale) / 2;
|
||||
|
||||
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
|
||||
_scale = 1.0 / ui.window.devicePixelRatio;
|
||||
}
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
_imageOverflow.value = _x < 0 || y < 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -571,8 +618,7 @@ class CanvasModel with ChangeNotifier {
|
||||
final style = await bind.sessionGetScrollStyle(id: id);
|
||||
if (style == kRemoteScrollStyleBar) {
|
||||
_scrollStyle = ScrollStyle.scrollbar;
|
||||
_scrollX = 0.0;
|
||||
_scrollY = 0.0;
|
||||
_resetScroll();
|
||||
} else {
|
||||
_scrollStyle = ScrollStyle.scrollauto;
|
||||
}
|
||||
@@ -586,8 +632,8 @@ class CanvasModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get cursorEmbeded =>
|
||||
parent.target?.ffiModel.display.cursorEmbeded ?? false;
|
||||
bool get cursorEmbedded =>
|
||||
parent.target?.ffiModel.display.cursorEmbedded ?? false;
|
||||
|
||||
int getDisplayWidth() {
|
||||
final defaultWidth = (isDesktop || isWebDesktop)
|
||||
@@ -606,14 +652,6 @@ class CanvasModel with ChangeNotifier {
|
||||
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
double get tabBarHeight => stateGlobal.tabBarHeight;
|
||||
|
||||
Size get size {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
// If minimized, w or h may be negative here.
|
||||
double w = size.width - windowBorderWidth * 2;
|
||||
double h = size.height - tabBarHeight - windowBorderWidth * 2;
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
}
|
||||
|
||||
moveDesktopMouse(double x, double y) {
|
||||
// On mobile platforms, move the canvas with the cursor.
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
@@ -1113,7 +1151,8 @@ class CursorModel with ChangeNotifier {
|
||||
_clearCache() {
|
||||
final keys = {...cachedKeys};
|
||||
for (var k in keys) {
|
||||
customCursorController.freeCache(k);
|
||||
debugPrint("deleting cursor with key $k");
|
||||
CursorManager.instance.deleteCursor(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1220,6 +1259,7 @@ class FFI {
|
||||
late final ChatModel chatModel; // session
|
||||
late final FileModel fileModel; // session
|
||||
late final AbModel abModel; // global
|
||||
late final GroupModel groupModel; // global
|
||||
late final UserModel userModel; // global
|
||||
late final QualityMonitorModel qualityMonitorModel; // session
|
||||
late final RecordingModel recordingModel; // recording
|
||||
@@ -1233,8 +1273,9 @@ class FFI {
|
||||
serverModel = ServerModel(WeakReference(this));
|
||||
chatModel = ChatModel(WeakReference(this));
|
||||
fileModel = FileModel(WeakReference(this));
|
||||
abModel = AbModel(WeakReference(this));
|
||||
userModel = UserModel(WeakReference(this));
|
||||
abModel = AbModel(WeakReference(this));
|
||||
groupModel = GroupModel(WeakReference(this));
|
||||
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||
recordingModel = RecordingModel(WeakReference(this));
|
||||
inputModel = InputModel(WeakReference(this));
|
||||
@@ -1318,7 +1359,7 @@ class Display {
|
||||
double y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool cursorEmbeded = false;
|
||||
bool cursorEmbedded = false;
|
||||
|
||||
Display() {
|
||||
width = (isDesktop || isWebDesktop)
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../consts.dart';
|
||||
class StateGlobal {
|
||||
int _windowId = -1;
|
||||
bool _fullscreen = false;
|
||||
bool grabKeyboard = false;
|
||||
final RxBool _showTabBar = true.obs;
|
||||
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@@ -10,17 +11,19 @@ import 'model.dart';
|
||||
import 'platform_model.dart';
|
||||
|
||||
class UserModel {
|
||||
var userName = ''.obs;
|
||||
final RxString userName = ''.obs;
|
||||
final RxString groupName = ''.obs;
|
||||
final RxBool isAdmin = false.obs;
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
UserModel(this.parent) {
|
||||
refreshCurrentUser();
|
||||
}
|
||||
UserModel(this.parent);
|
||||
|
||||
void refreshCurrentUser() async {
|
||||
await getUserName();
|
||||
final token = bind.mainGetLocalOption(key: 'access_token');
|
||||
if (token == '') return;
|
||||
if (token == '') {
|
||||
await _updateOtherModels();
|
||||
return;
|
||||
}
|
||||
final url = await bind.mainGetApiServer();
|
||||
final body = {
|
||||
'id': await bind.mainGetMyId(),
|
||||
@@ -35,96 +38,95 @@ class UserModel {
|
||||
body: json.encode(body));
|
||||
final status = response.statusCode;
|
||||
if (status == 401 || status == 400) {
|
||||
resetToken();
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
await _parseResp(response.body);
|
||||
final data = json.decode(response.body);
|
||||
final error = data['error'];
|
||||
if (error != null) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
final user = UserPayload.fromJson(data);
|
||||
await _parseAndUpdateUser(user);
|
||||
} catch (e) {
|
||||
print('Failed to refreshCurrentUser: $e');
|
||||
} finally {
|
||||
await _updateOtherModels();
|
||||
}
|
||||
}
|
||||
|
||||
void resetToken() async {
|
||||
Future<void> reset() async {
|
||||
await bind.mainSetLocalOption(key: 'access_token', value: '');
|
||||
await bind.mainSetLocalOption(key: 'user_info', value: '');
|
||||
await gFFI.abModel.reset();
|
||||
await gFFI.groupModel.reset();
|
||||
userName.value = '';
|
||||
groupName.value = '';
|
||||
statePeerTab.check();
|
||||
}
|
||||
|
||||
Future<String> _parseResp(String body) async {
|
||||
final data = json.decode(body);
|
||||
final error = data['error'];
|
||||
if (error != null) {
|
||||
return error!;
|
||||
}
|
||||
final token = data['access_token'];
|
||||
if (token != null) {
|
||||
await bind.mainSetLocalOption(key: 'access_token', value: token);
|
||||
}
|
||||
final info = data['user'];
|
||||
if (info != null) {
|
||||
final value = json.encode(info);
|
||||
await bind.mainSetOption(key: 'user_info', value: value);
|
||||
userName.value = info['name'];
|
||||
}
|
||||
return '';
|
||||
Future<void> _parseAndUpdateUser(UserPayload user) async {
|
||||
userName.value = user.name;
|
||||
groupName.value = user.grp;
|
||||
isAdmin.value = user.isAdmin;
|
||||
}
|
||||
|
||||
Future<String> getUserName() async {
|
||||
if (userName.isNotEmpty) {
|
||||
return userName.value;
|
||||
}
|
||||
final userInfo = bind.mainGetLocalOption(key: 'user_info');
|
||||
if (userInfo.trim().isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final m = jsonDecode(userInfo);
|
||||
if (m == null) {
|
||||
userName.value = '';
|
||||
} else {
|
||||
userName.value = m['name'] ?? '';
|
||||
}
|
||||
return userName.value;
|
||||
Future<void> _updateOtherModels() async {
|
||||
await gFFI.abModel.pullAb();
|
||||
await gFFI.groupModel.pull();
|
||||
}
|
||||
|
||||
Future<void> logOut() async {
|
||||
final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
|
||||
final url = await bind.mainGetApiServer();
|
||||
final _ = await http.post(Uri.parse('$url/api/logout'),
|
||||
body: {
|
||||
'id': await bind.mainGetMyId(),
|
||||
'uuid': await bind.mainGetUuid(),
|
||||
},
|
||||
headers: await getHttpHeaders());
|
||||
await Future.wait([
|
||||
bind.mainSetLocalOption(key: 'access_token', value: ''),
|
||||
bind.mainSetLocalOption(key: 'user_info', value: ''),
|
||||
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
|
||||
]);
|
||||
parent.target?.abModel.clear();
|
||||
userName.value = '';
|
||||
gFFI.dialogManager.dismissByTag(tag);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> login(String userName, String pass) async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
try {
|
||||
final resp = await http.post(Uri.parse('$url/api/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'username': userName,
|
||||
'password': pass,
|
||||
'id': await bind.mainGetMyId(),
|
||||
'uuid': await bind.mainGetUuid()
|
||||
}));
|
||||
final body = jsonDecode(resp.body);
|
||||
bind.mainSetLocalOption(
|
||||
key: 'access_token', value: body['access_token'] ?? '');
|
||||
bind.mainSetLocalOption(
|
||||
key: 'user_info', value: jsonEncode(body['user']));
|
||||
this.userName.value = body['user']?['name'] ?? '';
|
||||
return body;
|
||||
} catch (err) {
|
||||
return {'error': '$err'};
|
||||
final url = await bind.mainGetApiServer();
|
||||
await http
|
||||
.post(Uri.parse('$url/api/logout'),
|
||||
body: {
|
||||
'id': await bind.mainGetMyId(),
|
||||
'uuid': await bind.mainGetUuid(),
|
||||
},
|
||||
headers: getHttpHeaders())
|
||||
.timeout(Duration(seconds: 2));
|
||||
} catch (e) {
|
||||
print("request /api/logout failed: err=$e");
|
||||
} finally {
|
||||
await reset();
|
||||
gFFI.dialogManager.dismissByTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// throw [RequestException]
|
||||
Future<LoginResponse> login(LoginRequest loginRequest) async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
final resp = await http.post(Uri.parse('$url/api/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(loginRequest.toJson()));
|
||||
|
||||
final Map<String, dynamic> body;
|
||||
try {
|
||||
body = jsonDecode(resp.body);
|
||||
} catch (e) {
|
||||
print("jsonDecode resp body failed: ${e.toString()}");
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp.statusCode, body['error'] ?? '');
|
||||
}
|
||||
|
||||
final LoginResponse loginResponse;
|
||||
try {
|
||||
loginResponse = LoginResponse.fromJson(body);
|
||||
} catch (e) {
|
||||
print("jsonDecode LoginResponse failed: ${e.toString()}");
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (loginResponse.user != null) {
|
||||
await _parseAndUpdateUser(loginResponse.user!);
|
||||
}
|
||||
|
||||
return loginResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -34,7 +35,7 @@ class RustDeskMultiWindowManager {
|
||||
static final instance = RustDeskMultiWindowManager._();
|
||||
|
||||
final List<int> _activeWindows = List.empty(growable: true);
|
||||
final List<VoidCallback> _windowActiveCallbacks = List.empty(growable: true);
|
||||
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
|
||||
int? _remoteDesktopWindowId;
|
||||
int? _fileTransferWindowId;
|
||||
int? _portForwardWindowId;
|
||||
@@ -191,41 +192,41 @@ class RustDeskMultiWindowManager {
|
||||
return _activeWindows;
|
||||
}
|
||||
|
||||
void _notifyActiveWindow() {
|
||||
Future<void> _notifyActiveWindow() async {
|
||||
for (final callback in _windowActiveCallbacks) {
|
||||
callback.call();
|
||||
await callback.call();
|
||||
}
|
||||
}
|
||||
|
||||
void registerActiveWindow(int windowId) {
|
||||
Future<void> registerActiveWindow(int windowId) async {
|
||||
if (_activeWindows.contains(windowId)) {
|
||||
// ignore
|
||||
} else {
|
||||
_activeWindows.add(windowId);
|
||||
}
|
||||
_notifyActiveWindow();
|
||||
await _notifyActiveWindow();
|
||||
}
|
||||
|
||||
/// Remove active window which has [`windowId`]
|
||||
///
|
||||
/// [Avaliability]
|
||||
/// [Availability]
|
||||
/// This function should only be called from main window.
|
||||
/// For other windows, please post a unregister(hide) event to main window handler:
|
||||
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
|
||||
void unregisterActiveWindow(int windowId) {
|
||||
Future<void> unregisterActiveWindow(int windowId) async {
|
||||
if (!_activeWindows.contains(windowId)) {
|
||||
// ignore
|
||||
} else {
|
||||
_activeWindows.remove(windowId);
|
||||
}
|
||||
_notifyActiveWindow();
|
||||
await _notifyActiveWindow();
|
||||
}
|
||||
|
||||
void registerActiveWindowListener(VoidCallback callback) {
|
||||
void registerActiveWindowListener(AsyncCallback callback) {
|
||||
_windowActiveCallbacks.add(callback);
|
||||
}
|
||||
|
||||
void unregisterActiveWindowListener(VoidCallback callback) {
|
||||
void unregisterActiveWindowListener(AsyncCallback callback) {
|
||||
_windowActiveCallbacks.remove(callback);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user