160 lines
5.1 KiB
Dart
160 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.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/yanting_tokens.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 FeedPage extends HookConsumerWidget {
|
|
const FeedPage({
|
|
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 topic = useState('全部');
|
|
final snapshot = ref.watch(recommendedReportsProvider);
|
|
|
|
return snapshot.when(
|
|
loading: () => const LoadingState(),
|
|
error: (error, _) => ErrorState(
|
|
message: error.toString(),
|
|
onRetry: () => ref.invalidate(recommendedReportsProvider),
|
|
),
|
|
data: (items) {
|
|
final currentTopic = topic.value;
|
|
final topics = [
|
|
'全部',
|
|
...{for (final item in items) ...item.topics},
|
|
];
|
|
final visible = currentTopic == '全部'
|
|
? items
|
|
: items
|
|
.where((item) => item.topics.contains(currentTopic))
|
|
.toList();
|
|
if (items.isEmpty) {
|
|
return const EmptyState(title: '暂无可推荐的研报解读', message: '稍后再来看看最新内容');
|
|
}
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
YantingSpacing.screenX,
|
|
4,
|
|
YantingSpacing.screenX,
|
|
16,
|
|
),
|
|
children: [
|
|
const PageHeader(title: '研听', subtitle: '全球机构研报中文解读'),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
for (final t in topics)
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: YantingSpacing.x2),
|
|
child: AppChip(
|
|
label: t,
|
|
selected: t == currentTopic,
|
|
onTap: () => topic.value = t,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: YantingSpacing.x3),
|
|
const ShadSeparator.horizontal(),
|
|
const SizedBox(height: YantingSpacing.x3),
|
|
if (visible.isEmpty)
|
|
const EmptyState(
|
|
title: '暂无可推荐的研报解读',
|
|
message: '换个主题,或去研报页看看全部内容',
|
|
icon: Icons.filter_alt_off,
|
|
)
|
|
else ...[
|
|
ReportCardWidget(
|
|
report: visible.first,
|
|
hero: true,
|
|
onTap: () => openReportDetail(
|
|
context,
|
|
dataSource,
|
|
visible.first,
|
|
player: player,
|
|
onStartAudio: onStartModuleAudio,
|
|
onToggleAudio: onToggleAudio,
|
|
onSeekAudio: onSeekAudio,
|
|
onSpeed: onSpeed,
|
|
),
|
|
onPlayTap: () => _playFromReport(onPlay, visible.first),
|
|
),
|
|
const SizedBox(height: YantingSpacing.x6),
|
|
const SectionTitle(title: '最新解读', icon: Icons.chevron_right),
|
|
for (final report in visible.skip(1)) ...[
|
|
ReportCardWidget(
|
|
report: report,
|
|
onTap: () => openReportDetail(
|
|
context,
|
|
dataSource,
|
|
report,
|
|
player: player,
|
|
onStartAudio: onStartModuleAudio,
|
|
onToggleAudio: onToggleAudio,
|
|
onSeekAudio: onSeekAudio,
|
|
onSpeed: onSpeed,
|
|
),
|
|
onPlayTap: () => _playFromReport(onPlay, report),
|
|
),
|
|
const SizedBox(height: YantingSpacing.x3),
|
|
],
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|