Commit 9e6e8422 authored by csf's avatar csf

refactor: move peer_widget / peercard_widget / peer_tab_page & move connect

new address_book class; add peer tab onPageChanged

android settings_page.dart add dark mode

opt peer_tab_page search bar, add mobile peer_tab support
parent 0c407994
......@@ -1007,3 +1007,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();
}
}
This diff is collapsed.
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() {
super.initState();
}
// hard code for now
void _handleTabSelection(int index) {
// reset search text
peerSearchText.value = "";
peerSearchTextController.clear();
_tabIndex.value = index;
_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(
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(() => GestureDetector(
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: () => _handleTabSelection(t.key),
));
}).toList());
}
Widget _createTabBarView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: PageView(
physics: const 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: [
Obx(
() => Container(
padding: const EdgeInsets.all(4.0),
decoration:
peerCardUiType.value == PeerUiType.grid ? activeDeco : null,
child: InkWell(
onTap: () {
peerCardUiType.value = PeerUiType.grid;
},
child: Icon(
Icons.grid_view_rounded,
size: 18,
color: peerCardUiType.value == PeerUiType.grid
? MyTheme.color(context).text
: MyTheme.color(context).lightText,
)),
),
),
Obx(
() => Container(
padding: const EdgeInsets.all(4.0),
decoration:
peerCardUiType.value == PeerUiType.list ? activeDeco : null,
child: InkWell(
onTap: () {
peerCardUiType.value = PeerUiType.list;
},
child: Icon(
Icons.list,
size: 18,
color: peerCardUiType.value == PeerUiType.list
? MyTheme.color(context).text
: MyTheme.color(context).lightText,
)),
),
),
],
);
}
}
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,
)),
],
),
)
],
)),
);
}
}
......@@ -39,7 +39,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>{};
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
......@@ -47,6 +47,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();
}
......@@ -76,7 +87,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>(
......@@ -93,32 +103,36 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
final peers = snapshot.data!;
final cards = <Widget>[];
for (final peer in peers) {
final visibilityChild = VisibilityDetector(
key: ValueKey(peer.id),
onVisibilityChanged: (info) {
final peerId = (info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId);
} else {
_curPeers.remove(peerId);
}
_lastChangeTime = DateTime.now();
},
child: widget.peerCardWidgetFunc(peer),
);
cards.add(Offstage(
key: ValueKey("off${peer.id}"),
offstage: widget.offstageFunc(peer),
child: Obx(
() => SizedBox(
width: 220,
height:
peerCardUiType.value == PeerUiType.grid
? 140
: 42,
child: VisibilityDetector(
key: ValueKey(peer.id),
onVisibilityChanged: (info) {
final peerId =
(info.key as ValueKey).value;
if (info.visibleFraction > 0.00001) {
_curPeers.add(peerId);
} else {
_curPeers.remove(peerId);
}
_lastChangeTime = DateTime.now();
},
child: widget.peerCardWidgetFunc(peer),
),
),
)));
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);
......@@ -273,6 +287,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();
......@@ -296,7 +311,7 @@ class AddressBookPeerWidget extends BasePeerWidget {
@override
Widget build(BuildContext context) {
final widget = super.build(context);
gFFI.abModel.updateAb();
// gFFI.abModel.updateAb();
return widget;
}
}
......@@ -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.zero,
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(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,
......
import 'package:flutter/material.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page'
......@@ -17,6 +19,8 @@ const int kDesktopDefaultDisplayHeight = 720;
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>{
......
......@@ -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;
......
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();
}
}
......@@ -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")),
......
......@@ -101,7 +101,7 @@ class AbModel with ChangeNotifier {
final resp =
await http.post(Uri.parse(api), headers: authHeaders, body: body);
abLoading = false;
await getAb();
// await getAb(); // TODO
notifyListeners();
debugPrint("resp: ${resp.body}");
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment