merge: 合并 K90 点赞与评论按集
This commit is contained in:
commit
4d0be47ee3
25 files changed
+337
-69
No files matched your search
@@ -15,6 +15,8 @@ class Episode {
|
|||||||
this.videoUrl = '',
|
this.videoUrl = '',
|
||||||
this.durationSec = 0,
|
this.durationSec = 0,
|
||||||
this.locked = false,
|
this.locked = false,
|
||||||
|
this.likeCount,
|
||||||
|
this.commentCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@@ -28,16 +30,21 @@ class Episode {
|
|||||||
|
|
||||||
/// 后端判定需要解锁;解锁能力(金币/VIP)在 M3 接入。
|
/// 后端判定需要解锁;解锁能力(金币/VIP)在 M3 接入。
|
||||||
final bool locked;
|
final bool locked;
|
||||||
|
final int? likeCount;
|
||||||
|
final int? commentCount;
|
||||||
|
|
||||||
Episode withVideoUrl(String url) => Episode(
|
Episode withVideoUrl(String url, {int? likeCount, int? commentCount}) =>
|
||||||
id: id,
|
Episode(
|
||||||
number: number,
|
id: id,
|
||||||
title: title,
|
number: number,
|
||||||
isFree: isFree,
|
title: title,
|
||||||
videoUrl: url,
|
isFree: isFree,
|
||||||
durationSec: durationSec,
|
videoUrl: url,
|
||||||
locked: locked,
|
durationSec: durationSec,
|
||||||
);
|
locked: locked,
|
||||||
|
likeCount: likeCount ?? this.likeCount,
|
||||||
|
commentCount: commentCount ?? this.commentCount,
|
||||||
|
);
|
||||||
|
|
||||||
factory Episode.fromJson(Map<String, dynamic> json) {
|
factory Episode.fromJson(Map<String, dynamic> json) {
|
||||||
return Episode(
|
return Episode(
|
||||||
@@ -48,6 +55,8 @@ class Episode {
|
|||||||
durationSec: (json['durationSec'] as num?)?.toInt() ?? 0,
|
durationSec: (json['durationSec'] as num?)?.toInt() ?? 0,
|
||||||
locked: json['locked'] == true,
|
locked: json['locked'] == true,
|
||||||
videoUrl: json['playUrl'] as String? ?? '',
|
videoUrl: json['playUrl'] as String? ?? '',
|
||||||
|
likeCount: _optionalInt(json['likeCount']),
|
||||||
|
commentCount: _optionalInt(json['commentCount']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import 'json_numbers.dart';
|
import 'json_numbers.dart';
|
||||||
|
|
||||||
/// 互动目标类型,契约只允许短剧与评论。
|
/// 互动目标类型。
|
||||||
enum InteractionTargetType {
|
enum InteractionTargetType {
|
||||||
drama('DRAMA'),
|
drama('DRAMA'),
|
||||||
|
episode('EPISODE'),
|
||||||
comment('COMMENT');
|
comment('COMMENT');
|
||||||
|
|
||||||
const InteractionTargetType(this.apiValue);
|
const InteractionTargetType(this.apiValue);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class PlayTicket {
|
|||||||
this.unlockCoin = 0,
|
this.unlockCoin = 0,
|
||||||
this.previewSeconds = 0,
|
this.previewSeconds = 0,
|
||||||
this.qualities = const [],
|
this.qualities = const [],
|
||||||
|
this.likeCount,
|
||||||
|
this.commentCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const String free = 'FREE';
|
static const String free = 'FREE';
|
||||||
@@ -38,6 +40,8 @@ class PlayTicket {
|
|||||||
|
|
||||||
/// 后端下发的清晰度档位(如 AUTO / 720P / 1080P),HLS 由播放器自动切换。
|
/// 后端下发的清晰度档位(如 AUTO / 720P / 1080P),HLS 由播放器自动切换。
|
||||||
final List<String> qualities;
|
final List<String> qualities;
|
||||||
|
final int? likeCount;
|
||||||
|
final int? commentCount;
|
||||||
|
|
||||||
bool get needsUnlock =>
|
bool get needsUnlock =>
|
||||||
accessType == coinRequired || accessType == vipRequired;
|
accessType == coinRequired || accessType == vipRequired;
|
||||||
@@ -58,6 +62,12 @@ class PlayTicket {
|
|||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.where((quality) => quality.isNotEmpty)
|
.where((quality) => quality.isNotEmpty)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
likeCount: json['likeCount'] == null
|
||||||
|
? null
|
||||||
|
: jsonToInt(json['likeCount']),
|
||||||
|
commentCount: json['commentCount'] == null
|
||||||
|
? null
|
||||||
|
: jsonToInt(json['commentCount']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ class CommentApi {
|
|||||||
/// 拉取游标分页;`rootId` 用于加载某一楼层的回复。
|
/// 拉取游标分页;`rootId` 用于加载某一楼层的回复。
|
||||||
Future<CommentPage> page({
|
Future<CommentPage> page({
|
||||||
required String dramaId,
|
required String dramaId,
|
||||||
|
String? episodeId,
|
||||||
String? rootId,
|
String? rootId,
|
||||||
String? cursor,
|
String? cursor,
|
||||||
int size = commentPageSize,
|
int size = commentPageSize,
|
||||||
@@ -21,6 +22,7 @@ class CommentApi {
|
|||||||
'/app/comments',
|
'/app/comments',
|
||||||
query: {
|
query: {
|
||||||
'dramaId': dramaId,
|
'dramaId': dramaId,
|
||||||
|
'episodeId': ?episodeId,
|
||||||
'rootId': ?rootId,
|
'rootId': ?rootId,
|
||||||
'cursor': ?cursor,
|
'cursor': ?cursor,
|
||||||
'size': '$size',
|
'size': '$size',
|
||||||
@@ -31,10 +33,10 @@ class CommentApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 查询单部剧的有效评论总数;后端 Long 兼容数字与字符串。
|
/// 查询单部剧的有效评论总数;后端 Long 兼容数字与字符串。
|
||||||
Future<int> count(String dramaId) async {
|
Future<int> count(String dramaId, {String? episodeId}) async {
|
||||||
final data = await _client.get(
|
final data = await _client.get(
|
||||||
'/app/comments/count',
|
'/app/comments/count',
|
||||||
query: {'dramaId': dramaId},
|
query: {'dramaId': dramaId, 'episodeId': ?episodeId},
|
||||||
);
|
);
|
||||||
return jsonToInt(data);
|
return jsonToInt(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ abstract interface class CommentRepository {
|
|||||||
/// 拉取评论或回复的分页。
|
/// 拉取评论或回复的分页。
|
||||||
Future<CommentPage> page({
|
Future<CommentPage> page({
|
||||||
required String dramaId,
|
required String dramaId,
|
||||||
|
String? episodeId,
|
||||||
String? rootId,
|
String? rootId,
|
||||||
String? cursor,
|
String? cursor,
|
||||||
int size = commentPageSize,
|
int size = commentPageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 查询单部剧的有效评论总数。
|
/// 查询单部剧的有效评论总数。
|
||||||
Future<int> count(String dramaId);
|
Future<int> count(String dramaId, {String? episodeId});
|
||||||
|
|
||||||
/// 发表评论或回复。
|
/// 发表评论或回复。
|
||||||
Future<DramaComment> publish({
|
Future<DramaComment> publish({
|
||||||
@@ -37,18 +38,21 @@ class HttpCommentRepository implements CommentRepository {
|
|||||||
@override
|
@override
|
||||||
Future<CommentPage> page({
|
Future<CommentPage> page({
|
||||||
required String dramaId,
|
required String dramaId,
|
||||||
|
String? episodeId,
|
||||||
String? rootId,
|
String? rootId,
|
||||||
String? cursor,
|
String? cursor,
|
||||||
int size = commentPageSize,
|
int size = commentPageSize,
|
||||||
}) => _api.page(
|
}) => _api.page(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
rootId: rootId,
|
rootId: rootId,
|
||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> count(String dramaId) => _api.count(dramaId);
|
Future<int> count(String dramaId, {String? episodeId}) =>
|
||||||
|
_api.count(dramaId, episodeId: episodeId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DramaComment> publish({
|
Future<DramaComment> publish({
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class CommentController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final page = await _commentRepository.page(
|
final page = await _commentRepository.page(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
size: commentPageSize,
|
size: commentPageSize,
|
||||||
);
|
);
|
||||||
_comments
|
_comments
|
||||||
@@ -101,6 +102,7 @@ class CommentController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final page = await _commentRepository.page(
|
final page = await _commentRepository.page(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
size: commentPageSize,
|
size: commentPageSize,
|
||||||
);
|
);
|
||||||
@@ -128,6 +130,7 @@ class CommentController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final page = await _commentRepository.page(
|
final page = await _commentRepository.page(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
rootId: rootId,
|
rootId: rootId,
|
||||||
size: commentPageSize,
|
size: commentPageSize,
|
||||||
);
|
);
|
||||||
@@ -157,6 +160,7 @@ class CommentController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final page = await _commentRepository.page(
|
final page = await _commentRepository.page(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
rootId: rootId,
|
rootId: rootId,
|
||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
size: commentPageSize,
|
size: commentPageSize,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class InteractionController extends ChangeNotifier {
|
|||||||
final Set<String> _likedDramaIds = {};
|
final Set<String> _likedDramaIds = {};
|
||||||
final Set<String> _busyDramaIds = {};
|
final Set<String> _busyDramaIds = {};
|
||||||
final Map<String, int> _likeCounts = {};
|
final Map<String, int> _likeCounts = {};
|
||||||
|
final Set<String> _likedEpisodeIds = {};
|
||||||
|
final Set<String> _busyEpisodeIds = {};
|
||||||
|
final Map<String, int> _episodeLikeCounts = {};
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
/// 当前会话内该剧是否已点赞。
|
/// 当前会话内该剧是否已点赞。
|
||||||
@@ -28,42 +31,75 @@ class InteractionController extends ChangeNotifier {
|
|||||||
/// 服务端最新点赞数,仅在点赞接口成功返回后可用。
|
/// 服务端最新点赞数,仅在点赞接口成功返回后可用。
|
||||||
int? likeCount(String dramaId) => _likeCounts[dramaId];
|
int? likeCount(String dramaId) => _likeCounts[dramaId];
|
||||||
|
|
||||||
|
/// 当前会话内该集是否已点赞。
|
||||||
|
bool isEpisodeLiked(String episodeId) => _likedEpisodeIds.contains(episodeId);
|
||||||
|
|
||||||
|
/// 该集点赞请求是否进行中。
|
||||||
|
bool isEpisodeBusy(String episodeId) => _busyEpisodeIds.contains(episodeId);
|
||||||
|
|
||||||
|
/// 该集服务端最新点赞数。
|
||||||
|
int? episodeLikeCount(String episodeId) => _episodeLikeCounts[episodeId];
|
||||||
|
|
||||||
/// 乐观切换剧集点赞,失败时回滚并以服务端返回状态校准。
|
/// 乐观切换剧集点赞,失败时回滚并以服务端返回状态校准。
|
||||||
Future<OperationResult<LikeToggleResult>> toggleDramaLike(
|
Future<OperationResult<LikeToggleResult>> toggleDramaLike(String dramaId) =>
|
||||||
String dramaId,
|
_toggleLike(
|
||||||
) async {
|
id: dramaId,
|
||||||
if (_busyDramaIds.contains(dramaId)) {
|
targetType: InteractionTargetType.drama,
|
||||||
|
likedIds: _likedDramaIds,
|
||||||
|
busyIds: _busyDramaIds,
|
||||||
|
counts: _likeCounts,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 乐观切换分集点赞,失败时回滚并以服务端返回状态校准。
|
||||||
|
Future<OperationResult<LikeToggleResult>> toggleEpisodeLike(
|
||||||
|
String episodeId,
|
||||||
|
) => _toggleLike(
|
||||||
|
id: episodeId,
|
||||||
|
targetType: InteractionTargetType.episode,
|
||||||
|
likedIds: _likedEpisodeIds,
|
||||||
|
busyIds: _busyEpisodeIds,
|
||||||
|
counts: _episodeLikeCounts,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<OperationResult<LikeToggleResult>> _toggleLike({
|
||||||
|
required String id,
|
||||||
|
required InteractionTargetType targetType,
|
||||||
|
required Set<String> likedIds,
|
||||||
|
required Set<String> busyIds,
|
||||||
|
required Map<String, int> counts,
|
||||||
|
}) async {
|
||||||
|
if (busyIds.contains(id)) {
|
||||||
return const OperationResult.failure(null);
|
return const OperationResult.failure(null);
|
||||||
}
|
}
|
||||||
final wasLiked = _likedDramaIds.contains(dramaId);
|
final wasLiked = likedIds.contains(id);
|
||||||
_busyDramaIds.add(dramaId);
|
busyIds.add(id);
|
||||||
if (wasLiked) {
|
if (wasLiked) {
|
||||||
_likedDramaIds.remove(dramaId);
|
likedIds.remove(id);
|
||||||
} else {
|
} else {
|
||||||
_likedDramaIds.add(dramaId);
|
likedIds.add(id);
|
||||||
}
|
}
|
||||||
_notify();
|
_notify();
|
||||||
try {
|
try {
|
||||||
final result = await _repository.toggleLike(
|
final result = await _repository.toggleLike(
|
||||||
targetType: InteractionTargetType.drama,
|
targetType: targetType,
|
||||||
targetId: dramaId,
|
targetId: id,
|
||||||
);
|
);
|
||||||
if (result.liked) {
|
if (result.liked) {
|
||||||
_likedDramaIds.add(dramaId);
|
likedIds.add(id);
|
||||||
} else {
|
} else {
|
||||||
_likedDramaIds.remove(dramaId);
|
likedIds.remove(id);
|
||||||
}
|
}
|
||||||
_likeCounts[dramaId] = result.count;
|
counts[id] = result.count;
|
||||||
return OperationResult.success(result);
|
return OperationResult.success(result);
|
||||||
} catch (failure) {
|
} catch (failure) {
|
||||||
if (wasLiked) {
|
if (wasLiked) {
|
||||||
_likedDramaIds.add(dramaId);
|
likedIds.add(id);
|
||||||
} else {
|
} else {
|
||||||
_likedDramaIds.remove(dramaId);
|
likedIds.remove(id);
|
||||||
}
|
}
|
||||||
return OperationResult.failure(_errorMessage(failure));
|
return OperationResult.failure(_errorMessage(failure));
|
||||||
} finally {
|
} finally {
|
||||||
_busyDramaIds.remove(dramaId);
|
busyIds.remove(id);
|
||||||
_notify();
|
_notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,12 +129,18 @@ class InteractionController extends ChangeNotifier {
|
|||||||
void clear() {
|
void clear() {
|
||||||
if (_likedDramaIds.isEmpty &&
|
if (_likedDramaIds.isEmpty &&
|
||||||
_busyDramaIds.isEmpty &&
|
_busyDramaIds.isEmpty &&
|
||||||
_likeCounts.isEmpty) {
|
_likeCounts.isEmpty &&
|
||||||
|
_likedEpisodeIds.isEmpty &&
|
||||||
|
_busyEpisodeIds.isEmpty &&
|
||||||
|
_episodeLikeCounts.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_likedDramaIds.clear();
|
_likedDramaIds.clear();
|
||||||
_busyDramaIds.clear();
|
_busyDramaIds.clear();
|
||||||
_likeCounts.clear();
|
_likeCounts.clear();
|
||||||
|
_likedEpisodeIds.clear();
|
||||||
|
_busyEpisodeIds.clear();
|
||||||
|
_episodeLikeCounts.clear();
|
||||||
_notify();
|
_notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
final saved = context.watch<FavoriteController>().isSaved(drama.id);
|
final saved = context.watch<FavoriteController>().isSaved(drama.id);
|
||||||
final continueIndex = logic.continueIndex;
|
final continueIndex = logic.continueIndex;
|
||||||
final episodesReady = drama.detailLoaded && drama.episodes.isNotEmpty;
|
final episodesReady = drama.detailLoaded && drama.episodes.isNotEmpty;
|
||||||
|
final statsEpisode = episodesReady
|
||||||
|
? drama.episodes[continueIndex != null &&
|
||||||
|
continueIndex < drama.episodes.length
|
||||||
|
? continueIndex
|
||||||
|
: 0]
|
||||||
|
: null;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.ink,
|
backgroundColor: AppColors.ink,
|
||||||
bottomNavigationBar: DramaDetailBottomBar(
|
bottomNavigationBar: DramaDetailBottomBar(
|
||||||
@@ -146,6 +152,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
drama: drama,
|
drama: drama,
|
||||||
favoriteCount: logic.adjustedFavoriteCount,
|
favoriteCount: logic.adjustedFavoriteCount,
|
||||||
|
likeCount: statsEpisode?.likeCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ class DramaStatsCard extends StatelessWidget {
|
|||||||
required this.l10n,
|
required this.l10n,
|
||||||
required this.drama,
|
required this.drama,
|
||||||
this.favoriteCount,
|
this.favoriteCount,
|
||||||
|
this.likeCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
final Drama drama;
|
final Drama drama;
|
||||||
final int? favoriteCount;
|
final int? favoriteCount;
|
||||||
|
final int? likeCount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -46,9 +48,9 @@ class DramaStatsCard extends StatelessWidget {
|
|||||||
final favorite = currentFavoriteCount == null
|
final favorite = currentFavoriteCount == null
|
||||||
? l10n.episodeCountLabel(drama.episodeCount)
|
? l10n.episodeCountLabel(drama.episodeCount)
|
||||||
: '${formatInteractionCount(currentFavoriteCount)} ${l10n.favorite}';
|
: '${formatInteractionCount(currentFavoriteCount)} ${l10n.favorite}';
|
||||||
final like = drama.likeCount == null
|
final like = likeCount == null
|
||||||
? '—'
|
? '—'
|
||||||
: '${formatInteractionCount(drama.likeCount!)} ${l10n.like}';
|
: '${formatInteractionCount(likeCount!)} ${l10n.like}';
|
||||||
return Container(
|
return Container(
|
||||||
key: const Key('detail-stats'),
|
key: const Key('detail-stats'),
|
||||||
margin: const EdgeInsets.fromLTRB(
|
margin: const EdgeInsets.fromLTRB(
|
||||||
|
|||||||
@@ -230,7 +230,11 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
qualities = ticket.qualities;
|
qualities = ticket.qualities;
|
||||||
if (ticket.accessible && ticket.playUrl.isNotEmpty) {
|
if (ticket.accessible && ticket.playUrl.isNotEmpty) {
|
||||||
// 签名地址原样写回,不做任何拼接/改写(AppConfig.mediaUrl 只补 host)
|
// 签名地址原样写回,不做任何拼接/改写(AppConfig.mediaUrl 只补 host)
|
||||||
sessionEpisodes[index] = episode.withVideoUrl(ticket.playUrl);
|
sessionEpisodes[index] = episode.withVideoUrl(
|
||||||
|
ticket.playUrl,
|
||||||
|
likeCount: ticket.likeCount,
|
||||||
|
commentCount: ticket.commentCount,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
blockedTicket = ticket;
|
blockedTicket = ticket;
|
||||||
@@ -797,7 +801,11 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showComments(BuildContext context, Drama drama) async {
|
Future<void> showComments(
|
||||||
|
BuildContext context,
|
||||||
|
Drama drama, {
|
||||||
|
String? episodeId,
|
||||||
|
}) async {
|
||||||
session.setOverlayOpen(true);
|
session.setOverlayOpen(true);
|
||||||
await session.playbackSettled;
|
await session.playbackSettled;
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -812,7 +820,7 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
showDragHandle: false,
|
showDragHandle: false,
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
enableDrag: false,
|
enableDrag: false,
|
||||||
builder: (_) => DramaCommentsSheet(drama: drama),
|
builder: (_) => DramaCommentsSheet(drama: drama, episodeId: episodeId),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
session.setOverlayOpen(false);
|
session.setOverlayOpen(false);
|
||||||
|
|||||||
@@ -205,13 +205,22 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openComments(BuildContext context, Drama drama) async {
|
Future<void> _openComments(
|
||||||
|
BuildContext context,
|
||||||
|
Drama drama,
|
||||||
|
Episode episode,
|
||||||
|
) async {
|
||||||
final comments = context.read<CommentRepository>();
|
final comments = context.read<CommentRepository>();
|
||||||
await openCommentsWithAuth(context, drama, show: logic.showComments);
|
await openCommentsWithAuth(
|
||||||
|
context,
|
||||||
|
drama,
|
||||||
|
show: (context, drama) =>
|
||||||
|
logic.showComments(context, drama, episodeId: episode.id),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
final count = await comments.count(drama.id);
|
final count = await comments.count(drama.id, episodeId: episode.id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _commentCounts[drama.id] = count);
|
setState(() => _commentCounts[episode.id] = count);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// 评论面板已经关闭,计数刷新失败时保留旧值即可。
|
// 评论面板已经关闭,计数刷新失败时保留旧值即可。
|
||||||
}
|
}
|
||||||
@@ -456,18 +465,19 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
onBoostEnd: _endBoost,
|
onBoostEnd: _endBoost,
|
||||||
onTogglePlay: () =>
|
onTogglePlay: () =>
|
||||||
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
||||||
liked: interactions.isLiked(drama.id),
|
liked: interactions.isEpisodeLiked(episode.id),
|
||||||
likeCount: interactions.likeCount(drama.id) ?? drama.likeCount,
|
likeCount:
|
||||||
|
interactions.episodeLikeCount(episode.id) ?? episode.likeCount,
|
||||||
favoriteCount: drama.favoriteCount == null
|
favoriteCount: drama.favoriteCount == null
|
||||||
? null
|
? null
|
||||||
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
||||||
commentCount: _commentCounts[drama.id] ?? drama.commentCount,
|
commentCount: _commentCounts[episode.id] ?? episode.commentCount,
|
||||||
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
||||||
onLike: () async {
|
onLike: () async {
|
||||||
if (await ensureSignedIn(context)) {
|
if (await ensureSignedIn(context)) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
unawaited(context.read<Haptics>().lightImpact());
|
unawaited(context.read<Haptics>().lightImpact());
|
||||||
final result = await interactions.toggleDramaLike(drama.id);
|
final result = await interactions.toggleEpisodeLike(episode.id);
|
||||||
if (!result.isSuccess && context.mounted) {
|
if (!result.isSuccess && context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -481,7 +491,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSave: () => unawaited(_toggleSave(context, drama)),
|
onSave: () => unawaited(_toggleSave(context, drama)),
|
||||||
onComments: () => unawaited(_openComments(context, drama)),
|
onComments: () => unawaited(_openComments(context, drama, episode)),
|
||||||
onShare: () => logic.shareDrama(context, drama),
|
onShare: () => logic.shareDrama(context, drama),
|
||||||
onDetail: () => unawaited(logic.openDetail(context)),
|
onDetail: () => unawaited(logic.openDetail(context)),
|
||||||
onOpenFullPlayer: () async {
|
onOpenFullPlayer: () async {
|
||||||
@@ -502,9 +512,11 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
drama: drama,
|
drama: drama,
|
||||||
episode: episode,
|
episode: episode,
|
||||||
controller: ready ? videoPort!.controller : null,
|
controller: ready ? videoPort!.controller : null,
|
||||||
liked: interactions.isLiked(drama.id),
|
liked: interactions.isEpisodeLiked(episode.id),
|
||||||
likeCount: interactions.likeCount(drama.id) ?? drama.likeCount,
|
likeCount:
|
||||||
commentCount: _commentCounts[drama.id] ?? drama.commentCount,
|
interactions.episodeLikeCount(episode.id) ??
|
||||||
|
episode.likeCount,
|
||||||
|
commentCount: _commentCounts[episode.id] ?? episode.commentCount,
|
||||||
favoriteCount: drama.favoriteCount == null
|
favoriteCount: drama.favoriteCount == null
|
||||||
? null
|
? null
|
||||||
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
||||||
@@ -525,9 +537,10 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
onDetail: () => unawaited(logic.openDetail(context)),
|
onDetail: () => unawaited(logic.openDetail(context)),
|
||||||
onSpeed: () => unawaited(_openSpeedSheet(context)),
|
onSpeed: () => unawaited(_openSpeedSheet(context)),
|
||||||
onEpisodes: () => logic.showEpisodes(context),
|
onEpisodes: () => logic.showEpisodes(context),
|
||||||
onComments: () => unawaited(_openComments(context, drama)),
|
onComments: () =>
|
||||||
|
unawaited(_openComments(context, drama, episode)),
|
||||||
onLike: () =>
|
onLike: () =>
|
||||||
unawaited(_toggleLike(context, interactions, drama)),
|
unawaited(_toggleLike(context, interactions, episode)),
|
||||||
onSave: () => unawaited(_toggleSave(context, drama)),
|
onSave: () => unawaited(_toggleSave(context, drama)),
|
||||||
onShare: () => logic.shareDrama(context, drama),
|
onShare: () => logic.shareDrama(context, drama),
|
||||||
onMore: () => _openMore(context, episode),
|
onMore: () => _openMore(context, episode),
|
||||||
|
|||||||
@@ -81,11 +81,11 @@ extension _PlayerViewActions on _PlayerPageState {
|
|||||||
Future<void> _toggleLike(
|
Future<void> _toggleLike(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
InteractionController interactions,
|
InteractionController interactions,
|
||||||
Drama drama,
|
Episode episode,
|
||||||
) async {
|
) async {
|
||||||
if (!await ensureSignedIn(context) || !context.mounted) return;
|
if (!await ensureSignedIn(context) || !context.mounted) return;
|
||||||
unawaited(context.read<Haptics>().lightImpact());
|
unawaited(context.read<Haptics>().lightImpact());
|
||||||
final result = await interactions.toggleDramaLike(drama.id);
|
final result = await interactions.toggleEpisodeLike(episode.id);
|
||||||
if (result.isSuccess || !context.mounted) return;
|
if (result.isSuccess || !context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ const double _composerTopGap = 8;
|
|||||||
|
|
||||||
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复与登录发布。
|
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复与登录发布。
|
||||||
class DramaCommentsSheet extends StatefulWidget {
|
class DramaCommentsSheet extends StatefulWidget {
|
||||||
const DramaCommentsSheet({super.key, required this.drama});
|
const DramaCommentsSheet({super.key, required this.drama, this.episodeId});
|
||||||
|
|
||||||
final Drama drama;
|
final Drama drama;
|
||||||
|
final String? episodeId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DramaCommentsSheet> createState() => _DramaCommentsSheetState();
|
State<DramaCommentsSheet> createState() => _DramaCommentsSheetState();
|
||||||
@@ -56,6 +57,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
comments: _comments,
|
comments: _comments,
|
||||||
interactions: context.read<InteractionRepository>(),
|
interactions: context.read<InteractionRepository>(),
|
||||||
dramaId: widget.drama.id,
|
dramaId: widget.drama.id,
|
||||||
|
episodeId: widget.episodeId,
|
||||||
);
|
);
|
||||||
unawaited(_controller.load());
|
unawaited(_controller.load());
|
||||||
unawaited(_refreshCount());
|
unawaited(_refreshCount());
|
||||||
@@ -77,7 +79,10 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
Future<void> _refreshCount() async {
|
Future<void> _refreshCount() async {
|
||||||
final request = ++_countRequest;
|
final request = ++_countRequest;
|
||||||
try {
|
try {
|
||||||
final count = await _comments.count(widget.drama.id);
|
final count = await _comments.count(
|
||||||
|
widget.drama.id,
|
||||||
|
episodeId: widget.episodeId,
|
||||||
|
);
|
||||||
if (!mounted || request != _countRequest) return;
|
if (!mounted || request != _countRequest) return;
|
||||||
setState(() => _serverCount = count);
|
setState(() => _serverCount = count);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -464,8 +469,9 @@ class _CommentThread extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
expanded
|
expanded
|
||||||
? AppLocalizations.of(context)!.hideReplies
|
? AppLocalizations.of(context)!.hideReplies
|
||||||
: AppLocalizations.of(context)!
|
: AppLocalizations.of(
|
||||||
.viewReplies(comment.replyCount),
|
context,
|
||||||
|
)!.viewReplies(comment.replyCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.muted,
|
color: AppColors.muted,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ void main() {
|
|||||||
'title': '第 1 集',
|
'title': '第 1 集',
|
||||||
'isFree': true,
|
'isFree': true,
|
||||||
'playUrl': '/files/episode/9.mp4',
|
'playUrl': '/files/episode/9.mp4',
|
||||||
|
'likeCount': '18',
|
||||||
|
'commentCount': 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(drama.detailLoaded, isFalse);
|
expect(drama.detailLoaded, isFalse);
|
||||||
expect(drama.episodes, hasLength(1));
|
expect(drama.episodes, hasLength(1));
|
||||||
expect(drama.episodes.single.videoUrl, '/files/episode/9.mp4');
|
expect(drama.episodes.single.videoUrl, '/files/episode/9.mp4');
|
||||||
|
expect(drama.episodes.single.likeCount, 18);
|
||||||
|
expect(drama.episodes.single.commentCount, 4);
|
||||||
expect(drama.episodeCount, 77);
|
expect(drama.episodeCount, 77);
|
||||||
expect(drama.isHot, isTrue);
|
expect(drama.isHot, isTrue);
|
||||||
expect(drama.language, 'zh-CN');
|
expect(drama.language, 'zh-CN');
|
||||||
@@ -50,7 +54,14 @@ void main() {
|
|||||||
'cover': '/files/cover.jpg',
|
'cover': '/files/cover.jpg',
|
||||||
'totalEpisodes': 77,
|
'totalEpisodes': 77,
|
||||||
'episodes': [
|
'episodes': [
|
||||||
{'id': 9, 'number': 1, 'title': '第 1 集', 'isFree': true},
|
{
|
||||||
|
'id': 9,
|
||||||
|
'number': 1,
|
||||||
|
'title': '第 1 集',
|
||||||
|
'isFree': true,
|
||||||
|
'likeCount': '21',
|
||||||
|
'commentCount': '6',
|
||||||
|
},
|
||||||
{'id': 10, 'number': 2, 'title': '第 2 集', 'isFree': false},
|
{'id': 10, 'number': 2, 'title': '第 2 集', 'isFree': false},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -58,5 +69,7 @@ void main() {
|
|||||||
expect(drama.detailLoaded, isTrue);
|
expect(drama.detailLoaded, isTrue);
|
||||||
expect(drama.episodeCount, 77);
|
expect(drama.episodeCount, 77);
|
||||||
expect(drama.episodes.first.id, '9');
|
expect(drama.episodes.first.id, '9');
|
||||||
|
expect(drama.episodes.first.likeCount, 21);
|
||||||
|
expect(drama.episodes.first.commentCount, 6);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -55,12 +55,18 @@ void main() {
|
|||||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||||
);
|
);
|
||||||
|
|
||||||
final page = await api.page(dramaId: '5', cursor: '99', size: 20);
|
final page = await api.page(
|
||||||
|
dramaId: '5',
|
||||||
|
episodeId: '7',
|
||||||
|
cursor: '99',
|
||||||
|
size: 20,
|
||||||
|
);
|
||||||
|
|
||||||
expect(adapter.requests.single.method, 'GET');
|
expect(adapter.requests.single.method, 'GET');
|
||||||
expect(adapter.requests.single.uri.path, '/app/comments');
|
expect(adapter.requests.single.uri.path, '/app/comments');
|
||||||
expect(adapter.requests.single.queryParameters, {
|
expect(adapter.requests.single.queryParameters, {
|
||||||
'dramaId': '5',
|
'dramaId': '5',
|
||||||
|
'episodeId': '7',
|
||||||
'cursor': '99',
|
'cursor': '99',
|
||||||
'size': '20',
|
'size': '20',
|
||||||
});
|
});
|
||||||
@@ -93,6 +99,7 @@ void main() {
|
|||||||
|
|
||||||
final reply = await api.publish(
|
final reply = await api.publish(
|
||||||
dramaId: '5',
|
dramaId: '5',
|
||||||
|
episodeId: '7',
|
||||||
content: 'great badword',
|
content: 'great badword',
|
||||||
parentId: '2111012753262120962',
|
parentId: '2111012753262120962',
|
||||||
);
|
);
|
||||||
@@ -101,6 +108,7 @@ void main() {
|
|||||||
expect(adapter.requests.single.uri.path, '/app/comments');
|
expect(adapter.requests.single.uri.path, '/app/comments');
|
||||||
expect(adapter.requests.single.data, {
|
expect(adapter.requests.single.data, {
|
||||||
'dramaId': '5',
|
'dramaId': '5',
|
||||||
|
'episodeId': '7',
|
||||||
'content': 'great badword',
|
'content': 'great badword',
|
||||||
'parentId': '2111012753262120962',
|
'parentId': '2111012753262120962',
|
||||||
});
|
});
|
||||||
@@ -135,12 +143,15 @@ void main() {
|
|||||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||||
);
|
);
|
||||||
|
|
||||||
final count = await api.count('5');
|
final count = await api.count('5', episodeId: '7');
|
||||||
|
|
||||||
expect(count, 17);
|
expect(count, 17);
|
||||||
expect(adapter.requests.single.method, 'GET');
|
expect(adapter.requests.single.method, 'GET');
|
||||||
expect(adapter.requests.single.uri.path, '/app/comments/count');
|
expect(adapter.requests.single.uri.path, '/app/comments/count');
|
||||||
expect(adapter.requests.single.queryParameters, {'dramaId': '5'});
|
expect(adapter.requests.single.queryParameters, {
|
||||||
|
'dramaId': '5',
|
||||||
|
'episodeId': '7',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_parse_string_comment_count', () async {
|
test('should_parse_string_comment_count', () async {
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ void main() {
|
|||||||
CommentController controller(
|
CommentController controller(
|
||||||
FakeCommentRepository comments, {
|
FakeCommentRepository comments, {
|
||||||
FakeInteractionRepository? interactions,
|
FakeInteractionRepository? interactions,
|
||||||
|
String? episodeId,
|
||||||
}) => CommentController(
|
}) => CommentController(
|
||||||
comments: comments,
|
comments: comments,
|
||||||
interactions: interactions ?? FakeInteractionRepository(),
|
interactions: interactions ?? FakeInteractionRepository(),
|
||||||
dramaId: '5',
|
dramaId: '5',
|
||||||
|
episodeId: episodeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
test('should_append_next_page_with_cursor_when_has_more_is_true', () async {
|
test('should_append_next_page_with_cursor_when_has_more_is_true', () async {
|
||||||
@@ -63,6 +65,42 @@ void main() {
|
|||||||
model.dispose();
|
model.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should_pass_episode_id_to_every_page_and_publish_request', () async {
|
||||||
|
final root = commentFixture(id: '10', replyCount: 1);
|
||||||
|
final repository = FakeCommentRepository(
|
||||||
|
pages: [
|
||||||
|
CommentPage(
|
||||||
|
comments: [root],
|
||||||
|
nextCursor: '10',
|
||||||
|
hasMore: true,
|
||||||
|
),
|
||||||
|
CommentPage(comments: [commentFixture(id: '9')]),
|
||||||
|
CommentPage(
|
||||||
|
comments: [commentFixture(id: '11', rootId: '10')],
|
||||||
|
nextCursor: '11',
|
||||||
|
hasMore: true,
|
||||||
|
),
|
||||||
|
CommentPage(comments: [commentFixture(id: '12', rootId: '10')]),
|
||||||
|
],
|
||||||
|
publishResult: commentFixture(id: '13', mine: true),
|
||||||
|
);
|
||||||
|
final model = controller(repository, episodeId: 'episode-7');
|
||||||
|
|
||||||
|
await model.load();
|
||||||
|
await model.loadMore();
|
||||||
|
await model.loadReplies('10');
|
||||||
|
await model.loadMoreReplies('10');
|
||||||
|
await model.publish('Nice episode');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
repository.pageCalls.map((call) => call.episodeId),
|
||||||
|
everyElement('episode-7'),
|
||||||
|
);
|
||||||
|
expect(repository.pageCalls, hasLength(4));
|
||||||
|
expect(repository.publishCalls.single.episodeId, 'episode-7');
|
||||||
|
model.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('should_use_server_content_and_pending_status_when_review_is_returned', () async {
|
test('should_use_server_content_and_pending_status_when_review_is_returned', () async {
|
||||||
final repository = FakeCommentRepository(
|
final repository = FakeCommentRepository(
|
||||||
publishResult: commentFixture(
|
publishResult: commentFixture(
|
||||||
|
|||||||
@@ -239,6 +239,36 @@ void main() {
|
|||||||
expect(repository.countCalls, ['5']);
|
expect(repository.countCalls, ['5']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_scope_list_count_and_publish_to_episode', (tester) async {
|
||||||
|
final repository = FakeCommentRepository(
|
||||||
|
pages: [const CommentPage()],
|
||||||
|
countResult: 3,
|
||||||
|
publishResult: commentFixture(
|
||||||
|
id: '11',
|
||||||
|
content: 'Episode comment',
|
||||||
|
mine: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pumpSheet(tester, repository: repository, episodeId: 'episode-7');
|
||||||
|
|
||||||
|
expect(repository.pageCalls.single.episodeId, 'episode-7');
|
||||||
|
expect(repository.countEpisodeCalls, ['episode-7']);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('comment-input-placeholder')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(
|
||||||
|
find.byKey(const Key('comment-composer-input')),
|
||||||
|
'Episode comment',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byKey(const Key('comment-composer-post')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.publishCalls.single.episodeId, 'episode-7');
|
||||||
|
expect(repository.countEpisodeCalls, ['episode-7', 'episode-7']);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_fall_back_to_exact_count_when_server_total_fails', (
|
testWidgets('should_fall_back_to_exact_count_when_server_total_fails', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -964,6 +994,7 @@ Future<AuthController> _pumpSheet(
|
|||||||
required FakeCommentRepository repository,
|
required FakeCommentRepository repository,
|
||||||
FakeHaptics? haptics,
|
FakeHaptics? haptics,
|
||||||
Locale locale = const Locale('en'),
|
Locale locale = const Locale('en'),
|
||||||
|
String? episodeId,
|
||||||
}) async {
|
}) async {
|
||||||
final auth = await _auth();
|
final auth = await _auth();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -983,12 +1014,12 @@ Future<AuthController> _pumpSheet(
|
|||||||
initialRoute: '/comments',
|
initialRoute: '/comments',
|
||||||
routes: {
|
routes: {
|
||||||
'/': (_) => const Scaffold(backgroundColor: AppColors.ink),
|
'/': (_) => const Scaffold(backgroundColor: AppColors.ink),
|
||||||
'/comments': (_) => const Scaffold(
|
'/comments': (_) => Scaffold(
|
||||||
backgroundColor: AppColors.ink,
|
backgroundColor: AppColors.ink,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Align(
|
body: Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: DramaCommentsSheet(drama: _drama),
|
child: DramaCommentsSheet(drama: _drama, episodeId: episodeId),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ Drama _detailDrama(Drama card) => Drama(
|
|||||||
isFree: number == 1,
|
isFree: number == 1,
|
||||||
videoUrl: number == 1 ? '/files/${card.id}-1.mp4' : '',
|
videoUrl: number == 1 ? '/files/${card.id}-1.mp4' : '',
|
||||||
locked: number > 1,
|
locked: number > 1,
|
||||||
|
likeCount: card.likeCount,
|
||||||
|
commentCount: card.commentCount,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -463,6 +463,8 @@ Drama _detailDrama({
|
|||||||
durationSec: 135,
|
durationSec: 135,
|
||||||
locked: number > 1,
|
locked: number > 1,
|
||||||
videoUrl: number == 1 ? '/files/detail-k38-1.mp4' : '',
|
videoUrl: number == 1 ? '/files/detail-k38-1.mp4' : '',
|
||||||
|
likeCount: withCounts ? 12400 : null,
|
||||||
|
commentCount: withCounts ? 96 : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,56 @@ void main() {
|
|||||||
controller.dispose();
|
controller.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should_toggle_episode_like_with_plus_one_and_minus_one', () async {
|
||||||
|
final repository = FakeInteractionRepository(
|
||||||
|
nextLikeResult: const LikeToggleResult(liked: true, count: 8),
|
||||||
|
);
|
||||||
|
final controller = InteractionController(repository);
|
||||||
|
|
||||||
|
await controller.toggleEpisodeLike('episode-5');
|
||||||
|
|
||||||
|
expect(repository.likeTargets, ['EPISODE:episode-5']);
|
||||||
|
expect(controller.isEpisodeLiked('episode-5'), isTrue);
|
||||||
|
expect(controller.episodeLikeCount('episode-5'), 8);
|
||||||
|
expect(controller.isLiked('episode-5'), isFalse);
|
||||||
|
|
||||||
|
repository.nextLikeResult = const LikeToggleResult(liked: false, count: 7);
|
||||||
|
await controller.toggleEpisodeLike('episode-5');
|
||||||
|
|
||||||
|
expect(controller.isEpisodeLiked('episode-5'), isFalse);
|
||||||
|
expect(controller.episodeLikeCount('episode-5'), 7);
|
||||||
|
controller.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_isolate_episode_like_cache_by_episode_and_drama', () async {
|
||||||
|
final repository = FakeInteractionRepository(
|
||||||
|
nextLikeResult: const LikeToggleResult(liked: true, count: 8),
|
||||||
|
);
|
||||||
|
final controller = InteractionController(repository);
|
||||||
|
|
||||||
|
await controller.toggleEpisodeLike('episode-1');
|
||||||
|
repository.nextLikeResult = const LikeToggleResult(liked: true, count: 3);
|
||||||
|
await controller.toggleEpisodeLike('episode-2');
|
||||||
|
repository.nextLikeResult = const LikeToggleResult(liked: false, count: 7);
|
||||||
|
await controller.toggleEpisodeLike('episode-1');
|
||||||
|
repository.nextLikeResult = const LikeToggleResult(liked: true, count: 12);
|
||||||
|
await controller.toggleDramaLike('episode-1');
|
||||||
|
|
||||||
|
expect(controller.isEpisodeLiked('episode-1'), isFalse);
|
||||||
|
expect(controller.episodeLikeCount('episode-1'), 7);
|
||||||
|
expect(controller.isEpisodeLiked('episode-2'), isTrue);
|
||||||
|
expect(controller.episodeLikeCount('episode-2'), 3);
|
||||||
|
expect(controller.isLiked('episode-1'), isTrue);
|
||||||
|
expect(controller.likeCount('episode-1'), 12);
|
||||||
|
expect(repository.likeTargets, [
|
||||||
|
'EPISODE:episode-1',
|
||||||
|
'EPISODE:episode-2',
|
||||||
|
'EPISODE:episode-1',
|
||||||
|
'DRAMA:episode-1',
|
||||||
|
]);
|
||||||
|
controller.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('should_clear_account_scoped_drama_likes_when_session_changes', () async {
|
test('should_clear_account_scoped_drama_likes_when_session_changes', () async {
|
||||||
final repository = FakeInteractionRepository(
|
final repository = FakeInteractionRepository(
|
||||||
nextLikeResult: const LikeToggleResult(liked: true, count: 1),
|
nextLikeResult: const LikeToggleResult(liked: true, count: 1),
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ void main() {
|
|||||||
'accessType': 'FREE',
|
'accessType': 'FREE',
|
||||||
'playUrl': '/files/episode/12.m3u8?e=1&s=abc',
|
'playUrl': '/files/episode/12.m3u8?e=1&s=abc',
|
||||||
'qualities': ['AUTO', '720P', '1080P'],
|
'qualities': ['AUTO', '720P', '1080P'],
|
||||||
|
'likeCount': '17',
|
||||||
|
'commentCount': 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ticket.qualities, ['AUTO', '720P', '1080P']);
|
expect(ticket.qualities, ['AUTO', '720P', '1080P']);
|
||||||
|
expect(ticket.likeCount, 17);
|
||||||
|
expect(ticket.commentCount, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_default_to_empty_qualities_when_field_is_missing', () {
|
test('should_default_to_empty_qualities_when_field_is_missing', () {
|
||||||
|
|||||||
@@ -637,12 +637,10 @@ void main() {
|
|||||||
'auth.refreshToken': 'test-refresh',
|
'auth.refreshToken': 'test-refresh',
|
||||||
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
||||||
});
|
});
|
||||||
await _pumpApp(
|
final interactions = FakeInteractionRepository(
|
||||||
tester,
|
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
||||||
interactions: FakeInteractionRepository(
|
|
||||||
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
await _pumpApp(tester, interactions: interactions);
|
||||||
await _openSeriesPlayer(tester);
|
await _openSeriesPlayer(tester);
|
||||||
|
|
||||||
String railLabel(Key key) => tester
|
String railLabel(Key key) => tester
|
||||||
@@ -659,6 +657,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('player-rail-like')));
|
await tester.tap(find.byKey(const Key('player-rail-like')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(railLabel(const Key('player-rail-like')), '2.3万');
|
expect(railLabel(const Key('player-rail-like')), '2.3万');
|
||||||
|
expect(interactions.likeTargets, contains('EPISODE:city-lights-demo-1'));
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('player-rail-save')));
|
await tester.tap(find.byKey(const Key('player-rail-save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ Drama _drama(
|
|||||||
title: 'Episode ${index + 1}',
|
title: 'Episode ${index + 1}',
|
||||||
isFree: true,
|
isFree: true,
|
||||||
videoUrl: 'https://example.test/$format-$index.mp4',
|
videoUrl: 'https://example.test/$format-$index.mp4',
|
||||||
|
likeCount: likeCount,
|
||||||
|
commentCount: commentCount,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -10,12 +10,14 @@ import 'package:fruitshorts/core/data/repositories/interaction_repository.dart';
|
|||||||
class CommentPageCall {
|
class CommentPageCall {
|
||||||
const CommentPageCall({
|
const CommentPageCall({
|
||||||
required this.dramaId,
|
required this.dramaId,
|
||||||
|
required this.episodeId,
|
||||||
required this.rootId,
|
required this.rootId,
|
||||||
required this.cursor,
|
required this.cursor,
|
||||||
required this.size,
|
required this.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String dramaId;
|
final String dramaId;
|
||||||
|
final String? episodeId;
|
||||||
final String? rootId;
|
final String? rootId;
|
||||||
final String? cursor;
|
final String? cursor;
|
||||||
final int size;
|
final int size;
|
||||||
@@ -57,11 +59,13 @@ class FakeCommentRepository implements CommentRepository {
|
|||||||
final List<PublishCall> publishCalls = [];
|
final List<PublishCall> publishCalls = [];
|
||||||
final List<String> deleteCalls = [];
|
final List<String> deleteCalls = [];
|
||||||
final List<String> countCalls = [];
|
final List<String> countCalls = [];
|
||||||
|
final List<String> countEpisodeCalls = [];
|
||||||
int _pageIndex = 0;
|
int _pageIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CommentPage> page({
|
Future<CommentPage> page({
|
||||||
required String dramaId,
|
required String dramaId,
|
||||||
|
String? episodeId,
|
||||||
String? rootId,
|
String? rootId,
|
||||||
String? cursor,
|
String? cursor,
|
||||||
int size = commentPageSize,
|
int size = commentPageSize,
|
||||||
@@ -69,6 +73,7 @@ class FakeCommentRepository implements CommentRepository {
|
|||||||
pageCalls.add(
|
pageCalls.add(
|
||||||
CommentPageCall(
|
CommentPageCall(
|
||||||
dramaId: dramaId,
|
dramaId: dramaId,
|
||||||
|
episodeId: episodeId,
|
||||||
rootId: rootId,
|
rootId: rootId,
|
||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
size: size,
|
size: size,
|
||||||
@@ -81,8 +86,11 @@ class FakeCommentRepository implements CommentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> count(String dramaId) async {
|
Future<int> count(String dramaId, {String? episodeId}) async {
|
||||||
countCalls.add(dramaId);
|
countCalls.add(dramaId);
|
||||||
|
if (episodeId != null && episodeId.isNotEmpty) {
|
||||||
|
countEpisodeCalls.add(episodeId);
|
||||||
|
}
|
||||||
final error = countError;
|
final error = countError;
|
||||||
if (error != null) throw error;
|
if (error != null) throw error;
|
||||||
final result = countResult;
|
final result = countResult;
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.byKey(const Key('player-rail-like')));
|
await tester.tap(find.byKey(const Key('player-rail-like')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(interactions.likeTargets, contains('DRAMA:city-lights-demo'));
|
expect(interactions.likeTargets, contains('EPISODE:city-lights-demo-1'));
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('player-rail-save')));
|
await tester.tap(find.byKey(const Key('player-rail-save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|||||||
Reference in new issue
Block a user