109 lines
3.3 KiB
Dart
109 lines
3.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
|
|
import '../state/timecards_controller.dart';
|
|
import '../widgets/krow_nav_bar.dart';
|
|
|
|
/// The five primary tabs. Each keeps its own navigation stack.
|
|
class HomeShell extends ConsumerWidget {
|
|
const HomeShell({super.key, required this.navigationShell});
|
|
|
|
final StatefulNavigationShell navigationShell;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final needsReview = ref.watch(timecardsNeedingReviewProvider).isNotEmpty;
|
|
|
|
return Scaffold(
|
|
body: _TabTransition(
|
|
index: navigationShell.currentIndex,
|
|
child: navigationShell,
|
|
),
|
|
bottomNavigationBar: KrowNavBar(
|
|
currentIndex: navigationShell.currentIndex,
|
|
onSelect: (i) => navigationShell.goBranch(
|
|
i,
|
|
initialLocation: i == navigationShell.currentIndex,
|
|
),
|
|
// Must stay in the same order as the shell branches in the router.
|
|
items: [
|
|
const KrowNavItem(icon: LucideIcons.briefcase, label: 'Shifts'),
|
|
const KrowNavItem(icon: LucideIcons.wallet, label: 'Pay'),
|
|
const KrowNavItem(icon: LucideIcons.house, label: 'Today'),
|
|
KrowNavItem(
|
|
icon: LucideIcons.clock,
|
|
label: 'Clock',
|
|
showDot: needsReview,
|
|
),
|
|
const KrowNavItem(icon: LucideIcons.user, label: 'Profile'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Fades and lifts the tab content each time the tab changes.
|
|
///
|
|
/// Deliberately not an AnimatedSwitcher: that would hold two copies of the
|
|
/// shell in the tree at once, and the branch navigators are keyed by
|
|
/// GlobalKey, so duplicating them would throw. This keeps one subtree — the
|
|
/// branches stay alive and keep their scroll positions — and animates it in
|
|
/// place when the index changes.
|
|
class _TabTransition extends StatefulWidget {
|
|
const _TabTransition({required this.index, required this.child});
|
|
|
|
final int index;
|
|
final Widget child;
|
|
|
|
@override
|
|
State<_TabTransition> createState() => _TabTransitionState();
|
|
}
|
|
|
|
class _TabTransitionState extends State<_TabTransition>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 260),
|
|
value: 1,
|
|
);
|
|
|
|
late final Animation<double> _curve = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeOutCubic,
|
|
);
|
|
|
|
@override
|
|
void didUpdateWidget(_TabTransition oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.index != widget.index) _controller.forward(from: 0);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _curve,
|
|
// The shell is passed through untouched so it is never rebuilt.
|
|
child: widget.child,
|
|
builder: (context, child) {
|
|
final t = _curve.value;
|
|
return Opacity(
|
|
// Never fully transparent: a tab that blinks out reads as a glitch.
|
|
opacity: 0.35 + 0.65 * t,
|
|
child: Transform.translate(
|
|
offset: Offset(0, (1 - t) * 10),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|