Merge branch 'master' into mobile_feat_update_rebase

This commit is contained in:
csf
2022-09-21 14:12:22 +08:00
23 changed files with 404 additions and 729 deletions

View File

@@ -244,6 +244,32 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
),
);
List<Locale> supportedLocales = const [
// specify CN/TW to fix CJK issue in flutter
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('zh', 'SG'),
Locale('fr'),
Locale('de'),
Locale('it'),
Locale('ja'),
Locale('cs'),
Locale('pl'),
Locale('ko'),
Locale('hu'),
Locale('pt'),
Locale('ru'),
Locale('sk'),
Locale('id'),
Locale('da'),
Locale('eo'),
Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'),
Locale('en', 'US'),
];
String formatDurationToTime(Duration duration) {
var totalTime = duration.inSeconds;
final secs = totalTime % 60;
@@ -734,8 +760,9 @@ class PermissionManager {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type))
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
return gFFI.invokeMethod("check_permission", type);
}
@@ -743,8 +770,9 @@ class PermissionManager {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type))
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {

View File

@@ -33,6 +33,7 @@ class IDTextInputFormatter extends TextInputFormatter {
String formatID(String id) {
String id2 = id.replaceAll(' ', '');
if (int.tryParse(id2) == null) return id;
String newID = '';
if (id2.length <= 3) {
newID = id2;

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -41,6 +42,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
static const int _maxQueryCount = 3;
final space = isDesktop ? 12.0 : 8.0;
final _curPeers = <String>{};
final _scrollController = ScrollController();
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1));
@@ -94,57 +96,59 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
? Center(
child: Text(translate("Empty")),
)
: SingleChildScrollView(
controller: ScrollController(),
child: ObxValue<RxString>((searchText) {
return FutureBuilder<List<Peer>>(
builder: (context, snapshot) {
if (snapshot.hasData) {
final peers = snapshot.data!;
final cards = <Widget>[];
for (final peer in peers) {
final visibilityChild = VisibilityDetector(
key: ValueKey(peer.id),
onVisibilityChanged: (info) {
final peerId = (info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId);
} else {
_curPeers.remove(peerId);
}
_lastChangeTime = DateTime.now();
},
child: widget.peerCardWidgetFunc(peer),
);
cards.add(Offstage(
key: ValueKey("off${peer.id}"),
offstage: widget.offstageFunc(peer),
child: isDesktop
? Obx(
() => SizedBox(
width: 220,
height: peerCardUiType.value ==
PeerUiType.grid
: DesktopScrollWrapper(
scrollController: _scrollController,
child: SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
child: ObxValue<RxString>((searchText) {
return FutureBuilder<List<Peer>>(
builder: (context, snapshot) {
if (snapshot.hasData) {
final peers = snapshot.data!;
final cards = <Widget>[];
for (final peer in peers) {
cards.add(Offstage(
key: ValueKey("off${peer.id}"),
offstage: widget.offstageFunc(peer),
child: Obx(
() => SizedBox(
width: 220,
height:
peerCardUiType.value == PeerUiType.grid
? 140
: 42,
child: visibilityChild,
),
)
: SizedBox(
width: mobileWidth,
child: visibilityChild)));
child: VisibilityDetector(
key: ValueKey(peer.id),
onVisibilityChanged: (info) {
final peerId =
(info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId);
} else {
_curPeers.remove(peerId);
}
_lastChangeTime = DateTime.now();
},
child: widget.peerCardWidgetFunc(peer),
),
),
)));
}
return Wrap(
spacing: space,
runSpacing: space,
children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
return Wrap(
spacing: space, runSpacing: space, children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
},
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
),
),
),
);

View File

@@ -17,6 +17,13 @@ const int kMobileDefaultDisplayHeight = 1280;
const int kDesktopDefaultDisplayWidth = 1080;
const int kDesktopDefaultDisplayHeight = 720;
/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse
const kDefaultScrollAmountMultiplier = 5.0;
const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWhellThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kWindowEdgeSize = 1.0;
const kInvalidValueStr = "InvalidValueStr";
const kMobilePageConstraints = BoxConstraints(maxWidth: 600);

View File

@@ -1,3 +1,5 @@
// main window right pane
import 'dart:async';
import 'dart:convert';
@@ -82,8 +84,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
).marginSymmetric(horizontal: 22),
),
const Divider(),
SizedBox(height: 50, child: Obx(() => buildStatus()))
.paddingSymmetric(horizontal: 12.0)
SizedBox(child: Obx(() => buildStatus()))
.paddingOnly(bottom: 12, top: 6),
]),
);
}
@@ -187,7 +189,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
onConnect(isFileTransfer: true);
},
child: Container(
height: 24,
height: 27,
alignment: Alignment.center,
decoration: BoxDecoration(
color: ftPressed.value
@@ -224,31 +226,36 @@ class _ConnectionPageState extends State<ConnectionPage> {
onTapCancel: () => connPressed.value = false,
onHover: (value) => connHover.value = value,
onTap: onConnect,
child: Container(
height: 24,
decoration: BoxDecoration(
color: connPressed.value
? MyTheme.accent
: MyTheme.button,
border: Border.all(
color: connPressed.value
? MyTheme.accent
: connHover.value
? MyTheme.hoverBorder
: MyTheme.button,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 80.0,
),
borderRadius: BorderRadius.circular(5),
),
child: Center(
child: Text(
translate(
"Connect",
child: Container(
height: 27,
decoration: BoxDecoration(
color: connPressed.value
? MyTheme.accent
: MyTheme.button,
border: Border.all(
color: connPressed.value
? MyTheme.accent
: connHover.value
? MyTheme.hoverBorder
: MyTheme.button,
),
borderRadius: BorderRadius.circular(5),
),
style: TextStyle(
fontSize: 12, color: MyTheme.color(context).bg),
),
).marginSymmetric(horizontal: 12),
),
child: Center(
child: Text(
translate(
"Connect",
),
style: TextStyle(
fontSize: 12,
color: MyTheme.color(context).bg),
),
).marginSymmetric(horizontal: 12),
)),
),
),
],
@@ -275,6 +282,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
var svcIsUsingPublicServer = true.obs;
Widget buildStatus() {
final fontSize = 14.0;
final textStyle = TextStyle(fontSize: fontSize);
final light = Container(
height: 8,
width: 8,
@@ -282,13 +291,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
borderRadius: BorderRadius.circular(20),
color: svcStopped.value ? Colors.redAccent : Colors.green,
),
).paddingSymmetric(horizontal: 10.0);
).paddingSymmetric(horizontal: 12.0);
if (svcStopped.value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
light,
Text(translate("Service is not running")),
Text(translate("Service is not running"), style: textStyle),
TextButton(
onPressed: () async {
bool checked = await bind.mainCheckSuperUserPermission();
@@ -296,19 +305,25 @@ class _ConnectionPageState extends State<ConnectionPage> {
bind.mainSetOption(key: "stop-service", value: "");
}
},
child: Text(translate("Start Service")))
child: Text(translate("Start Service"), style: textStyle))
],
);
} else {
if (svcStatusCode.value == 0) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [light, Text(translate("connecting_status"))],
children: [
light,
Text(translate("connecting_status"), style: textStyle)
],
);
} else if (svcStatusCode.value == -1) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [light, Text(translate("not_ready_status"))],
children: [
light,
Text(translate("not_ready_status"), style: textStyle)
],
);
}
}
@@ -316,13 +331,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
light,
Text(translate('Ready')),
Text(translate('Ready'), style: textStyle),
Text(', ', style: textStyle),
svcIsUsingPublicServer.value
? InkWell(
onTap: onUsePublicServerGuide,
child: Text(
', ${translate('setup_server_tip')}',
style: TextStyle(decoration: TextDecoration.underline),
translate('setup_server_tip'),
style: TextStyle(
decoration: TextDecoration.underline, fontSize: fontSize),
),
)
: Offstage()

View File

@@ -5,29 +5,14 @@ import 'package:flutter/material.dart' hide MenuItem;
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart';
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension
static const double height = 25.0;
static const double dividerHeight = 12.0;
}
class DesktopHomePage extends StatefulWidget {
const DesktopHomePage({Key? key}) : super(key: key);
@@ -66,19 +51,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
super.build(context);
return Row(
children: [
buildServerInfo(context),
buildLeftPane(context),
const VerticalDivider(
width: 1,
thickness: 1,
),
Expanded(
child: buildServerBoard(context),
child: buildRightPane(context),
),
],
);
}
buildServerInfo(BuildContext context) {
buildLeftPane(BuildContext context) {
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Container(
@@ -95,7 +80,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
buildServerBoard(BuildContext context) {
buildRightPane(BuildContext context) {
return Container(
color: MyTheme.color(context).grayBg,
child: ConnectionPage(),
@@ -167,93 +152,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
Widget buildPopupMenu(BuildContext context) {
var position;
RxBool hover = false.obs;
return InkWell(
onTapDown: (detail) {
final x = detail.globalPosition.dx;
final y = detail.globalPosition.dy;
position = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () async {
final userName = await gFFI.userModel.getUserName();
final enabledInput = await bind.mainGetOption(key: 'enable-audio');
final defaultInput = await gFFI.getDefaultAudioInput();
var menu = <PopupMenuEntry>[
await genEnablePopupMenuItem(
translate("Enable Keyboard/Mouse"),
'enable-keyboard',
),
await genEnablePopupMenuItem(
translate("Enable Clipboard"),
'enable-clipboard',
),
await genEnablePopupMenuItem(
translate("Enable File Transfer"),
'enable-file-transfer',
),
await genEnablePopupMenuItem(
translate("Enable TCP Tunneling"),
'enable-tunnel',
),
genAudioInputPopupMenuItem(enabledInput != "N", defaultInput),
PopupMenuDivider(),
PopupMenuItem(
child: Text(translate("ID/Relay Server")),
value: 'custom-server',
),
PopupMenuItem(
child: Text(translate("IP Whitelisting")),
value: 'whitelist',
),
PopupMenuItem(
child: Text(translate("Socks5 Proxy")),
value: 'socks5-proxy',
),
PopupMenuDivider(),
await genEnablePopupMenuItem(
translate("Enable Service"),
'stop-service',
),
// TODO: direct server
await genEnablePopupMenuItem(
translate("Always connected via relay"),
'allow-always-relay',
),
await genEnablePopupMenuItem(
translate("Start ID/relay service"),
'stop-rendezvous-service',
),
PopupMenuDivider(),
userName.isEmpty
? PopupMenuItem(
child: Text(translate("Login")),
value: 'login',
)
: PopupMenuItem(
child: Text("${translate("Logout")} $userName"),
value: 'logout',
),
PopupMenuItem(
child: Text(translate("Change ID")),
value: 'change-id',
),
PopupMenuDivider(),
await genEnablePopupMenuItem(
translate("Dark Theme"),
'allow-darktheme',
),
PopupMenuItem(
child: Text(translate("About")),
value: 'about',
),
];
final v =
await showMenu(context: context, position: position, items: menu);
if (v != null) {
onSelectMenu(v);
}
},
onTap: () async {},
child: Obx(
() => CircleAvatar(
radius: 15,
@@ -276,6 +177,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
buildPasswordBoard(BuildContext context) {
final model = gFFI.serverModel;
RxBool refreshHover = false.obs;
RxBool editHover = false.obs;
return Container(
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
child: Row(
@@ -334,7 +236,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
onTap: () => bind.mainUpdateTemporaryPassword(),
onHover: (value) => refreshHover.value = value,
),
const _PasswordPopupMenu(),
InkWell(
child: Obx(
() => Icon(
Icons.edit,
color: editHover.value
? MyTheme.color(context).text
: Color(0xFFDDDDDD),
size: 22,
).marginOnly(right: 8, bottom: 2),
),
onTap: () => {},
onHover: (value) => editHover.value = value,
),
],
),
],
@@ -376,7 +290,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override
void onTrayMenuItemClick(MenuItem menuItem) {
print('click ${menuItem.key}');
debugPrint('click ${menuItem.key}');
switch (menuItem.key) {
case "quit":
exit(0);
@@ -394,8 +308,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
trayManager.addListener(this);
windowManager.addListener(this);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
debugPrint(
"call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == "main_window_on_top") {
window_on_top(null);
}
@@ -408,236 +322,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
windowManager.removeListener(this);
super.dispose();
}
void changeTheme(String choice) async {
if (choice == "Y") {
Get.changeTheme(MyTheme.darkTheme);
} else {
Get.changeTheme(MyTheme.lightTheme);
}
Get.find<SharedPreferences>().setString("darkTheme", choice);
Get.forceAppUpdate();
}
void onSelectMenu(String key) async {
if (key.startsWith('enable-')) {
final option = await bind.mainGetOption(key: key);
bind.mainSetOption(key: key, value: option == "N" ? "" : "N");
} else if (key.startsWith('allow-')) {
final option = await bind.mainGetOption(key: key);
final choice = option == "Y" ? "" : "Y";
bind.mainSetOption(key: key, value: choice);
if (key == "allow-darktheme") changeTheme(choice);
} else if (key == "stop-service") {
final option = await bind.mainGetOption(key: key);
bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y");
} else if (key == "change-id") {
changeIdDialog();
} else if (key == "custom-server") {
changeServer();
} else if (key == "whitelist") {
changeWhiteList();
} else if (key == "socks5-proxy") {
changeSocks5Proxy();
} else if (key == "about") {
about();
} else if (key == "logout") {
logOut();
} else if (key == "login") {
login();
}
}
Future<PopupMenuItem<String>> genEnablePopupMenuItem(
String label, String key) async {
final v = await bind.mainGetOption(key: key);
bool enable;
if (key == "stop-service") {
enable = v != "Y";
} else if (key.startsWith("allow-")) {
enable = v == "Y";
} else {
enable = v != "N";
}
return PopupMenuItem(
child: Row(
children: [
Icon(Icons.check,
color: enable ? null : MyTheme.accent.withAlpha(00)),
Text(
label,
style: genTextStyle(enable),
),
],
),
value: key,
);
}
TextStyle genTextStyle(bool isPositive) {
return isPositive
? TextStyle()
: TextStyle(
color: Colors.redAccent, decoration: TextDecoration.lineThrough);
}
PopupMenuItem<String> genAudioInputPopupMenuItem(
bool enableInput, String defaultAudioInput) {
final defaultInput = defaultAudioInput.obs;
final enabled = enableInput.obs;
return PopupMenuItem(
child: FutureBuilder<List<String>>(
future: gFFI.getAudioInputs(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final inputs = snapshot.data!.toList();
if (Platform.isWindows) {
inputs.insert(0, translate("System Sound"));
}
var inputList = inputs
.map((e) => PopupMenuItem(
child: Row(
children: [
Obx(() => Offstage(
offstage: defaultInput.value != e,
child: Icon(Icons.check))),
Expanded(
child: Tooltip(
message: e,
child: Text(
"$e",
maxLines: 1,
overflow: TextOverflow.ellipsis,
))),
],
),
value: e,
))
.toList();
inputList.insert(
0,
PopupMenuItem(
child: Row(
children: [
Obx(() => Offstage(
offstage: enabled.value, child: Icon(Icons.check))),
Expanded(child: Text(translate("Mute"))),
],
),
value: "Mute",
));
return PopupMenuButton<String>(
padding: EdgeInsets.zero,
child: Container(
alignment: Alignment.centerLeft,
child: Text(translate("Audio Input"))),
itemBuilder: (context) => inputList,
onSelected: (dev) async {
if (dev == "Mute") {
await bind.mainSetOption(
key: 'enable-audio', value: enabled.value ? '' : 'N');
enabled.value =
await bind.mainGetOption(key: 'enable-audio') != 'N';
} else if (dev != await gFFI.getDefaultAudioInput()) {
gFFI.setDefaultAudioInput(dev);
defaultInput.value = dev;
}
},
);
} else {
return Text("...");
}
},
),
value: 'audio-input',
);
}
void about() async {
final appName = await bind.mainGetAppName();
final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion();
const linkStyle = TextStyle(decoration: TextDecoration.underline);
gFFI.dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text("About $appName"),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Text("Version: $version").marginSymmetric(vertical: 4.0),
InkWell(
onTap: () {
launchUrlString("https://rustdesk.com/privacy");
},
child: const Text(
"Privacy Statement",
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString("https://rustdesk.com");
},
child: const Text(
"Website",
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
Container(
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
padding:
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Copyright &copy; 2022 Purslane Ltd.\n$license",
style: const TextStyle(color: Colors.white),
),
const Text(
"Made with heart in this chaotic world!",
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white),
)
],
),
),
],
),
).marginSymmetric(vertical: 4.0)
],
),
),
actions: [
TextButton(onPressed: close, child: Text(translate("OK"))),
],
onSubmit: close,
onCancel: close,
);
});
}
void login() {
loginDialog().then((success) {
if (success) {
// refresh frame
setState(() {});
}
});
}
void logOut() {
gFFI.userModel.logOut().then((_) => {setState(() {})});
}
}
/// common login dialog for desktop
@@ -689,8 +373,7 @@ Future<bool> loginDialog() async {
debugPrint("$resp");
completer.complete(true);
} catch (err) {
// ignore: avoid_print
print(err.toString());
debugPrint(err.toString());
cancel();
return;
}
@@ -874,120 +557,3 @@ void setPasswordDialog() async {
);
});
}
class _PasswordPopupMenu extends StatefulWidget {
const _PasswordPopupMenu({Key? key}) : super(key: key);
@override
State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState();
}
class _PasswordPopupMenuState extends State<_PasswordPopupMenu> {
final RxBool _tempEnabled = true.obs;
final RxBool _permEnabled = true.obs;
List<MenuEntryBase<String>> _buildMenus() {
return <MenuEntryBase<String>>[
MenuEntryRadios<String>(
text: translate('Password type'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Use temporary password'),
value: kUseTemporaryPassword),
MenuEntryRadioOption(
text: translate('Use permanent password'),
value: kUsePermanentPassword),
MenuEntryRadioOption(
text: translate('Use both passwords'),
value: kUseBothPasswords),
],
curOptionGetter: () async {
return gFFI.serverModel.verificationMethod;
},
optionSetter: (String oldValue, String newValue) async {
await bind.mainSetOption(
key: "verification-method", value: newValue);
await gFFI.serverModel.updatePasswordModel();
setState(() {
_tempEnabled.value =
gFFI.serverModel.verificationMethod != kUsePermanentPassword;
_permEnabled.value =
gFFI.serverModel.verificationMethod != kUseTemporaryPassword;
});
}),
MenuEntryDivider(),
MenuEntryButton<String>(
enabled: _permEnabled,
childBuilder: (TextStyle? style) => Text(
translate('Set permanent password'),
style: style,
),
proc: () {
setPasswordDialog();
},
dismissOnClicked: true,
),
MenuEntrySubMenu(
enabled: _tempEnabled,
text: translate('Set temporary password length'),
entries: [
MenuEntryRadios<String>(
enabled: _tempEnabled,
text: translate(''),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('6'),
value: '6',
enabled: _tempEnabled,
),
MenuEntryRadioOption(
text: translate('8'),
value: '8',
enabled: _tempEnabled,
),
MenuEntryRadioOption(
text: translate('10'),
value: '10',
enabled: _tempEnabled,
),
],
curOptionGetter: () async {
return gFFI.serverModel.temporaryPasswordLength;
},
optionSetter: (String oldValue, String newValue) async {
if (oldValue != newValue) {
await gFFI.serverModel.setTemporaryPasswordLength(newValue);
await gFFI.serverModel.updatePasswordModel();
}
}),
])
];
}
@override
Widget build(BuildContext context) {
final editHover = false.obs;
return mod_menu.PopupMenuButton(
padding: EdgeInsets.zero,
onHover: (v) => editHover.value = v,
tooltip: translate(''),
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => _buildMenus()
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
child: Obx(() => Icon(Icons.edit,
size: 22,
color: editHover.value
? MyTheme.color(context).text
: const Color(0xFFDDDDDD))
.marginOnly(bottom: 2)),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
@@ -35,28 +37,30 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
Widget build(BuildContext context) {
RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen');
return Obx(() => DragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Overlay(initialEntries: [
OverlayEntry(builder: (context) {
gFFI.dialogManager.setOverlayState(Overlay.of(context));
return Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
onTap: onAddSetting,
isClose: false,
),
));
})
]),
)));
final tabWidget = Container(
child: Overlay(initialEntries: [
OverlayEntry(builder: (context) {
gFFI.dialogManager.setOverlayState(Overlay.of(context));
return Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
tail: ActionIcon(
message: 'Settings',
icon: IconFont.menu,
onTap: onAddSetting,
isClose: false,
),
));
})
]),
);
return Platform.isMacOS
? tabWidget
: Obx(() => DragToResizeArea(
resizeEdgeSize:
fullscreen.value ? kFullScreenEdgeSize : kWindowEdgeSize,
child: tabWidget));
}
void onAddSetting() {

View File

@@ -1,8 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -66,20 +68,24 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override
Widget build(BuildContext context) {
return SubWindowDragToResizeArea(
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
)),
),
final tabWidget = Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
)),
);
return Platform.isMacOS
? tabWidget
: SubWindowDragToResizeArea(
resizeEdgeSize: kWindowEdgeSize,
windowId: windowId(),
child: tabWidget,
);
}
void onRemoveId(String id) {

View File

@@ -1,8 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/port_forward_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -74,23 +76,27 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
@override
Widget build(BuildContext context) {
return SubWindowDragToResizeArea(
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
tabController.clear();
return true;
},
tail: AddButton().paddingOnly(left: 10),
)),
),
final tabWidget = Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
tabController.clear();
return true;
},
tail: AddButton().paddingOnly(left: 10),
)),
);
return Platform.isMacOS
? tabWidget
: SubWindowDragToResizeArea(
resizeEdgeSize: kWindowEdgeSize,
windowId: windowId(),
child: tabWidget,
);
}
void onRemoveId(String id) {

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
@@ -86,63 +87,66 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override
Widget build(BuildContext context) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
windowId: windowId(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
showTabBar: fullscreen.isFalse,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(fullscreen.isTrue);
return pageView;
},
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
final msgDirect = translate(connectionType.direct.value ==
ConnectionType.strDirect
? 'Direct Connection'
: 'Relay Connection');
final msgSecure = translate(connectionType.secure.value ==
ConnectionType.strSecure
? 'Secure Connection'
: 'Insecure Connection');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgDirect\n$msgSecure',
child: Image.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
],
);
}
}),
)),
),
));
final tabWidget = Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: MyTheme.color(context).bg,
body: DesktopTab(
controller: tabController,
showTabBar: fullscreen.isFalse,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId())
.setFullscreen(fullscreen.isTrue);
return pageView;
},
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
final msgDirect = translate(
connectionType.direct.value == ConnectionType.strDirect
? 'Direct Connection'
: 'Relay Connection');
final msgSecure = translate(
connectionType.secure.value == ConnectionType.strSecure
? 'Secure Connection'
: 'Insecure Connection');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgDirect\n$msgSecure',
child: Image.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
],
);
}
}),
)),
);
return Platform.isMacOS
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize:
fullscreen.value ? kFullScreenEdgeSize : kWindowEdgeSize,
windowId: windowId(),
child: tabWidget));
}
void onRemoveId(String id) {

View File

@@ -0,0 +1,26 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
class DesktopScrollWrapper extends StatelessWidget {
final ScrollController scrollController;
final Widget child;
const DesktopScrollWrapper(
{Key? key, required this.scrollController, required this.child})
: super(key: key);
@override
Widget build(BuildContext context) {
return ImprovedScrolling(
scrollController: scrollController,
enableCustomMouseWheelScrolling: true,
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
scrollDuration: kDefaultScrollDuration,
scrollCurve: Curves.linearToEaseOut,
mouseWheelTurnsThrottleTimeMs:
kDefaultMouseWhellThrottleDuration.inMilliseconds,
scrollAmountMultiplier: kDefaultScrollAmountMultiplier),
child: child,
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -123,6 +124,12 @@ void runRemoteScreen(Map<String, dynamic> argument) async {
home: DesktopRemoteScreen(
params: argument,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
@@ -141,6 +148,12 @@ void runFileTransferScreen(Map<String, dynamic> argument) async {
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.initialThemeMode(),
home: DesktopFileTransferScreen(params: argument),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
@@ -160,6 +173,12 @@ void runPortForwardScreen(Map<String, dynamic> argument) async {
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.initialThemeMode(),
home: DesktopPortForwardScreen(params: argument),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
@@ -178,14 +197,20 @@ void runConnectionManagerScreen() async {
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.initialThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage(),
builder: _keepScaleBuilder()));
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setAlignment(Alignment.topRight);
await windowManager.show();
await windowManager.focus();
await windowManager.setAlignment(Alignment.topRight); // ensure
});
await windowManager.setAlignment(Alignment.topRight);
await windowManager.show();
await windowManager.focus();
await windowManager.setAlignment(Alignment.topRight); // ensure
});
}
WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
@@ -247,6 +272,12 @@ class _AppState extends State<App> {
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
builder: isAndroid
? (context, child) => AccessibilityListener(
child: MediaQuery(

View File

@@ -45,8 +45,8 @@ class AbModel with ChangeNotifier {
} catch (err) {
abError = err.toString();
} finally {
notifyListeners();
abLoading = false;
notifyListeners();
}
return null;
}
@@ -98,12 +98,18 @@ class AbModel with ChangeNotifier {
final body = jsonEncode({
"data": jsonEncode({"tags": tags, "peers": peers})
});
final resp =
await http.post(Uri.parse(api), headers: authHeaders, body: body);
abLoading = false;
// await getAb(); // TODO
try {
final resp =
await http.post(Uri.parse(api), headers: authHeaders, body: body);
abError = "";
await getAb();
debugPrint("resp: ${resp.body}");
} catch (e) {
abError = e.toString();
} finally {
abLoading = false;
}
notifyListeners();
debugPrint("resp: ${resp.body}");
}
bool idContainBy(String id) {