💄 Redesigned post item
This commit is contained in:
parent
ac2aec48aa
commit
9ec0f1ff19
@ -837,5 +837,6 @@
|
||||
"fieldContactContent": "Contact method",
|
||||
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
||||
"accountContactMethodsDelete": "Delete Contact Method",
|
||||
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible."
|
||||
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
|
||||
"postCommentAdd": "Write a comment"
|
||||
}
|
||||
|
@ -837,5 +837,6 @@
|
||||
"fieldContactContent": "联系方式",
|
||||
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
||||
"accountContactMethodsDelete": "删除联系方式",
|
||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。"
|
||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
||||
"postCommentAdd": "撰写一条评论"
|
||||
}
|
||||
|
@ -837,5 +837,6 @@
|
||||
"fieldContactContent": "聯繫方式",
|
||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||
"postCommentAdd": "撰寫一條評論"
|
||||
}
|
||||
|
@ -837,5 +837,6 @@
|
||||
"fieldContactContent": "聯繫方式",
|
||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||
"postCommentAdd": "撰寫一條評論"
|
||||
}
|
||||
|
@ -551,12 +551,18 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
maxWidth: 640,
|
||||
);
|
||||
case 'reader.news':
|
||||
return NewsFeedEntry(data: ele);
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: NewsFeedEntry(data: ele),
|
||||
);
|
||||
default:
|
||||
return FeedUnknownEntry(data: ele);
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: FeedUnknownEntry(data: ele),
|
||||
);
|
||||
}
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -14,14 +15,13 @@ import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/sn_network.dart';
|
||||
|
||||
class PostCommentQuickAction extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final SnPost parentPost;
|
||||
final Function? onPosted;
|
||||
|
||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
const PostCommentQuickAction(
|
||||
{super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -30,7 +30,9 @@ class PostCommentQuickAction extends StatelessWidget {
|
||||
return Container(
|
||||
height: 240,
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero,
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const EdgeInsets.symmetric(vertical: 8)
|
||||
: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
@ -99,7 +101,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
Future<void> _selectAnswer(SnPost answer) async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||
await sn.client
|
||||
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||
'publisher': answer.publisherId,
|
||||
'answer_id': answer.id,
|
||||
});
|
||||
@ -135,7 +138,9 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: widget.maxWidth,
|
||||
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
|
||||
onSelectAnswer: widget.parentPost.type == 'question'
|
||||
? () => _selectAnswer(_posts[idx])
|
||||
: null,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
@ -153,7 +158,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -188,7 +194,9 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(widget.commentCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
|
@ -7,7 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -72,7 +72,9 @@ class OpenablePostItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return Center(
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
@ -94,14 +96,15 @@ class OpenablePostItem extends StatelessWidget {
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
closedElevation: 0,
|
||||
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -202,61 +205,6 @@ class PostItem extends StatelessWidget {
|
||||
final ua = context.read<UserProvider>();
|
||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||
|
||||
// Video full view
|
||||
if (showFullPost &&
|
||||
data.type == 'video' &&
|
||||
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
if (data.preload?.video != null)
|
||||
_PostVideoPlayer(data: data).padding(bottom: 8),
|
||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||
_PostFeaturedComment(data: data),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: true,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onChanged: _onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
width: 340,
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
PostCommentSliverList(
|
||||
parentPost: data,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Article headline preview
|
||||
if (!showFullPost && data.type == 'article') {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
@ -265,14 +213,7 @@ class PostItem extends StatelessWidget {
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||
if (data.preload?.video != null)
|
||||
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
@ -325,14 +266,6 @@ class PostItem extends StatelessWidget {
|
||||
.padding(horizontal: 24, bottom: 8),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||
.padding(horizontal: 12),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: showComments,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onChanged: _onChanged,
|
||||
).padding(left: 8, right: 14),
|
||||
],
|
||||
),
|
||||
).center();
|
||||
@ -345,6 +278,12 @@ class PostItem extends StatelessWidget {
|
||||
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
var attachmentSize = math.min(
|
||||
MediaQuery.of(context).size.width, maxWidth ?? double.infinity);
|
||||
if ((data.preload?.attachments?.length ?? 0) > 1) {
|
||||
attachmentSize -= 80;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -353,54 +292,85 @@ class PostItem extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostAvatar(
|
||||
data: data,
|
||||
showMenu: showMenu,
|
||||
isCompact: false,
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _PostContentHeader(
|
||||
isRelativeDate: !showFullPost,
|
||||
isCompact: true,
|
||||
data: data,
|
||||
),
|
||||
),
|
||||
_PostActionPopup(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
if (data.preload?.video != null)
|
||||
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
_PostVideoPlayer(data: data).padding(bottom: 8),
|
||||
if (data.type == 'question')
|
||||
_PostQuestionHint(data: data)
|
||||
.padding(horizontal: 16, bottom: 8),
|
||||
_PostQuestionHint(data: data).padding(bottom: 8),
|
||||
if (data.body['title'] != null ||
|
||||
data.body['description'] != null)
|
||||
_PostHeadline(
|
||||
data: data,
|
||||
isEnlarge: data.type == 'article' && showFullPost,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
).padding(bottom: 8),
|
||||
if (data.body['content']?.isNotEmpty ?? false)
|
||||
_PostContentBody(
|
||||
data: data,
|
||||
isSelectable: showFullPost,
|
||||
isEnlarge: data.type == 'article' && showFullPost,
|
||||
).padding(horizontal: 16, bottom: 6),
|
||||
).padding(bottom: 6),
|
||||
if (data.repostTo != null)
|
||||
_PostQuoteContent(child: data.repostTo!).padding(
|
||||
horizontal: 12,
|
||||
bottom:
|
||||
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
||||
data.preload?.attachments?.isNotEmpty ?? false
|
||||
? 12
|
||||
: 0,
|
||||
),
|
||||
if (data.visibility > 0)
|
||||
_PostVisibilityHint(data: data).padding(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
if (data.body['content_truncated'] == true)
|
||||
_PostTruncatedHint(data: data).padding(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
if (data.tags.isNotEmpty)
|
||||
_PostTagsList(data: data)
|
||||
.padding(horizontal: 16, top: 4, bottom: 6),
|
||||
_PostTagsList(data: data).padding(top: 4, bottom: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.play_circle, size: 20),
|
||||
const Gap(4),
|
||||
Text('postViews').plural(data.totalViews),
|
||||
],
|
||||
).opacity(0.75).padding(vertical: 4),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
).padding(horizontal: 12, top: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -409,9 +379,10 @@ class PostItem extends StatelessWidget {
|
||||
data: displayableAttachments!,
|
||||
bordered: true,
|
||||
maxHeight: showFullPost ? null : 480,
|
||||
maxWidth: MediaQuery.of(context).size.width - 20,
|
||||
minWidth: attachmentSize,
|
||||
maxWidth: attachmentSize,
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 60, right: 12),
|
||||
),
|
||||
if (data.preload?.poll != null)
|
||||
PostPoll(poll: data.preload!.poll!)
|
||||
@ -420,22 +391,15 @@ class PostItem extends StatelessWidget {
|
||||
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
).padding(horizontal: 4),
|
||||
).padding(left: 60, right: 4),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||
.padding(horizontal: 12),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Column(
|
||||
children: [
|
||||
_PostBottomAction(
|
||||
.padding(left: 60, right: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _PostReactionList(
|
||||
data: data,
|
||||
showComments: showComments,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
padding: const EdgeInsets.only(left: 60, right: 12),
|
||||
onChanged: _onChanged,
|
||||
).padding(left: 8, right: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -476,12 +440,7 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
_PostContentHeader(
|
||||
isAuthor: false,
|
||||
data: data,
|
||||
onDeleted: () {},
|
||||
onShare: () {},
|
||||
onShareImage: () {},
|
||||
showMenu: false,
|
||||
isRelativeDate: false,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
if (data.type == 'question')
|
||||
@ -516,14 +475,6 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
_PostTruncatedHint(data: data),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: true,
|
||||
showReactions: true,
|
||||
onShare: () {},
|
||||
onShareImage: () {},
|
||||
onChanged: (SnPost data) {},
|
||||
).padding(left: 8, right: 14),
|
||||
const Divider(height: 1),
|
||||
const Gap(12),
|
||||
SizedBox(
|
||||
@ -622,50 +573,90 @@ class _PostQuestionHint extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostBottomAction extends StatelessWidget {
|
||||
class _PostReactionList extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final bool showComments;
|
||||
final bool showReactions;
|
||||
final Function(SnPost data) onChanged;
|
||||
final Function() onShare, onShareImage;
|
||||
|
||||
const _PostBottomAction({
|
||||
final EdgeInsets? padding;
|
||||
final Function(SnPost) onChanged;
|
||||
const _PostReactionList({
|
||||
required this.data,
|
||||
required this.showComments,
|
||||
required this.showReactions,
|
||||
this.padding,
|
||||
required this.onChanged,
|
||||
required this.onShare,
|
||||
required this.onShareImage,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha(
|
||||
(255 * 0.8).round(),
|
||||
);
|
||||
State<_PostReactionList> createState() => _PostReactionListState();
|
||||
}
|
||||
|
||||
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
||||
? data.metric.reactionList.entries
|
||||
class _PostReactionListState extends State<_PostReactionList> {
|
||||
bool _isSubmitting = false;
|
||||
late int _totalUpvote = widget.data.totalUpvote;
|
||||
late int _totalDownvote = widget.data.totalDownvote;
|
||||
late Map<String, int> _reactions = Map.from(widget.data.metric.reactionList);
|
||||
|
||||
Future<void> _reactPost(String symbol, int attitude) async {
|
||||
if (_isSubmitting) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
setState(() => _isSubmitting = true);
|
||||
final resp = await sn.client.post(
|
||||
'/cgi/co/posts/${widget.data.id}/react',
|
||||
data: {
|
||||
'symbol': symbol,
|
||||
'attitude': attitude,
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 201) {
|
||||
_reactions[symbol] = (_reactions[symbol] ?? 0) + 1;
|
||||
widget.onChanged(
|
||||
widget.data.copyWith(
|
||||
metric: widget.data.metric.copyWith(reactionList: _reactions),
|
||||
),
|
||||
);
|
||||
} else if (resp.statusCode == 204) {
|
||||
_reactions[symbol] = (_reactions[symbol] ?? 0) - 1;
|
||||
widget.onChanged(
|
||||
widget.data.copyWith(
|
||||
metric: widget.data.metric.copyWith(reactionList: _reactions),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (attitude == 1) {
|
||||
setState(() => _totalUpvote += 1);
|
||||
} else if (attitude == 2) {
|
||||
setState(() => _totalDownvote += 1);
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
itemCount: widget.data.metric.reactionList.length + 1,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
final String? mostTypicalReaction = _reactions.isNotEmpty
|
||||
? _reactions.entries
|
||||
.reduce((a, b) => a.value > b.value ? a : b)
|
||||
.key
|
||||
: null;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (showReactions || showComments)
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (showReactions)
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
if (mostTypicalReaction == null ||
|
||||
kTemplateReactions[mostTypicalReaction] == null)
|
||||
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
||||
else
|
||||
Text(
|
||||
ActionChip(
|
||||
avatar: (kTemplateReactions[mostTypicalReaction] == null)
|
||||
? Icon(Symbols.add_reaction, size: 20)
|
||||
: Text(
|
||||
kTemplateReactions[mostTypicalReaction]!.icon,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
@ -673,81 +664,73 @@ class _PostBottomAction extends StatelessWidget {
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
if (data.totalUpvote > 0 &&
|
||||
data.totalUpvote >= data.totalDownvote)
|
||||
Text('postReactionUpvote').plural(
|
||||
data.totalUpvote,
|
||||
)
|
||||
else if (data.totalDownvote > 0)
|
||||
Text('postReactionDownvote').plural(
|
||||
data.totalDownvote,
|
||||
)
|
||||
else
|
||||
Text('postReact').tr(),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 8),
|
||||
onTap: () {
|
||||
label: (_totalUpvote > 0 && _totalUpvote >= _totalDownvote)
|
||||
? Text('postReactionUpvote').plural(_totalUpvote)
|
||||
: (_totalDownvote > 0)
|
||||
? Text('postReactionDownvote').plural(_totalDownvote)
|
||||
: Text('postReact').tr(),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => PostReactionPopup(
|
||||
data: data,
|
||||
data: widget.data,
|
||||
onChanged: (value, attr, delta) {
|
||||
onChanged(data.copyWith(
|
||||
final metric =
|
||||
widget.data.metric.copyWith(reactionList: value);
|
||||
if (attr == 1) {
|
||||
_totalUpvote += delta;
|
||||
} else if (attr == 2) {
|
||||
_totalDownvote += delta;
|
||||
}
|
||||
_reactions = Map.from(metric.reactionList);
|
||||
widget.onChanged(widget.data.copyWith(
|
||||
totalUpvote: attr == 1
|
||||
? data.totalUpvote + delta
|
||||
: data.totalUpvote,
|
||||
? widget.data.totalUpvote + delta
|
||||
: widget.data.totalUpvote,
|
||||
totalDownvote: attr == 2
|
||||
? data.totalDownvote + delta
|
||||
: data.totalDownvote,
|
||||
metric: data.metric.copyWith(reactionList: value),
|
||||
? widget.data.totalDownvote + delta
|
||||
: widget.data.totalDownvote,
|
||||
metric: metric,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (showComments)
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.comment, size: 20, color: iconColor),
|
||||
const Gap(8),
|
||||
Text('postComments').plural(data.metric.replyCount),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 8),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostCommentListPopup(
|
||||
post: data,
|
||||
commentCount: data.metric.replyCount,
|
||||
if (_reactions.isNotEmpty) const Gap(8),
|
||||
if (_reactions.isNotEmpty)
|
||||
SizedBox(
|
||||
width: 1,
|
||||
height: 20,
|
||||
child: const VerticalDivider(width: 1),
|
||||
),
|
||||
if (_reactions.isNotEmpty) const Gap(4),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final ele = _reactions.entries.elementAt(index - 1);
|
||||
return ActionChip(
|
||||
avatar: Text(kTemplateReactions[ele.key]!.icon),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(ele.key),
|
||||
Text('x${ele.value}').bold(),
|
||||
],
|
||||
),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () {
|
||||
_reactPost(ele.key, kTemplateReactions[ele.key]!.attitude);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(4),
|
||||
),
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.play_circle, size: 20, color: iconColor),
|
||||
const Gap(8),
|
||||
Text('postViews').plural(data.totalViews),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
InkWell(
|
||||
onTap: onShare,
|
||||
onLongPress: onShareImage,
|
||||
child: Icon(
|
||||
Symbols.share,
|
||||
size: 20,
|
||||
color: iconColor,
|
||||
).padding(horizontal: 8, vertical: 8),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -849,22 +832,81 @@ class _PostHeadline extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostContentHeader extends StatelessWidget {
|
||||
class _PostAvatar extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool isCompact;
|
||||
const _PostAvatar({required this.data, required this.isCompact});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final user = data.publisher.type == 0
|
||||
? ud.getFromCache(data.publisher.accountId)
|
||||
: null;
|
||||
|
||||
return GestureDetector(
|
||||
child: data.preload?.realm == null
|
||||
? AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? AccountBadge(
|
||||
badge: user!.badges.first,
|
||||
radius: 16,
|
||||
padding: EdgeInsets.all(2),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: AccountImage(
|
||||
content: data.preload!.realm!.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius: isCompact ? 4 : 8,
|
||||
badgeOffset: Offset(-6, -4),
|
||||
badge: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: PublisherPopoverCard(
|
||||
data: data.publisher,
|
||||
),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostActionPopup extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool isAuthor;
|
||||
final bool isCompact;
|
||||
final bool isRelativeDate;
|
||||
final bool showMenu;
|
||||
final Function onDeleted;
|
||||
final Function() onShare, onShareImage;
|
||||
final Function()? onSelectAnswer;
|
||||
|
||||
const _PostContentHeader({
|
||||
const _PostActionPopup({
|
||||
required this.data,
|
||||
required this.isAuthor,
|
||||
this.isCompact = false,
|
||||
this.isRelativeDate = true,
|
||||
this.showMenu = true,
|
||||
required this.onDeleted,
|
||||
required this.onShare,
|
||||
required this.onShareImage,
|
||||
@ -914,124 +956,14 @@ class _PostContentHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final user = data.publisher.type == 0
|
||||
? ud.getFromCache(data.publisher.accountId)
|
||||
: null;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: data.preload?.realm == null
|
||||
? AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius:
|
||||
data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? AccountBadge(
|
||||
badge: user!.badges.first,
|
||||
radius: 16,
|
||||
padding: EdgeInsets.all(2),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: AccountImage(
|
||||
content: data.preload!.realm!.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius: isCompact ? 4 : 8,
|
||||
badgeOffset: Offset(-6, -4),
|
||||
badge: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: PublisherPopoverCard(
|
||||
data: data.publisher,
|
||||
),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
),
|
||||
Gap(isCompact ? 8 : 12),
|
||||
if (isCompact)
|
||||
Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
const Gap(4),
|
||||
Text(
|
||||
isRelativeDate
|
||||
? RelativeTime(context).format(
|
||||
(data.publishedAt ?? data.createdAt).toLocal())
|
||||
: DateFormat('y/M/d HH:mm').format(
|
||||
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
if (data.preload?.realm != null)
|
||||
const Icon(Symbols.arrow_right, size: 16)
|
||||
.padding(horizontal: 2)
|
||||
.opacity(0.5),
|
||||
if (data.preload?.realm != null)
|
||||
Text(data.preload!.realm!.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
const Gap(4),
|
||||
Text(
|
||||
isRelativeDate
|
||||
? RelativeTime(context).format(
|
||||
(data.publishedAt ?? data.createdAt).toLocal())
|
||||
: DateFormat('y/M/d HH:mm').format(
|
||||
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showMenu)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.more_horiz),
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: PopupMenuButton(
|
||||
icon: const Icon(Symbols.more_horiz, size: 20),
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
if (isAuthor && onSelectAnswer != null)
|
||||
PopupMenuItem(
|
||||
@ -1187,8 +1119,71 @@ class _PostContentHeader extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostContentHeader extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool isCompact;
|
||||
final bool isRelativeDate;
|
||||
|
||||
const _PostContentHeader({
|
||||
required this.data,
|
||||
this.isCompact = false,
|
||||
this.isRelativeDate = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isCompact) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
isRelativeDate
|
||||
? RelativeTime(context)
|
||||
.format((data.publishedAt ?? data.createdAt).toLocal())
|
||||
: DateFormat('y/M/d HH:mm')
|
||||
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
if (data.preload?.realm != null)
|
||||
const Icon(Symbols.arrow_right, size: 16)
|
||||
.padding(horizontal: 2)
|
||||
.opacity(0.5),
|
||||
if (data.preload?.realm != null) Text(data.preload!.realm!.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
const Gap(4),
|
||||
Text(
|
||||
isRelativeDate
|
||||
? RelativeTime(context)
|
||||
.format((data.publishedAt ?? data.createdAt).toLocal())
|
||||
: DateFormat('y/M/d HH:mm')
|
||||
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1248,14 +1243,9 @@ class _PostQuoteContent extends StatelessWidget {
|
||||
Column(
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
isAuthor: false,
|
||||
data: child,
|
||||
isCompact: true,
|
||||
isRelativeDate: isRelativeDate,
|
||||
showMenu: false,
|
||||
onShare: () {},
|
||||
onShareImage: () {},
|
||||
onDeleted: () {},
|
||||
).padding(bottom: 4),
|
||||
_PostContentBody(data: child),
|
||||
if (child.visibility > 0)
|
||||
@ -1486,12 +1476,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
|
||||
if (_featuredComment == null) return const SizedBox.shrink();
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AnimateWidgetExtensions(Container(
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: double.infinity,
|
||||
@ -1499,7 +1486,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: _isAnswer
|
||||
? Colors.green.withOpacity(0.5)
|
||||
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
@ -1519,29 +1506,33 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Gap(2),
|
||||
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
|
||||
size: 20),
|
||||
Transform.flip(
|
||||
flipX: !_isAnswer,
|
||||
child: Icon(
|
||||
_isAnswer ? Symbols.task_alt : Symbols.reply,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Gap(10),
|
||||
Text(
|
||||
_isAnswer
|
||||
? 'postQuestionAnswerTitle'
|
||||
: 'postFeaturedComment',
|
||||
? 'postQuestionAnswerTitle'.tr()
|
||||
: 'postComments'.plural(widget.data.metric.replyCount),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 15),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
if (_featuredComment != null)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
AccountImage(
|
||||
content: _featuredComment!.publisher.avatar,
|
||||
radius: 12,
|
||||
backgroundImage: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(_featuredComment!.publisher.avatar),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
@ -1551,12 +1542,37 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: ua.user?.avatar,
|
||||
radius: 12,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text('postCommentAdd').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user