♻️ Refactored post layout

This commit is contained in:
LittleSheep 2024-10-12 00:41:03 +08:00
parent 81a616157e
commit 7b7988e6cb
27 changed files with 504 additions and 407 deletions

View File

@ -192,7 +192,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
const Gap(24), const Gap(24),
Stack( Stack(
children: [ children: [
AccountAvatar(content: _avatar, radius: 40), AttachedCircleAvatar(content: _avatar, radius: 40),
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 40, left: 40,

View File

@ -260,7 +260,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
const Gap(8), const Gap(8),
const Gap(8), const Gap(8),
if (_userinfo != null) if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16), AttachedCircleAvatar(
content: _userinfo!.avatar, radius: 16),
const Gap(12), const Gap(12),
Expanded( Expanded(
child: Column( child: Column(

View File

@ -252,7 +252,7 @@ class _ChatListState extends State<ChatList> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: x.avatar, content: x.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -392,7 +392,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.colorScheme .colorScheme
.surfaceContainerLow, .surfaceContainerLow,
), ).paddingAll(8),
), ),
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),

View File

@ -6,13 +6,13 @@ import 'package:get/get.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/navigation/realm_switcher.dart'; import 'package:solian/widgets/navigation/realm_switcher.dart';
import 'package:solian/widgets/posts/post_creation.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
@ -225,106 +225,3 @@ class _ExploreScreenState extends State<ExploreScreen>
super.dispose(); super.dispose();
} }
} }
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

View File

@ -24,11 +24,11 @@ class PostDetailScreen extends StatefulWidget {
class _PostDetailScreenState extends State<PostDetailScreen> { class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; Post? item;
Future<Post?> getDetail() async { Future<Post?> _getDetail() async {
if (widget.post != null) { if (widget.post != null) {
setState(() {
item = widget.post; item = widget.post;
Get.find<LastReadProvider>().feedLastReadAt = item?.id; });
return widget.post;
} }
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
@ -48,7 +48,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return FutureBuilder(
future: getDetail(), future: _getDetail(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) { if (!snapshot.hasData || snapshot.data == null) {
return const Center( return const Center(

View File

@ -156,7 +156,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
size: 18, size: 18,
), ),
) )
: AccountAvatar( : AttachedCircleAvatar(
content: element.avatar!, content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
), ),

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
class AccountAvatar extends StatelessWidget { class AttachedCircleAvatar extends StatelessWidget {
final dynamic content; final dynamic content;
final Color? bgColor; final Color? bgColor;
final Color? feColor; final Color? feColor;
final double? radius; final double? radius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
const AccountAvatar({ const AttachedCircleAvatar({
super.key, super.key,
required this.content, required this.content,
this.bgColor, this.bgColor,
@ -39,7 +40,7 @@ class AccountAvatar extends StatelessWidget {
child: isEmpty child: isEmpty
? (fallbackWidget ?? ? (fallbackWidget ??
Icon( Icon(
Icons.account_circle, Icons.image,
size: radius != null ? radius! * 1.2 : 24, size: radius != null ? radius! * 1.2 : 24,
color: feColor, color: feColor,
)) ))
@ -48,6 +49,54 @@ class AccountAvatar extends StatelessWidget {
} }
} }
class AccountAvatar extends StatelessWidget {
final dynamic content;
final String username;
final Color? bgColor;
final Color? feColor;
final double? radius;
final Widget? fallbackWidget;
const AccountAvatar({
super.key,
required this.content,
required this.username,
this.bgColor,
this.feColor,
this.radius,
this.fallbackWidget,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: AttachedCircleAvatar(
content: content,
bgColor: bgColor,
feColor: feColor,
radius: radius,
fallbackWidget: (fallbackWidget ??
Icon(
Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: feColor,
)),
),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: username,
),
);
},
);
}
}
class AccountProfileImage extends StatelessWidget { class AccountProfileImage extends StatelessWidget {
final dynamic content; final dynamic content;
final BoxFit fit; final BoxFit fit;

View File

@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
Positioned( Positioned(
bottom: -30, bottom: -30,
left: 32, left: 32,
child: AccountAvatar(content: avatar, radius: 40), child: AttachedCircleAvatar(content: avatar, radius: 40),
), ),
], ],
), ),

View File

@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AccountAvatar(content: element.avatar), leading: AttachedCircleAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: _checkSelected(element) : _checkSelected(element)

View File

@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
title: Text(element.related.nick), title: Text(element.related.nick),
subtitle: Text(element.related.name), subtitle: Text(element.related.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.related.avatar), child: AttachedCircleAvatar(content: element.related.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AccountAvatar(content: element.avatar), leading: AttachedCircleAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: null, : null,

View File

@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountAvatar( child: AttachedCircleAvatar(
content: widget.item.account!.avatar, content: widget.item.account!.avatar,
radius: 19, radius: 19,
), ),

View File

@ -19,9 +19,8 @@ class AttachmentList extends StatefulWidget {
final List<Attachment>? attachments; final List<Attachment>? attachments;
final bool isGrid; final bool isGrid;
final bool isColumn; final bool isColumn;
final bool isForceGrid; final bool isFullWidth;
final bool autoload; final bool autoload;
final double flatMaxHeight;
final double columnMaxWidth; final double columnMaxWidth;
final double? width; final double? width;
@ -34,9 +33,8 @@ class AttachmentList extends StatefulWidget {
this.attachments, this.attachments,
this.isGrid = false, this.isGrid = false,
this.isColumn = false, this.isColumn = false,
this.isForceGrid = false, this.isFullWidth = false,
this.autoload = false, this.autoload = false,
this.flatMaxHeight = 720,
this.columnMaxWidth = 480, this.columnMaxWidth = 480,
this.width, this.width,
this.viewport, this.viewport,
@ -175,9 +173,70 @@ class _AttachmentListState extends State<AttachmentList> {
.fadeIn(duration: 1250.ms); .fadeIn(duration: 1250.ms);
} }
const radius = BorderRadius.all(Radius.circular(8));
if (widget.isFullWidth && _attachments.length == 1) {
final element = _attachments.first;
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
border: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: _buildEntry(element, 0),
),
),
);
}
final isNotPureImage = _attachments.any(
(x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && !isNotPureImage) {
return GridView.builder(
padding: EdgeInsets.zero,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, _attachments.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: _attachments.length,
itemBuilder: (context, idx) {
final element = _attachments[idx];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
);
},
);
}
if (widget.isColumn) { if (widget.isColumn) {
var idx = 0; var idx = 0;
const radius = BorderRadius.all(Radius.circular(8));
return Wrap( return Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
@ -212,27 +271,31 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachments.any( return SizedBox(
(x) => x?.mimetype.split('/').firstOrNull != 'image', width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
); child: CarouselSlider.builder(
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { options: CarouselOptions(
const radius = BorderRadius.all(Radius.circular(8)); disableCenter: true,
return GridView.builder( animateToClosest: true,
padding: EdgeInsets.zero, aspectRatio: _aspectRatio,
primary: false, enlargeCenterPage: true,
physics: const NeverScrollableScrollPhysics(), viewportFraction: widget.viewport ?? 0.95,
shrinkWrap: true, enableInfiniteScroll: false,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, _attachments.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
), ),
itemCount: _attachments.length, itemCount: _attachments.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx, _) {
final element = _attachments[idx]; final element = _attachments[idx];
if (element == null) const SizedBox.shrink();
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container( return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -243,38 +306,10 @@ class _AttachmentListState extends State<AttachmentList> {
borderRadius: radius, borderRadius: radius,
child: _buildEntry(element, idx), child: _buildEntry(element, idx),
), ),
),
),
); );
}, },
).paddingSymmetric(horizontal: 24);
}
return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxHeight: widget.flatMaxHeight,
),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.symmetric(
horizontal: BorderSide(
width: 0.3,
color: Theme.of(context).dividerColor,
),
),
),
child: CarouselSlider.builder(
options: CarouselOptions(
animateToClosest: true,
aspectRatio: _aspectRatio,
viewportFraction:
widget.viewport ?? (_attachments.length > 1 ? 0.95 : 1),
enableInfiniteScroll: false,
),
itemCount: _attachments.length,
itemBuilder: (context, idx, _) {
final element = _attachments[idx];
return _buildEntry(element, idx);
},
), ),
); );
} }

View File

@ -205,7 +205,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull; item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
if (item.type == 1 && otherside != null) { if (item.type == 1 && otherside != null) {
final avatar = AccountAvatar( final avatar = AttachedCircleAvatar(
content: otherside.account.avatar, content: otherside.account.avatar,
radius: 20, radius: 20,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
@ -241,7 +241,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
elevation: 8, elevation: 8,
), ),
badgeContent: AccountAvatar( badgeContent: AttachedCircleAvatar(
content: item.realm?.avatar, content: item.realm?.avatar,
radius: 10, radius: 10,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.account.avatar), child:
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
), ),
) )
], ],
child: AccountAvatar( child: AttachedCircleAvatar(
content: widget.userinfo!.avatar, content: widget.userinfo!.avatar,
bgColor: Colors.transparent, bgColor: Colors.transparent,
radius: radius, radius: radius,

View File

@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: item.sender.account.avatar, content: item.sender.account.avatar,
radius: 9, radius: 9,
), ),
@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
child: AccountAvatar(content: item.sender.account.avatar), child:
AttachedCircleAvatar(content: item.sender.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'users', type: 'users',
leading: AccountAvatar(content: x.avatar), leading: AttachedCircleAvatar(content: x.avatar),
display: x.nick, display: x.nick,
content: '@${x.name}', content: '@${x.name}',
), ),

View File

@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State<AppAccountWidget> {
bottom: 0, bottom: 0,
end: -2, end: -2,
), ),
child: AccountAvatar( child: AttachedCircleAvatar(
radius: 14, radius: 14,
content: auth.userProfile.value!['avatar'], content: auth.userProfile.value!['avatar'],
), ),

View File

@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (item != null) if (item != null)
AccountAvatar( AttachedCircleAvatar(
content: item.avatar, content: item.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

View File

@ -12,7 +12,6 @@ import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/link_expansion.dart'; import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
@ -36,6 +35,7 @@ class PostItem extends StatefulWidget {
final bool showFeaturedReply; final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
final Function? onComment;
const PostItem({ const PostItem({
super.key, super.key,
@ -52,6 +52,7 @@ class PostItem extends StatefulWidget {
this.showFeaturedReply = false, this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
this.onComment,
}); });
@override @override
@ -92,8 +93,6 @@ class _PostItemState extends State<PostItem> {
item: item, item: item,
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12), _PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
Stack(
children: [
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight: widget.isFullContent ? double.infinity : 80, maxHeight: widget.isFullContent ? double.infinity : 80,
@ -110,15 +109,12 @@ class _PostItemState extends State<PostItem> {
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
), ),
).paddingOnly( ).paddingOnly(
left: 16, left: 12,
right: 12, right: 12,
top: 2,
bottom: hasAttachment ? 4 : 0, bottom: hasAttachment ? 4 : 0,
), ),
), ),
), ),
],
),
if (_contentHeight >= 80 && !widget.isFullContent) if (_contentHeight >= 80 && !widget.isFullContent)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
@ -132,7 +128,7 @@ class _PostItemState extends State<PostItem> {
right: 8, right: 8,
top: 4, top: 4,
), ),
_PostFooterWidget(item: item).paddingOnly(left: 16), _PostFooterWidget(item: item).paddingOnly(left: 12),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
children: [ children: [
@ -148,7 +144,7 @@ class _PostItemState extends State<PostItem> {
style: TextStyle(color: _unFocusColor), style: TextStyle(color: _unFocusColor),
) )
], ],
).paddingOnly(left: 16, top: 4), ).paddingOnly(left: 14, top: 4),
], ],
); );
} }
@ -162,25 +158,7 @@ class _PostItemState extends State<PostItem> {
rid: item.body['thumbnail'], rid: item.body['thumbnail'],
parentId: widget.item.id.toString(), parentId: widget.item.id.toString(),
).paddingOnly(bottom: 4), ).paddingOnly(bottom: 4),
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: AccountAvatar(content: item.author.avatar),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: item.author.name,
),
);
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostHeaderWidget( _PostHeaderWidget(
@ -188,12 +166,9 @@ class _PostItemState extends State<PostItem> {
item: item, item: item,
), ),
_PostHeaderDividerWidget(item: item), _PostHeaderDividerWidget(item: item),
Stack(
children: [
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight: maxHeight: widget.isFullContent ? double.infinity : 320,
widget.isFullContent ? double.infinity : 320,
child: _MeasureSize( child: _MeasureSize(
onChange: (size) { onChange: (size) {
setState(() => _contentHeight = size.height); setState(() => _contentHeight = size.height);
@ -205,30 +180,24 @@ class _PostItemState extends State<PostItem> {
content: item.body['content'], content: item.body['content'],
isAutoWarp: item.type == 'story', isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
isLargeText: item.type == 'article' && isLargeText:
widget.isFullContent, item.type == 'article' && widget.isFullContent,
).paddingOnly(left: 12, right: 8),
), ),
), ),
), ),
],
), ),
if (_contentHeight >= 320 && !widget.isFullContent) if (_contentHeight >= 320 && !widget.isFullContent)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)), child: InkWell(child: Text('readMore'.tr)),
).paddingOnly( ).paddingOnly(top: 4),
left: 12,
top: 4,
),
if (widget.item.replyTo != null && widget.isShowEmbed) if (widget.item.replyTo != null && widget.isShowEmbed)
Container( Container(
constraints: const BoxConstraints(maxWidth: 480), constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget( child: _PostEmbedWidget(
isClickable: widget.isClickable, isClickable: widget.isClickable,
isOverrideEmbedClickable: isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
widget.isOverrideEmbedClickable,
item: widget.item.replyTo!, item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name, username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify', hintText: 'postRepliedNotify',
@ -239,11 +208,10 @@ class _PostItemState extends State<PostItem> {
if (widget.item.repostTo != null && widget.isShowEmbed) if (widget.item.repostTo != null && widget.isShowEmbed)
Container( Container(
constraints: const BoxConstraints(maxWidth: 480), constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget( child: _PostEmbedWidget(
isClickable: widget.isClickable, isClickable: widget.isClickable,
isOverrideEmbedClickable: isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
widget.isOverrideEmbedClickable,
item: widget.item.repostTo!, item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name, username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify', hintText: 'postRepostedNotify',
@ -251,24 +219,19 @@ class _PostItemState extends State<PostItem> {
id: widget.item.repostTo!.id.toString(), id: widget.item.repostTo!.id.toString(),
), ),
), ),
_PostFooterWidget(item: item).paddingOnly(left: 12), _PostFooterWidget(item: item),
LinkExpansion(content: item.body['content']) LinkExpansion(content: item.body['content']).paddingOnly(top: 4),
.paddingOnly(top: 4),
],
),
),
], ],
).paddingOnly( ).paddingOnly(
top: 10,
bottom:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 0,
right: 16, right: 16,
left: 16, left: 16,
), ),
_PostAttachmentWidget(item: item), _PostAttachmentWidget(item: item),
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item), if (widget.showFeaturedReply)
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
horizontal: 12,
),
if (widget.showFeaturedReply) const Gap(8),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -280,19 +243,15 @@ class _PostItemState extends State<PostItem> {
(item.metric!.reactionList[symbol] ?? 0) + changes; (item.metric!.reactionList[symbol] ?? 0) + changes;
}); });
}, },
onComment: () {
if (widget.onComment != null) {
widget.onComment!();
}
},
).paddingOnly( ).paddingOnly(
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context)) left: 14,
? 10 right: 14,
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
bottom: 10,
) )
else
const Gap(10),
], ],
), ),
openBuilder: (_, __) => TitleShell( openBuilder: (_, __) => TitleShell(
@ -317,7 +276,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLargeScreen = AppTheme.isLargeScreen(context);
final unFocusColor = final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@ -325,13 +283,10 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
return FutureBuilder( return FutureBuilder(
future: future: Get.find<PostProvider>().listPostFeaturedReply(
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()), item.id.toString(),
),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) { if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -351,7 +306,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: reply.author.avatar, content: reply.author.avatar,
radius: 10, radius: 10,
), ),
@ -423,16 +378,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
.toList(), .toList(),
), ),
), ),
) ).animate().fadeIn(
.animate()
.fadeIn(
duration: 300.ms, duration: 300.ms,
curve: Curves.easeIn, curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
right: 16,
); );
}, },
); );
@ -452,30 +400,33 @@ class _PostAttachmentWidget extends StatelessWidget {
? List.from(item.body['attachments']?.whereType<String>()) ? List.from(item.body['attachments']?.whereType<String>())
: List.empty(); : List.empty();
if (attachments.length > 3) { if (attachments.isEmpty) return const SizedBox.shrink();
if (attachments.length == 1) {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false,
isFullWidth: true,
).paddingOnly(top: 4);
} else if (attachments.length > 1 &&
attachments.length % 3 == 0 &&
!isLargeScreen) {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments, attachments: item.preload?.attachments,
autoload: false, autoload: false,
isGrid: true, isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4); ).paddingSymmetric(horizontal: 14, vertical: 8);
} else if (attachments.length > 1 || isLargeScreen) {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false,
isColumn: true,
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
} else { } else {
return AttachmentList( return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments, attachments: item.preload?.attachments,
autoload: false, autoload: false,
); ).paddingOnly(bottom: 8, top: 4);
} }
} }
} }
@ -515,16 +466,17 @@ class _PostEmbedWidget extends StatelessWidget {
size: 16, size: 16,
color: unFocusColor, color: unFocusColor,
), ),
const Gap(6),
Expanded( Expanded(
child: Text( child: Text(
hintText.trParams( hintText.trParams(
{'username': '@$username'}, {'username': '@$username'},
), ),
style: TextStyle(color: unFocusColor), style: TextStyle(color: unFocusColor),
).paddingOnly(left: 6), ),
), ),
], ],
).paddingOnly(left: 12), ).paddingOnly(left: 2),
Card( Card(
elevation: 1, elevation: 1,
child: PostItem( child: PostItem(
@ -560,9 +512,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (item.body['description'] != null || item.body['title'] != null) { if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric( return const Gap(8);
vertical: 8,
);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -634,49 +584,59 @@ class _PostHeaderWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isCompact)
AccountAvatar( AccountAvatar(
content: item.author.avatar, content: item.author.avatar,
radius: 10, username: item.author.name,
).paddingOnly(left: 2, top: 1), radius: isCompact ? 10 : null,
),
Gap(isCompact ? 6 : 8),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
item.author.nick, item.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()) if (isCompact) const Gap(4),
.paddingOnly(left: 4), if (isCompact)
RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(),
).paddingOnly(top: 1),
], ],
), ),
if (item.body['title'] != null) if (!isCompact)
Text( RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()),
item.body['title'],
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 15),
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.bodySmall,
),
], ],
).paddingOnly(left: isCompact ? 6 : 12), ),
), ),
if (item.type == 'article') if (item.type == 'article')
Badge( Badge(
label: Text('article'.tr), label: Text('article'.tr),
).paddingOnly(top: 3), ).paddingOnly(top: 3),
], ],
),
const Gap(8),
if (item.body['title'] != null)
Text(
item.body['title'],
style: Theme.of(context).textTheme.titleMedium,
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.titleSmall,
),
],
); );
} }
} }

View File

@ -3,6 +3,8 @@ import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
@ -12,6 +14,7 @@ class PostListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Color? backgroundColor; final Color? backgroundColor;
final EdgeInsets? padding;
const PostListWidget({ const PostListWidget({
super.key, super.key,
@ -20,6 +23,7 @@ class PostListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.backgroundColor, this.backgroundColor,
this.padding,
}); });
@override @override
@ -29,7 +33,9 @@ class PostListWidget extends StatelessWidget {
pagingController: controller, pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>( builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return PostListEntryWidget( return Padding(
padding: padding ?? EdgeInsets.zero,
child: PostListEntryWidget(
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
@ -39,6 +45,7 @@ class PostListWidget extends StatelessWidget {
onUpdate: () { onUpdate: () {
controller.refresh(); controller.refresh();
}, },
),
); );
}, },
), ),
@ -77,6 +84,22 @@ class PostListEntryWidget extends StatelessWidget {
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply, showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
onComment: () {
AppRouter.instance
.pushNamed(
'postEditor',
extra: PostPublishArguments(reply: item),
)
.then((value) {
if (value is Future) {
value.then((_) {
onUpdate();
});
} else if (value != null) {
onUpdate();
}
});
},
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onLongPress: () { onLongPress: () {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget {
final Post item; final Post item;
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final Function onComment;
final void Function(String symbol, int num) onReact; final void Function(String symbol, int num) onReact;
const PostQuickAction({ const PostQuickAction({
@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget {
required this.item, required this.item,
this.isShowReply = true, this.isShowReply = true,
this.isReactable = true, this.isReactable = true,
required this.onComment,
required this.onReact, required this.onReact,
}); });
@ -106,7 +108,11 @@ class _PostQuickActionState extends State<PostQuickAction> {
builder: (context) { builder: (context) {
return PostReplyListPopup(item: widget.item); return PostReplyListPopup(item: widget.item);
}, },
); ).then((signal) {
if (signal == true) {
widget.onComment();
}
});
}, },
), ),
), ),

View File

@ -53,6 +53,7 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: EdgeInsets.symmetric(horizontal: 10),
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -70,16 +71,30 @@ class PostReplyListPopup extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
children: [
Expanded(
child: Text(
'postReplies'.tr, 'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ),
),
IconButton(
icon: const Icon(Icons.add_comment),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: () {
Navigator.pop(context, true);
},
),
],
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostReplyList( PostReplyList(
item: item, item: item,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
), ),
], ],
), ),

View File

@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.account.avatar), child:
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,