add
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/comment_item.dart';
|
||||
import '../services/comment_service.dart';
|
||||
import 'auth_provider.dart';
|
||||
|
||||
/// 回复目标(null 表示直接评论视频)
|
||||
class ReplyTarget {
|
||||
const ReplyTarget({
|
||||
required this.parentId,
|
||||
required this.replyToUserId,
|
||||
required this.nickname,
|
||||
});
|
||||
|
||||
/// 一级评论 ID(楼中楼统一挂一级楼下)
|
||||
final int parentId;
|
||||
|
||||
/// 被回复人 ID(回复一级评论时等于其作者)
|
||||
final int replyToUserId;
|
||||
|
||||
/// 被回复人昵称(输入框提示用)
|
||||
final String nickname;
|
||||
}
|
||||
|
||||
/// 评论区状态
|
||||
class CommentState {
|
||||
const CommentState({
|
||||
this.items = const [],
|
||||
this.total = 0,
|
||||
this.isLoading = false,
|
||||
this.isLoadingMore = false,
|
||||
this.loadFailed = false,
|
||||
this.page = 1,
|
||||
this.replies = const {},
|
||||
this.repliesLoading = const {},
|
||||
this.replyTarget,
|
||||
this.posting = false,
|
||||
});
|
||||
|
||||
/// 一级评论列表
|
||||
final List<CommentItem> items;
|
||||
final int total;
|
||||
final bool isLoading;
|
||||
final bool isLoadingMore;
|
||||
|
||||
/// 首屏加载失败
|
||||
final bool loadFailed;
|
||||
final int page;
|
||||
|
||||
/// 已展开的一级评论的二级回复列表
|
||||
final Map<int, List<CommentItem>> replies;
|
||||
|
||||
/// 回复加载中的一级评论 ID
|
||||
final Set<int> repliesLoading;
|
||||
|
||||
/// 当前回复目标(null = 评论视频本身)
|
||||
final ReplyTarget? replyTarget;
|
||||
|
||||
/// 发表中(防重复提交)
|
||||
final bool posting;
|
||||
|
||||
static const int pageSize = 20;
|
||||
|
||||
bool get hasMore => items.length < total;
|
||||
|
||||
CommentState copyWith({
|
||||
List<CommentItem>? items,
|
||||
int? total,
|
||||
bool? isLoading,
|
||||
bool? isLoadingMore,
|
||||
bool? loadFailed,
|
||||
int? page,
|
||||
Map<int, List<CommentItem>>? replies,
|
||||
Set<int>? repliesLoading,
|
||||
ReplyTarget? Function()? replyTarget,
|
||||
bool? posting,
|
||||
}) =>
|
||||
CommentState(
|
||||
items: items ?? this.items,
|
||||
total: total ?? this.total,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
loadFailed: loadFailed ?? this.loadFailed,
|
||||
page: page ?? this.page,
|
||||
replies: replies ?? this.replies,
|
||||
repliesLoading: repliesLoading ?? this.repliesLoading,
|
||||
replyTarget: replyTarget != null ? replyTarget() : this.replyTarget,
|
||||
posting: posting ?? this.posting,
|
||||
);
|
||||
}
|
||||
|
||||
/// 评论区 Notifier(按 videoId 家族隔离)
|
||||
class CommentNotifier extends StateNotifier<CommentState> {
|
||||
CommentNotifier(this._ref, this.videoId) : super(const CommentState()) {
|
||||
load();
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
final int videoId;
|
||||
|
||||
/// 首屏加载
|
||||
Future<void> load() async {
|
||||
state = state.copyWith(isLoading: true, loadFailed: false);
|
||||
try {
|
||||
final page = await CommentService.list(videoId);
|
||||
state = state.copyWith(
|
||||
items: page.items,
|
||||
total: page.total,
|
||||
isLoading: false,
|
||||
page: 1,
|
||||
);
|
||||
} catch (_) {
|
||||
state = state.copyWith(isLoading: false, loadFailed: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 触底加载更多一级评论
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoadingMore || state.isLoading) return;
|
||||
state = state.copyWith(isLoadingMore: true);
|
||||
try {
|
||||
final page = await CommentService.list(videoId, page: state.page + 1);
|
||||
state = state.copyWith(
|
||||
items: [...state.items, ...page.items],
|
||||
total: page.total,
|
||||
isLoadingMore: false,
|
||||
page: state.page + 1,
|
||||
);
|
||||
} catch (_) {
|
||||
state = state.copyWith(isLoadingMore: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 展开/收起二级回复;首次展开时加载(一次拉 50 条,超出部分显示总数)
|
||||
Future<void> toggleReplies(CommentItem comment) async {
|
||||
final replies = Map<int, List<CommentItem>>.from(state.replies);
|
||||
if (replies.containsKey(comment.commentId)) {
|
||||
replies.remove(comment.commentId);
|
||||
state = state.copyWith(replies: replies);
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
repliesLoading: {...state.repliesLoading, comment.commentId},
|
||||
);
|
||||
try {
|
||||
final page = await CommentService.list(
|
||||
videoId,
|
||||
parentId: comment.commentId,
|
||||
pageSize: 50,
|
||||
);
|
||||
state = state.copyWith(
|
||||
replies: {...state.replies, comment.commentId: page.items},
|
||||
repliesLoading: {...state.repliesLoading}..remove(comment.commentId),
|
||||
);
|
||||
} catch (_) {
|
||||
state = state.copyWith(
|
||||
repliesLoading: {...state.repliesLoading}..remove(comment.commentId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置/清除回复目标
|
||||
void setReplyTarget(ReplyTarget? target) =>
|
||||
state = state.copyWith(replyTarget: () => target);
|
||||
|
||||
/// 发表评论/回复。成功返回结果(含奖励进度),失败抛异常由 UI 提示
|
||||
Future<CommentCreateResult> post(String content) async {
|
||||
final user = _ref.read(authProvider).user;
|
||||
final target = state.replyTarget;
|
||||
state = state.copyWith(posting: true);
|
||||
try {
|
||||
final result = await CommentService.create(
|
||||
videoId,
|
||||
content,
|
||||
parentId: target?.parentId,
|
||||
replyToUserId: target?.replyToUserId,
|
||||
);
|
||||
final item = CommentItem(
|
||||
commentId: result.commentId,
|
||||
userId: user?.userId ?? 0,
|
||||
nickname: user?.nickname ?? '我',
|
||||
avatarUrl: user?.avatarUrl ?? '',
|
||||
content: content,
|
||||
createdAt: '刚刚',
|
||||
);
|
||||
if (target == null) {
|
||||
// 一级评论:插入顶部
|
||||
state = state.copyWith(
|
||||
items: [item, ...state.items],
|
||||
total: state.total + 1,
|
||||
posting: false,
|
||||
);
|
||||
} else {
|
||||
// 回复:插入已展开列表,父评论 replyCount+1
|
||||
final replies = Map<int, List<CommentItem>>.from(state.replies);
|
||||
if (replies.containsKey(target.parentId)) {
|
||||
replies[target.parentId] = [item, ...replies[target.parentId]!];
|
||||
}
|
||||
state = state.copyWith(
|
||||
replies: replies,
|
||||
items: [
|
||||
for (final c in state.items)
|
||||
c.commentId == target.parentId
|
||||
? c.copyWith(replyCount: c.replyCount + 1)
|
||||
: c,
|
||||
],
|
||||
total: state.total + 1,
|
||||
posting: false,
|
||||
replyTarget: () => null,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (_) {
|
||||
state = state.copyWith(posting: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除评论:parentId=0 为一级评论,否则为某一级楼下的回复
|
||||
Future<void> remove(CommentItem comment, {int parentId = 0}) async {
|
||||
await CommentService.delete(comment.commentId);
|
||||
if (parentId == 0) {
|
||||
final replies = Map<int, List<CommentItem>>.from(state.replies)
|
||||
..remove(comment.commentId);
|
||||
state = state.copyWith(
|
||||
items: state.items.where((c) => c.commentId != comment.commentId).toList(),
|
||||
total: (state.total - 1).clamp(0, 1 << 31),
|
||||
replies: replies,
|
||||
);
|
||||
} else {
|
||||
final replies = Map<int, List<CommentItem>>.from(state.replies);
|
||||
replies[parentId] = (replies[parentId] ?? [])
|
||||
.where((c) => c.commentId != comment.commentId)
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
replies: replies,
|
||||
items: [
|
||||
for (final c in state.items)
|
||||
c.commentId == parentId
|
||||
? c.copyWith(replyCount: (c.replyCount - 1).clamp(0, 1 << 31))
|
||||
: c,
|
||||
],
|
||||
total: (state.total - 1).clamp(0, 1 << 31),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 评论区 Provider(按视频 ID 家族)
|
||||
final commentProvider =
|
||||
StateNotifierProvider.family<CommentNotifier, CommentState, int>(
|
||||
(ref, videoId) => CommentNotifier(ref, videoId),
|
||||
);
|
||||
Reference in New Issue
Block a user