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
|
full screen width, rounded top corners and no gap above the bar; the
|
||||||
avatar stays in the bar, and Search keeps its inline position.
|
avatar stays in the bar, and Search keeps its inline position.
|
||||||
The attached popup has no outer border or shadow, blending into the bar.
|
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.
|
then expanded secondary accounts and Add/Manage Account controls.
|
||||||
The iOS shell places Upload and the top avatar on the large-title row.
|
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
|
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 expanded;
|
||||||
final bool movingAvatar;
|
final bool movingAvatar;
|
||||||
final bool showHeaderAvatar;
|
final bool showHeaderAvatar;
|
||||||
|
final Key? headerAvatarKey;
|
||||||
final VoidCallback onExpand;
|
final VoidCallback onExpand;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
const AvatarMenuCard({
|
const AvatarMenuCard({
|
||||||
@@ -80,6 +81,7 @@ class AvatarMenuCard extends StatelessWidget {
|
|||||||
this.bottom = false,
|
this.bottom = false,
|
||||||
this.movingAvatar = false,
|
this.movingAvatar = false,
|
||||||
this.showHeaderAvatar = true,
|
this.showHeaderAvatar = true,
|
||||||
|
this.headerAvatarKey,
|
||||||
required this.expanded,
|
required this.expanded,
|
||||||
required this.onExpand,
|
required this.onExpand,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
@@ -133,17 +135,20 @@ class AvatarMenuCard extends StatelessWidget {
|
|||||||
if (showHeaderAvatar) const SizedBox(width: 12),
|
if (showHeaderAvatar) const SizedBox(width: 12),
|
||||||
if (showHeaderAvatar)
|
if (showHeaderAvatar)
|
||||||
movingAvatar
|
movingAvatar
|
||||||
? const SizedBox.square(dimension: 32)
|
? SizedBox.square(key: headerAvatarKey, dimension: 32)
|
||||||
: NooAvatar(
|
: GestureDetector(
|
||||||
initials: accountInitial(session.username),
|
onTap: onClose,
|
||||||
current: true,
|
child: NooAvatar(
|
||||||
size: 32,
|
key: headerAvatarKey,
|
||||||
|
initials: accountInitial(session.username),
|
||||||
|
current: true,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final attached = bottom && !showHeaderAvatar;
|
|
||||||
final accountSection = AnimatedSize(
|
final accountSection = AnimatedSize(
|
||||||
duration: NooMotion.base,
|
duration: NooMotion.base,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
@@ -199,7 +204,7 @@ class AvatarMenuCard extends StatelessWidget {
|
|||||||
final rows = Column(
|
final rows = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (!attached) accountSection,
|
if (!bottom) accountSection,
|
||||||
for (final tab in settings.tabOrder.where(settings.hiddenTabs.contains))
|
for (final tab in settings.tabOrder.where(settings.hiddenTabs.contains))
|
||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -223,7 +228,7 @@ class AvatarMenuCard extends StatelessWidget {
|
|||||||
label: const Text('Settings'),
|
label: const Text('Settings'),
|
||||||
onTap: settingsTap,
|
onTap: settingsTap,
|
||||||
),
|
),
|
||||||
if (attached) ...[header, accountSection],
|
if (bottom) ...[header, accountSection],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final content = SizedBox(
|
final content = SizedBox(
|
||||||
@@ -242,7 +247,6 @@ class AvatarMenuCard extends StatelessWidget {
|
|||||||
fit: FlexFit.loose,
|
fit: FlexFit.loose,
|
||||||
child: SingleChildScrollView(child: rows),
|
child: SingleChildScrollView(child: rows),
|
||||||
),
|
),
|
||||||
if (bottom && !attached) header,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -278,9 +282,7 @@ class _AvatarSurface extends StatelessWidget {
|
|||||||
color: frosted
|
color: frosted
|
||||||
? colors.surface.withValues(alpha: settings.bottomBarFrostedOpacity)
|
? colors.surface.withValues(alpha: settings.bottomBarFrostedOpacity)
|
||||||
: colors.surface,
|
: colors.surface,
|
||||||
border: connected
|
border: connected ? null : Border.all(color: colors.line),
|
||||||
? null
|
|
||||||
: Border.all(color: colors.line),
|
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
),
|
),
|
||||||
child: Padding(padding: const EdgeInsets.all(1), child: child),
|
child: Padding(padding: const EdgeInsets.all(1), child: child),
|
||||||
@@ -419,6 +421,8 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
|||||||
reverseDuration: const Duration(milliseconds: 320),
|
reverseDuration: const Duration(milliseconds: 320),
|
||||||
);
|
);
|
||||||
final GlobalKey measureKey = GlobalKey();
|
final GlobalKey measureKey = GlobalKey();
|
||||||
|
final GlobalKey measureAvatarKey = GlobalKey();
|
||||||
|
Offset avatarOffset = const Offset(0, 17);
|
||||||
Rect? origin;
|
Rect? origin;
|
||||||
bool bottom = false;
|
bool bottom = false;
|
||||||
bool expanded = false;
|
bool expanded = false;
|
||||||
@@ -431,8 +435,18 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
|||||||
measuring = false;
|
measuring = false;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final box = measureKey.currentContext?.findRenderObject() as RenderBox?;
|
final box = measureKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
if (box != null && (box.size.height - cardHeight).abs() > .5) {
|
final avatarBox =
|
||||||
setState(() => cardHeight = box.size.height);
|
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,
|
height * .8,
|
||||||
);
|
);
|
||||||
measure();
|
measure();
|
||||||
Widget card() => ConstrainedBox(
|
Widget card({bool measurement = false}) => ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||||
child: AvatarMenuCard(
|
child: AvatarMenuCard(
|
||||||
bottom: bottom,
|
bottom: bottom,
|
||||||
movingAvatar: true,
|
movingAvatar: measurement || progress < 1,
|
||||||
|
headerAvatarKey: measurement ? measureAvatarKey : null,
|
||||||
showHeaderAvatar: !attached,
|
showHeaderAvatar: !attached,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
onExpand: () => setState(() => expanded = !expanded),
|
onExpand: () => setState(() => expanded = !expanded),
|
||||||
@@ -560,7 +575,7 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
|||||||
);
|
);
|
||||||
final avatarTarget = Rect.fromLTWH(
|
final avatarTarget = Rect.fromLTWH(
|
||||||
target.right - 49,
|
target.right - 49,
|
||||||
bottom ? target.bottom - 49 : target.top + 17,
|
target.top + avatarOffset.dy,
|
||||||
32,
|
32,
|
||||||
32,
|
32,
|
||||||
);
|
);
|
||||||
@@ -595,7 +610,7 @@ class _AvatarNavigationHostState extends State<AvatarNavigationHost>
|
|||||||
child: SizeChangedLayoutNotifier(
|
child: SizeChangedLayoutNotifier(
|
||||||
child: KeyedSubtree(
|
child: KeyedSubtree(
|
||||||
key: measureKey,
|
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(
|
Positioned.fromRect(
|
||||||
rect: avatar,
|
rect: avatar,
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
|
|||||||
@@ -550,6 +550,81 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
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', (
|
testWidgets('expanding accounts also grows the top content offset', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user