Add "Search in bottom bar" to Settings
Build APK / build (push) Successful in 5m30s

New Settings > Appearance toggle moves Search into the bottom bar and
out of the top bar, so there's only one search entry point on screen.
Tapping it pushes SearchView same as before - it's never highlighted,
since it isn't a real destination.

- Attached: Search becomes the row's last item, styled like any other
  tab but permanently idle.
- Floating: Search gets its own separate, always fully round satellite
  circle beside the bar instead.

Enabling it costs one regular tab: SettingsController.maxVisibleTabs
is now a dynamic getter (defaultMaxVisibleTabs - 1 while the toggle is
on) instead of the old flat constant, re-enforced immediately if the
user already has all 5 slots filled - the same cleanup path used for
a fresh install or an app update that added a new tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 22:19:13 -04:00
co-authored by Claude Sonnet 5
parent a70be19f80
commit 58312511c5
11 changed files with 292 additions and 23 deletions
+91
View File
@@ -212,5 +212,96 @@ void main() {
expect(idleIcon.size, greaterThan(activeIcon.size!));
},
);
testNooWidgets(
'attached: search is the row\'s last item and never highlights',
(tester, theme, c) async {
var searchTaps = 0;
var selected = 0;
await pumpNoo(
tester,
StatefulBuilder(
builder: (context, setState) => SizedBox(
width: 360,
height: 80,
child: NooBottomBar(
style: NooNavStyle.android,
destinations: _destinations,
selectedIndex: selected,
onSelected: (i) => setState(() => selected = i),
searchDestination: const NooNavDestination(
icon: LucideIcons.search,
label: 'Search',
),
onSearchTap: () => searchTaps++,
),
),
),
theme: theme,
);
expect(find.byIcon(LucideIcons.search), findsOneWidget);
final iconCenters = [
tester.getCenter(find.byIcon(LucideIcons.folder)),
tester.getCenter(find.byIcon(LucideIcons.images)),
tester.getCenter(find.byIcon(LucideIcons.star)),
tester.getCenter(find.byIcon(LucideIcons.search)),
];
// Evenly spaced, search landing after every real destination.
for (var i = 1; i < iconCenters.length; i++) {
expect(iconCenters[i].dx, greaterThan(iconCenters[i - 1].dx));
}
await tester.tap(find.byIcon(LucideIcons.search));
await tester.pump();
expect(searchTaps, 1);
// Tapping search never selects it or moves the indicator.
expect(selected, 0);
expect(_indicator(tester, c), findsOneWidget);
final indicatorLeft = tester.getTopLeft(_indicator(tester, c)).dx;
expect(indicatorLeft, lessThan(iconCenters[3].dx));
},
);
testNooWidgets('floating: search is a separate round satellite', (
tester,
theme,
c,
) async {
var searchTaps = 0;
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 96,
child: NooBottomBar(
style: NooNavStyle.ios,
barStyle: NooBottomBarStyle.floating,
destinations: _destinations,
selectedIndex: 0,
onSelected: (_) {},
searchDestination: const NooNavDestination(
icon: LucideIcons.search,
label: 'Search',
),
onSearchTap: () => searchTaps++,
),
),
theme: theme,
);
// Not part of the main pill - its own separately-tappable icon.
expect(find.text('Search'), findsNothing);
final satellite = tester.widget<Container>(
find.ancestor(
of: find.byIcon(LucideIcons.search),
matching: find.byType(Container),
),
);
final decoration = satellite.decoration as BoxDecoration;
expect(decoration.shape, BoxShape.circle);
await tester.tap(find.byIcon(LucideIcons.search));
await tester.pump();
expect(searchTaps, 1);
});
});
}