Merge remote-tracking branch 'rd/master' into feat/x11/clipboard-file/init

Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
ClSlaid
2023-10-07 17:32:25 +08:00
88 changed files with 1434 additions and 871 deletions

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696255389449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1922" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M435.2 704c-9 0-17.8-3.8-23.8-10.6l-115.2-128c-11.8-13.2-10.8-33.4 2.4-45.2 13.2-11.8 33.4-10.8 45.2 2.4l90.6 100.6 245.2-291.8c11.4-13.6 31.6-15.2 45-4 13.6 11.4 15.2 31.6 4 45l-268.8 320c-6 7-14.6 11.2-24 11.4-0.2 0.2-0.4 0.2-0.6 0.2z" p-id="1923"></path><path d="M800 928H224c-70.6 0-128-57.4-128-128V224c0-70.6 57.4-128 128-128h576c70.6 0 128 57.4 128 128v576c0 70.6-57.4 128-128 128zM224 160c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64H224z" p-id="1924"></path></svg>

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696245886035" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 132.717714c-9.435429 0-18.852571 3.84-29.147429 12.434286L194.011429 379.574857c-7.277714 6.418286-11.556571 15.433143-11.556572 28.288 0 22.272 16.713143 39.003429 39.424 39.003429 8.996571 0 18.432-3.437714 28.288-11.154286L512 222.281143l261.851429 213.430857c9.874286 7.716571 19.291429 11.154286 28.708571 11.154286 22.308571 0 39.003429-16.731429 39.003429-39.003429 0-12.854857-4.278857-21.869714-11.556572-28.288L541.147429 144.713143c-10.294857-8.137143-19.291429-11.995429-29.147429-11.995429z m0 758.564572c9.856 0 18.852571-3.84 29.147429-11.995429L829.988571 644.425143c7.277714-6.418286 11.556571-15.433143 11.556572-28.288 0-22.272-16.713143-39.424-38.985143-39.424-9.435429 0-18.870857 3.858286-28.708571 11.574857L512 801.718857 250.148571 588.288c-9.874286-7.716571-19.291429-11.574857-28.288-11.574857-22.710857 0-39.424 17.152-39.424 39.424 0 12.854857 4.278857 21.869714 11.556572 28.288l288.859428 234.422857c10.294857 8.594286 19.712 12.434286 29.147429 12.434286z" p-id="4134"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -91,7 +91,6 @@ class IconFont {
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook");
static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
}
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@@ -102,6 +101,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
required this.drag_indicator,
required this.shadow,
required this.errorBannerBg,
required this.me,
});
final Color? border;
@@ -110,6 +110,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
final Color? drag_indicator;
final Color? shadow;
final Color? errorBannerBg;
final Color? me;
static final light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
@@ -118,6 +119,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
drag_indicator: Colors.grey[800],
shadow: Colors.black,
errorBannerBg: Color(0xFFFDEEEB),
me: Colors.green,
);
static final dark = ColorThemeExtension(
@@ -127,6 +129,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
drag_indicator: Colors.grey,
shadow: Colors.grey,
errorBannerBg: Color(0xFF470F2D),
me: Colors.greenAccent,
);
@override
@@ -137,6 +140,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
Color? drag_indicator,
Color? shadow,
Color? errorBannerBg,
Color? me,
}) {
return ColorThemeExtension(
border: border ?? this.border,
@@ -145,6 +149,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
drag_indicator: drag_indicator ?? this.drag_indicator,
shadow: shadow ?? this.shadow,
errorBannerBg: errorBannerBg ?? this.errorBannerBg,
me: me ?? this.me,
);
}
@@ -161,6 +166,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
shadow: Color.lerp(shadow, other.shadow, t),
errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
me: Color.lerp(shadow, other.me, t),
);
}
}
@@ -266,11 +272,29 @@ class MyTheme {
: EdgeInsets.only(left: dialogPadding / 3);
static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData(
thickness: MaterialStateProperty.all(kScrollbarThickness),
thickness: MaterialStateProperty.all(6),
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[900];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[700];
} else {
return Colors.grey[500];
}
}),
crossAxisMargin: 4,
);
static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[100];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[300];
} else {
return Colors.grey[500];
}
}),
);
static ThemeData lightTheme = ThemeData(
@@ -971,11 +995,22 @@ void msgBox(SessionID sessionId, String type, String title, String text,
}));
}
if (reconnect != null && title == "Connection Error") {
buttons.insert(
0,
dialogButton('Reconnect', isOutline: true, onPressed: () {
reconnect(dialogManager, sessionId, false);
}));
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button);
}
if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
@@ -2303,7 +2338,7 @@ String getWindowName({WindowType? overrideType}) {
}
String getWindowNameWithId(String id, {WindowType? overrideType}) {
return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}";
return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}";
}
Future<void> updateSystemWindowTheme() async {
@@ -2536,3 +2571,21 @@ Widget buildErrorBanner(BuildContext context,
)).marginOnly(bottom: 14),
));
}
String getDesktopTabLabel(String peerId, String alias) {
String label = alias.isEmpty ? peerId : alias;
try {
String peer = bind.mainGetPeerSync(id: peerId);
Map<String, dynamic> config = jsonDecode(peer);
if (config['info']['hostname'] is String) {
String hostname = config['info']['hostname'];
if (hostname.isNotEmpty &&
!label.toLowerCase().contains(hostname.toLowerCase())) {
label += "@$hostname";
}
}
} catch (e) {
debugPrint("Failed to get hostname:$e");
}
return label;
}

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
@@ -156,20 +159,31 @@ class _AddressBookState extends State<AddressBook> {
} else {
tags = gFFI.abModel.tags;
}
return Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: gFFI.abModel.selectedTags,
onTap: () {
if (gFFI.abModel.selectedTags.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
}))
.toList(),
);
tagBuilder(String e) {
return AddressBookTag(
name: e,
tags: gFFI.abModel.selectedTags,
onTap: () {
if (gFFI.abModel.selectedTags.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
});
}
final gridView = DynamicGridView.builder(
shrinkWrap: isMobile,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
final e = tags[index];
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
});
}

View File

@@ -711,6 +711,13 @@ void showWaitUacDialog(
(setState, close, context) => CustomAlertDialog(
title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
actions: [
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
));
}
@@ -931,7 +938,7 @@ void showElevationError(SessionID sessionId, String type, String title,
dialogButton('Cancel', onPressed: () {
close();
}, isOutline: true),
dialogButton('Retry', onPressed: submit),
if (text != 'No permission') dialogButton('Retry', onPressed: submit),
],
onSubmit: submit,
onCancel: close,

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart';
@@ -120,6 +122,7 @@ class _MyGroupState extends State<MyGroup> {
}
Widget _buildLeftHeader() {
final fontSize = 14.0;
return Row(
children: [
Expanded(
@@ -128,16 +131,16 @@ class _MyGroupState extends State<MyGroup> {
onChanged: (value) {
searchUserText.value = value;
},
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
filled: false,
prefixIcon: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
).paddingOnly(top: 2),
hintText: translate("Search"),
hintStyle:
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
hintStyle: TextStyle(fontSize: fontSize),
border: InputBorder.none,
isDense: true,
),
@@ -148,16 +151,22 @@ class _MyGroupState extends State<MyGroup> {
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))
.toList());
final items = gFFI.groupModel.users.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchUserText.value.toLowerCase());
}
return true;
}).toList();
final listView = ListView.builder(
shrinkWrap: isMobile,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
});
}
@@ -173,6 +182,7 @@ class _MyGroupState extends State<MyGroup> {
() {
bool selected = selectedUser.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
@@ -184,9 +194,42 @@ class _MyGroupState extends State<MyGroup> {
child: Container(
child: Row(
children: [
Icon(Icons.person_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4),
Expanded(child: Text(isMe ? translate('Me') : username)),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: str2color(username, 0xAF),
shape: BoxShape.circle,
),
child: Align(
alignment: Alignment.center,
child: Center(
child: Text(
username.characters.first.toUpperCase(),
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
).marginOnly(right: 4),
if (isMe) Flexible(child: Text(username)),
if (isMe)
Flexible(
child: Container(
margin: EdgeInsets.only(left: 5),
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: colorMe.withAlpha(20),
borderRadius: BorderRadius.all(Radius.circular(2)),
border: Border.all(color: colorMe.withAlpha(100))),
child: Text(
translate('Me'),
style: TextStyle(
color: colorMe.withAlpha(200), fontSize: 12),
),
),
),
if (!isMe) Expanded(child: Text(username)),
],
).paddingSymmetric(vertical: 4),
),

View File

@@ -863,12 +863,12 @@ class RecentPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -917,12 +917,12 @@ class FavoritePeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -971,12 +971,12 @@ class DiscoveredPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
@@ -1021,12 +1021,12 @@ class AddressBookPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -1089,18 +1089,18 @@ class MyGroupPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
// menuItems.add(MenuEntryDivider());
// menuItems.add(_renameAction(peer.id));
// if (await bind.mainPeerHasPassword(id: peer.id)) {
// menuItems.add(_unrememberPasswordAction(peer.id));

View File

@@ -15,8 +15,10 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:pull_down_button/pull_down_button.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
@@ -110,43 +112,14 @@ class _PeerTabPageState extends State<PeerTabPage>
Expanded(
child:
visibleContextMenuListener(_createSwitchBar(context))),
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.group,
loading: gFFI.groupModel.groupLoading),
_createMultiSelection(),
Offstage(
offstage: !isDesktop,
child: _createPeerViewTypeSwitch(context)),
Offstage(
offstage: gFFI.peerTabModel.currentTab == 0,
child: PeerSortDropdown(),
),
Offstage(
offstage: gFFI.peerTabModel.currentTab != 3,
child: _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel",
value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
},
),
),
if (isMobile)
..._mobileRightActions(context)
else
..._desktopRightActions(context)
],
)),
),
),
).paddingOnly(right: isDesktop ? 12 : 0),
_createPeersView(),
],
);
@@ -270,17 +243,20 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createMultiSelection() {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context);
if (model.currentTabCachedPeers.isEmpty) return Offstage();
return _hoverAction(
context: context,
onTap: () {
model.setMultiSelectionMode(true);
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
},
child: Tooltip(
message: translate('Select'),
child: Icon(
IconFont.checkbox,
size: 18,
child: SvgPicture.asset(
"assets/checkbox-outline.svg",
width: 18,
height: 18,
color: textColor,
)),
);
@@ -564,6 +540,130 @@ class _PeerTabPageState extends State<PeerTabPage>
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 6);
}
Widget _toggleTags() {
return _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
});
}
List<Widget> _desktopRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage(
offstage: model.currentTabCachedPeers.isEmpty,
child: _createMultiSelection(),
),
_createPeerViewTypeSwitch(context),
Offstage(
offstage: model.currentTab == PeerTabIndex.recent.index,
child: PeerSortDropdown(),
),
Offstage(
offstage: model.currentTab != PeerTabIndex.ab.index,
child: _toggleTags(),
),
];
}
List<Widget> _mobileRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120;
final otherActionWidth = 18 + 10;
dropDown(List<Widget> menus) {
final padding = 6.0;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return PullDownButton(
buttonBuilder:
(BuildContext context, Future<void> Function() showMenu) {
return _hoverAction(
context: context,
child: Tooltip(
message: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
color: textColor,
)),
onTap: showMenu,
);
},
routeTheme: PullDownMenuRouteTheme(
width: menus.length * (otherActionWidth + padding * 2) * 1.0),
itemBuilder: (context) => [
PullDownMenuEntryImpl(
child: Row(
mainAxisSize: MainAxisSize.min,
children: menus
.map((e) =>
Material(child: e.paddingSymmetric(horizontal: padding)))
.toList(),
),
)
],
);
}
// Always show search, refresh
List<Widget> actions = [
const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
if (model.currentTab == PeerTabIndex.group.index)
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
];
final List<Widget> dynamicActions = [
if (model.currentTabCachedPeers.isNotEmpty) _createMultiSelection(),
if (model.currentTab != PeerTabIndex.recent.index) PeerSortDropdown(),
if (model.currentTab == PeerTabIndex.ab.index) _toggleTags()
];
final rightWidth = availableWidth -
searchWidth -
(actions.length == 2 ? otherActionWidth : 0);
final availablePositions = rightWidth ~/ otherActionWidth;
debugPrint(
"dynamic action count:${dynamicActions.length}, available positions: $availablePositions");
if (availablePositions < dynamicActions.length &&
dynamicActions.length > 1) {
if (availablePositions < 2) {
actions.addAll([
dropDown(dynamicActions),
]);
} else {
actions.addAll([
...dynamicActions.sublist(0, availablePositions - 1),
dropDown(dynamicActions.sublist(availablePositions - 1)),
]);
}
} else {
actions.addAll(dynamicActions);
}
return actions;
}
}
class PeerSearchBar extends StatefulWidget {
@@ -839,3 +939,14 @@ Widget _hoverAction(
child: Container(padding: padding, child: child))),
);
}
class PullDownMenuEntryImpl extends StatelessWidget
implements PullDownMenuEntry {
final Widget child;
const PullDownMenuEntryImpl({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart';
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';
@@ -95,6 +96,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return width;
}();
final _scrollController = ScrollController();
_PeersViewState() {
_startCheckOnlines();
}
@@ -176,31 +179,52 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return FutureBuilder<List<Peer>>(
builder: (context, snapshot) {
if (snapshot.hasData) {
final peers = snapshot.data!;
var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
final child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peers[index].id)),
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peers[index]),
);
return isDesktop
? Obx(
() => SizedBox(
width: 220,
height: peerCardUiType.value == PeerUiType.grid
? 140
: 42,
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild);
},
);
buildOnePeer(Peer peer) {
final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer),
);
return isDesktop
? Obx(
() => SizedBox(
width: 220,
height:
peerCardUiType.value == PeerUiType.grid ? 140 : 42,
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild);
}
final Widget child;
if (isMobile) {
child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
},
);
} else {
child = DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
);
}
if (updateEvent == UpdateEvent.load) {
_curPeers.clear();
_curPeers.addAll(peers.map((e) => e.id));

View File

@@ -93,6 +93,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (handleTouch) {
// Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapDown(MouseButtons.left);
}
@@ -112,7 +113,10 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
inputModel.tap(MouseButtons.left);
if (!handleTouch) {
// Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left);
}
}
onDoubleTapDown(TapDownDetails d) {

View File

@@ -49,7 +49,8 @@ class TToggleMenu {
handleOsPasswordEditIcon(
SessionID sessionId, OverlayDialogManager dialogManager) {
isEditOsPassword = true;
showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false);
showSetOSPassword(
sessionId, false, dialogManager, null, () => isEditOsPassword = false);
}
handleOsPasswordAction(
@@ -62,7 +63,8 @@ handleOsPasswordAction(
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
'';
if (password.isEmpty) {
showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false);
showSetOSPassword(sessionId, true, dialogManager, password,
() => isEditOsPassword = false);
} else {
bind.sessionInputOsPassword(sessionId: sessionId, value: password);
}
@@ -76,7 +78,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
List<TTextMenu> v = [];
// elevation
if (ffi.elevationModel.showRequestMenu) {
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
v.add(
TTextMenu(
child: Text(translate('Request Elevation')),

View File

@@ -7,7 +7,6 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -33,9 +32,6 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar.
final _idController = IDTextEditingController();
/// Nested scroll controller
final _scrollController = ScrollController();
Timer? _updateTimer;
final RxBool _idInputFocused = false.obs;
@@ -121,30 +117,18 @@ class _ConnectionPageState extends State<ConnectionPage>
return Column(
children: [
Expanded(
child: DesktopScrollWrapper(
scrollController: _scrollController,
child: CustomScrollView(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
])),
SliverFillRemaining(
hasScrollBody: true,
child: PeerTabPage().paddingOnly(right: 12.0),
)
child: Column(
children: [
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).paddingOnly(left: 12.0),
),
),
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
Expanded(child: PeerTabPage()),
],
).paddingOnly(left: 12.0)),
const Divider(height: 1),
buildStatus()
],

View File

@@ -499,13 +499,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override
void initState() {
super.initState();
Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl != url) {
updateUrl = url;
setState(() {});
}
final error = await bind.mainGetError();
if (systemError != error) {
systemError = error;

View File

@@ -88,6 +88,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
selectedIndex.value = controller.page!.toInt();
}
});
}
@override
@@ -154,7 +159,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller,
child: PageView(
controller: controller,
physics: DraggableNeverScrollableScrollPhysics(),
physics: NeverScrollableScrollPhysics(),
children: _children(),
)),
),
@@ -330,9 +335,11 @@ class _GeneralState extends State<_General> {
child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'),
));
children.add(
_OptionCheckBox(context, 'Check for software update on startup','enable-check-update',
isServer: false,
children.add(_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
));
if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox(

View File

@@ -99,7 +99,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -266,7 +266,7 @@ class _PortForwardPageState extends State<PortForwardPage>
}
void refreshTunnelConfig() async {
String peer = await bind.mainGetPeer(id: widget.id);
String peer = bind.mainGetPeerSync(id: widget.id);
Map<String, dynamic> config = jsonDecode(peer);
List<dynamic> infos = config['port_forwards'] as List;
List<_PortForward> result = List.empty(growable: true);

View File

@@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true;
},
tail: AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -209,7 +209,7 @@ class _RemotePageState extends State<RemotePage>
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
await _renderTexture.destroy(closeSession);
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
_ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
@@ -329,7 +329,7 @@ class _RemotePageState extends State<RemotePage>
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
bind.sessionEnterOrLeave(sessionId: sessionId, enter: true);
_ffi.inputModel.enterOrLeave(true);
}
}
@@ -349,7 +349,7 @@ class _RemotePageState extends State<RemotePage>
}
// See [onWindowBlur].
if (!Platform.isWindows) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
_ffi.inputModel.enterOrLeave(false);
}
}
@@ -614,7 +614,7 @@ class _ImagePaintState extends State<ImagePaint> {
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always

View File

@@ -210,7 +210,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
@@ -415,8 +415,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
await WindowController.fromWindowId(windowId()).close();
stateGlobal.setFullscreen(false, procWnd: false);
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);
_update_remote_count();

View File

@@ -175,34 +175,47 @@ class ConnectionManagerState extends State<ConnectionManager> {
],
);
},
pageViewBuilder: (pageView) => Row(
children: [
Consumer<ChatModel>(
builder: (_, model, child) => model.isShowCMSidePage
? Expanded(
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
flex: (kConnectionManagerWindowSizeOpenChat.width -
kConnectionManagerWindowSizeClosedChat
.width)
.toInt(),
)
: Offstage(),
),
Expanded(
child: pageView,
flex: kConnectionManagerWindowSizeClosedChat.width
.toInt() -
4 // prevent stretch of the page view when chat is open,
),
],
pageViewBuilder: (pageView) => LayoutBuilder(
builder: (context, constrains) {
var borderWidth = 0.0;
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width) {
borderWidth = kConnectionManagerWindowSizeOpenChat.width -
constrains.maxWidth;
} else {
borderWidth = kConnectionManagerWindowSizeClosedChat.width -
constrains.maxWidth;
}
if (borderWidth < 0 || borderWidth > 50) {
borderWidth = 0;
}
final realClosedWidth =
kConnectionManagerWindowSizeClosedChat.width -
borderWidth;
final realChatPageWidth =
constrains.maxWidth - realClosedWidth;
return Row(children: [
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width)
Consumer<ChatModel>(
builder: (_, model, child) => SizedBox(
width: realChatPageWidth,
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
)),
SizedBox(
width: realClosedWidth,
child:
SizedBox(width: realClosedWidth, child: pageView)),
]);
},
),
),
);
@@ -966,8 +979,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
return PreferredSize(
preferredSize: const Size(200, double.infinity),
child: Container(
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(12.0),
child: Obx(
() {
final jobTable = gFFI.cmFileModel.currentJobTable;

View File

@@ -1053,10 +1053,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display;
List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland();
@override
void initState() {
super.initState();
_getLocalResolutionWayland();
}
@override
@@ -1065,7 +1067,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage();
_getLocalResolution();
final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
@@ -1101,6 +1102,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
);
}
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
}
_getLocalResolution() {
_localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay();

View File

@@ -8,7 +8,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
@@ -267,13 +266,9 @@ class DesktopTab extends StatelessWidget {
tabType == DesktopTabType.install;
}
static RxString labelGetterAlias(String peerId) {
final opt = 'alias';
PeerStringOption.init(peerId, opt, () {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: opt);
return alias.isEmpty ? peerId : alias;
});
return PeerStringOption.find(peerId, opt);
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
@@ -921,14 +916,17 @@ class _TabState extends State<_Tab> with RestorationMixin {
final labelWidget = Obx(() {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
child: Tooltip(
message: translate(widget.label.value),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
),
));
});

View File

@@ -125,8 +125,7 @@ void runMainApp(bool startService) async {
bind.pluginSyncUi(syncTo: kAppTypeMain);
bind.pluginListReload();
}
gFFI.abModel.loadCache();
gFFI.groupModel.loadCache();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
// Set window option.
@@ -154,8 +153,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath();
gFFI.abModel.loadCache();
gFFI.groupModel.loadCache();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
}
@@ -236,19 +234,24 @@ void runConnectionManagerScreen(bool hide) async {
listenUniLinks(handleByFlutter: false);
}
bool _isCmReadyToShow = false;
showCmWindow({bool isStartup = false}) async {
if (isStartup) {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
bind.mainHideDocker();
await windowManager.show();
await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]);
// ensure initial window size to be changed
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
});
} else {
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await Future.wait([
windowManager.show(),
windowManager.focus(),
windowManager.setOpacity(1)
]);
// ensure initial window size to be changed
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 1) {
await windowManager.setOpacity(1);
await windowManager.focus();
@@ -265,12 +268,12 @@ hideCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0);
windowManager.waitUntilReadyToShow(windowOptions, () async {
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
});
} else {
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0);
bind.mainHideDocker();

View File

@@ -57,7 +57,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}();
}
if (isAndroid) {
Timer(const Duration(seconds: 5), () async {
Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {});
});
@@ -80,7 +80,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
_buildRemoteIDTextField(),
])),
SliverFillRemaining(
hasScrollBody: false,
hasScrollBody: true,
child: PeerTabPage(),
)
],

View File

@@ -421,6 +421,9 @@ class _RemotePageState extends State<RemotePage> {
);
}
bool get showCursorPaint =>
!gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
Widget getBodyForMobile() {
final keyboardIsVisible = keyboardVisibilityController.isVisible;
return Container(
@@ -453,7 +456,7 @@ class _RemotePageState extends State<RemotePage> {
),
),
];
if (!gFFI.canvasModel.cursorEmbedded) {
if (showCursorPaint) {
paints.add(CursorPaint());
}
return paints;
@@ -462,7 +465,7 @@ class _RemotePageState extends State<RemotePage> {
Widget getBodyForDesktopWithListener(bool keyboard) {
var paints = <Widget>[ImagePaint()];
if (!gFFI.canvasModel.cursorEmbedded) {
if (showCursorPaint) {
final cursor = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
if (keyboard || cursor) {
@@ -737,8 +740,8 @@ class CursorPaint extends StatelessWidget {
return CustomPaint(
painter: ImagePainter(
image: m.image ?? preDefaultCursor.image,
x: m.x * s - hotx * s + c.x,
y: m.y * s - hoty * s + c.y - adjust,
x: m.x * s - hotx + c.x,
y: m.y * s - hoty + c.y - adjust,
scale: 1),
);
}

View File

@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
@@ -105,9 +104,6 @@ class AbModel {
if (!quiet) {
pullError.value =
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) {
BotToast.showText(contentColor: Colors.red, text: pullError.value);
}
}
} finally {
abLoading.value = false;
@@ -132,6 +128,7 @@ class AbModel {
'alias': alias,
'tags': tags,
});
_mergePeerFromGroup(peer);
peers.add(peer);
}
@@ -480,7 +477,7 @@ class AbModel {
}
}
loadCache() async {
Future<void> loadCache() async {
try {
if (_cacheLoadOnceFlag || abLoading.value || initialized) return;
_cacheLoadOnceFlag = true;
@@ -573,4 +570,18 @@ class AbModel {
peers.clear();
await bind.mainClearAb();
}
_mergePeerFromGroup(Peer p) {
final g = gFFI.groupModel.peers.firstWhereOrNull((e) => p.id == e.id);
if (g == null) return;
if (p.username.isEmpty) {
p.username = g.username;
}
if (p.hostname.isEmpty) {
p.hostname = g.hostname;
}
if (p.platform.isEmpty) {
p.platform = g.platform;
}
}
}

View File

@@ -285,7 +285,10 @@ class ChatModel with ChangeNotifier {
await toggleCMSidePage();
}
var _togglingCMSidePage = false; // protect order for await
toggleCMSidePage() async {
if (_togglingCMSidePage) return false;
_togglingCMSidePage = true;
if (_isShowCMSidePage) {
_isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
@@ -300,6 +303,7 @@ class ChatModel with ChangeNotifier {
_isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
}
_togglingCMSidePage = false;
}
changeCurrentKey(MessageKey key) {

View File

@@ -133,7 +133,8 @@ class GroupModel {
return true;
} catch (err) {
debugPrint('get accessible users: $err');
groupLoadError.value = err.toString();
groupLoadError.value =
'${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
@@ -172,9 +173,6 @@ class GroupModel {
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (total > 1000) {
total = 1000;
}
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
@@ -187,9 +185,6 @@ class GroupModel {
} else {
tmpPeers[index] = peer;
}
if (tmpPeers.length >= 1000) {
break;
}
}
}
}
@@ -198,7 +193,8 @@ class GroupModel {
return true;
} catch (err) {
debugPrint('get accessible peers: $err');
groupLoadError.value = err.toString();
groupLoadError.value =
'${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
@@ -229,7 +225,7 @@ class GroupModel {
}
}
loadCache() async {
Future<void> loadCache() async {
try {
if (_cacheLoadOnceFlag || groupLoading.value || initialized) return;
_cacheLoadOnceFlag = true;

View File

@@ -202,10 +202,12 @@ class FfiModel with ChangeNotifier {
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
handlePeerInfo(data.peerInfo, peerId);
for (var element in data.cursorDataList) {
handleCursorData(element);
await handlePeerInfo(data.peerInfo, peerId);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
await handleCursorData(element);
}
updateLastCursorId(data.lastCursorId);
handleCursorId(data.lastCursorId);
}
@@ -225,9 +227,11 @@ class FfiModel with ChangeNotifier {
} else if (name == 'switch_display') {
handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') {
updateLastCursorId(evt);
await handleCursorData(evt);
} else if (name == 'cursor_id') {
await handleCursorId(evt);
updateLastCursorId(evt);
handleCursorId(evt);
} else if (name == 'cursor_position') {
await parent.target?.cursorModel.updateCursorPosition(evt, peerId);
} else if (name == 'clipboard') {
@@ -265,8 +269,6 @@ class FfiModel with ChangeNotifier {
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, sessionId, peerId);
} else if (name == 'alias') {
handleAliasChanged(evt);
} else if (name == 'show_elevation') {
final show = evt['show'].toString() == 'true';
parent.target?.serverModel.setShowElevation(show);
@@ -352,13 +354,6 @@ class FfiModel with ChangeNotifier {
platformFFI.setEventCallback(startEventListener(sessionId, peerId));
}
handleAliasChanged(Map<String, dynamic> evt) {
final rxAlias = PeerStringOption.find(evt['id'], 'alias');
if (rxAlias.value != evt['alias']) {
rxAlias.value = evt['alias'];
}
}
_updateCurDisplay(SessionID sessionId, Display newDisplay) {
if (newDisplay != _display) {
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
@@ -660,9 +655,13 @@ class FfiModel with ChangeNotifier {
return d;
}
handleCursorId(Map<String, dynamic> evt) async {
updateLastCursorId(Map<String, dynamic> evt) {
parent.target?.cursorModel.id = int.parse(evt['id']);
}
handleCursorId(Map<String, dynamic> evt) {
cachedPeerData.lastCursorId = evt;
await parent.target?.cursorModel.updateCursorId(evt);
parent.target?.cursorModel.updateCursorId(evt);
}
handleCursorData(Map<String, dynamic> evt) async {
@@ -1288,6 +1287,7 @@ class CursorModel with ChangeNotifier {
final _cacheKeys = <String>{};
double _x = -10000;
double _y = -10000;
int _id = -1;
double _hotx = 0;
double _hoty = 0;
double _displayOriginX = 0;
@@ -1296,7 +1296,7 @@ class CursorModel with ChangeNotifier {
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = '';
String peerId = '';
WeakReference<FFI> parent;
ui.Image? get image => _image;
@@ -1310,6 +1310,8 @@ class CursorModel with ChangeNotifier {
double get hotx => _hotx;
double get hoty => _hoty;
set id(int id) => _id = id;
bool get isPeerControlProtected =>
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec;
@@ -1448,32 +1450,33 @@ class CursorModel with ChangeNotifier {
}
updateCursorData(Map<String, dynamic> evt) async {
var id = int.parse(evt['id']);
_hotx = double.parse(evt['hotx']);
_hoty = double.parse(evt['hoty']);
var width = int.parse(evt['width']);
var height = int.parse(evt['height']);
final id = int.parse(evt['id']);
final hotx = double.parse(evt['hotx']);
final hoty = double.parse(evt['hoty']);
final width = int.parse(evt['width']);
final height = int.parse(evt['height']);
List<dynamic> colors = json.decode(evt['colors']);
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
final image = await img.decodeImageFromPixels(
rgba, width, height, ui.PixelFormat.rgba8888);
_image = image;
if (await _updateCache(rgba, image, id, width, height)) {
_images[id] = Tuple3(image, _hotx, _hoty);
} else {
_hotx = 0;
_hoty = 0;
}
try {
// my throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint('WARNING: updateCursorId $id, without notifyListeners(). $e');
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
_images[id] = Tuple3(image, hotx, hoty);
}
// Update last cursor data.
// Do not use the previous `image` and `id`, because `_id` may be changed.
_updateCurData();
}
Future<bool> _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async {
Uint8List rgba,
ui.Image image,
int id,
double hotx,
double hoty,
int w,
int h,
) async {
Uint8List? data;
img2.Image imgOrigin = img2.Image.fromBytes(
width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
@@ -1487,33 +1490,45 @@ class CursorModel with ChangeNotifier {
}
data = imgBytes.buffer.asUint8List();
}
_cache = CursorData(
peerId: this.id,
final cache = CursorData(
peerId: peerId,
id: id,
image: imgOrigin,
scale: 1.0,
data: data,
hotxOrigin: _hotx,
hotyOrigin: _hoty,
hotxOrigin: hotx,
hotyOrigin: hoty,
width: w,
height: h,
);
_cacheMap[id] = _cache!;
_cacheMap[id] = cache;
return true;
}
updateCursorId(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']);
_cache = _cacheMap[id];
final tmp = _images[id];
bool _updateCurData() {
_cache = _cacheMap[_id];
final tmp = _images[_id];
if (tmp != null) {
_image = tmp.item1;
_hotx = tmp.item2;
_hoty = tmp.item3;
notifyListeners();
try {
// may throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint(
'WARNING: updateCursorId $_id, without notifyListeners(). $e');
}
return true;
} else {
return false;
}
}
updateCursorId(Map<String, dynamic> evt) {
if (!_updateCurData()) {
debugPrint(
'WARNING: updateCursorId $id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()');
'WARNING: updateCursorId $_id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()');
}
}
@@ -1668,6 +1683,7 @@ class ElevationModel with ChangeNotifier {
bool get showRequestMenu => _canElevate && !_running;
onPeerInfo(PeerInfo pi) {
_canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false;
_running = false;
}
onPortableServiceRunning(Map<String, dynamic> evt) {
@@ -1756,7 +1772,7 @@ class FFI {
connType = ConnType.defaultConn;
canvasModel.id = id;
imageModel.id = id;
cursorModel.id = id;
cursorModel.peerId = id;
}
// If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
@@ -1797,7 +1813,7 @@ class FFI {
debugPrint('Unreachable, the cached data cannot be decoded.');
return;
}
ffiModel.handleCachedPeerData(data, id);
await ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId);
});
isToNewWindowNotified.value = true;

View File

@@ -328,7 +328,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: e51fddf7f3b46d4423b7aa79ba824a45a1ea1b7a
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -399,10 +399,10 @@ packages:
dynamic_layouts:
dependency: "direct main"
description:
path: "packages/dynamic_layouts"
ref: "0023d01996576e494094793a6552463f01c5627a"
resolved-ref: "0023d01996576e494094793a6552463f01c5627a"
url: "https://github.com/flutter/packages.git"
path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git"
source: git
version: "0.0.1+1"
event_bus:
@@ -1045,6 +1045,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
pull_down_button:
dependency: "direct main"
description:
name: pull_down_button
sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f"
url: "https://pub.dev"
source: hosted
version: "0.9.3"
puppeteer:
dependency: transitive
description:
@@ -1080,12 +1088,11 @@ packages:
screen_retriever:
dependency: transitive
description:
path: "."
ref: "406b9b0"
resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372"
url: "https://github.com/Kingtous/rustdesk_screen_retriever.git"
source: git
version: "0.1.2"
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
scroll_pos:
dependency: "direct main"
description:
@@ -1529,10 +1536,10 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "2c4b242e668acf4e652b09b13f650bcfbbaa3871"
resolved-ref: f19acdb008645366339444a359a45c3257c8b32e
url: "https://github.com/rustdesk-org/window_manager"
source: git
version: "0.3.4"
version: "0.3.6"
window_size:
dependency: "direct main"
description:

View File

@@ -102,9 +102,9 @@ dependencies:
flex_color_picker: ^3.3.0
dynamic_layouts:
git:
url: https://github.com/flutter/packages.git
path: packages/dynamic_layouts
ref: 0023d01996576e494094793a6552463f01c5627a
url: https://github.com/21pages/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3
dev_dependencies:
icons_launcher: ^2.0.4
@@ -157,9 +157,6 @@ flutter:
- family: AddressBook
fonts:
- asset: assets/address_book.ttf
- family: CheckBox
fonts:
- asset: assets/checkbox.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.

View File

@@ -16,7 +16,7 @@ final testClients = [
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
];
/// flutter run -d {platform} -t lib/cm_test.dart to test cm
/// flutter run -d {platform} -t test/cm_test.dart to test cm
void main(List<String> args) async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();