Merge pull request #1592 from Heap-Hop/mobile_feat_update_rebase

Update android
This commit is contained in:
RustDesk 2022-09-21 18:58:37 +08:00 committed by GitHub
commit c547e75b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 988 additions and 921 deletions

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

View File

@ -1035,3 +1035,29 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
}
return false;
}
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
void connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
FocusScopeNode currentFocus = FocusScope.of(context);
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
}
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}

View File

@ -0,0 +1,415 @@
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peer_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart';
import '../../models/platform_model.dart';
class AddressBook extends StatefulWidget {
const AddressBook({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _AddressBookState();
}
}
class _AddressBookState extends State<AddressBook> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.getAb());
}
@override
Widget build(BuildContext context) => FutureBuilder<Widget>(
future: buildAddressBook(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return const Offstage();
}
});
handleLogin() {
loginDialog().then((success) {
if (success) {
setState(() {});
}
});
}
Future<Widget> buildAddressBook(BuildContext context) async {
final token = await bind.mainGetLocalOption(key: 'access_token');
if (token.trim().isEmpty) {
return Center(
child: InkWell(
onTap: handleLogin,
child: Text(
translate("Login"),
style: const TextStyle(decoration: TextDecoration.underline),
),
),
);
}
final model = gFFI.abModel;
return FutureBuilder(
future: model.getAb(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return _buildAddressBook(context);
} else if (snapshot.hasError) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate("${snapshot.error}")),
TextButton(
onPressed: () {
setState(() {});
},
child: Text(translate("Retry")))
],
);
} else {
if (model.abLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (model.abError.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(model.abError)),
TextButton(
onPressed: () {
setState(() {});
},
child: Text(translate("Retry")))
],
),
);
} else {
return const Offstage();
}
}
});
}
Widget _buildAddressBook(BuildContext context) {
return Consumer<AbModel>(
builder: (context, model, child) => Row(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: MyTheme.grayBg)),
child: Container(
width: 200,
height: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(translate('Tags')),
InkWell(
child: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'add-id',
child: Text(translate("Add ID")),
),
PopupMenuItem(
value: 'add-tag',
child: Text(translate("Add Tag")),
),
PopupMenuItem(
value: 'unset-all-tag',
child: Text(
translate("Unselect all tags")),
),
],
onSelected: handleAbOp,
child: const Icon(Icons.more_vert_outlined)),
)
],
),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: MyTheme.darkGray)),
child: Obx(
() => Wrap(
children: gFFI.abModel.tags
.map((e) =>
buildTag(e, gFFI.abModel.selectedTags,
onTap: () {
//
if (gFFI.abModel.selectedTags
.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
}))
.toList(),
),
),
).marginSymmetric(vertical: 8.0),
)
],
),
),
).marginOnly(right: 8.0),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: AddressBookPeerWidget()),
)
],
));
}
Widget buildTag(String tagName, RxList<dynamic> rxTags, {Function()? onTap}) {
return ContextMenuArea(
width: 100,
builder: (context) => [
ListTile(
title: Text(translate("Delete")),
onTap: () {
gFFI.abModel.deleteTag(tagName);
gFFI.abModel.updateAb();
Future.delayed(Duration.zero, () => Get.back());
},
)
],
child: GestureDetector(
onTap: onTap,
child: Obx(
() => Container(
decoration: BoxDecoration(
color: rxTags.contains(tagName) ? Colors.blue : null,
border: Border.all(color: MyTheme.darkGray),
borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
child: Text(
tagName,
style: TextStyle(
color: rxTags.contains(tagName) ? MyTheme.white : null),
),
),
),
),
);
}
/// tag operation
void handleAbOp(String value) {
if (value == 'add-id') {
abAddId();
} else if (value == 'add-tag') {
abAddTag();
} else if (value == 'unset-all-tag') {
gFFI.abModel.unsetSelectedTags();
}
}
void abAddId() async {
var field = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
msg = "";
isInProgress = true;
});
field = controller.text.trim();
if (field.isEmpty) {
// pass
} else {
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
field = ids.join(',');
for (final newId in ids) {
if (gFFI.abModel.idContainBy(newId)) {
continue;
}
gFFI.abModel.addId(newId);
}
await gFFI.abModel.updateAb();
this.setState(() {});
// final currentPeers
}
close();
}
return CustomAlertDialog(
title: Text(translate("Add ID")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
focusNode: FocusNode()..requestFocus()),
),
],
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
void abAddTag() async {
var field = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
msg = "";
isInProgress = true;
});
field = controller.text.trim();
if (field.isEmpty) {
// pass
} else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(',');
for (final tag in tags) {
gFFI.abModel.addTag(tag);
}
await gFFI.abModel.updateAb();
// final currentPeers
}
close();
}
return CustomAlertDialog(
title: Text(translate("Add Tag")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
focusNode: FocusNode()..requestFocus(),
),
),
],
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
void abEditTag(String id) {
var isInProgress = false;
final tags = List.of(gFFI.abModel.tags);
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
}
return CustomAlertDialog(
title: Text(translate("Edit Tag")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Wrap(
children: tags
.map((e) => buildTag(e, selectedTag, onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
}))
.toList(growable: false),
),
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
}

View File

@ -0,0 +1,276 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peer_widget.dart';
import 'package:flutter_hbb/common/widgets/peercard_widget.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
class PeerTabPage extends StatefulWidget {
final List<String> tabs;
final List<Widget> children;
const PeerTabPage({required this.tabs, required this.children, Key? key})
: super(key: key);
@override
State<PeerTabPage> createState() => _PeerTabPageState();
}
class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin {
late final PageController _pageController = PageController();
final RxInt _tabIndex = 0.obs;
@override
void initState() {
() async {
await bind.mainGetLocalOption(key: 'peer-tab-index').then((value) {
if (value == '') return;
final tab = int.parse(value);
_tabIndex.value = tab;
_pageController.jumpToPage(tab);
});
await bind.mainGetLocalOption(key: 'peer-card-ui-type').then((value) {
if (value == '') return;
final tab = int.parse(value);
peerCardUiType.value =
tab == PeerUiType.list.index ? PeerUiType.list : PeerUiType.grid;
});
}();
super.initState();
}
// hard code for now
Future<void> _handleTabSelection(int index) async {
// reset search text
peerSearchText.value = "";
peerSearchTextController.clear();
_tabIndex.value = index;
await bind.mainSetLocalOption(
key: 'peer-tab-index', value: index.toString());
_pageController.jumpToPage(index);
switch (index) {
case 0:
bind.mainLoadRecentPeers();
break;
case 1:
bind.mainLoadFavPeers();
break;
case 2:
bind.mainDiscover();
break;
case 3:
gFFI.abModel.getAb();
break;
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 28,
child: Container(
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
constraints: isDesktop ? null : kMobilePageConstraints,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: _createTabBar(context)),
const SizedBox(width: 10),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
child: _createPeerViewTypeSwitch(context)
.marginOnly(left: 13)),
],
)),
),
_createTabBarView(),
],
);
}
Widget _createTabBar(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: ScrollController(),
children: super.widget.tabs.asMap().entries.map((t) {
return Obx(() => InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: _tabIndex.value == t.key
? MyTheme.color(context).bg
: null,
borderRadius: BorderRadius.circular(2),
),
child: Align(
alignment: Alignment.center,
child: Text(
t.value,
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: _tabIndex.value == t.key
? MyTheme.color(context).text
: MyTheme.color(context).lightText),
),
)),
onTap: () async => await _handleTabSelection(t.key),
));
}).toList());
}
Widget _createTabBarView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: PageView(
physics: isDesktop
? NeverScrollableScrollPhysics()
: BouncingScrollPhysics(),
controller: _pageController,
children: super.widget.children,
onPageChanged: (to) => _tabIndex.value = to)
.marginSymmetric(vertical: verticalMargin));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
final activeDeco = BoxDecoration(color: MyTheme.color(context).bg);
return Row(
children: [PeerUiType.grid, PeerUiType.list]
.map((type) => Obx(
() => Container(
padding: EdgeInsets.all(4.0),
decoration: peerCardUiType.value == type ? activeDeco : null,
child: InkWell(
onTap: () async {
await bind.mainSetLocalOption(
key: 'peer-card-ui-type',
value: type.index.toString());
peerCardUiType.value = type;
},
child: Icon(
type == PeerUiType.grid
? Icons.grid_view_rounded
: Icons.list,
size: 18,
color: peerCardUiType.value == type
? MyTheme.color(context).text
: MyTheme.color(context).lightText,
)),
),
))
.toList(),
);
}
}
class PeerSearchBar extends StatefulWidget {
const PeerSearchBar({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _PeerSearchBarState();
}
class _PeerSearchBarState extends State<PeerSearchBar> {
var drawer = false;
@override
Widget build(BuildContext context) {
return drawer
? _buildSearchBar()
: IconButton(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 2),
onPressed: () {
setState(() {
drawer = true;
});
},
icon: const Icon(
Icons.search_rounded,
color: MyTheme.dark,
));
}
Widget _buildSearchBar() {
RxBool focused = false.obs;
FocusNode focusNode = FocusNode();
focusNode.addListener(() => focused.value = focusNode.hasFocus);
return Container(
width: 120,
decoration: BoxDecoration(
color: MyTheme.color(context).bg,
borderRadius: BorderRadius.circular(6),
),
child: Obx(() => Row(
children: [
Expanded(
child: Row(
children: [
Icon(
Icons.search_rounded,
color: MyTheme.color(context).placeholder,
).marginSymmetric(horizontal: 4),
Expanded(
child: TextField(
autofocus: true,
controller: peerSearchTextController,
onChanged: (searchText) {
peerSearchText.value = searchText;
},
focusNode: focusNode,
textAlign: TextAlign.start,
maxLines: 1,
cursorColor: MyTheme.color(context).lightText,
cursorHeight: 18,
cursorWidth: 1,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 6),
hintText:
focused.value ? null : translate("Search ID"),
hintStyle: TextStyle(
fontSize: 14,
color: MyTheme.color(context).placeholder),
border: InputBorder.none,
isDense: true,
),
),
),
// Icon(Icons.close),
IconButton(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 2),
onPressed: () {
setState(() {
peerSearchTextController.clear();
peerSearchText.value = "";
drawer = false;
});
},
icon: const Icon(
Icons.close,
color: MyTheme.dark,
)),
],
),
)
],
)),
);
}
}

View File

@ -40,7 +40,7 @@ class _PeerWidget extends StatefulWidget {
/// State for the peer widget.
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();
@ -49,6 +49,17 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
var _queryCoun = 0;
var _exit = false;
late final mobileWidth = () {
const minWidth = 320.0;
final windowWidth = MediaQuery.of(context).size.width;
var width = windowWidth - 2 * space;
if (windowWidth > minWidth + 2 * space) {
final n = (windowWidth / (minWidth + 2 * space)).floor();
width = windowWidth / n - 2 * space;
}
return width;
}();
_PeerWidgetState() {
_startCheckOnlines();
}
@ -78,7 +89,6 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
@override
Widget build(BuildContext context) {
const space = 12.0;
return ChangeNotifierProvider<Peers>(
create: (context) => widget.peers,
child: Consumer<Peers>(
@ -86,33 +96,22 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
? Center(
child: Text(translate("Empty")),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
child: ObxValue<RxString>((searchText) {
: _buildPeersView(peers)),
);
}
Widget _buildPeersView(Peers peers) {
final body = 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: VisibilityDetector(
final visibilityChild = VisibilityDetector(
key: ValueKey(peer.id),
onVisibilityChanged: (info) {
final peerId =
(info.key as ValueKey).value;
final peerId = (info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId);
} else {
@ -121,14 +120,23 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
_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
? 140
: 42,
child: visibilityChild,
),
),
)));
)
: SizedBox(width: mobileWidth, child: visibilityChild)));
}
return Wrap(
spacing: space,
runSpacing: space,
children: cards);
return Wrap(spacing: space, runSpacing: space, children: cards);
} else {
return const Center(
child: CircularProgressIndicator(),
@ -137,11 +145,23 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
},
future: matchPeers(searchText.value, peers.peers),
);
}, peerSearchText),
),
),
),
}, peerSearchText);
if (isDesktop) {
return DesktopScrollWrapper(
scrollController: _scrollController,
child: SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
child: body),
);
} else {
return SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: _scrollController,
child: body,
);
}
}
// ignore: todo
@ -281,6 +301,7 @@ class AddressBookPeerWidget extends BasePeerWidget {
);
static List<Peer> _loadPeers() {
debugPrint("_loadPeers : ${gFFI.abModel.peers.toString()}");
return gFFI.abModel.peers.map((e) {
return Peer.fromJson(e['id'], e);
}).toList();
@ -304,7 +325,7 @@ class AddressBookPeerWidget extends BasePeerWidget {
@override
Widget build(BuildContext context) {
final widget = super.build(context);
gFFI.abModel.updateAb();
// gFFI.abModel.updateAb();
return widget;
}
}

View File

@ -8,9 +8,8 @@ import '../../common/formatter/id_formatter.dart';
import '../../models/model.dart';
import '../../models/peer_model.dart';
import '../../models/platform_model.dart';
import './material_mod_popup_menu.dart' as mod_menu;
import './popup_menu.dart';
import './utils.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
class _PopupMenuTheme {
static const Color commonColor = MyTheme.accent;
@ -55,6 +54,50 @@ class _PeerCardState extends State<_PeerCard>
@override
Widget build(BuildContext context) {
super.build(context);
if (isDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
}
}
Widget _buildMobile() {
final peer = super.widget.peer;
return Card(
margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector(
onTap: !isWebDesktop ? () => connect(context, peer.id) : null,
onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null,
onLongPressStart: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
_showPeerMenu(peer.id);
},
child: ListTile(
contentPadding: const EdgeInsets.only(left: 12),
subtitle: Text('${peer.username}@${peer.hostname}'),
title: Text(formatID(peer.id)),
leading: Container(
padding: const EdgeInsets.all(6),
color: str2color('${peer.id}${peer.platform}', 0x7f),
child: getPlatformImage(peer.platform)),
trailing: InkWell(
child: const Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.more_vert)),
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () {
_showPeerMenu(peer.id);
}),
)));
}
Widget _buildDesktop() {
final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>(BoxDecoration(
border: Border.all(color: Colors.transparent, width: _borderWidth),
@ -264,7 +307,7 @@ class _PeerCardState extends State<_PeerCard>
final y = e.position.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onPointerUp: (_) => _showPeerMenu(context, peer.id),
onPointerUp: (_) => _showPeerMenu(peer.id),
child: MouseRegion(
onEnter: (_) => _iconMoreHover.value = true,
onExit: (_) => _iconMoreHover.value = false,
@ -281,7 +324,7 @@ class _PeerCardState extends State<_PeerCard>
/// Show the peer menu and handle user's choice.
/// User might remove the peer or send a file to the peer.
void _showPeerMenu(BuildContext context, String id) async {
void _showPeerMenu(String id) async {
await mod_menu.showMenu(
context: context,
position: _menuPos,

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page'
@ -24,6 +26,8 @@ const kWindowEdgeSize = 1.0;
const kInvalidValueStr = "InvalidValueStr";
const kMobilePageConstraints = BoxConstraints(maxWidth: 600);
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map<int, String> logicalKeyMap = <int, String>{

View File

@ -3,17 +3,15 @@
import 'dart:async';
import 'dart:convert';
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/widgets/peer_widget.dart';
import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peer_widget.dart';
import '../../models/platform_model.dart';
/// Connection page for connecting to a remote peer.
@ -62,13 +60,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
children: [
Row(
children: [
getSearchBarUI(context),
_buildRemoteIDTextField(context),
],
).marginOnly(top: 22),
SizedBox(height: 12),
Divider(),
Expanded(
child: _PeerTabbedPage(
child: PeerTabPage(
tabs: [
translate('Recent Sessions'),
translate('Favorites'),
@ -79,15 +77,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
RecentPeerWidget(),
FavoritePeerWidget(),
DiscoveredPeerWidget(),
FutureBuilder<Widget>(
future: buildAddressBook(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return const Offstage();
}
}),
const AddressBook(),
],
)),
],
@ -104,28 +94,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
final id = _idController.id;
connect(id, isFileTransfer: isFileTransfer);
connect(context, id, isFileTransfer: isFileTransfer);
}
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
void connect(String id, {bool isFileTransfer = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
}
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
/// UI for the search bar.
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget getSearchBarUI(BuildContext context) {
Widget _buildRemoteIDTextField(BuildContext context) {
RxBool ftHover = false.obs;
RxBool ftPressed = false.obs;
RxBool connHover = false.obs;
@ -389,609 +363,4 @@ class _ConnectionPageState extends State<ConnectionPage> {
svcStatusCode.value = status["status_num"];
svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
}
handleLogin() {
loginDialog().then((success) {
if (success) {
setState(() {});
}
});
}
Future<Widget> buildAddressBook(BuildContext context) async {
final token = await bind.mainGetLocalOption(key: 'access_token');
if (token.trim().isEmpty) {
return Center(
child: InkWell(
onTap: handleLogin,
child: Text(
translate("Login"),
style: TextStyle(decoration: TextDecoration.underline),
),
),
);
}
final model = gFFI.abModel;
return FutureBuilder(
future: model.getAb(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return _buildAddressBook(context);
} else if (snapshot.hasError) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate("${snapshot.error}")),
TextButton(
onPressed: () {
setState(() {});
},
child: Text(translate("Retry")))
],
);
} else {
if (model.abLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (model.abError.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(model.abError)),
TextButton(
onPressed: () {
setState(() {});
},
child: Text(translate("Retry")))
],
),
);
} else {
return Offstage();
}
}
});
}
Widget _buildAddressBook(BuildContext context) {
return Row(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: MyTheme.grayBg)),
child: Container(
width: 200,
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(translate('Tags')),
InkWell(
child: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Text(translate("Add ID")),
value: 'add-id',
),
PopupMenuItem(
child: Text(translate("Add Tag")),
value: 'add-tag',
),
PopupMenuItem(
child: Text(translate("Unselect all tags")),
value: 'unset-all-tag',
),
],
onSelected: handleAbOp,
child: Icon(Icons.more_vert_outlined)),
)
],
),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: MyTheme.darkGray)),
child: Obx(
() => Wrap(
children: gFFI.abModel.tags
.map((e) => buildTag(e, gFFI.abModel.selectedTags,
onTap: () {
//
if (gFFI.abModel.selectedTags.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
}))
.toList(),
),
),
).marginSymmetric(vertical: 8.0),
)
],
),
),
).marginOnly(right: 8.0),
Expanded(
child: Align(
alignment: Alignment.topLeft, child: AddressBookPeerWidget()),
)
],
);
}
Widget buildTag(String tagName, RxList<dynamic> rxTags, {Function()? onTap}) {
return ContextMenuArea(
width: 100,
builder: (context) => [
ListTile(
title: Text(translate("Delete")),
onTap: () {
gFFI.abModel.deleteTag(tagName);
gFFI.abModel.updateAb();
Future.delayed(Duration.zero, () => Get.back());
},
)
],
child: GestureDetector(
onTap: onTap,
child: Obx(
() => Container(
decoration: BoxDecoration(
color: rxTags.contains(tagName) ? Colors.blue : null,
border: Border.all(color: MyTheme.darkGray),
borderRadius: BorderRadius.circular(10)),
margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
child: Text(
tagName,
style: TextStyle(
color: rxTags.contains(tagName) ? MyTheme.white : null),
),
),
),
),
);
}
/// tag operation
void handleAbOp(String value) {
if (value == 'add-id') {
abAddId();
} else if (value == 'add-tag') {
abAddTag();
} else if (value == 'unset-all-tag') {
gFFI.abModel.unsetSelectedTags();
}
}
void abAddId() async {
var field = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
msg = "";
isInProgress = true;
});
field = controller.text.trim();
if (field.isEmpty) {
// pass
} else {
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
field = ids.join(',');
for (final newId in ids) {
if (gFFI.abModel.idContainBy(newId)) {
continue;
}
gFFI.abModel.addId(newId);
}
await gFFI.abModel.updateAb();
this.setState(() {});
// final currentPeers
}
close();
}
return CustomAlertDialog(
title: Text(translate("Add ID")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
focusNode: FocusNode()..requestFocus()),
),
],
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
void abAddTag() async {
var field = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
msg = "";
isInProgress = true;
});
field = controller.text.trim();
if (field.isEmpty) {
// pass
} else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(',');
for (final tag in tags) {
gFFI.abModel.addTag(tag);
}
await gFFI.abModel.updateAb();
// final currentPeers
}
close();
}
return CustomAlertDialog(
title: Text(translate("Add Tag")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("whitelist_sep")),
const SizedBox(
height: 8.0,
),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
focusNode: FocusNode()..requestFocus(),
),
),
],
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
void abEditTag(String id) {
var isInProgress = false;
final tags = List.of(gFFI.abModel.tags);
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
}
return CustomAlertDialog(
title: Text(translate("Edit Tag")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Wrap(
children: tags
.map((e) => buildTag(e, selectedTag, onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
}))
.toList(growable: false),
),
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
}
}
class _PeerTabbedPage extends StatefulWidget {
final List<String> tabs;
final List<Widget> children;
const _PeerTabbedPage({required this.tabs, required this.children, Key? key})
: super(key: key);
@override
_PeerTabbedPageState createState() => _PeerTabbedPageState();
}
class _PeerTabbedPageState extends State<_PeerTabbedPage>
with SingleTickerProviderStateMixin {
late final PageController _pageController = PageController();
final RxInt _tabIndex = 0.obs;
@override
void initState() {
() async {
await bind.mainGetLocalOption(key: 'peer-tab-index').then((value) {
if (value == '') return;
final tab = int.parse(value);
_tabIndex.value = tab;
_pageController.jumpToPage(tab);
});
await bind.mainGetLocalOption(key: 'peer-card-ui-type').then((value) {
if (value == '') return;
final tab = int.parse(value);
peerCardUiType.value =
tab == PeerUiType.list.index ? PeerUiType.list : PeerUiType.grid;
});
}();
super.initState();
}
// hard code for now
Future<void> _handleTabSelection(int index) async {
if (index == _tabIndex.value) return;
// reset search text
peerSearchText.value = "";
peerSearchTextController.clear();
_tabIndex.value = index;
await bind.mainSetLocalOption(
key: 'peer-tab-index', value: index.toString());
_pageController.jumpToPage(index);
switch (index) {
case 0:
bind.mainLoadRecentPeers();
break;
case 1:
bind.mainLoadFavPeers();
break;
case 2:
bind.mainDiscover();
break;
case 3:
gFFI.abModel.updateAb();
break;
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 28,
child: Row(
children: [
Expanded(child: _createTabBar(context)),
_createSearchBar(context),
_createPeerViewTypeSwitch(context),
],
),
),
_createTabBarView(),
],
);
}
Widget _createTabBar(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: ScrollController(),
children: super.widget.tabs.asMap().entries.map((t) {
return Obx(() => InkWell(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: _tabIndex.value == t.key
? MyTheme.color(context).bg
: null,
borderRadius: BorderRadius.circular(2),
),
child: Align(
alignment: Alignment.center,
child: Text(
t.value,
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: _tabIndex.value == t.key
? MyTheme.color(context).text
: MyTheme.color(context).lightText),
),
)),
onTap: () async => await _handleTabSelection(t.key),
));
}).toList());
}
Widget _createTabBarView() {
return Expanded(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageController,
children: super.widget.children)
.marginSymmetric(vertical: 12));
}
_createSearchBar(BuildContext context) {
RxBool focused = false.obs;
FocusNode focusNode = FocusNode();
focusNode.addListener(() => focused.value = focusNode.hasFocus);
RxBool rowHover = false.obs;
RxBool clearHover = false.obs;
return Container(
width: 120,
height: 25,
margin: EdgeInsets.only(right: 13),
decoration: BoxDecoration(color: MyTheme.color(context).bg),
child: Obx(() => Row(
children: [
Expanded(
child: MouseRegion(
onEnter: (_) => rowHover.value = true,
onExit: (_) => rowHover.value = false,
child: Row(
children: [
Icon(
IconFont.search,
size: 16,
color: MyTheme.color(context).placeholder,
).marginSymmetric(horizontal: 4),
Expanded(
child: TextField(
controller: peerSearchTextController,
onChanged: (searchText) {
peerSearchText.value = searchText;
},
focusNode: focusNode,
textAlign: TextAlign.start,
maxLines: 1,
cursorColor: MyTheme.color(context).lightText,
cursorHeight: 18,
cursorWidth: 1,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 6),
hintText:
focused.value ? null : translate("Search ID"),
hintStyle: TextStyle(
fontSize: 14,
color: MyTheme.color(context).placeholder),
border: InputBorder.none,
isDense: true,
),
),
),
],
),
),
),
Offstage(
offstage: !(peerSearchText.value.isNotEmpty &&
(rowHover.value || clearHover.value)),
child: InkWell(
onHover: (value) => clearHover.value = value,
child: Icon(
IconFont.round_close,
size: 16,
color: clearHover.value
? MyTheme.color(context).text
: MyTheme.color(context).placeholder,
).marginSymmetric(horizontal: 4),
onTap: () {
peerSearchTextController.clear();
peerSearchText.value = "";
}),
)
],
)),
);
}
_createPeerViewTypeSwitch(BuildContext context) {
final activeDeco = BoxDecoration(color: MyTheme.color(context).bg);
return Row(
children: [PeerUiType.grid, PeerUiType.list]
.map((type) => Obx(
() => Container(
padding: EdgeInsets.all(4.0),
decoration: peerCardUiType.value == type ? activeDeco : null,
child: InkWell(
onTap: () async {
await bind.mainSetLocalOption(
key: 'peer-card-ui-type',
value: type.index.toString());
peerCardUiType.value = type;
},
child: Icon(
type == PeerUiType.grid
? Icons.grid_view_rounded
: Icons.list,
size: 18,
color: peerCardUiType.value == type
? MyTheme.color(context).text
: MyTheme.color(context).lightText,
)),
),
))
.toList(),
);
}
}

View File

@ -15,7 +15,6 @@ import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as mod_menu;
import './utils.dart';
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;

View File

@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
void connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
FocusScopeNode currentFocus = FocusScope.of(context);
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
}
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}

View File

@ -1,13 +1,18 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/address_book.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peer_widget.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/peer_model.dart';
import '../../models/platform_model.dart';
import 'home_page.dart';
import 'remote_page.dart';
@ -19,26 +24,25 @@ class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key);
@override
final icon = Icon(Icons.connected_tv);
final icon = const Icon(Icons.connected_tv);
@override
final title = translate("Connection");
@override
final appBarActions = !isAndroid ? <Widget>[WebMenu()] : <Widget>[];
final appBarActions = !isAndroid ? <Widget>[const WebMenu()] : <Widget>[];
@override
_ConnectionPageState createState() => _ConnectionPageState();
State<ConnectionPage> createState() => _ConnectionPageState();
}
/// State for the connection page.
class _ConnectionPageState extends State<ConnectionPage> {
/// Controller for the id input bar.
final _idController = TextEditingController();
final _idController = IDTextEditingController();
/// Update url. If it's not null, means an update is available.
var _updateUrl = '';
var _menuPos;
@override
void initState() {
@ -46,17 +50,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (_idController.text.isEmpty) {
() async {
final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.text) {
if (lastRemoteId != _idController.id) {
setState(() {
_idController.text = lastRemoteId;
_idController.id = lastRemoteId;
});
}
}();
}
if (isAndroid) {
Timer(Duration(seconds: 5), () async {
Timer(const Duration(seconds: 5), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
;
if (_updateUrl.isNotEmpty) setState(() {});
});
}
@ -65,25 +68,35 @@ class _ConnectionPageState extends State<ConnectionPage> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return SingleChildScrollView(
controller: ScrollController(),
child: Column(
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
getUpdateUI(),
getSearchBarUI(),
Container(height: 12),
getPeers(),
]),
);
_buildUpdateUI(),
_buildRemoteIDTextField(),
Expanded(
child: PeerTabPage(
tabs: [
translate('Recent Sessions'),
translate('Favorites'),
translate('Discovered'),
translate('Address Book')
],
children: [
RecentPeerWidget(),
FavoritePeerWidget(),
DiscoveredPeerWidget(),
const AddressBook(),
],
)),
]).marginOnly(top: 2, left: 10, right: 10);
}
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect() {
var id = _idController.text.trim();
var id = _idController.id;
connect(id);
}
@ -120,12 +133,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// UI for software update.
/// If [_updateUrl] is not empty, shows a button to update the software.
Widget getUpdateUI() {
Widget _buildUpdateUI() {
return _updateUrl.isEmpty
? SizedBox(height: 0)
? const SizedBox(height: 0)
: InkWell(
onTap: () async {
final url = _updateUrl + '.apk';
final url = '$_updateUrl.apk';
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
}
@ -134,25 +147,23 @@ class _ConnectionPageState extends State<ConnectionPage> {
alignment: AlignmentDirectional.center,
width: double.infinity,
color: Colors.pinkAccent,
padding: EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(translate('Download new version'),
style: TextStyle(
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold))));
}
/// UI for the search bar.
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget getSearchBarUI() {
var w = Padding(
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0),
child: Container(
Widget _buildRemoteIDTextField() {
final w = SizedBox(
height: 84,
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
child: Ink(
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: MyTheme.white,
borderRadius: const BorderRadius.all(Radius.circular(13)),
borderRadius: BorderRadius.all(Radius.circular(13)),
),
child: Row(
children: <Widget>[
@ -164,7 +175,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
// keyboardType: TextInputType.number,
style: TextStyle(
style: const TextStyle(
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
fontSize: 30,
@ -174,12 +185,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
labelText: translate('Remote ID'),
// hintText: 'Enter your remote ID',
border: InputBorder.none,
helperStyle: TextStyle(
helperStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: MyTheme.darkGray,
),
labelStyle: TextStyle(
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.2,
@ -187,6 +198,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
),
),
controller: _idController,
inputFormatters: [IDTextInputFormatter()],
),
),
),
@ -194,7 +206,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
width: 60,
height: 60,
child: IconButton(
icon: Icon(Icons.arrow_forward,
icon: const Icon(Icons.arrow_forward,
color: MyTheme.darkGray, size: 45),
onPressed: onConnect,
),
@ -203,10 +215,10 @@ class _ConnectionPageState extends State<ConnectionPage> {
),
),
),
),
);
return Center(
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
return Align(
alignment: Alignment.topLeft,
child: Container(constraints: kMobilePageConstraints, child: w));
}
@override
@ -214,97 +226,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
_idController.dispose();
super.dispose();
}
/// Get all the saved peers.
Widget getPeers() {
final windowWidth = MediaQuery.of(context).size.width;
final space = 8.0;
var width = windowWidth - 2 * space;
final minWidth = 320.0;
if (windowWidth > minWidth + 2 * space) {
final n = (windowWidth / (minWidth + 2 * space)).floor();
width = windowWidth / n - 2 * space;
}
return FutureBuilder<List<Peer>>(
future: gFFI.peers(),
builder: (context, snapshot) {
final cards = <Widget>[];
if (snapshot.hasData) {
final peers = snapshot.data!;
peers.forEach((p) {
cards.add(Container(
width: width,
child: Card(
child: GestureDetector(
onTap:
!isWebDesktop ? () => connect('${p.id}') : null,
onDoubleTap:
isWebDesktop ? () => connect('${p.id}') : null,
onLongPressStart: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
showPeerMenu(context, p.id);
},
child: ListTile(
contentPadding: const EdgeInsets.only(left: 12),
subtitle: Text('${p.username}@${p.hostname}'),
title: Text('${p.id}'),
leading: Container(
padding: const EdgeInsets.all(6),
child: getPlatformImage('${p.platform}'),
color: str2color('${p.id}${p.platform}', 0x7f)),
trailing: InkWell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(Icons.more_vert)),
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () {
showPeerMenu(context, p.id);
}),
)))));
});
}
return Wrap(children: cards, spacing: space, runSpacing: space);
});
}
/// Show the peer menu and handle user's choice.
/// User might remove the peer or send a file to the peer.
void showPeerMenu(BuildContext context, String id) async {
var value = await showMenu(
context: context,
position: this._menuPos,
items: [
PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'remove')
] +
(!isAndroid
? []
: [
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file')
]),
elevation: 8,
);
if (value == 'remove') {
setState(() => bind.mainRemovePeer(id: id));
() async {
removePreference(id);
}();
} else if (value == 'file') {
connect(id, isFileTransfer: true);
}
}
}
class WebMenu extends StatefulWidget {
const WebMenu({Key? key}) : super(key: key);
@override
_WebMenuState createState() => _WebMenuState();
State<WebMenu> createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@ -337,36 +265,36 @@ class _WebMenuState extends State<WebMenu> {
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
PopupMenuItem(
child: Icon(Icons.qr_code_scanner, color: Colors.black),
const PopupMenuItem(
value: "scan",
child: Icon(Icons.qr_code_scanner, color: Colors.black),
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
child: Text(translate('ID/Relay Server')),
value: "server",
child: Text(translate('ID/Relay Server')),
)
] +
(url.contains('admin.rustdesk.com')
? <PopupMenuItem<String>>[]
: [
PopupMenuItem(
value: "login",
child: Text(username == null
? translate("Login")
: translate("Logout") + ' ($username)'),
value: "login",
: '${translate("Logout")} ($username)'),
)
]) +
[
PopupMenuItem(
child: Text(translate('About') + ' RustDesk'),
value: "about",
child: Text('${translate('About')} RustDesk'),
)
];
},

View File

@ -32,6 +32,7 @@ const url = 'https://rustdesk.com/';
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
var _enableAbr = false;
var _isDarkMode = false;
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
String? username;
@ -59,6 +60,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableAbr = enableAbrRes;
}
_enableAbr = isDarkTheme();
if (update) {
setState(() {});
}
@ -173,7 +176,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.translate),
onPressed: (context) {
showLanguageSettings(gFFI.dialogManager);
})
}),
SettingsTile.switchTile(
title: Text(translate('Dark Theme')),
leading: Icon(Icons.dark_mode),
initialValue: _isDarkMode,
onToggle: (v) {
setState(() {
_isDarkMode = !_isDarkMode;
MyTheme.changeTo(_isDarkMode);
});
},
)
]),
SettingsSection(
title: Text(translate("Enhancements")),

View File

@ -1150,7 +1150,7 @@ packages:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
version: "0.6.2"
wakelock_macos:
dependency: transitive
description:

View File

@ -35,7 +35,7 @@ dependencies:
external_path: ^1.0.1
provider: ^6.0.3
tuple: ^2.0.0
wakelock: ^0.5.2
wakelock: ^0.6.2
device_info_plus: ^4.1.2
#firebase_analytics: ^9.1.5
package_info_plus: ^1.4.2