197 lines
6.2 KiB
Dart
197 lines
6.2 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../core/data/models/comment.dart';
|
|
import '../../../core/formatting/relative_date_time.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/theme/app_dimens.dart';
|
|
import '../../../core/widgets/remote_image.dart';
|
|
import '../../../l10n/app_localizations.dart';
|
|
|
|
const String _deleteAction = 'delete';
|
|
const String _reportAction = 'report';
|
|
|
|
/// 评论条目的交互回调集合。
|
|
class CommentInteractionHandlers {
|
|
const CommentInteractionHandlers({
|
|
required this.onReply,
|
|
required this.onDelete,
|
|
required this.onReport,
|
|
});
|
|
|
|
final ValueChanged<DramaComment> onReply;
|
|
final Future<void> Function(DramaComment) onDelete;
|
|
final Future<void> Function(DramaComment) onReport;
|
|
}
|
|
|
|
/// 单条评论或回复的展示与操作行。
|
|
class CommentTile extends StatelessWidget {
|
|
const CommentTile({
|
|
super.key,
|
|
required this.comment,
|
|
required this.handlers,
|
|
this.isReply = false,
|
|
});
|
|
|
|
final DramaComment comment;
|
|
final CommentInteractionHandlers handlers;
|
|
final bool isReply;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final name = comment.nickname.isEmpty
|
|
? l10n.anonymousUser
|
|
: comment.nickname;
|
|
return Padding(
|
|
key: Key('comment-item-${comment.id}'),
|
|
padding: const EdgeInsets.only(top: 10, bottom: 12),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_avatar(),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
name,
|
|
style: const TextStyle(
|
|
color: AppColors.muted,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
SizedBox.square(
|
|
dimension: 28,
|
|
child: PopupMenuButton<String>(
|
|
key: Key('comment-menu-${comment.id}'),
|
|
tooltip: l10n.more,
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 18,
|
|
icon: const Icon(
|
|
Icons.more_horiz_rounded,
|
|
color: AppColors.subtle,
|
|
),
|
|
onSelected: (value) {
|
|
if (value == _deleteAction) {
|
|
handlers.onDelete(comment);
|
|
} else if (value == _reportAction) {
|
|
handlers.onReport(comment);
|
|
}
|
|
},
|
|
itemBuilder: (_) => [
|
|
if (comment.mine)
|
|
PopupMenuItem(
|
|
value: _deleteAction,
|
|
child: Text(l10n.deleteComment),
|
|
),
|
|
PopupMenuItem(
|
|
value: _reportAction,
|
|
child: Text(l10n.reportComment),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
comment.content,
|
|
style: const TextStyle(
|
|
color: AppColors.cream,
|
|
fontSize: 16,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
if (comment.isPending)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Text(
|
|
l10n.underReview,
|
|
key: Key('comment-pending-${comment.id}'),
|
|
style: const TextStyle(
|
|
color: AppColors.blueSoft,
|
|
fontSize: AppDimens.caption,
|
|
),
|
|
),
|
|
),
|
|
if (comment.createdAt != null || !isReply)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: _metadata(context, l10n),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _avatar() {
|
|
final size = isReply ? 28.0 : 40.0;
|
|
if (comment.avatarUrl.isEmpty) {
|
|
return CircleAvatar(
|
|
radius: size / 2,
|
|
backgroundColor: AppColors.raised,
|
|
child: Icon(
|
|
Icons.person_rounded,
|
|
size: isReply ? 16 : 22,
|
|
color: AppColors.cream,
|
|
),
|
|
);
|
|
}
|
|
return ClipOval(
|
|
child: SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: RemoteImage(comment.avatarUrl, fit: BoxFit.cover),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _metadata(BuildContext context, AppLocalizations l10n) {
|
|
final createdAt = comment.createdAt;
|
|
return Row(
|
|
children: [
|
|
if (createdAt != null)
|
|
Text(
|
|
formatRelativeDateTime(createdAt, l10n),
|
|
style: const TextStyle(color: AppColors.subtle, fontSize: 12),
|
|
),
|
|
if (createdAt != null && !isReply) ...[
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'·',
|
|
style: TextStyle(color: AppColors.muted, fontSize: 12),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
if (!isReply)
|
|
TextButton(
|
|
key: Key('comment-reply-${comment.id}'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: AppColors.muted,
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: const Size(0, 24),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
onPressed: () => handlers.onReply(comment),
|
|
child: Text(
|
|
l10n.reply,
|
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|