feat(app): 展示真实点赞与评论计数

This commit is contained in:
Zhou Weidong committed 2026-10-19 23:56:12 +08:00
1 parent 7b927c794e
commit dfc650456a
9 files changed
+123 -8

No files matched your search

@@ -36,12 +36,17 @@ class CommentController extends ChangeNotifier {
bool loadingMore = false;
bool submitting = false;
bool hasMore = false;
bool _loaded = false;
String? nextCursor;
String? error;
/// 当前已加载的顶层评论。
List<DramaComment> get comments => List.unmodifiable(_comments);
/// 全部分页加载完后返回精确的顶层评论数,否则返回 null。
int? get exactCount =>
_loaded && !loading && !loadingMore && !hasMore ? _comments.length : null;
/// 某一楼层已加载的回复。
List<DramaComment> repliesFor(String rootId) =>
List.unmodifiable(_replies[rootId] ?? const []);
@@ -77,6 +82,7 @@ class CommentController extends ChangeNotifier {
..addAll(page.comments);
nextCursor = page.nextCursor;
hasMore = page.hasMore;
_loaded = true;
} catch (failure) {
error = _errorMessage(failure) ?? 'commentsLoadFailed';
} finally {
@@ -8,14 +8,15 @@ import '../repositories/interaction_repository.dart';
/// 剧集点赞与举报的会话状态控制器。
///
/// 剧集契约暂未下发初始点赞状态与计数,本批只维护当前会话内的 liked 状态,
/// 不展示数字,也不伪造初始值。
/// 剧集契约暂未下发初始点赞状态与计数,因此仅在点赞接口成功返回后展示
/// 服务端最新计数,不伪造初始值。
class InteractionController extends ChangeNotifier {
InteractionController(this._repository);
final InteractionRepository _repository;
final Set<String> _likedDramaIds = {};
final Set<String> _busyDramaIds = {};
final Map<String, int> _likeCounts = {};
bool _disposed = false;
/// 当前会话内该剧是否已点赞。
@@ -24,6 +25,9 @@ class InteractionController extends ChangeNotifier {
/// 点赞请求是否进行中,用于防连点。
bool isBusy(String dramaId) => _busyDramaIds.contains(dramaId);
/// 服务端最新点赞数,仅在点赞接口成功返回后可用。
int? likeCount(String dramaId) => _likeCounts[dramaId];
/// 乐观切换剧集点赞,失败时回滚并以服务端返回状态校准。
Future<OperationResult<LikeToggleResult>> toggleDramaLike(
String dramaId,
@@ -49,6 +53,7 @@ class InteractionController extends ChangeNotifier {
} else {
_likedDramaIds.remove(dramaId);
}
_likeCounts[dramaId] = result.count;
return OperationResult.success(result);
} catch (failure) {
if (wasLiked) {
@@ -86,9 +91,14 @@ class InteractionController extends ChangeNotifier {
/// 登录账号变化时清理会话内状态,避免跨账号串联。
void clear() {
if (_likedDramaIds.isEmpty && _busyDramaIds.isEmpty) return;
if (_likedDramaIds.isEmpty &&
_busyDramaIds.isEmpty &&
_likeCounts.isEmpty) {
return;
}
_likedDramaIds.clear();
_busyDramaIds.clear();
_likeCounts.clear();
_notify();
}
+11 -3
View File
@@ -337,6 +337,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
drama: drama,
episode: episode,
liked: interactions.isLiked(drama.id),
likeCount: interactions.likeCount(drama.id),
saved: context.watch<FavoriteController>().isSaved(drama.id),
onLike: () async {
if (await ensureSignedIn(context)) {
@@ -388,6 +389,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
episode: episode,
controller: ready ? videoPort!.controller : null,
liked: interactions.isLiked(drama.id),
likeCount: interactions.likeCount(drama.id),
// 收藏以服务端为准(FavoriteController 由 app 注入,登录后自动加载)
saved: context.watch<FavoriteController>().isSaved(drama.id),
onBack: widget.embedded
@@ -506,9 +508,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorText(l10n, logic.unlockError, l10n.unlockFailed),
),
content: Text(errorText(l10n, logic.unlockError, l10n.unlockFailed)),
),
);
}
@@ -688,6 +688,7 @@ class _PlaybackOverlay extends StatefulWidget {
required this.episode,
required this.controller,
required this.liked,
this.likeCount,
required this.saved,
required this.playing,
required this.playbackSpeed,
@@ -714,6 +715,7 @@ class _PlaybackOverlay extends StatefulWidget {
final Episode episode;
final VideoPlayerController? controller;
final bool liked;
final int? likeCount;
final bool saved;
final bool playing;
final double playbackSpeed;
@@ -985,6 +987,12 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> {
color: widget.liked ? AppColors.blue : AppColors.muted,
),
title: Text(widget.l10n.like),
trailing: widget.likeCount == null
? null
: Text(
'${widget.likeCount}',
key: const Key('player-more-like-count'),
),
onTap: () {
Navigator.pop(sheetContext);
widget.onLike();
@@ -178,11 +178,13 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
Widget _content(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final count = _controller.exactCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.comments,
count == null ? l10n.comments : '${l10n.comments} · $count',
key: count == null ? null : const Key('comment-count-label'),
style: const TextStyle(
color: AppColors.cream,
fontSize: 18,
@@ -19,6 +19,7 @@ class HomeFeedOverlay extends StatelessWidget {
required this.drama,
required this.episode,
required this.liked,
this.likeCount,
required this.saved,
required this.onLike,
required this.onSave,
@@ -35,6 +36,7 @@ class HomeFeedOverlay extends StatelessWidget {
final Drama drama;
final Episode episode;
final bool liked;
final int? likeCount;
final bool saved;
final VoidCallback onLike;
final VoidCallback onSave;
@@ -123,7 +125,8 @@ class HomeFeedOverlay extends StatelessWidget {
_RailItem(
key: const Key('rail-like'),
icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
label: l10n.like,
label: likeCount?.toString() ?? l10n.like,
countKey: likeCount == null ? null : const Key('rail-like-count'),
selected: liked,
onTap: onLike,
),
@@ -375,12 +378,14 @@ class _RailItem extends StatelessWidget {
required this.label,
required this.onTap,
this.selected = false,
this.countKey,
});
final IconData icon;
final String label;
final VoidCallback onTap;
final bool selected;
final Key? countKey;
@override
Widget build(BuildContext context) {
@@ -404,6 +409,7 @@ class _RailItem extends StatelessWidget {
const SizedBox(height: 2),
Text(
label,
key: countKey,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(