feat(train): a recording under way above the bar on every tab, and a saved one opens its page (ADR 0030, D60)
halkyon_ui's NavigationFrame takes a status, standing above the floating bar below medium windows and at the foot of the screen's panel from there on. Train's RecordingBar fills it while a recording runs or waits to be saved: its activity, moving time and distance, paused or stopped, a tap back to it. Saving now opens the workout's page in Home's tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
25191055ae
commit
7473366f66
3 changed files with 301 additions and 15 deletions
|
|
@ -29,7 +29,8 @@ LeanCode's lints.
|
|||
`TopGradient`, `ScreenTint`, `ScreenColor`, `TintFrame`, `FramedScreen`
|
||||
and `TabVisibility`; `FloatingNavigationBar`, and `NavigationFrame`, every
|
||||
app's frame around its tabs (the rail with the app's icon, the floating
|
||||
bar, a tick on every choice), whichever router drives it;
|
||||
bar, a tick on every choice, a status such as a recording under way above
|
||||
the bar), whichever router drives it;
|
||||
`AppLoadingIndicator`, `ButtonLoadingIndicator`, `AppRefreshIndicator`;
|
||||
`SectionCard`, `SliverCentered`, `EmptyListMessage`, `CountBadge`,
|
||||
`TitleWithKicker`, `CollapsingRow`, `ScrollHidingFab`, `liftWhileDragged`,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ class const NavigationFrame({
|
|||
/// top-level screens and the actions of those below in a toolbar, never
|
||||
/// both at once (as Vaia does). The rail of wider windows stays.
|
||||
final bool hidesNavigationBar = false,
|
||||
|
||||
/// What stands above the bar on every tab while it matters (Train's
|
||||
/// recording under way, as Strava's): floating above the bar below
|
||||
/// medium windows, at the foot of the screen's panel from there on; none
|
||||
/// while null.
|
||||
final Widget? status,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -102,7 +108,15 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
|||
),
|
||||
],
|
||||
),
|
||||
child: widget.child,
|
||||
child: switch (widget.status) {
|
||||
final status? => Column(
|
||||
children: [
|
||||
Expanded(child: widget.child),
|
||||
Padding(padding: const EdgeInsets.all(12), child: status),
|
||||
],
|
||||
),
|
||||
null => widget.child,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -142,9 +156,30 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
|||
// Without the bar, a strip as high as the system's own bar, in
|
||||
// the screen's background, keeps every screen inside clear of
|
||||
// it.
|
||||
bottomNavigationBar: widget.hidesNavigationBar
|
||||
? SizedBox(height: MediaQuery.paddingOf(context).bottom)
|
||||
: bar,
|
||||
bottomNavigationBar: switch (widget.status) {
|
||||
final status? => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
FloatingNavigationBar.sideMargin,
|
||||
0,
|
||||
FloatingNavigationBar.sideMargin,
|
||||
FloatingNavigationBar.lift,
|
||||
),
|
||||
child: status,
|
||||
),
|
||||
if (widget.hidesNavigationBar)
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom)
|
||||
else
|
||||
bar,
|
||||
],
|
||||
),
|
||||
null when widget.hidesNavigationBar => SizedBox(
|
||||
height: MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
null => bar,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -160,4 +160,40 @@ void main() {
|
|||
expect(find.byType(FloatingNavigationBar), findsNothing);
|
||||
expect(tester.getBottomLeft(find.byType(Scaffold).last).dy, 800 - 24);
|
||||
});
|
||||
|
||||
testWidgets('a status stands above the bar below medium windows, and at '
|
||||
"the foot of the screen's panel from there on", (tester) async {
|
||||
for (final width in const [400.0, 800.0]) {
|
||||
tester.view.physicalSize = Size(width, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
NavigationFrame(
|
||||
appName: 'Halkyon Test',
|
||||
icon: icon,
|
||||
selectedIndex: 0,
|
||||
onSelected: (_) {},
|
||||
status: const Text('Recording'),
|
||||
destinations: const [
|
||||
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
|
||||
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
|
||||
],
|
||||
child: const Scaffold(body: Text('The tab')),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final status = tester.getBottomLeft(find.text('Recording')).dy;
|
||||
final tab = tester.getTopLeft(find.text('The tab')).dy;
|
||||
expect(status, greaterThan(tab), reason: '$width');
|
||||
if (width < 600) {
|
||||
expect(
|
||||
status,
|
||||
lessThan(tester.getTopLeft(find.byType(NavigationBar)).dy),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue