add
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/utils/share_text.dart';
|
||||
import '../../models/video_item.dart';
|
||||
import '../../providers/feed_provider.dart';
|
||||
import 'widgets/comment_sheet.dart';
|
||||
import 'widgets/feed_overlays.dart';
|
||||
import 'widgets/feed_video_page.dart';
|
||||
|
||||
/// 首页 Feed:全屏竖向 PageView 沉浸式播放(《01-开发文档》5.2.1)
|
||||
/// - 顶部:策略 Tab(推荐/最新/热度)+ 搜索图标(占位)
|
||||
/// - 底部:游客/非会员免费剩余次数提示条
|
||||
/// - 滑出暂停、滑入自动播放并上报观看计费
|
||||
class FeedScreen extends ConsumerStatefulWidget {
|
||||
const FeedScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<FeedScreen> createState() => _FeedScreenState();
|
||||
}
|
||||
|
||||
class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
with WidgetsBindingObserver {
|
||||
FeedStrategy _strategy = FeedStrategy.recommend;
|
||||
final PageController _pageController = PageController();
|
||||
StreamSubscription<FeedEvent>? _eventSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// 监听计费/UI 事件(弹窗与 Toast 由 Notifier 事件流驱动)
|
||||
_eventSub = ref
|
||||
.read(feedProvider(_strategy).notifier)
|
||||
.events
|
||||
.listen(_handleEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_eventSub?.cancel();
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// App 退后台暂停、回前台恢复播放
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
notifier.pool.pauseCurrent();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
notifier.pool.playCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEvent(FeedEvent event) {
|
||||
if (!mounted) return;
|
||||
switch (event) {
|
||||
case FeedToastEvent(:final message):
|
||||
showTopToast(context, message);
|
||||
case FeedGuestLimitEvent():
|
||||
showGuestLimitDialog(context);
|
||||
case FeedPointsInsufficientEvent(:final points):
|
||||
showPointsInsufficientDialog(context, points);
|
||||
}
|
||||
}
|
||||
|
||||
void _switchStrategy(FeedStrategy next) {
|
||||
if (next == _strategy) return;
|
||||
// 暂停旧策略播放,切换后新策略独立维护状态与播放器池
|
||||
ref.read(feedProvider(_strategy).notifier).pool.pauseCurrent();
|
||||
setState(() => _strategy = next);
|
||||
_eventSub?.cancel();
|
||||
_eventSub = ref
|
||||
.read(feedProvider(_strategy).notifier)
|
||||
.events
|
||||
.listen(_handleEvent);
|
||||
_pageController.jumpToPage(0);
|
||||
}
|
||||
|
||||
/// 打开评论弹层:打开期间暂停当前播放,关闭后恢复
|
||||
Future<void> _openComments(VideoItem item) async {
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
notifier.pool.pauseCurrent();
|
||||
await showCommentSheet(
|
||||
context,
|
||||
videoId: item.videoId,
|
||||
onCountChanged: (delta) =>
|
||||
notifier.adjustCommentCount(item.videoId, delta),
|
||||
);
|
||||
if (mounted) notifier.pool.playCurrent();
|
||||
}
|
||||
|
||||
/// 系统分享面板(分享文案:标题 + 标签 + 播放链接)
|
||||
void _share(VideoItem item) => Share.share(buildShareText(item));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(feedProvider(_strategy));
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 内容区
|
||||
if (state.isRefreshing && state.items.isEmpty)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary),
|
||||
)
|
||||
else if (state.loadFailed && state.items.isEmpty)
|
||||
_buildRetry(notifier)
|
||||
else if (state.items.isEmpty)
|
||||
const Center(
|
||||
child: Text('暂无视频', style: TextStyle(color: AppColors.textSecondary)),
|
||||
)
|
||||
else
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: state.items.length,
|
||||
onPageChanged: notifier.onPageChanged,
|
||||
itemBuilder: (context, index) {
|
||||
final item = state.items[index];
|
||||
return FeedVideoPage(
|
||||
key: ValueKey(item.videoId),
|
||||
item: item,
|
||||
slotIndex: notifier.pool.slotOf(index),
|
||||
pool: notifier.pool,
|
||||
isCurrent: index == state.currentIndex,
|
||||
onLike: notifier.toggleLike,
|
||||
onFavorite: notifier.toggleFavorite,
|
||||
onComment: _openComments,
|
||||
onShare: _share,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 顶部:策略 Tab + 搜索(占位)
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
for (final s in FeedStrategy.values) ...[
|
||||
_StrategyTab(
|
||||
label: s.label,
|
||||
selected: s == _strategy,
|
||||
onTap: () => _switchStrategy(s),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, color: Colors.white),
|
||||
onPressed: () => context.push('/search'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 底部:免费剩余次数提示条(≤3 时显示)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: FreeQuotaBanner(remaining: state.freeRemainingToday),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRetry(FeedNotifier notifier) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('网络异常', style: TextStyle(color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: notifier.refresh,
|
||||
child: const Text('点击重试', style: TextStyle(color: AppColors.primary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 顶部策略 Tab
|
||||
class _StrategyTab extends StatelessWidget {
|
||||
const _StrategyTab({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
color: selected ? Colors.white : AppColors.textSecondary,
|
||||
shadows: const [Shadow(color: Colors.black54, blurRadius: 4)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user