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 items; final int total; final bool isLoading; final bool isLoadingMore; /// 首屏加载失败 final bool loadFailed; final int page; /// 已展开的一级评论的二级回复列表 final Map> replies; /// 回复加载中的一级评论 ID final Set repliesLoading; /// 当前回复目标(null = 评论视频本身) final ReplyTarget? replyTarget; /// 发表中(防重复提交) final bool posting; static const int pageSize = 20; bool get hasMore => items.length < total; CommentState copyWith({ List? items, int? total, bool? isLoading, bool? isLoadingMore, bool? loadFailed, int? page, Map>? replies, Set? 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 { CommentNotifier(this._ref, this.videoId) : super(const CommentState()) { load(); } final Ref _ref; final int videoId; /// 首屏加载 Future 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 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 toggleReplies(CommentItem comment) async { final replies = Map>.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 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>.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 remove(CommentItem comment, {int parentId = 0}) async { await CommentService.delete(comment.commentId); if (parentId == 0) { final replies = Map>.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>.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( (ref, videoId) => CommentNotifier(ref, videoId), );