Feature: Make the address book & accessible devices side panels resizable to some degree

Replace the fixed-width left panels in the Address book and Accessible devices tabs with a draggable splitter.

The chosen width is clamped to 120-300px and remembered as a local option so it survives restarts.

The divider is invisible but still shows the resize cursor on hover.

Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
This commit is contained in:
StealUrKill 2026-06-16 20:05:03 -05:00
commit aad1082595
3 changed files with 92 additions and 34 deletions

View file

@ -9,6 +9,7 @@ import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common/widgets/resizable_side_panel.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
@ -36,6 +37,8 @@ class AddressBook extends StatefulWidget {
class _AddressBookState extends State<AddressBook> {
var menuPos = RelativeRect.fill;
final _tagsPanel = ResizablePanelController(
optionKey: kOptionAbTagsPanelWidth, defaultWidth: 200, maxWidth: 300);
@override
Widget build(BuildContext context) => Obx(() {
@ -77,33 +80,32 @@ class _AddressBookState extends State<AddressBook> {
children: [
Offstage(
offstage: hideAbTagsPanel.value,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.background)),
child: Container(
width: 200,
height: double.infinity,
child: Column(
children: [
_buildAbDropdown(),
_buildTagHeader().marginOnly(
left: 8.0,
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
child: _buildTags(),
child: Obx(() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.background)),
width: _tagsPanel.width.value,
height: double.infinity,
child: Column(
children: [
_buildAbDropdown(),
_buildTagHeader().marginOnly(
left: 8.0,
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
child: _buildTags(),
),
),
),
_buildAbPermission(),
],
),
),
).marginOnly(right: 12.0)),
_buildAbPermission(),
],
),
))),
if (!hideAbTagsPanel.value) _tagsPanel.buildDivider(),
_buildPeersViews()
],
);

View file

@ -5,6 +5,7 @@ import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/common/widgets/resizable_side_panel.dart';
import 'package:get/get.dart';
import '../../common.dart';
@ -26,6 +27,10 @@ class _MyGroupState extends State<MyGroup> {
RxString get searchAccessibleItemNameText =>
gFFI.groupModel.searchAccessibleItemNameText;
static TextEditingController searchUserController = TextEditingController();
final _devicesPanel = ResizablePanelController(
optionKey: kOptionAccessibleDevicesPanelWidth,
defaultWidth: 150,
maxWidth: 300);
@override
Widget build(BuildContext context) {
@ -60,13 +65,13 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildLandscape() {
return Row(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: Theme.of(context).colorScheme.background)),
child: Container(
width: 150,
Obx(
() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.background)),
width: _devicesPanel.width.value,
height: double.infinity,
child: Column(
children: [
@ -81,7 +86,8 @@ class _MyGroupState extends State<MyGroup> {
],
),
),
).marginOnly(right: 12.0),
),
_devicesPanel.buildDivider(),
Expanded(
child: Align(
alignment: Alignment.topLeft,

View file

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
// Persisted-width option keys for the resizable left panels.
const String kOptionAbTagsPanelWidth = 'ab-tags-panel-width';
const String kOptionAccessibleDevicesPanelWidth = 'accessible-devices-panel-width';
class ResizablePanelController {
final String optionKey;
final double defaultWidth;
final double minWidth;
final double maxWidth;
late final RxDouble width;
ResizablePanelController({
required this.optionKey,
required this.defaultWidth,
this.minWidth = 120,
this.maxWidth = 300,
}) {
final saved = double.tryParse(bind.mainGetLocalOption(key: optionKey));
width =
RxDouble((saved ?? defaultWidth).clamp(minWidth, maxWidth).toDouble());
}
void _onDrag(double dx) {
width.value = (width.value + dx).clamp(minWidth, maxWidth).toDouble();
}
void _persist() {
bind.mainSetLocalOption(
key: optionKey, value: width.value.toStringAsFixed(0));
}
Widget buildDivider() {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragUpdate: (details) => _onDrag(details.delta.dx),
onHorizontalDragEnd: (_) => _persist(),
onHorizontalDragCancel: _persist,
child: DraggableDivider(
axis: Axis.vertical,
padding: const EdgeInsets.symmetric(horizontal: 4.0),
color: Colors.transparent,
),
);
}
}