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

@@ -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')),