refact: peer card, orientation (#9235)

* refact: peer card, orientation

Signed-off-by: fufesou <linlong1266@gmail.com>

* Do not change landscape/portrait on Desktop

Signed-off-by: fufesou <linlong1266@gmail.com>

* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-09-03 19:06:11 +08:00 committed by GitHub
parent 39e713838f
commit d4377a13c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 186 additions and 155 deletions

View File

@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -61,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookDesktop() ? _buildAddressBookPortrait()
: _buildAddressBookMobile()) : _buildAddressBookLandscape()),
),
], ],
); );
} }
}); });
Widget _buildAddressBookDesktop() { Widget _buildAddressBookLandscape() {
return Row( return Row(
children: [ children: [
Offstage( Offstage(
@ -106,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookPortrait() {
const padding = 8.0; const padding = 8.0;
return Column( return Column(
children: [ children: [
@ -239,14 +241,14 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
} }
}, },
customButton: Container( customButton: Obx(()=>Container(
height: isDesktop ? 48 : 40, height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)), child: buildItem(gFFI.abModel.currentName.value, button: true)),
Icon(Icons.arrow_drop_down), Icon(Icons.arrow_drop_down),
]), ]),
), )),
underline: Container( underline: Container(
height: 0.7, height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
@ -335,8 +337,8 @@ class _AddressBookState extends State<AddressBook> {
showActionMenu: editPermission); showActionMenu: editPermission);
} }
final gridView = DynamicGridView.builder( gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(), gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length, itemCount: tags.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@ -344,9 +346,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? gridView ? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
}); });
} }
@ -506,9 +508,9 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4; double marginBottom = 4;
row({required Widget lable, required Widget input}) { row({required Widget lable, required Widget input}) {
return Row( makeChild(bool isPortrait) => Row(
children: [ children: [
!isMobile !isPortrait
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10)) child: lable.marginOnly(right: 10))
@ -519,7 +521,8 @@ class _AddressBookState extends State<AddressBook> {
child: input), child: input),
), ),
], ],
).marginOnly(bottom: !isMobile ? 8 : 0); ).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
} }
return CustomAlertDialog( return CustomAlertDialog(
@ -542,24 +545,24 @@ class _AddressBookState extends State<AddressBook> {
), ),
], ],
), ),
input: TextField( input: Obx(() => TextField(
controller: idController, controller: idController,
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'), labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
errorText: errorMsg, errorText: errorMsg,
errorMaxLines: 5), errorMaxLines: 5),
)), ))),
row( row(
lable: Text( lable: Text(
translate('Alias'), translate('Alias'),
style: style, style: style,
), ),
input: TextField( input: Obx(() => TextField(
controller: aliasController, controller: aliasController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'), labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
)), ),)),
), ),
if (isCurrentAbShared) if (isCurrentAbShared)
row( row(
@ -567,11 +570,11 @@ class _AddressBookState extends State<AddressBook> {
translate('Password'), translate('Password'),
style: style, style: style,
), ),
input: TextField( input: Obx(() => TextField(
controller: passwordController, controller: passwordController,
obscureText: !passwordVisible, obscureText: !passwordVisible,
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('Password'), labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
passwordVisible passwordVisible
@ -585,7 +588,7 @@ class _AddressBookState extends State<AddressBook> {
}, },
), ),
), ),
)), ),)),
if (gFFI.abModel.currentAbTags.isNotEmpty) if (gFFI.abModel.currentAbTags.isNotEmpty)
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,

View File

@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: widget.peer.tags.isNotEmpty : widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@ -1123,7 +1124,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@ -45,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildDesktop() ? _buildPortrait()
: _buildMobile()) : _buildLandscape())),
], ],
); );
}); });
} }
Widget _buildDesktop() { Widget _buildLandscape() {
return Row( return Row(
children: [ children: [
Container( Container(
@ -89,7 +90,7 @@ class _MyGroupState extends State<MyGroup> {
); );
} }
Widget _buildMobile() { Widget _buildPortrait() {
return Column( return Column(
children: [ children: [
Container( Container(
@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
} }
return true; return true;
}).toList(); }).toList();
final listView = ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? listView ? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView(true)));
}); });
} }

View File

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -53,14 +54,11 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop || isWebDesktop) { return Obx(() =>
return _buildDesktop(); stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
} else {
return _buildMobile();
}
} }
Widget _buildMobile() { Widget _buildPortrait() {
final peer = super.widget.peer; final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
return Card( return Card(
@ -87,7 +85,7 @@ class _PeerCardState extends State<_PeerCard>
)); ));
} }
Widget _buildDesktop() { Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer; final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>( var deco = Rx<BoxDecoration?>(
@ -140,13 +138,13 @@ class _PeerCardState extends State<_PeerCard>
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row( makeChild(bool isPortrait) => Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f), color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile borderRadius: isPortrait
? BorderRadius.circular(_tileRadius) ? BorderRadius.circular(_tileRadius)
: BorderRadius.only( : BorderRadius.only(
topLeft: Radius.circular(_tileRadius), topLeft: Radius.circular(_tileRadius),
@ -154,11 +152,11 @@ class _PeerCardState extends State<_PeerCard>
), ),
), ),
alignment: Alignment.center, alignment: Alignment.center,
width: isMobile ? 50 : 42, width: isPortrait ? 50 : 42,
height: isMobile ? 50 : null, height: isPortrait ? 50 : null,
child: Stack( child: Stack(
children: [ children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30) getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6), .paddingAll(6),
if (_shouldBuildPasswordIcon(peer)) if (_shouldBuildPasswordIcon(peer))
Positioned( Positioned(
@ -183,19 +181,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column( child: Column(
children: [ children: [
Row(children: [ Row(children: [
getOnline(isMobile ? 4 : 8, peer.online), getOnline(isPortrait ? 4 : 8, peer.online),
Expanded( Expanded(
child: Text( child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias, peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isMobile ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
name, name,
style: isMobile ? null : greyStyle, style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -203,9 +201,9 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
isMobile isPortrait
? checkBoxOrActionMoreMobile(peer) ? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true), : checkBoxOrActionMoreLandscape(peer, isTile: true),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
@ -216,28 +214,27 @@ class _PeerCardState extends State<_PeerCard>
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: peer.tags.isNotEmpty : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
deco == null Obx(() => deco == null
? child ? makeChild(stateGlobal.isPortrait.isTrue)
: Obx( : Container(
() => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
child: child, child: makeChild(stateGlobal.isPortrait.isTrue),
), ),
), ),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Obx(()=> Positioned(
top: 2, top: 2,
right: isMobile ? 20 : 10, right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
) ))
]), ]),
); );
} }
@ -316,7 +313,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false), checkBoxOrActionMoreLandscape(peer, isTile: false),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@ -362,7 +359,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreMobile(Peer peer) { Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@ -390,7 +387,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@ -1257,9 +1254,9 @@ void _rdpDialog(String id) async {
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@ -1270,17 +1267,17 @@ void _rdpDialog(String id) async {
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText: isDesktop
? null ? null
: translate('Username')), : translate('Username')),
controller: userController, controller: userController,
), ),
), ),
], ],
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0), ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@ -1292,7 +1289,7 @@ void _rdpDialog(String id) async {
child: Obx(() => TextField( child: Obx(() => TextField(
obscureText: secure.value, obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText: isDesktop
? null ? null
: translate('Password'), : translate('Password'),
suffixIcon: IconButton( suffixIcon: IconButton(
@ -1304,7 +1301,7 @@ void _rdpDialog(String id) async {
)), )),
), ),
], ],
) ))
], ],
), ),
), ),

View File

@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -114,26 +115,26 @@ class _PeerTabPageState extends State<PeerTabPage>
textBaseline: TextBaseline.ideographic, textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Obx(() => SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: (isDesktop || isWebDesktop) padding: stateGlobal.isPortrait.isTrue
? null ? EdgeInsets.symmetric(horizontal: 2)
: EdgeInsets.symmetric(horizontal: 2), : null,
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: child: visibleContextMenuListener(
visibleContextMenuListener(_createSwitchBar(context))), _createSwitchBar(context))),
if (isMobile) if (stateGlobal.isPortrait.isTrue)
..._mobileRightActions(context) ..._portraitRightActions(context)
else else
..._desktopRightActions(context) ..._landscapeRightActions(context)
], ],
)), )),
), ),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(), _createPeersView(),
], ],
); );
@ -299,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenuListener(Widget child) { Widget visibleContextMenuListener(Widget child) {
if (isMobile) { if (!(isDesktop || isWebDesktop)) {
return GestureDetector( return GestureDetector(
onLongPressDown: (e) { onLongPressDown: (e) {
final x = e.globalPosition.dx; final x = e.globalPosition.dx;
@ -456,7 +457,7 @@ class _PeerTabPageState extends State<PeerTabPage>
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@ -477,7 +478,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@ -500,7 +501,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
}, },
child: Icon(Icons.tag)) child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6), .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@ -556,10 +557,10 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
} }
List<Widget> _desktopRightActions(BuildContext context) { List<Widget> _landscapeRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return [ return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: 13),
_createRefresh( _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh( _createRefresh(
@ -580,7 +581,7 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
} }
List<Widget> _mobileRightActions(BuildContext context) { List<Widget> _portraitRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@ -701,13 +702,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0, baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length); extentOffset: peerSearchTextController.value.text.length);
}); });
return Container( return Obx(() => Container(
width: isMobile ? 120 : 140, width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Obx(() => Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
@ -768,8 +769,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
), ),
) )
], ],
)), ),
); ));
} }
} }

View File

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.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:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
@ -128,7 +129,7 @@ class _PeersViewState extends State<_PeersView>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (isDesktop) return; if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
_isActive = true; _isActive = true;
_queryCount = 0; _queryCount = 0;
@ -194,7 +195,7 @@ class _PeersViewState extends State<_PeersView>
var peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000); if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) { buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
@ -206,7 +207,7 @@ class _PeersViewState extends State<_PeersView>
// No need to listen the currentTab change event. // No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event, // Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView(). // and the peers change event will trigger _buildPeersView().
return (isDesktop || isWebDesktop) return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list ? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild) ? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid : peerCardUiType.value == PeerUiType.grid
@ -217,44 +218,41 @@ class _PeersViewState extends State<_PeersView>
: Container(child: visibilityChild); : Container(child: visibilityChild);
} }
final Widget child; final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
if (isMobile) { ? ListView.builder(
child = ListView.builder( itemCount: peers.length,
itemCount: peers.length, itemBuilder: (BuildContext context, int index) {
itemBuilder: (BuildContext context, int index) { return buildOnePeer(peers[index], true).marginOnly(
return buildOnePeer(peers[index]).marginOnly( top: index == 0 ? 0 : space / 2, bottom: space / 2);
top: index == 0 ? 0 : space / 2, bottom: space / 2); },
}, )
); : peerCardUiType.value == PeerUiType.list
} else { ? DesktopScrollWrapper(
child = Obx(() => peerCardUiType.value == PeerUiType.list scrollController: _scrollController,
? DesktopScrollWrapper( child: ListView.builder(
scrollController: _scrollController, controller: _scrollController,
child: ListView.builder( physics: DraggableNeverScrollableScrollPhysics(),
controller: _scrollController, itemCount: peers.length,
physics: DraggableNeverScrollableScrollPhysics(), itemBuilder: (BuildContext context, int index) {
itemCount: peers.length, return buildOnePeer(peers[index], false).marginOnly(
itemBuilder: (BuildContext context, int index) { right: space,
return buildOnePeer(peers[index]).marginOnly( top: index == 0 ? 0 : space / 2,
right: space, bottom: space / 2);
top: index == 0 ? 0 : space / 2, }),
bottom: space / 2); )
}), : DesktopScrollWrapper(
) scrollController: _scrollController,
: DesktopScrollWrapper( child: DynamicGridView.builder(
scrollController: _scrollController, controller: _scrollController,
child: DynamicGridView.builder( physics: DraggableNeverScrollableScrollPhysics(),
controller: _scrollController, gridDelegate: SliverGridDelegateWithWrapping(
physics: DraggableNeverScrollableScrollPhysics(), mainAxisSpacing: space / 2,
gridDelegate: SliverGridDelegateWithWrapping( crossAxisSpacing: space),
mainAxisSpacing: space / 2, itemCount: peers.length,
crossAxisSpacing: space), itemBuilder: (BuildContext context, int index) {
itemCount: peers.length, return buildOnePeer(peers[index], false);
itemBuilder: (BuildContext context, int index) { }),
return buildOnePeer(peers[index]); ));
}),
));
}
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();

View File

@ -243,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords(); ffi.cursorModel.trySetRemoteWindowCoords();
} }
// Workaround for the issue that the first pan event is sent a long time after the start event. // Workaround for the issue that the first pan event is sent a long time after the start event.
@ -285,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords(); ffi.cursorModel.clearRemoteWindowCoords();
} }
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);

View File

@ -372,7 +372,7 @@ class App extends StatefulWidget {
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
class _AppState extends State<App> { class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -396,6 +396,34 @@ class _AppState extends State<App> {
bind.mainChangeTheme(dark: to.toShortString()); bind.mainChangeTheme(dark: to.toShortString());
} }
}; };
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
_updateOrientation();
}
void _updateOrientation() {
if (isDesktop) return;
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
// https://github.com/flutter/flutter/issues/60899
// stateGlobal.isPortrait.value =
// MediaQuery.of(context).orientation == Orientation.portrait;
final orientation = View.of(context).physicalSize.aspectRatio > 1
? Orientation.landscape
: Orientation.portrait;
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
} }
@override @override

View File

@ -20,6 +20,8 @@ class StateGlobal {
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
final RxBool isFocused = false.obs; final RxBool isFocused = false.obs;
final isPortrait = false.obs;
String _inputSource = ''; String _inputSource = '';
// Use for desktop -> remote toolbar -> resolution // Use for desktop -> remote toolbar -> resolution