222 lines
6.1 KiB
Dart
222 lines
6.1 KiB
Dart
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/api/report_data_source.dart';
|
|
import '../../data/content_providers.dart';
|
|
import '../../data/models/models.dart';
|
|
import '../../routing/app_routes.dart';
|
|
import '../../theme/app_icons.dart';
|
|
import '../../theme/yanting_text.dart';
|
|
import '../../theme/yanting_tokens.dart';
|
|
import '../../widgets/app_card.dart';
|
|
import '../../widgets/badges.dart';
|
|
import '../../widgets/mini_player.dart';
|
|
import '../../widgets/page_header.dart';
|
|
import '../../widgets/states.dart';
|
|
import '../shared/report_card_widget.dart';
|
|
|
|
class HomePage extends HookConsumerWidget {
|
|
const HomePage({
|
|
required this.dataSource,
|
|
required this.onPlay,
|
|
this.player = const PlayerStateModel(),
|
|
this.onStartModuleAudio,
|
|
this.onToggleAudio,
|
|
this.onSeekAudio,
|
|
this.onSpeed,
|
|
super.key,
|
|
});
|
|
|
|
final ReportDataSource dataSource;
|
|
final void Function(AudioItem item) onPlay;
|
|
final PlayerStateModel player;
|
|
final void Function(
|
|
String audioId,
|
|
String reportId,
|
|
String title,
|
|
int durationSec,
|
|
)?
|
|
onStartModuleAudio;
|
|
final VoidCallback? onToggleAudio;
|
|
final void Function(int delta)? onSeekAudio;
|
|
final VoidCallback? onSpeed;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final snapshot = ref.watch(recommendedReportsProvider);
|
|
|
|
return snapshot.when(
|
|
loading: () => const LoadingState(),
|
|
error: (error, _) => ErrorState(
|
|
message: error.toString(),
|
|
onRetry: () => ref.invalidate(recommendedReportsProvider),
|
|
),
|
|
data: (items) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
YantingSpacing.screenX,
|
|
4,
|
|
YantingSpacing.screenX,
|
|
16,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const PageHeader(title: '研听', subtitle: '全球机构研报中文解读'),
|
|
const SectionTitle(title: '推荐'),
|
|
if (items.isEmpty)
|
|
const EmptyState(title: '暂无可推荐的研报解读', message: '稍后再来看看最新内容')
|
|
else
|
|
ReportCardWidget(
|
|
report: items.first,
|
|
hero: true,
|
|
onTap: () => openReportDetail(
|
|
context,
|
|
dataSource,
|
|
items.first,
|
|
player: player,
|
|
onStartAudio: onStartModuleAudio,
|
|
onToggleAudio: onToggleAudio,
|
|
onSeekAudio: onSeekAudio,
|
|
onSpeed: onSpeed,
|
|
),
|
|
onPlayTap: () => _playFromReport(onPlay, items.first),
|
|
),
|
|
const SizedBox(height: YantingSpacing.x6),
|
|
for (final item in _directoryItems)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: _DirectoryCard(item: item),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DirectoryItem {
|
|
const _DirectoryItem({
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.icon,
|
|
required this.path,
|
|
});
|
|
|
|
final String title;
|
|
final String subtitle;
|
|
final IconData icon;
|
|
final String path;
|
|
}
|
|
|
|
class _DirectoryCard extends StatelessWidget {
|
|
const _DirectoryCard({required this.item});
|
|
|
|
final _DirectoryItem item;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = ShadTheme.of(context);
|
|
|
|
return AppCard(
|
|
onTap: () => context.push(item.path),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.secondary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
item.icon,
|
|
size: 20,
|
|
color: theme.colorScheme.secondaryForeground,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Flexible(
|
|
child: Text(item.title, style: YantingText.listTitle),
|
|
),
|
|
if (item.title == '推荐') ...[
|
|
const SizedBox(width: 8),
|
|
const AppBadge(text: '首页', kind: BadgeKind.tier),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(item.subtitle, style: YantingText.meta),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 16,
|
|
color: theme.colorScheme.mutedForeground,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const _directoryItems = [
|
|
_DirectoryItem(
|
|
title: '推荐',
|
|
subtitle: '主题筛选后的重点研报解读',
|
|
icon: AppIcons.sparkle,
|
|
path: AppRoutes.home,
|
|
),
|
|
_DirectoryItem(
|
|
title: '研报',
|
|
subtitle: '搜索、筛选和浏览全部研报',
|
|
icon: AppIcons.article,
|
|
path: AppRoutes.reports,
|
|
),
|
|
_DirectoryItem(
|
|
title: '机构',
|
|
subtitle: '按机构查看来源与覆盖主题',
|
|
icon: AppIcons.bank,
|
|
path: AppRoutes.institutions,
|
|
),
|
|
_DirectoryItem(
|
|
title: '听单',
|
|
subtitle: '继续收听音频解读',
|
|
icon: AppIcons.headphones,
|
|
path: AppRoutes.listen,
|
|
),
|
|
_DirectoryItem(
|
|
title: '我的',
|
|
subtitle: '登录、收藏与合规说明',
|
|
icon: AppIcons.user,
|
|
path: AppRoutes.profile,
|
|
),
|
|
];
|
|
|
|
void _playFromReport(
|
|
void Function(AudioItem item) onPlay,
|
|
ReportCardModel report,
|
|
) {
|
|
onPlay(
|
|
AudioItem(
|
|
audioId: 'local_${report.id}',
|
|
reportId: report.id,
|
|
titleCn: report.titleCn,
|
|
reportTitleCn: report.titleCn,
|
|
durationSec: 180,
|
|
institution: report.institution,
|
|
),
|
|
);
|
|
}
|