import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../data/api/report_data_source.dart'; import '../../data/models/models.dart'; import '../../theme/app_icons.dart'; import '../../theme/yanting_text.dart'; import '../../theme/yanting_tokens.dart'; import '../../theme/wise_tokens.dart'; import '../../widgets/app_buttons.dart'; import '../../widgets/app_card.dart'; import '../../widgets/badges.dart'; import '../../widgets/mini_player.dart'; import '../../widgets/sheets.dart'; import '../../widgets/states.dart'; import 'modules/renderer_registry.dart'; class ReportDetailPage extends HookConsumerWidget { const ReportDetailPage({ required this.reportId, required this.dataSource, this.player = const PlayerStateModel(), this.onStartAudio, this.onToggleAudio, this.onSeekAudio, this.onSpeed, super.key, }); final String reportId; final ReportDataSource dataSource; final PlayerStateModel player; final void Function( String audioId, String reportId, String title, int durationSec, )? onStartAudio; final VoidCallback? onToggleAudio; final void Function(int delta)? onSeekAudio; final VoidCallback? onSpeed; @override Widget build(BuildContext context, WidgetRef ref) { final retryCount = useState(0); final detailFuture = useMemoized(() => dataSource.reportDetail(reportId), [ dataSource, reportId, retryCount.value, ]); final snapshot = useFuture(detailFuture); const registry = ModuleRendererRegistry(); return Scaffold( appBar: AppBar(title: const Text('研报详情')), body: snapshot.connectionState != ConnectionState.done ? const LoadingState() : snapshot.hasError ? ErrorState( message: snapshot.error.toString(), onRetry: () => retryCount.value++, ) : _ReportDetailContent( detail: snapshot.data!, dataSource: dataSource, player: player, onStartAudio: onStartAudio, onToggleAudio: onToggleAudio, onSeekAudio: onSeekAudio, onSpeed: onSpeed, registry: registry, ), ); } } class _ReportDetailContent extends StatelessWidget { const _ReportDetailContent({ required this.detail, required this.dataSource, required this.player, required this.registry, this.onStartAudio, this.onToggleAudio, this.onSeekAudio, this.onSpeed, }); final ReportDetail detail; final ReportDataSource dataSource; final PlayerStateModel player; final ModuleRendererRegistry registry; final void Function( String audioId, String reportId, String title, int durationSec, )? onStartAudio; final VoidCallback? onToggleAudio; final void Function(int delta)? onSeekAudio; final VoidCallback? onSpeed; @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16), children: [ AppCard( color: YantingColors.brandSoft, borderColor: YantingColors.brandSoftBorder, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: WiseSpacing.x2, runSpacing: WiseSpacing.x2, children: [ AppBadge( text: detail.interpretationLabel, kind: BadgeKind.brand, ), if (detail.hasAudio) const AppBadge( text: '音频', icon: AppIcons.playCircle, kind: BadgeKind.audio, ), AppBadge( text: asString(detail.source['source_tier']), kind: BadgeKind.tier, ), ], ), const SizedBox(height: WiseSpacing.x3), Text( detail.titleCn, maxLines: 3, overflow: TextOverflow.ellipsis, style: YantingText.sectionTitle.copyWith(fontSize: 21), ), if (detail.oneLiner.isNotEmpty) ...[ const SizedBox(height: WiseSpacing.x2), Text(detail.oneLiner, style: YantingText.body), ], const SizedBox(height: WiseSpacing.x3), Text( '${detail.institution.nameCn} · ${formatDate(detail.releasedAt)}', style: YantingText.meta, ), ], ), ), const SizedBox(height: WiseSpacing.x4), _ActionBar(detail: detail), const SizedBox(height: WiseSpacing.x4), _Toc(modules: detail.modules), const SizedBox(height: WiseSpacing.x4), for (final module in detail.modules) ...[ registry.card( context: context, module: module, report: detail, dataSource: dataSource, player: player, onStartAudio: onStartAudio, onToggleAudio: onToggleAudio, onSeekAudio: onSeekAudio, onSpeed: onSpeed, ), const SizedBox(height: WiseSpacing.x4), ], ], ); } } class _ActionBar extends StatelessWidget { const _ActionBar({required this.detail}); final ReportDetail detail; @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: AppButton( label: '收藏', icon: AppIcons.heart, kind: AppButtonKind.ghost, onPressed: () => showLoginSheet(context, reason: '登录后保存到你的收藏'), ), ), const SizedBox(width: WiseSpacing.x2), Expanded( child: AppButton( label: '原文', icon: AppIcons.externalLink, kind: AppButtonKind.ghost, onPressed: () => showOutboundSheet(context, title: detail.titleCn), ), ), ], ); } } class _Toc extends StatelessWidget { const _Toc({required this.modules}); final List modules; @override Widget build(BuildContext context) { if (modules.isEmpty) return const SizedBox.shrink(); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final module in modules) Padding( padding: const EdgeInsets.only(right: WiseSpacing.x2), child: AppBadge(text: module.titleCn, kind: BadgeKind.brand), ), ], ), ); } }