1
This commit is contained in:
@@ -37,6 +37,10 @@ final activeFeedStrategyProvider =
|
||||
/// (否则在"我的"等页面退后台再回来会误恢复声音)
|
||||
final feedTabVisibleProvider = StateProvider<bool>((_) => true);
|
||||
|
||||
/// 游客今日免费额度已用尽(全局):所有 Feed 策略/搜索播放统一锁死播放,
|
||||
/// 直到登录成功才解除——避免切换 Tab 或点「再逛逛」后继续免费观看
|
||||
final guestLockedProvider = StateProvider<bool>((_) => false);
|
||||
|
||||
/// Feed 页 UI 事件(Notifier 不持有 BuildContext,通过事件流驱动弹窗/Toast)
|
||||
sealed class FeedEvent {
|
||||
const FeedEvent();
|
||||
@@ -208,6 +212,11 @@ class FeedNotifier extends StateNotifier<FeedState> {
|
||||
/// 翻页:滑出即暂停、滑入自动播放并上报观看计费(《01-开发文档》5.3)
|
||||
void onPageChanged(int index) {
|
||||
if (index < 0 || index >= state.items.length) return;
|
||||
if (_ref.read(guestLockedProvider)) {
|
||||
// 游客已锁:只记位置,不绑定播放
|
||||
state = state.copyWith(currentIndex: index);
|
||||
return;
|
||||
}
|
||||
final item = state.items[index];
|
||||
// 同一视频重新绑定(冷启动跳页)时保留播放位置,否则新页从 0 开始
|
||||
final sameVideo = pool.currentVideoId == item.videoId;
|
||||
@@ -245,6 +254,8 @@ class FeedNotifier extends StateNotifier<FeedState> {
|
||||
/// 绑定播放窗口 + 观看计费上报
|
||||
Future<void> _bindAndReport(int index) async {
|
||||
if (index < 0 || index >= state.items.length) return;
|
||||
// 游客免费额度已用尽:锁死播放(不绑定、不播放),直到登录成功
|
||||
if (_ref.read(guestLockedProvider)) return;
|
||||
final items = state.items;
|
||||
await pool.bindWindow(
|
||||
index,
|
||||
@@ -267,6 +278,8 @@ class FeedNotifier extends StateNotifier<FeedState> {
|
||||
} on ApiException catch (e) {
|
||||
pool.pauseCurrent();
|
||||
if (e.isGuestLimit) {
|
||||
// 全局锁:此后任何策略/搜索播放都不再起播,点「再逛逛」也看不了
|
||||
_ref.read(guestLockedProvider.notifier).state = true;
|
||||
_eventController.add(const FeedGuestLimitEvent());
|
||||
} else if (e.isPointsInsufficient) {
|
||||
final points = (e.data?['points'] as num?)?.toInt() ?? 0;
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../core/log/action_logger.dart';
|
||||
import '../../core/storage/feed_progress_store.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/video_item.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/feed_provider.dart';
|
||||
import '../../router.dart';
|
||||
import 'widgets/comment_sheet.dart';
|
||||
@@ -85,6 +86,8 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
void didPopNext() {
|
||||
if (!mounted) return;
|
||||
if (!ref.read(feedTabVisibleProvider)) return;
|
||||
// 游客额度已用尽:保持锁定,不恢复播放
|
||||
if (ref.read(guestLockedProvider)) return;
|
||||
ActionLogger.I.log('feed_did_pop_next', page: '/home', detail: {
|
||||
'strategy': _strategy.name,
|
||||
});
|
||||
@@ -124,8 +127,8 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
notifier.pool.pauseCurrent();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
_appActive = true;
|
||||
// 仅在首页 Tab 可见时恢复(在"我的"等页面回前台不播)
|
||||
if (ref.read(feedTabVisibleProvider)) {
|
||||
// 仅在首页 Tab 可见时恢复(在"我的"等页面回前台不播);游客锁定时不恢复
|
||||
if (ref.read(feedTabVisibleProvider) && !ref.read(guestLockedProvider)) {
|
||||
unawaited(() async {
|
||||
await notifier.pool.resume();
|
||||
await notifier.rebindCurrent();
|
||||
@@ -183,11 +186,13 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
}
|
||||
// jumpToPage 后 resume + rebindCurrent:suspend 已 stop 槽位,仅 play
|
||||
// 播不起来,必须 rebind 重新 open(有断点记录则 seek 续播)
|
||||
unawaited(() async {
|
||||
final n = ref.read(feedProvider(_strategy).notifier);
|
||||
await n.pool.resume();
|
||||
await n.rebindCurrent();
|
||||
}());
|
||||
if (!ref.read(guestLockedProvider)) {
|
||||
unawaited(() async {
|
||||
final n = ref.read(feedProvider(_strategy).notifier);
|
||||
await n.pool.resume();
|
||||
await n.rebindCurrent();
|
||||
}());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,6 +213,15 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(feedProvider(_strategy));
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
final guestLocked = ref.watch(guestLockedProvider);
|
||||
|
||||
// 登录成功后解除游客锁并恢复当前视频播放(登录态下改为积分/会员计费)
|
||||
ref.listen<AuthState>(authProvider, (previous, next) {
|
||||
if (next.isLoggedIn && ref.read(guestLockedProvider)) {
|
||||
ref.read(guestLockedProvider.notifier).state = false;
|
||||
unawaited(ref.read(feedProvider(_strategy).notifier).rebindCurrent());
|
||||
}
|
||||
});
|
||||
|
||||
// 冷启动续播:列表加载完成后把 PageView 定位到恢复的下标(每个策略一次)。
|
||||
// Notifier 会在 refresh 时按上次 videoId 恢复 currentIndex,此处只负责跳页,
|
||||
@@ -304,6 +318,9 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
||||
bottom: 0,
|
||||
child: FreeQuotaBanner(remaining: state.freeRemainingToday),
|
||||
),
|
||||
|
||||
// 游客免费额度已用尽:全屏锁(必须登录,点「再逛逛」也无法继续观看)
|
||||
if (guestLocked) const Positioned.fill(child: GuestLockOverlay()),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -59,6 +59,50 @@ void showGuestLimitDialog(BuildContext context) {
|
||||
);
|
||||
}
|
||||
|
||||
/// 游客免费额度用完后的全屏锁:盖住播放区并吞掉手势,
|
||||
/// 用户点「再逛逛」关闭弹窗后依然无法继续观看(必须登录)
|
||||
class GuestLockOverlay extends StatelessWidget {
|
||||
const GuestLockOverlay({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black.withValues(alpha: 0.92),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.lock_outline, size: 48, color: AppColors.gold),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'今日免费次数已用完',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'注册或登录后继续观看,登录即可解锁全部视频',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: AppColors.textSecondary, height: 1.6),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/login'),
|
||||
child: const Text('去登录/注册'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 积分不足:引导充值 / 开通会员(2001)
|
||||
void showPointsInsufficientDialog(BuildContext context, int points) {
|
||||
showDialog<void>(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../core/log/action_logger.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/video_item.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/feed_provider.dart';
|
||||
import '../../providers/search_provider.dart';
|
||||
import '../../router.dart';
|
||||
@@ -66,6 +67,8 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
|
||||
@override
|
||||
void didPopNext() {
|
||||
if (!mounted) return;
|
||||
// 游客额度已用尽:保持锁定,不恢复播放
|
||||
if (ref.read(guestLockedProvider)) return;
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
unawaited(() async {
|
||||
await notifier.pool.resume();
|
||||
@@ -94,8 +97,9 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
|
||||
} else if (state == AppLifecycleState.inactive) {
|
||||
notifier.pool.pauseCurrent();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
// 本页不可见时(压在登录页/评论页下)不恢复播放
|
||||
if (ModalRoute.of(context)?.isCurrent ?? false) {
|
||||
// 本页不可见时(压在登录页/评论页下)不恢复播放;游客锁定时也不恢复
|
||||
if ((ModalRoute.of(context)?.isCurrent ?? false) &&
|
||||
!ref.read(guestLockedProvider)) {
|
||||
unawaited(() async {
|
||||
await notifier.pool.resume();
|
||||
await notifier.rebindCurrent();
|
||||
@@ -134,6 +138,15 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
|
||||
final state = ref.watch(feedProvider(_strategy));
|
||||
final notifier = ref.read(feedProvider(_strategy).notifier);
|
||||
final keyword = ref.watch(searchProvider).keyword;
|
||||
final guestLocked = ref.watch(guestLockedProvider);
|
||||
|
||||
// 登录成功后解除游客锁并恢复当前视频
|
||||
ref.listen<AuthState>(authProvider, (previous, next) {
|
||||
if (next.isLoggedIn && ref.read(guestLockedProvider)) {
|
||||
ref.read(guestLockedProvider.notifier).state = false;
|
||||
unawaited(ref.read(feedProvider(_strategy).notifier).rebindCurrent());
|
||||
}
|
||||
});
|
||||
|
||||
return PopScope(
|
||||
// 系统返回键/手势也要先停搜索池再放行 pop
|
||||
@@ -222,6 +235,9 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
|
||||
bottom: 0,
|
||||
child: FreeQuotaBanner(remaining: state.freeRemainingToday),
|
||||
),
|
||||
|
||||
// 游客免费额度已用尽:全屏锁(必须登录,点「再逛逛」也无法继续观看)
|
||||
if (guestLocked) const Positioned.fill(child: GuestLockOverlay()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: video_app
|
||||
description: "短视频应用(Flutter,iOS/Android 双端)"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.3.32+48
|
||||
version: 1.3.33+49
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
Reference in New Issue
Block a user