fix(player): 解锁状态登录期内共享,重进播放页不再丢解锁
解锁记录原本是播放页实例的私有集合,退出再进入会先闪一次锁定态,还会 被 selectLocked 提前释放播放器。改为进程内共享的 UnlockedEpisodeRegistry, 由全局注入并在登录态变化时清空;播放器三处渲染分支统一走同一判定, 会话锁态只约束当前页,避免相邻预加载页被误判为锁定。
This commit is contained in:
1 parent
a4d08e7c1e
commit
57f84c45bd
6 files changed
+103
-18
No files matched your search
@@ -47,6 +47,7 @@ import 'core/data/services/playlist_controller.dart';
|
||||
import 'core/data/services/event_tracker.dart';
|
||||
import 'core/data/services/playback_position_cache.dart';
|
||||
import 'core/data/services/playback_session.dart';
|
||||
import 'core/data/services/unlocked_episode_registry.dart';
|
||||
import 'core/data/services/video_player_port.dart';
|
||||
import 'core/data/services/wallet_controller.dart';
|
||||
import 'core/navigation/app_route_observer.dart';
|
||||
@@ -207,6 +208,10 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
|
||||
late final LocaleController _localeController = LocaleController();
|
||||
|
||||
/// 登录期内共享的「已解锁收费集」:播放页进出、feed 与全屏切换都要沿用。
|
||||
late final UnlockedEpisodeRegistry _unlockedEpisodes =
|
||||
UnlockedEpisodeRegistry();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -226,6 +231,8 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
if (_auth.isSignedIn == _wasSignedIn) return;
|
||||
_wasSignedIn = _auth.isSignedIn;
|
||||
_interactionController.clear();
|
||||
// 解锁结论属于账号:换人登录后不能沿用上一个人的可播判断。
|
||||
_unlockedEpisodes.clear();
|
||||
if (_auth.isSignedIn) {
|
||||
unawaited(_favoriteController.load());
|
||||
unawaited(_walletController.load());
|
||||
@@ -271,6 +278,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
Provider<CatalogApi>.value(value: _catalogApi),
|
||||
Provider<PlaybackRepository>.value(value: _playback),
|
||||
Provider<PlaybackPositionCache>.value(value: _playbackPositionCache),
|
||||
Provider<UnlockedEpisodeRegistry>.value(value: _unlockedEpisodes),
|
||||
Provider<WatchHistoryRepository>.value(value: _watchHistory),
|
||||
Provider<FavoriteRepository>.value(value: _favorites),
|
||||
Provider<CommentRepository>.value(value: _commentRepository),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// 本次登录期内已确认可播的收费集。
|
||||
///
|
||||
/// 解锁本身是服务端行为(`POST /app/episodes/{id}/unlock` 成功即生效),
|
||||
/// 客户端只需要记住「这一集不用再挡」。若不记住,退出播放页再进来会先闪
|
||||
/// 一次锁定态,且会被 [PlaybackSession.selectLocked] 提前释放播放器。
|
||||
///
|
||||
/// 生命周期跟随账号:登录态变化时由 `app.dart` 调用 [clear],避免换账号后
|
||||
/// 沿用上一个人的解锁结论。
|
||||
class UnlockedEpisodeRegistry {
|
||||
final Set<String> _episodeIds = <String>{};
|
||||
|
||||
bool contains(String episodeId) => _episodeIds.contains(episodeId);
|
||||
|
||||
void markUnlocked(String episodeId) {
|
||||
if (episodeId.isEmpty) return;
|
||||
_episodeIds.add(episodeId);
|
||||
}
|
||||
|
||||
void clear() => _episodeIds.clear();
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import '../../core/data/repositories/playback_repository.dart';
|
||||
import '../../core/data/services/android_picture_in_picture.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/unlocked_episode_registry.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import 'player_logic.dart';
|
||||
|
||||
@@ -13,6 +14,7 @@ class PlayerBinding {
|
||||
required WatchHistoryRepository history,
|
||||
required PlaybackRepository playback,
|
||||
required PlaybackPositionCache positionCache,
|
||||
UnlockedEpisodeRegistry? unlockedEpisodes,
|
||||
WalletController? wallet,
|
||||
DownloadController? downloads,
|
||||
bool Function()? isSignedIn,
|
||||
@@ -20,6 +22,7 @@ class PlayerBinding {
|
||||
history: history,
|
||||
playback: playback,
|
||||
positionCache: positionCache,
|
||||
unlockedEpisodes: unlockedEpisodes,
|
||||
pictureInPicture: AndroidPictureInPicture(),
|
||||
wallet: wallet,
|
||||
downloads: downloads,
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../../core/data/services/platform_share_service.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/playback_queue.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/unlocked_episode_registry.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
@@ -64,11 +65,13 @@ class PlayerLogic extends ChangeNotifier {
|
||||
required this.pictureInPicture,
|
||||
this.wallet,
|
||||
this.downloads,
|
||||
UnlockedEpisodeRegistry? unlockedEpisodes,
|
||||
PlaybackPositionCache? positionCache,
|
||||
Future<void> Function()? waitForFrame,
|
||||
DateTime Function()? clock,
|
||||
bool Function()? isSignedIn,
|
||||
}) : positionCache = positionCache ?? PlaybackPositionCache(),
|
||||
}) : unlockedEpisodes = unlockedEpisodes ?? UnlockedEpisodeRegistry(),
|
||||
positionCache = positionCache ?? PlaybackPositionCache(),
|
||||
_waitForFrame =
|
||||
waitForFrame ?? (() => WidgetsBinding.instance.endOfFrame),
|
||||
_clock = clock ?? DateTime.now,
|
||||
@@ -79,6 +82,9 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final AndroidPictureInPicture pictureInPicture;
|
||||
final WalletController? wallet;
|
||||
final DownloadController? downloads;
|
||||
|
||||
/// 已确认可播的收费集(登录期内共享):退出播放页再进来不必重新挡一次。
|
||||
final UnlockedEpisodeRegistry unlockedEpisodes;
|
||||
final PlaybackPositionCache positionCache;
|
||||
final Future<void> Function() _waitForFrame;
|
||||
final DateTime Function() _clock;
|
||||
@@ -169,14 +175,15 @@ class PlayerLogic extends ChangeNotifier {
|
||||
|
||||
/// 当前设备是否为 iOS 真机环境(面板据此显示自动小窗开关)。
|
||||
bool get iosDevice => !kIsWeb && Platform.isIOS;
|
||||
|
||||
/// PiP 状态是全局单通道回调,须广播给所有存活的 PlayerLogic:
|
||||
/// 推荐页每页一个 logic,若只保留最后一次注册的 handler,
|
||||
/// 相邻预加载页会盖掉正在播放页的注册,导致小窗 UI 不隐藏且无声音。
|
||||
static final Set<PlayerLogic> _pipSubscribers = <PlayerLogic>{};
|
||||
|
||||
/// 「记住上次选集」:dramaId → 集号。只在用户从选集面板主动挑集时写入
|
||||
/// (见 [_recordPickedEpisode]),feed 里同一部剧据此显示上次选中的集。
|
||||
static final Map<String, int> pickedEpisode = {};
|
||||
final Set<String> _unlockedEpisodeIds = {};
|
||||
final Map<String, EpisodeStats> _episodeStats = {};
|
||||
final Map<String, DateTime> _episodeStatsFetchedAt = {};
|
||||
final Map<String, Future<void>> _episodeStatsInFlight = {};
|
||||
@@ -562,7 +569,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final localPath = await downloads?.findLocalPath(episode.id);
|
||||
if (request != pageRequest) return false;
|
||||
if (localPath != null) {
|
||||
_unlockedEpisodeIds.add(episode.id);
|
||||
unlockedEpisodes.markUnlocked(episode.id);
|
||||
blockedTicket = null;
|
||||
blockedEpisodeId = null;
|
||||
qualities = const [];
|
||||
@@ -587,7 +594,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final playUrl = preferred ?? ticket.playUrl;
|
||||
if (preferred == null) selectedQuality = 'AUTO';
|
||||
if (ticket.accessible && playUrl.isNotEmpty) {
|
||||
_unlockedEpisodeIds.add(episode.id);
|
||||
unlockedEpisodes.markUnlocked(episode.id);
|
||||
// 签名地址原样写回,不做任何拼接/改写(AppConfig.mediaUrl 只补 host)
|
||||
sessionEpisodes[index] = episode.withVideoUrl(
|
||||
playUrl,
|
||||
@@ -860,7 +867,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
unlockError = service.unlockError;
|
||||
return false;
|
||||
}
|
||||
_unlockedEpisodeIds.add(episode.id);
|
||||
unlockedEpisodes.markUnlocked(episode.id);
|
||||
final ready = await ensureTicket(index, forceRefresh: true);
|
||||
if (ready) {
|
||||
await _selectVisible(index);
|
||||
@@ -1688,7 +1695,16 @@ class PlayerLogic extends ChangeNotifier {
|
||||
/// 收费集只有在本会话确认可播或解锁成功后才解除门禁。
|
||||
bool isEpisodeLocked(Episode episode) {
|
||||
return (episode.locked || !episode.isFree) &&
|
||||
!_unlockedEpisodeIds.contains(episode.id);
|
||||
!unlockedEpisodes.contains(episode.id);
|
||||
}
|
||||
|
||||
/// 单页锁定判定:剧集自身锁定 + 会话锁态(只约束当前页)+ 后端拒绝。
|
||||
///
|
||||
/// 播放页的多个渲染分支都必须走这里,否则相邻预加载页会被会话锁态误判成锁定。
|
||||
bool isEpisodeBlockedFor(Episode episode, int index) {
|
||||
return isEpisodeLocked(episode) ||
|
||||
(index == session.currentIndex && session.isLocked) ||
|
||||
blockedEpisodeId == episode.id;
|
||||
}
|
||||
|
||||
/// 按可见队列选择一集,并预热上一集与下一集。
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/sprite_thumbnail_axis.dart';
|
||||
import '../../core/data/services/playback_thumbnail_service.dart';
|
||||
import '../../core/data/services/unlocked_episode_registry.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
@@ -167,6 +168,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
history: context.read<WatchHistoryRepository>(),
|
||||
playback: context.read<PlaybackRepository>(),
|
||||
positionCache: context.read<PlaybackPositionCache>(),
|
||||
unlockedEpisodes: _readUnlockedEpisodes(context),
|
||||
wallet: widget.wallet,
|
||||
downloads: widget.downloads,
|
||||
isSignedIn: () => context.read<AuthController>().isSignedIn,
|
||||
@@ -349,6 +351,16 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
setState(() => _playbackSpeed = value);
|
||||
}
|
||||
|
||||
/// 全局解锁登记表(登录期内共享);直接构造播放页的测试没有 Provider,
|
||||
/// 返回 null 让 PlayerLogic 退化成页面自用的一份。
|
||||
UnlockedEpisodeRegistry? _readUnlockedEpisodes(BuildContext context) {
|
||||
try {
|
||||
return context.read<UnlockedEpisodeRegistry>();
|
||||
} on ProviderNotFoundException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _hideChrome() {
|
||||
if (!_chromeVisible) return;
|
||||
setState(() => _chromeVisible = false);
|
||||
@@ -956,10 +968,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
}
|
||||
final episode = _resolvedEpisode(index);
|
||||
final selected = index == currentIndex;
|
||||
final locked =
|
||||
logic.isEpisodeLocked(episode) ||
|
||||
(selected && logic.session.isLocked) ||
|
||||
logic.blockedEpisodeId == episode.id;
|
||||
final locked = logic.isEpisodeBlockedFor(episode, index);
|
||||
final port = locked
|
||||
? null
|
||||
: (selected ? logic.session.active : null) ??
|
||||
@@ -1042,10 +1051,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
final drama = entry.drama;
|
||||
final episode = _resolvedEpisode(index);
|
||||
final selected = index == logic.session.currentIndex;
|
||||
final locked =
|
||||
logic.isEpisodeLocked(episode) ||
|
||||
logic.session.isLocked ||
|
||||
logic.blockedEpisodeId == episode.id;
|
||||
final locked = logic.isEpisodeBlockedFor(episode, index);
|
||||
// Selection changes before the prepared slot is promoted. Use its controller
|
||||
// throughout the handoff instead of briefly falling back to a placeholder.
|
||||
final port = locked
|
||||
@@ -1294,10 +1300,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
final drama = entry.drama;
|
||||
final episode = _resolvedEpisode(index);
|
||||
final selected = index == logic.session.currentIndex;
|
||||
final locked =
|
||||
logic.isEpisodeLocked(episode) ||
|
||||
logic.session.isLocked ||
|
||||
logic.blockedEpisodeId == episode.id;
|
||||
final locked = logic.isEpisodeBlockedFor(episode, index);
|
||||
final port = locked
|
||||
? null
|
||||
: (selected ? logic.session.active : null) ??
|
||||
|
||||
Reference in new issue
Block a user