💄 Redesigned post item
This commit is contained in:
parent
ac2aec48aa
commit
9ec0f1ff19
@ -837,5 +837,6 @@
|
|||||||
"fieldContactContent": "Contact method",
|
"fieldContactContent": "Contact method",
|
||||||
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
||||||
"accountContactMethodsDelete": "Delete Contact Method",
|
"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": "联系方式",
|
"fieldContactContent": "联系方式",
|
||||||
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
||||||
"accountContactMethodsDelete": "删除联系方式",
|
"accountContactMethodsDelete": "删除联系方式",
|
||||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。"
|
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
||||||
|
"postCommentAdd": "撰写一条评论"
|
||||||
}
|
}
|
||||||
|
@ -837,5 +837,6 @@
|
|||||||
"fieldContactContent": "聯繫方式",
|
"fieldContactContent": "聯繫方式",
|
||||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
|
"postCommentAdd": "撰寫一條評論"
|
||||||
}
|
}
|
||||||
|
@ -837,5 +837,6 @@
|
|||||||
"fieldContactContent": "聯繫方式",
|
"fieldContactContent": "聯繫方式",
|
||||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
|
"postCommentAdd": "撰寫一條評論"
|
||||||
}
|
}
|
||||||
|
@ -551,12 +551,18 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
);
|
);
|
||||||
case 'reader.news':
|
case 'reader.news':
|
||||||
return NewsFeedEntry(data: ele);
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: NewsFeedEntry(data: ele),
|
||||||
|
);
|
||||||
default:
|
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:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.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:surface/widgets/post/post_mini_editor.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/sn_network.dart';
|
|
||||||
|
|
||||||
class PostCommentQuickAction extends StatelessWidget {
|
class PostCommentQuickAction extends StatelessWidget {
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
final SnPost parentPost;
|
final SnPost parentPost;
|
||||||
final Function? onPosted;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -30,7 +30,9 @@ class PostCommentQuickAction extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 240,
|
height: 240,
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
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(
|
decoration: BoxDecoration(
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
? const BorderRadius.all(Radius.circular(8))
|
||||||
@ -99,7 +101,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
Future<void> _selectAnswer(SnPost answer) async {
|
Future<void> _selectAnswer(SnPost answer) async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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,
|
'publisher': answer.publisherId,
|
||||||
'answer_id': answer.id,
|
'answer_id': answer.id,
|
||||||
});
|
});
|
||||||
@ -135,7 +138,9 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: _posts[idx],
|
data: _posts[idx],
|
||||||
maxWidth: widget.maxWidth,
|
maxWidth: widget.maxWidth,
|
||||||
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
|
onSelectAnswer: widget.parentPost.type == 'question'
|
||||||
|
? () => _selectAnswer(_posts[idx])
|
||||||
|
: null,
|
||||||
onChanged: (data) {
|
onChanged: (data) {
|
||||||
setState(() => _posts[idx] = 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: [
|
children: [
|
||||||
const Icon(Symbols.comment, size: 24),
|
const Icon(Symbols.comment, size: 24),
|
||||||
const Gap(16),
|
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),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -7,7 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.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:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@ -72,7 +72,9 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
return Center(
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
|
child: Center(
|
||||||
child: OpenContainer(
|
child: OpenContainer(
|
||||||
closedBuilder: (_, __) => Container(
|
closedBuilder: (_, __) => Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
@ -94,14 +96,15 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
openColor: Colors.transparent,
|
openColor: Colors.transparent,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
transitionType: ContainerTransitionType.fade,
|
transitionType: ContainerTransitionType.fade,
|
||||||
closedColor:
|
closedElevation: 0,
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||||
),
|
),
|
||||||
closedShape: const RoundedRectangleBorder(
|
closedShape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,61 +205,6 @@ class PostItem extends StatelessWidget {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
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') {
|
if (!showFullPost && data.type == 'article') {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
@ -265,14 +213,7 @@ class PostItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
data: data,
|
data: data,
|
||||||
isAuthor: isAuthor,
|
|
||||||
isRelativeDate: !showFullPost,
|
isRelativeDate: !showFullPost,
|
||||||
onShare: () => _doShare(context),
|
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
|
||||||
onSelectAnswer: onSelectAnswer,
|
|
||||||
onDeleted: () {
|
|
||||||
onDeleted?.call();
|
|
||||||
},
|
|
||||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||||
if (data.preload?.video != null)
|
if (data.preload?.video != null)
|
||||||
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
@ -325,14 +266,6 @@ class PostItem extends StatelessWidget {
|
|||||||
.padding(horizontal: 24, bottom: 8),
|
.padding(horizontal: 24, bottom: 8),
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
.padding(horizontal: 12),
|
.padding(horizontal: 12),
|
||||||
_PostBottomAction(
|
|
||||||
data: data,
|
|
||||||
showComments: showComments,
|
|
||||||
showReactions: showReactions,
|
|
||||||
onShare: () => _doShare(context),
|
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
|
||||||
onChanged: _onChanged,
|
|
||||||
).padding(left: 8, right: 14),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).center();
|
).center();
|
||||||
@ -345,6 +278,12 @@ class PostItem extends StatelessWidget {
|
|||||||
|
|
||||||
final cfg = context.read<ConfigProvider>();
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -353,54 +292,85 @@ class PostItem extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostContentHeader(
|
Row(
|
||||||
isAuthor: isAuthor,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
isRelativeDate: !showFullPost,
|
children: [
|
||||||
|
_PostAvatar(
|
||||||
data: data,
|
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),
|
onShare: () => _doShare(context),
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: onSelectAnswer,
|
onSelectAnswer: onSelectAnswer,
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
onDeleted?.call();
|
onDeleted?.call();
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, vertical: 8),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
if (data.preload?.video != null)
|
if (data.preload?.video != null)
|
||||||
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
_PostVideoPlayer(data: data).padding(bottom: 8),
|
||||||
if (data.type == 'question')
|
if (data.type == 'question')
|
||||||
_PostQuestionHint(data: data)
|
_PostQuestionHint(data: data).padding(bottom: 8),
|
||||||
.padding(horizontal: 16, bottom: 8),
|
|
||||||
if (data.body['title'] != null ||
|
if (data.body['title'] != null ||
|
||||||
data.body['description'] != null)
|
data.body['description'] != null)
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(bottom: 8),
|
||||||
if (data.body['content']?.isNotEmpty ?? false)
|
if (data.body['content']?.isNotEmpty ?? false)
|
||||||
_PostContentBody(
|
_PostContentBody(
|
||||||
data: data,
|
data: data,
|
||||||
isSelectable: showFullPost,
|
isSelectable: showFullPost,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
).padding(horizontal: 16, bottom: 6),
|
).padding(bottom: 6),
|
||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(child: data.repostTo!).padding(
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
horizontal: 12,
|
|
||||||
bottom:
|
bottom:
|
||||||
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
data.preload?.attachments?.isNotEmpty ?? false
|
||||||
|
? 12
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
if (data.visibility > 0)
|
if (data.visibility > 0)
|
||||||
_PostVisibilityHint(data: data).padding(
|
_PostVisibilityHint(data: data).padding(
|
||||||
horizontal: 16,
|
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
if (data.body['content_truncated'] == true)
|
if (data.body['content_truncated'] == true)
|
||||||
_PostTruncatedHint(data: data).padding(
|
_PostTruncatedHint(data: data).padding(
|
||||||
horizontal: 16,
|
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
if (data.tags.isNotEmpty)
|
if (data.tags.isNotEmpty)
|
||||||
_PostTagsList(data: data)
|
_PostTagsList(data: data).padding(top: 4, bottom: 6),
|
||||||
.padding(horizontal: 16, 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!,
|
data: displayableAttachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
maxHeight: showFullPost ? null : 480,
|
maxHeight: showFullPost ? null : 480,
|
||||||
maxWidth: MediaQuery.of(context).size.width - 20,
|
minWidth: attachmentSize,
|
||||||
|
maxWidth: attachmentSize,
|
||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
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)
|
if (data.preload?.poll != null)
|
||||||
PostPoll(poll: data.preload!.poll!)
|
PostPoll(poll: data.preload!.poll!)
|
||||||
@ -420,22 +391,15 @@ class PostItem extends StatelessWidget {
|
|||||||
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(left: 60, right: 4),
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
.padding(horizontal: 12),
|
.padding(left: 60, right: 12),
|
||||||
Container(
|
Padding(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Column(
|
child: _PostReactionList(
|
||||||
children: [
|
|
||||||
_PostBottomAction(
|
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
padding: const EdgeInsets.only(left: 60, right: 12),
|
||||||
showReactions: showReactions,
|
|
||||||
onShare: () => _doShare(context),
|
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
|
||||||
onChanged: _onChanged,
|
onChanged: _onChanged,
|
||||||
).padding(left: 8, right: 14),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -476,12 +440,7 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
isAuthor: false,
|
|
||||||
data: data,
|
data: data,
|
||||||
onDeleted: () {},
|
|
||||||
onShare: () {},
|
|
||||||
onShareImage: () {},
|
|
||||||
showMenu: false,
|
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type == 'question')
|
if (data.type == 'question')
|
||||||
@ -516,14 +475,6 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
_PostTruncatedHint(data: data),
|
_PostTruncatedHint(data: data),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).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 Divider(height: 1),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -622,50 +573,90 @@ class _PostQuestionHint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostBottomAction extends StatelessWidget {
|
class _PostReactionList extends StatefulWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final bool showComments;
|
final EdgeInsets? padding;
|
||||||
final bool showReactions;
|
final Function(SnPost) onChanged;
|
||||||
final Function(SnPost data) onChanged;
|
const _PostReactionList({
|
||||||
final Function() onShare, onShareImage;
|
|
||||||
|
|
||||||
const _PostBottomAction({
|
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.showComments,
|
this.padding,
|
||||||
required this.showReactions,
|
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onShare,
|
|
||||||
required this.onShareImage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<_PostReactionList> createState() => _PostReactionListState();
|
||||||
final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha(
|
}
|
||||||
(255 * 0.8).round(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
class _PostReactionListState extends State<_PostReactionList> {
|
||||||
? data.metric.reactionList.entries
|
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)
|
.reduce((a, b) => a.value > b.value ? a : b)
|
||||||
.key
|
.key
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
if (showReactions || showComments)
|
ActionChip(
|
||||||
Row(
|
avatar: (kTemplateReactions[mostTypicalReaction] == null)
|
||||||
spacing: 8,
|
? Icon(Symbols.add_reaction, size: 20)
|
||||||
children: [
|
: Text(
|
||||||
if (showReactions)
|
|
||||||
InkWell(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (mostTypicalReaction == null ||
|
|
||||||
kTemplateReactions[mostTypicalReaction] == null)
|
|
||||||
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
kTemplateReactions[mostTypicalReaction]!.icon,
|
kTemplateReactions[mostTypicalReaction]!.icon,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -673,81 +664,73 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
label: (_totalUpvote > 0 && _totalUpvote >= _totalDownvote)
|
||||||
if (data.totalUpvote > 0 &&
|
? Text('postReactionUpvote').plural(_totalUpvote)
|
||||||
data.totalUpvote >= data.totalDownvote)
|
: (_totalDownvote > 0)
|
||||||
Text('postReactionUpvote').plural(
|
? Text('postReactionDownvote').plural(_totalDownvote)
|
||||||
data.totalUpvote,
|
: Text('postReact').tr(),
|
||||||
)
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
else if (data.totalDownvote > 0)
|
onPressed: () {
|
||||||
Text('postReactionDownvote').plural(
|
|
||||||
data.totalDownvote,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text('postReact').tr(),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8, vertical: 8),
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PostReactionPopup(
|
builder: (context) => PostReactionPopup(
|
||||||
data: data,
|
data: widget.data,
|
||||||
onChanged: (value, attr, delta) {
|
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
|
totalUpvote: attr == 1
|
||||||
? data.totalUpvote + delta
|
? widget.data.totalUpvote + delta
|
||||||
: data.totalUpvote,
|
: widget.data.totalUpvote,
|
||||||
totalDownvote: attr == 2
|
totalDownvote: attr == 2
|
||||||
? data.totalDownvote + delta
|
? widget.data.totalDownvote + delta
|
||||||
: data.totalDownvote,
|
: widget.data.totalDownvote,
|
||||||
metric: data.metric.copyWith(reactionList: value),
|
metric: metric,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (showComments)
|
if (_reactions.isNotEmpty) const Gap(8),
|
||||||
InkWell(
|
if (_reactions.isNotEmpty)
|
||||||
child: Row(
|
SizedBox(
|
||||||
children: [
|
width: 1,
|
||||||
Icon(Symbols.comment, size: 20, color: iconColor),
|
height: 20,
|
||||||
const Gap(8),
|
child: const VerticalDivider(width: 1),
|
||||||
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(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 SnPost data;
|
||||||
final bool isAuthor;
|
final bool isAuthor;
|
||||||
final bool isCompact;
|
|
||||||
final bool isRelativeDate;
|
|
||||||
final bool showMenu;
|
|
||||||
final Function onDeleted;
|
final Function onDeleted;
|
||||||
final Function() onShare, onShareImage;
|
final Function() onShare, onShareImage;
|
||||||
final Function()? onSelectAnswer;
|
final Function()? onSelectAnswer;
|
||||||
|
const _PostActionPopup({
|
||||||
const _PostContentHeader({
|
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.isAuthor,
|
required this.isAuthor,
|
||||||
this.isCompact = false,
|
|
||||||
this.isRelativeDate = true,
|
|
||||||
this.showMenu = true,
|
|
||||||
required this.onDeleted,
|
required this.onDeleted,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
required this.onShareImage,
|
required this.onShareImage,
|
||||||
@ -914,124 +956,14 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
return SizedBox(
|
||||||
final user = data.publisher.type == 0
|
height: 20,
|
||||||
? ud.getFromCache(data.publisher.accountId)
|
child: PopupMenuButton(
|
||||||
: null;
|
icon: const Icon(Symbols.more_horiz, size: 20),
|
||||||
|
|
||||||
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),
|
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
if (isAuthor && onSelectAnswer != null)
|
if (isAuthor && onSelectAnswer != null)
|
||||||
PopupMenuItem(
|
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(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
isAuthor: false,
|
|
||||||
data: child,
|
data: child,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
isRelativeDate: isRelativeDate,
|
isRelativeDate: isRelativeDate,
|
||||||
showMenu: false,
|
|
||||||
onShare: () {},
|
|
||||||
onShareImage: () {},
|
|
||||||
onDeleted: () {},
|
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
_PostContentBody(data: child),
|
_PostContentBody(data: child),
|
||||||
if (child.visibility > 0)
|
if (child.visibility > 0)
|
||||||
@ -1486,12 +1476,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
|
final ua = context.read<UserProvider>();
|
||||||
if (_featuredComment == null) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
return Container(
|
||||||
|
|
||||||
return AnimateWidgetExtensions(Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||||
margin: const EdgeInsets.only(top: 8),
|
margin: const EdgeInsets.only(top: 8),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -1499,7 +1486,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
color: _isAnswer
|
color: _isAnswer
|
||||||
? Colors.green.withOpacity(0.5)
|
? Colors.green.withOpacity(0.5)
|
||||||
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -1519,29 +1506,33 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
|
Transform.flip(
|
||||||
size: 20),
|
flipX: !_isAnswer,
|
||||||
|
child: Icon(
|
||||||
|
_isAnswer ? Symbols.task_alt : Symbols.reply,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text(
|
Text(
|
||||||
_isAnswer
|
_isAnswer
|
||||||
? 'postQuestionAnswerTitle'
|
? 'postQuestionAnswerTitle'.tr()
|
||||||
: 'postFeaturedComment',
|
: 'postComments'.plural(widget.data.metric.replyCount),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleMedium!
|
.titleMedium!
|
||||||
.copyWith(fontSize: 15),
|
.copyWith(fontSize: 15),
|
||||||
).tr(),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
|
if (_featuredComment != null)
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
AccountImage(
|
||||||
|
content: _featuredComment!.publisher.avatar,
|
||||||
radius: 12,
|
radius: 12,
|
||||||
backgroundImage: UniversalImage.provider(
|
|
||||||
sn.getAttachmentUrl(_featuredComment!.publisher.avatar),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
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),
|
).padding(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user