feat(player): 切集刷新互动计数统计
This commit is contained in:
1 parent
37353dac15
commit
188811fde6
11 files changed
+694
-20
No files matched your search
@@ -9,6 +9,7 @@ import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/models/episode_stats.dart';
|
||||
import '../../core/data/models/play_ticket.dart';
|
||||
import '../../core/data/models/watch_progress.dart';
|
||||
import '../../core/data/models/player_port.dart';
|
||||
@@ -51,9 +52,11 @@ class PlayerLogic extends ChangeNotifier {
|
||||
this.downloads,
|
||||
PlaybackPositionCache? positionCache,
|
||||
Future<void> Function()? waitForFrame,
|
||||
DateTime Function()? clock,
|
||||
}) : positionCache = positionCache ?? PlaybackPositionCache(),
|
||||
_waitForFrame =
|
||||
waitForFrame ?? (() => WidgetsBinding.instance.endOfFrame);
|
||||
waitForFrame ?? (() => WidgetsBinding.instance.endOfFrame),
|
||||
_clock = clock ?? DateTime.now;
|
||||
|
||||
final WatchHistoryRepository history;
|
||||
final PlaybackRepository playback;
|
||||
@@ -62,6 +65,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final DownloadController? downloads;
|
||||
final PlaybackPositionCache positionCache;
|
||||
final Future<void> Function() _waitForFrame;
|
||||
final DateTime Function() _clock;
|
||||
|
||||
late PageController pages;
|
||||
late PlaybackQueue queue;
|
||||
@@ -100,11 +104,19 @@ class PlayerLogic extends ChangeNotifier {
|
||||
bool muted = false;
|
||||
double playbackSpeed = 1.0;
|
||||
bool stoppedAtFeedEnd = false;
|
||||
Duration episodeStatsTtl = const Duration(seconds: 60);
|
||||
ValueChanged<EpisodeStats>? onEpisodeStatsUpdated;
|
||||
VideoPlayerPort? scrubbingPlayer;
|
||||
bool pip = false;
|
||||
bool pipPreparing = false;
|
||||
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 = {};
|
||||
final Map<String, int> _episodeStatsRevision = {};
|
||||
final Map<String, int> _dramaStatsRevision = {};
|
||||
final Map<String, int> _dramaFavoriteCounts = {};
|
||||
|
||||
/// 解锁中的集 id 与失败文案;空表示没有进行中的解锁。
|
||||
String? unlockingEpisodeId;
|
||||
@@ -168,6 +180,10 @@ class PlayerLogic extends ChangeNotifier {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
EpisodeStats? episodeStats(String episodeId) => _episodeStats[episodeId];
|
||||
|
||||
int? dramaFavoriteCount(String dramaId) => _dramaFavoriteCounts[dramaId];
|
||||
|
||||
void notify() {
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
@@ -536,6 +552,179 @@ class PlayerLogic extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前集激活后刷新轻量互动计数;同集命中 TTL 或已有请求时不重复发请求。
|
||||
Future<void> refreshEpisodeStats(int index) {
|
||||
if (_disposed || index < 0 || index >= sessionEpisodes.length) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
final episodeId = sessionEpisodes[index].id;
|
||||
final cached = _episodeStats[episodeId];
|
||||
final fetchedAt = _episodeStatsFetchedAt[episodeId];
|
||||
if (cached != null &&
|
||||
fetchedAt != null &&
|
||||
_clock().difference(fetchedAt) < episodeStatsTtl) {
|
||||
_logEpisodeStatsRefresh(episodeId, cached: true);
|
||||
onEpisodeStatsUpdated?.call(cached);
|
||||
return Future<void>.value();
|
||||
}
|
||||
final inFlight = _episodeStatsInFlight[episodeId];
|
||||
if (inFlight != null) {
|
||||
_logEpisodeStatsRefresh(episodeId, cached: true, inFlight: true);
|
||||
return inFlight;
|
||||
}
|
||||
_logEpisodeStatsRefresh(episodeId, cached: false);
|
||||
final expectedDramaId = index < queue.length
|
||||
? queue.entryAt(index).drama.id
|
||||
: '';
|
||||
final episodeRevision = _episodeStatsRevision[episodeId] ?? 0;
|
||||
final dramaRevision = _dramaStatsRevision[expectedDramaId] ?? 0;
|
||||
late final Future<void> request;
|
||||
request =
|
||||
_fetchEpisodeStats(
|
||||
episodeId,
|
||||
expectedDramaId: expectedDramaId,
|
||||
episodeRevision: episodeRevision,
|
||||
dramaRevision: dramaRevision,
|
||||
).whenComplete(() {
|
||||
if (identical(_episodeStatsInFlight[episodeId], request)) {
|
||||
_episodeStatsInFlight.remove(episodeId);
|
||||
}
|
||||
});
|
||||
_episodeStatsInFlight[episodeId] = request;
|
||||
return request;
|
||||
}
|
||||
|
||||
Future<void> _fetchEpisodeStats(
|
||||
String episodeId, {
|
||||
required String expectedDramaId,
|
||||
required int episodeRevision,
|
||||
required int dramaRevision,
|
||||
}) async {
|
||||
try {
|
||||
final fetched = await playback.episodeStats(episodeId);
|
||||
if (_disposed) return;
|
||||
if ((_episodeStatsRevision[episodeId] ?? 0) != episodeRevision ||
|
||||
(_dramaStatsRevision[expectedDramaId] ?? 0) != dramaRevision) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[player] phase=episode-stats-discarded episodeId=$episodeId '
|
||||
'reason=local-state-newer',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final stats = fetched.copyWith(
|
||||
dramaId: fetched.dramaId.isEmpty ? expectedDramaId : fetched.dramaId,
|
||||
);
|
||||
_episodeStats[episodeId] = stats;
|
||||
_episodeStatsFetchedAt[episodeId] = _clock();
|
||||
if (stats.dramaId.isNotEmpty) {
|
||||
_dramaFavoriteCounts[stats.dramaId] = stats.favoriteCount;
|
||||
}
|
||||
final episodeIndex = sessionEpisodes.indexWhere(
|
||||
(episode) => episode.id == episodeId,
|
||||
);
|
||||
if (episodeIndex >= 0) {
|
||||
sessionEpisodes[episodeIndex] = sessionEpisodes[episodeIndex].withStats(
|
||||
likeCount: stats.likeCount,
|
||||
commentCount: stats.commentCount,
|
||||
);
|
||||
}
|
||||
onEpisodeStatsUpdated?.call(stats);
|
||||
notify();
|
||||
} catch (_) {
|
||||
// 统计接口只负责校准计数,失败时保留播放与旧值。
|
||||
}
|
||||
}
|
||||
|
||||
void _logEpisodeStatsRefresh(
|
||||
String episodeId, {
|
||||
required bool cached,
|
||||
bool inFlight = false,
|
||||
}) {
|
||||
if (!kDebugMode) return;
|
||||
debugPrint(
|
||||
'[player] phase=episode-stats-refresh episodeId=$episodeId '
|
||||
'cached=${cached ? 1 : 0}${inFlight ? ' inFlight=1' : ''}',
|
||||
);
|
||||
}
|
||||
|
||||
/// 点赞成功后同步本地统计快照,避免切回时被旧缓存覆盖。
|
||||
void applyEpisodeLikeState(
|
||||
String episodeId, {
|
||||
required bool liked,
|
||||
required int likeCount,
|
||||
}) {
|
||||
_episodeStatsRevision.update(
|
||||
episodeId,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
final stats = _episodeStats[episodeId];
|
||||
if (stats != null) {
|
||||
_episodeStats[episodeId] = stats.copyWith(
|
||||
liked: liked,
|
||||
likeCount: likeCount,
|
||||
);
|
||||
}
|
||||
final index = sessionEpisodes.indexWhere(
|
||||
(episode) => episode.id == episodeId,
|
||||
);
|
||||
if (index >= 0) {
|
||||
sessionEpisodes[index] = sessionEpisodes[index].withStats(
|
||||
likeCount: likeCount,
|
||||
);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
/// 评论面板关闭后同步评论数。
|
||||
void applyEpisodeCommentCount(String episodeId, int commentCount) {
|
||||
_episodeStatsRevision.update(
|
||||
episodeId,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
final stats = _episodeStats[episodeId];
|
||||
if (stats != null) {
|
||||
_episodeStats[episodeId] = stats.copyWith(commentCount: commentCount);
|
||||
}
|
||||
final index = sessionEpisodes.indexWhere(
|
||||
(episode) => episode.id == episodeId,
|
||||
);
|
||||
if (index >= 0) {
|
||||
sessionEpisodes[index] = sessionEpisodes[index].withStats(
|
||||
commentCount: commentCount,
|
||||
);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
/// 收藏成功时同时修正剧级计数和所有已缓存的同剧互动状态。
|
||||
void applyDramaFavoriteState(
|
||||
String dramaId, {
|
||||
required bool favorited,
|
||||
int? favoriteCount,
|
||||
}) {
|
||||
_dramaStatsRevision.update(
|
||||
dramaId,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
if (favoriteCount != null) {
|
||||
_dramaFavoriteCounts[dramaId] = favoriteCount;
|
||||
}
|
||||
for (final entry in _episodeStats.entries.toList()) {
|
||||
final stats = entry.value;
|
||||
if (stats.dramaId != dramaId) continue;
|
||||
_episodeStats[entry.key] = stats.copyWith(
|
||||
favorited: favorited,
|
||||
favoriteCount: favoriteCount,
|
||||
);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
String _fileUrl(String path) =>
|
||||
path.startsWith('file:') ? path : Uri.file(path).toString();
|
||||
|
||||
@@ -1076,7 +1265,10 @@ class PlayerLogic extends ChangeNotifier {
|
||||
!isMounted()) {
|
||||
return;
|
||||
}
|
||||
if (!ready) return;
|
||||
if (!ready) {
|
||||
unawaited(refreshEpisodeStats(index));
|
||||
return;
|
||||
}
|
||||
await _selectVisible(index, userWantsPlay: userWantsPlay);
|
||||
}
|
||||
|
||||
@@ -1087,13 +1279,16 @@ class PlayerLogic extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 按可见队列选择一集,并预热上一集与下一集。
|
||||
Future<void> _selectVisible(int index, {bool? userWantsPlay}) {
|
||||
return session.select(
|
||||
Future<void> _selectVisible(int index, {bool? userWantsPlay}) async {
|
||||
await session.select(
|
||||
index,
|
||||
preloadIndex: nextVisibleIndex(index),
|
||||
previousIndex: previousVisibleIndex(index),
|
||||
userWantsPlay: userWantsPlay,
|
||||
);
|
||||
if (!_disposed && session.currentIndex == index) {
|
||||
unawaited(refreshEpisodeStats(index));
|
||||
}
|
||||
}
|
||||
|
||||
int? nextVisibleIndex(int index) {
|
||||
|
||||
Reference in new issue
Block a user