Fix bottom avatar popup account ordering on Android
This commit is contained in:
@@ -311,7 +311,7 @@ blocks are noted where they matter:
|
||||
full screen width, rounded top corners and no gap above the bar; the
|
||||
avatar stays in the bar, and Search keeps its inline position.
|
||||
The attached popup has no outer border or shadow, blending into the bar.
|
||||
Attached popup order is navigation (including Settings), current account,
|
||||
All bottom popups order navigation (including Settings), current account,
|
||||
then expanded secondary accounts and Add/Manage Account controls.
|
||||
The iOS shell places Upload and the top avatar on the large-title row.
|
||||
Upload uses the shared `ui_fab_style` preference: Auto expands on Files
|
||||
|
||||
@@ -73,6 +73,7 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
final bool expanded;
|
||||
final bool movingAvatar;
|
||||
final bool showHeaderAvatar;
|
||||
final Key? headerAvatarKey;
|
||||
final VoidCallback onExpand;
|
||||
final VoidCallback onClose;
|
||||
const AvatarMenuCard({
|
||||
@@ -80,6 +81,7 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
this.bottom = false,
|
||||
this.movingAvatar = false,
|
||||
this.showHeaderAvatar = true,
|
||||
this.headerAvatarKey,
|
||||
required this.expanded,
|
||||
required this.onExpand,
|
||||
required this.onClose,
|
||||
@@ -133,17 +135,20 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
if (showHeaderAvatar) const SizedBox(width: 12),
|
||||
if (showHeaderAvatar)
|
||||
movingAvatar
|
||||
? const SizedBox.square(dimension: 32)
|
||||
: NooAvatar(
|
||||
initials: accountInitial(session.username),
|
||||
current: true,
|
||||
size: 32,
|
||||
? SizedBox.square(key: headerAvatarKey, dimension: 32)
|
||||
: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: NooAvatar(
|
||||
key: headerAvatarKey,
|
||||
initials: accountInitial(session.username),
|
||||
current: true,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
final attached = bottom && !showHeaderAvatar;
|
||||
final accountSection = AnimatedSize(
|
||||
duration: NooMotion.base,
|
||||
curve: Curves.easeOutCubic,
|
||||
@@ -199,7 +204,7 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
final rows = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!attached) accountSection,
|
||||
if (!bottom) accountSection,
|
||||
for (final tab in settings.tabOrder.where(settings.hiddenTabs.contains))
|
||||
NooSettingsRow(
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -223,7 +228,7 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
label: const Text('Settings'),
|
||||
onTap: settingsTap,
|
||||
),
|
||||
if (attached) ...[header, accountSection],
|
||||
if (bottom) ...[header, accountSection],
|
||||
],
|
||||
);
|
||||
final content = SizedBox(
|
||||
@@ -242,7 +247,6 @@ class AvatarMenuCard extends StatelessWidget {
|
||||
fit: FlexFit.loose,
|
||||
child: SingleChildScrollView(child: rows),
|
||||
),
|
||||
if (bottom && !attached) header,
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -278,9 +282,7 @@ class _AvatarSurface extends StatelessWidget {
|
||||
color: frosted
|
||||
? colors.surface.withValues(alpha: settings.bottomBarFrostedOpacity)
|
||||
: colors.surface,
|
||||
border: connected
|
||||
? null
|
||||
: Border.all(color: colors.line),
|
||||
border: connected ? null : Border.all(color: colors.line),
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: Padding(padding: const EdgeInsets.all(1), child: child),
|
||||
@@ -419,6 +421,8 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
reverseDuration: const Duration(milliseconds: 320),
|
||||
);
|
||||
final GlobalKey measureKey = GlobalKey();
|
||||
final GlobalKey measureAvatarKey = GlobalKey();
|
||||
Offset avatarOffset = const Offset(0, 17);
|
||||
Rect? origin;
|
||||
bool bottom = false;
|
||||
bool expanded = false;
|
||||
@@ -431,8 +435,18 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
measuring = false;
|
||||
if (!mounted) return;
|
||||
final box = measureKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box != null && (box.size.height - cardHeight).abs() > .5) {
|
||||
setState(() => cardHeight = box.size.height);
|
||||
final avatarBox =
|
||||
measureAvatarKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final offset = box != null && avatarBox != null
|
||||
? avatarBox.localToGlobal(Offset.zero, ancestor: box)
|
||||
: avatarOffset;
|
||||
if (box != null &&
|
||||
((box.size.height - cardHeight).abs() > .5 ||
|
||||
(offset - avatarOffset).distance > .5)) {
|
||||
setState(() {
|
||||
cardHeight = box.size.height;
|
||||
avatarOffset = offset;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -523,11 +537,12 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
height * .8,
|
||||
);
|
||||
measure();
|
||||
Widget card() => ConstrainedBox(
|
||||
Widget card({bool measurement = false}) => ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: AvatarMenuCard(
|
||||
bottom: bottom,
|
||||
movingAvatar: true,
|
||||
movingAvatar: measurement || progress < 1,
|
||||
headerAvatarKey: measurement ? measureAvatarKey : null,
|
||||
showHeaderAvatar: !attached,
|
||||
expanded: expanded,
|
||||
onExpand: () => setState(() => expanded = !expanded),
|
||||
@@ -560,7 +575,7 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
);
|
||||
final avatarTarget = Rect.fromLTWH(
|
||||
target.right - 49,
|
||||
bottom ? target.bottom - 49 : target.top + 17,
|
||||
target.top + avatarOffset.dy,
|
||||
32,
|
||||
32,
|
||||
);
|
||||
@@ -595,7 +610,7 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: KeyedSubtree(
|
||||
key: measureKey,
|
||||
child: card(),
|
||||
child: card(measurement: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -643,7 +658,7 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!attached)
|
||||
if (!attached && progress < 1)
|
||||
Positioned.fromRect(
|
||||
rect: avatar,
|
||||
child: Semantics(
|
||||
|
||||
@@ -550,6 +550,81 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Android floating popup orders navigation before accounts', (
|
||||
tester,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final accounts = jsonDecode(prefs.getString('accounts_list')!) as List;
|
||||
accounts.add(
|
||||
const SavedAccount(
|
||||
id: 'other_example_org__bob',
|
||||
serverUrl: 'https://other.example.org',
|
||||
username: 'bob',
|
||||
).toJson(),
|
||||
);
|
||||
await prefs.setString('accounts_list', jsonEncode(accounts));
|
||||
await pumpTopBar(
|
||||
tester,
|
||||
navMenuStyle: NooNavMenuStyle.avatarMenu,
|
||||
withHost: true,
|
||||
position: AvatarPosition.bottom,
|
||||
search: true,
|
||||
style: NooNavStyle.android,
|
||||
barStyle: NooBottomBarStyle.floating,
|
||||
);
|
||||
final origin = tester.getCenter(find.byTooltip('Menu'));
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(BottomNavBar),
|
||||
matching: find.text('Files'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Photos'), findsNothing);
|
||||
await tester.tap(find.byTooltip('Menu'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(
|
||||
tester.getBottomLeft(find.byType(AvatarMenuCard)).dy,
|
||||
origin.dy - 32 - 12 - 1,
|
||||
);
|
||||
expect(tester.getSize(find.byType(AvatarMenuCard)).width, 376);
|
||||
expect(find.bySemanticsLabel('Search'), findsOneWidget);
|
||||
await tester.tap(find.text('alice'));
|
||||
await tester.pump();
|
||||
for (var i = 0; i < 8; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
expect(
|
||||
tester.getCenter(find.text('Settings')).dy,
|
||||
lessThan(tester.getCenter(find.text('alice')).dy),
|
||||
);
|
||||
expect(
|
||||
tester.getCenter(find.text('alice')).dy,
|
||||
lessThan(tester.getCenter(find.text('bob')).dy),
|
||||
);
|
||||
expect(
|
||||
tester.getCenter(find.text('bob')).dy,
|
||||
lessThan(tester.getCenter(find.text('Add Account')).dy),
|
||||
);
|
||||
final currentAvatar = find
|
||||
.descendant(
|
||||
of: find.byType(AvatarMenuCard),
|
||||
matching: find.byType(NooAvatar),
|
||||
)
|
||||
.first;
|
||||
expect(
|
||||
tester.getCenter(currentAvatar).dy,
|
||||
lessThan(tester.getCenter(find.text('bob')).dy),
|
||||
);
|
||||
await tester.tap(currentAvatar);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(find.byType(AvatarMenuCard), findsNothing);
|
||||
expect(tester.getCenter(find.byTooltip('Menu')), origin);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('expanding accounts also grows the top content offset', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user