import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../data/providers.dart'; import '../routing/app_routes.dart'; import '../theme/yanting_text.dart'; import '../widgets/bottom_tab_bar.dart'; import '../widgets/mini_player.dart'; class ShellPage extends ConsumerStatefulWidget { const ShellPage({required this.child, required this.currentPath, super.key}); final Widget child; final String currentPath; @override ConsumerState createState() => _ShellPageState(); } class _ShellPageState extends ConsumerState { static const double _compactHeaderThreshold = 34; bool _showCompactHeader = false; @override void didUpdateWidget(covariant ShellPage oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.currentPath != widget.currentPath && _showCompactHeader) { _showCompactHeader = false; } } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); final player = ref.watch(audioPlayerControllerProvider); final controller = ref.read(audioPlayerControllerProvider.notifier); final canPop = GoRouter.of(context).canPop(); final selectedIndex = _tabs.indexWhere( (tab) => tab.path == widget.currentPath, ); final safeIndex = selectedIndex < 0 ? 0 : selectedIndex; final header = _headerForPath(widget.currentPath); return Scaffold( backgroundColor: theme.colorScheme.background, appBar: AppBar( backgroundColor: theme.colorScheme.background, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, leading: canPop ? ShadIconButton.ghost( onPressed: () => context.pop(), icon: const Icon(LucideIcons.chevronLeft, size: 18), ) : null, title: AnimatedOpacity( opacity: _showCompactHeader ? 1 : 0, duration: const Duration(milliseconds: 160), curve: Curves.easeOut, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( header.title, textAlign: TextAlign.center, style: YantingText.listTitle, ), if (header.subtitle.isNotEmpty) Text( header.subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: YantingText.meta.copyWith(fontSize: 12), ), ], ), ), ), body: ColoredBox( color: theme.colorScheme.background, child: NotificationListener( onNotification: _handleScrollNotification, child: Stack(children: [Positioned.fill(child: widget.child)]), ), ), bottomNavigationBar: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ MiniPlayer(player: player, onToggle: controller.toggleAudio), BottomTabBar( items: yantingBottomTabItems, selectedIndex: safeIndex, onSelected: (index) => context.go(_tabs[index].path), ), ], ), ), ); } bool _handleScrollNotification(ScrollNotification notification) { if (notification.metrics.axis != Axis.vertical) { return false; } final next = notification.metrics.pixels > _compactHeaderThreshold; if (next != _showCompactHeader && mounted) { setState(() => _showCompactHeader = next); } return false; } } _ShellHeader _headerForPath(String path) { return switch (path) { AppRoutes.reports => const _ShellHeader('研报', '全部已发布研报解读'), AppRoutes.institutions => const _ShellHeader('机构', '可获取研报的机构'), AppRoutes.listen => const _ShellHeader('听单', '已转音频的研报解读'), AppRoutes.profile => const _ShellHeader('我的', ''), _ => const _ShellHeader('研听', '全球机构研报中文解读'), }; } class _ShellHeader { const _ShellHeader(this.title, this.subtitle); final String title; final String subtitle; } class _TabItem { const _TabItem({required this.path}); final String path; } const List<_TabItem> _tabs = [ _TabItem(path: AppRoutes.home), _TabItem(path: AppRoutes.reports), _TabItem(path: AppRoutes.institutions), _TabItem(path: AppRoutes.listen), _TabItem(path: AppRoutes.profile), ];