Edited the post item styles

This commit is contained in:
2025-07-30 22:16:36 +08:00
parent e4bb031138
commit 7c92dee097
11 changed files with 271 additions and 581 deletions

View File

@@ -399,19 +399,9 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) { switch (item.type) {
case 'posts.new': case 'posts.new':
case 'posts.new.replies': case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies'; itemWidget = PostActionableItem(
itemWidget = PostItem( borderRadius: 8,
backgroundColor:
isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data!), item: SnPost.fromJson(item.data!),
padding:
isReply
? const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
)
: null,
onRefresh: () { onRefresh: () {
activitiesNotifier.forceRefresh(); activitiesNotifier.forceRefresh();
}, },
@@ -422,21 +412,10 @@ class _ActivityListView extends HookConsumerWidget {
); );
}, },
); );
if (isReply) { itemWidget = Card(
itemWidget = Column( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
crossAxisAlignment: CrossAxisAlignment.stretch, child: itemWidget,
children: [ );
Row(
children: [
const Icon(Symbols.reply),
const Gap(8),
Text('Replying your post'),
],
).padding(horizontal: 20, vertical: 8),
itemWidget,
],
);
}
break; break;
case 'discovery': case 'discovery':
itemWidget = _DiscoveryActivityItem(data: item.data!); itemWidget = _DiscoveryActivityItem(data: item.data!);
@@ -445,7 +424,7 @@ class _ActivityListView extends HookConsumerWidget {
itemWidget = const Placeholder(); itemWidget = const Placeholder();
} }
return Column(children: [itemWidget, const Divider(height: 1)]); return itemWidget;
}, },
), ),
SliverGap(getTabbedPadding(context).bottom), SliverGap(getTabbedPadding(context).bottom),

View File

@@ -470,7 +470,9 @@ class PostComposeScreen extends HookConsumerWidget {
color: colorScheme.primary, color: colorScheme.primary,
), ),
IconButton( IconButton(
onPressed: () => ComposeLogic.addAttachmentById(ref, state, context), onPressed:
() =>
ComposeLogic.addAttachmentById(ref, state, context),
icon: const Icon(Symbols.attach_file), icon: const Icon(Symbols.attach_file),
color: colorScheme.primary, color: colorScheme.primary,
), ),
@@ -655,7 +657,7 @@ class PostComposeScreen extends HookConsumerWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: PostItem(item: post, isOpenable: false), child: PostItem(item: post),
), ),
), ),
], ],

View File

@@ -71,7 +71,6 @@ class PostDetailScreen extends HookConsumerWidget {
children: [ children: [
PostItem( PostItem(
item: post!, item: post!,
isOpenable: false,
isFullPost: true, isFullPost: true,
backgroundColor: isWide ? Colors.transparent : null, backgroundColor: isWide ? Colors.transparent : null,
onUpdate: (newItem) { onUpdate: (newItem) {

View File

@@ -86,6 +86,7 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
builder: builder:
(context) => AccountStatusCreationSheet( (context) => AccountStatusCreationSheet(
initialStatus: initialStatus:

View File

@@ -49,7 +49,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
await apiClient.request( await apiClient.request(
'/accounts/me/statuses', '/id/accounts/me/statuses',
data: { data: {
'attitude': attitude.value, 'attitude': attitude.value,
'is_invisible': isInvisible.value, 'is_invisible': isInvisible.value,

View File

@@ -228,7 +228,8 @@ class MessageItem extends HookConsumerWidget {
return CloudFileList( return CloudFileList(
files: remoteMessage.attachments, files: remoteMessage.attachments,
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
).padding(vertical: 4); padding: EdgeInsets.symmetric(vertical: 4),
);
}, },
), ),
if (remoteMessage.meta['embeds'] != null) if (remoteMessage.meta['embeds'] != null)

View File

@@ -27,6 +27,7 @@ class CloudFileList extends HookConsumerWidget {
final double? minWidth; final double? minWidth;
final bool disableZoomIn; final bool disableZoomIn;
final bool disableConstraint; final bool disableConstraint;
final EdgeInsets? padding;
const CloudFileList({ const CloudFileList({
super.key, super.key,
required this.files, required this.files,
@@ -35,6 +36,7 @@ class CloudFileList extends HookConsumerWidget {
this.minWidth, this.minWidth,
this.disableZoomIn = false, this.disableZoomIn = false,
this.disableConstraint = false, this.disableConstraint = false,
this.padding,
}); });
double calculateAspectRatio() { double calculateAspectRatio() {
@@ -60,7 +62,8 @@ class CloudFileList extends HookConsumerWidget {
if (files.isEmpty) return const SizedBox.shrink(); if (files.isEmpty) return const SizedBox.shrink();
if (files.length == 1) { if (files.length == 1) {
final isImage = files.first.mimeType?.startsWith('image') ?? false; final isImage = files.first.mimeType?.startsWith('image') ?? false;
return ConstrainedBox( return Container(
padding: padding,
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: disableConstraint ? double.infinity : maxHeight, maxHeight: disableConstraint ? double.infinity : maxHeight,
minWidth: minWidth ?? 0, minWidth: minWidth ?? 0,
@@ -75,7 +78,7 @@ class CloudFileList extends HookConsumerWidget {
child: AspectRatio( child: AspectRatio(
aspectRatio: calculateAspectRatio(), aspectRatio: calculateAspectRatio(),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _CloudFileListEntry( child: _CloudFileListEntry(
file: files.first, file: files.first,
heroTag: heroTags.first, heroTag: heroTags.first,
@@ -95,7 +98,7 @@ class CloudFileList extends HookConsumerWidget {
), ),
), ),
), ),
).padding(horizontal: 3); );
} }
return ConstrainedBox( return ConstrainedBox(
@@ -105,7 +108,7 @@ class CloudFileList extends HookConsumerWidget {
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: files.length, itemCount: files.length,
padding: EdgeInsets.symmetric(horizontal: 3), padding: padding,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return AspectRatio( return AspectRatio(
aspectRatio: aspectRatio:
@@ -133,6 +136,7 @@ class CloudFileList extends HookConsumerWidget {
item: files[index], item: files[index],
heroTag: heroTags[index], heroTag: heroTags[index],
), ),
rootNavigator: true,
); );
} }
}, },
@@ -184,7 +188,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}'; final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
await client.download( await client.download(
'/files/${item.id}', '/drive/files/${item.id}',
filePath, filePath,
queryParameters: {'original': true}, queryParameters: {'original': true},
); );
@@ -334,7 +338,6 @@ class CloudFileZoomIn extends HookConsumerWidget {
imageProvider: CloudImageWidget.provider( imageProvider: CloudImageWidget.provider(
fileId: item.id, fileId: item.id,
serverUrl: serverUrl, serverUrl: serverUrl,
original: true,
), ),
// Apply rotation transformation // Apply rotation transformation
customSize: MediaQuery.of(context).size, customSize: MediaQuery.of(context).size,
@@ -475,7 +478,6 @@ class _CloudFileListEntry extends StatelessWidget {
final bool isImage; final bool isImage;
final bool disableZoomIn; final bool disableZoomIn;
final VoidCallback? onTap; final VoidCallback? onTap;
final BoxFit fit;
const _CloudFileListEntry({ const _CloudFileListEntry({
required this.file, required this.file,
@@ -483,7 +485,6 @@ class _CloudFileListEntry extends StatelessWidget {
required this.isImage, required this.isImage,
required this.disableZoomIn, required this.disableZoomIn,
this.onTap, this.onTap,
this.fit = BoxFit.contain,
}); });
@override @override
@@ -506,10 +507,10 @@ class _CloudFileListEntry extends StatelessWidget {
item: file, item: file,
heroTag: heroTag, heroTag: heroTag,
noBlurhash: true, noBlurhash: true,
fit: fit, fit: BoxFit.contain,
) )
else else
CloudFileWidget(item: file, heroTag: heroTag, fit: fit), CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain),
], ],
); );

View File

@@ -92,7 +92,10 @@ class CloudImageWidget extends ConsumerWidget {
required String serverUrl, required String serverUrl,
bool original = false, bool original = false,
}) { }) {
final uri = '$serverUrl/drive/files/$fileId?original=$original'; final uri =
original
? '$serverUrl/drive/files/$fileId?original=true'
: '$serverUrl/drive/files/$fileId';
return CachedNetworkImageProvider(uri); return CachedNetworkImageProvider(uri);
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -5,36 +6,79 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'dart:math' as math;
import 'package:island/models/embed.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/post/post_replies_sheet.dart';
import 'package:island/widgets/share/share_sheet.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart';
class PostItem extends HookConsumerWidget { class PostActionableItem extends HookConsumerWidget {
final Color? backgroundColor; final Color? backgroundColor;
final SnPost item; final SnPost item;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool isOpenable;
final bool isFullPost; final bool isFullPost;
final bool showReferencePost; final bool isShowReference;
final double? borderRadius;
final Function? onRefresh;
final Function(SnPost)? onUpdate;
const PostActionableItem({
super.key,
required this.item,
this.backgroundColor,
this.padding,
this.isFullPost = false,
this.isShowReference = true,
this.borderRadius,
this.onRefresh,
this.onUpdate,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
final isAuthor = useMemoized(
() => user.value != null && user.value?.id == item.publisher.accountId,
[user],
);
final widgetItem = InkWell(
borderRadius:
borderRadius != null
? BorderRadius.all(Radius.circular(borderRadius!))
: null,
child: PostItem(
key: key,
item: item,
backgroundColor: backgroundColor,
padding: padding,
isFullPost: isFullPost,
isShowReference: isShowReference,
isTextSelectable: false,
onRefresh: onRefresh,
onUpdate: onUpdate,
),
onTap: () {
context.pushNamed('postDetail', pathParameters: {'id': item.id});
},
);
return widgetItem;
}
}
class PostItem extends HookConsumerWidget {
final SnPost item;
final Color? backgroundColor;
final EdgeInsets? padding;
final bool isFullPost;
final bool isShowReference;
final bool isTextSelectable;
final Function? onRefresh; final Function? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
const PostItem({ const PostItem({
@@ -42,9 +86,9 @@ class PostItem extends HookConsumerWidget {
required this.item, required this.item,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.isOpenable = true,
this.isFullPost = false, this.isFullPost = false,
this.showReferencePost = true, this.isShowReference = true,
this.isTextSelectable = true,
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
}); });
@@ -52,559 +96,213 @@ class PostItem extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final renderingPadding = final renderingPadding =
padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 16); padding ?? EdgeInsets.symmetric(horizontal: 8, vertical: 8);
final user = ref.watch(userInfoProvider); final reacting = useState(false);
final isAuthor = useMemoized(
() => user.value != null && user.value?.id == item.publisher.accountId,
[user],
);
final hasBackground = Future<void> reactPost(String symbol, int attitude) async {
ref.watch(backgroundImageFileProvider).valueOrNull != null; final client = ref.watch(apiClientProvider);
reacting.value = true;
await client
.post(
'/sphere/posts/${item.id}/reactions',
data: {'symbol': symbol, 'attitude': attitude},
)
.catchError((err) {
showErrorAlert(err);
return err;
})
.then((resp) {
final isRemoving = resp.statusCode == 204;
final delta = isRemoving ? -1 : 1;
final reactionsCount = Map<String, int>.from(item.reactionsCount);
reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(item.copyWith(reactionsCount: reactionsCount));
HapticFeedback.heavyImpact();
});
reacting.value = false;
}
Widget child; final mostReaction =
if (item.type == 1 && isFullPost) { item.reactionsCount.isEmpty
child = Padding( ? null
padding: renderingPadding, : item.reactionsCount.entries
child: Column( .sortedBy((e) => e.value)
crossAxisAlignment: CrossAxisAlignment.start, .map((e) => e.key)
.first;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gap(renderingPadding.horizontal),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 12,
children: [ children: [
GestureDetector( GestureDetector(
child: ProfilePictureWidget(
file: item.publisher.picture,
radius: 16,
),
onTap: () { onTap: () {
context.pushNamed( context.pushNamed(
'publisherProfile', 'publisherProfile',
pathParameters: {'name': item.publisher.name}, pathParameters: {'name': item.publisher.name},
); );
}, },
child: Row( ),
crossAxisAlignment: CrossAxisAlignment.center, Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ProfilePictureWidget(file: item.publisher.picture), Row(
const Gap(12), spacing: 4,
Expanded( children: [
child: Column( Text(item.publisher.nick).bold(),
crossAxisAlignment: CrossAxisAlignment.start, if (item.publisher.verification != null)
children: [ VerificationMark(mark: item.publisher.verification!),
Text(item.publisher.nick).bold(), Text('@${item.publisher.name}').fontSize(11),
if (item.publisher.verification != null) ],
VerificationMark(
mark: item.publisher.verification!,
).padding(left: 4),
],
),
), ),
Text( Text(
isFullPost isFullPost
? item.publishedAt?.formatSystem() ?? '' ? (item.publishedAt ?? item.createdAt)!.formatSystem()
: item.publishedAt?.formatRelative(context) ?? '', : (item.publishedAt ?? item.createdAt)!.formatRelative(
).fontSize(11), context,
),
).fontSize(10),
], ],
), ),
), ),
if (item.visibility != 0) IconButton(
Row( icon:
mainAxisSize: MainAxisSize.min, mostReaction == null
children: [ ? const Icon(Symbols.add_reaction)
Icon( : Badge(
_getVisibilityIcon(item.visibility), label: Text(
size: 14, 'x${item.reactionsCount[mostReaction]}',
color: Theme.of(context).colorScheme.secondary, style: TextStyle(fontSize: 11),
), ),
const SizedBox(width: 4), offset: Offset(4, 20),
Text( backgroundColor: Theme.of(
_getVisibilityText(item.visibility).tr(), context,
style: TextStyle( ).colorScheme.primary.withOpacity(0.75),
fontSize: 12, textColor: Theme.of(context).colorScheme.onPrimary,
color: Theme.of(context).colorScheme.secondary, child: Text(
), kReactionTemplates[mostReaction]!.icon,
), style: TextStyle(fontSize: 20),
], ),
).padding(top: 10, bottom: 2),
const Gap(16),
_ArticlePostDisplay(item: item, isFullPost: isFullPost),
if (item.tags.isNotEmpty || item.categories.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.tags.isNotEmpty)
Wrap(
children: [
for (final tag in item.tags)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(Symbols.label, size: 13),
Text(tag.name ?? '#${tag.slug}').fontSize(13),
],
),
onTap: () {},
),
],
),
if (item.categories.isNotEmpty)
Wrap(
children: [
for (final category in item.categories)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(Symbols.category, size: 13),
Text(
category.name ?? '#${category.slug}',
).fontSize(13),
],
),
onTap: () {},
),
],
),
],
),
if ((item.repliedPost != null || item.forwardedPost != null) &&
showReferencePost)
_buildReferencePost(context, item),
if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList(
disableConstraint: isFullPost,
files: item.attachments,
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
),
if (item.meta?['embeds'] != null)
...((item.meta!['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link')
.map(
(embedData) => EmbedLinkWidget(
link: SnEmbedLink.fromJson(
embedData as Map<String, dynamic>,
), ),
maxWidth: math.min( onPressed: () {
MediaQuery.of(context).size.width, showModalBottomSheet(
kWideScreenWidth, context: context,
), useRootNavigator: true,
margin: EdgeInsets.only(top: 8), builder: (BuildContext context) {
), return _PostReactionSheet(
)), reactionsCount: item.reactionsCount,
const Gap(8), onReact: (symbol, attitude) {
Row( reactPost(symbol, attitude);
children: [ },
Padding(
padding: const EdgeInsets.only(right: 12),
child: ActionChip(
avatar: Icon(Symbols.reply, size: 16),
label: Text(
(item.repliesCount > 0)
? 'repliesCount'.plural(item.repliesCount)
: 'reply'.tr(),
),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
onPressed: () {
if (isOpenable) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: item),
);
}
},
),
),
Expanded(
child: PostReactionList(
parentId: item.id,
reactions: item.reactionsCount,
padding: EdgeInsets.zero,
onReact: (symbol, attitude, delta) {
final reactionsCount = Map<String, int>.from(
item.reactionsCount,
);
reactionsCount[symbol] =
(reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(
item.copyWith(reactionsCount: reactionsCount),
);
},
),
),
],
),
],
),
);
} else {
child = Padding(
padding: renderingPadding,
child: Column(
spacing: 8,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
GestureDetector(
child: ProfilePictureWidget(file: item.publisher.picture),
onTap: () {
context.pushNamed(
'publisherProfile',
pathParameters: {'name': item.publisher.name},
); );
}, },
), );
Expanded( },
child: GestureDetector( padding: EdgeInsets.zero,
child: Column( visualDensity: VisualDensity(horizontal: -3, vertical: -3),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(item.publisher.nick).bold(),
if (item.publisher.verification != null)
VerificationMark(
mark: item.publisher.verification!,
).padding(left: 4),
Spacer(),
Text(
isFullPost
? item.publishedAt?.formatSystem() ?? ''
: item.publishedAt?.formatRelative(context) ??
'',
).fontSize(11).alignment(Alignment.bottomRight),
const Gap(4),
],
),
// Add visibility indicator if not public (visibility != 0)
if (item.visibility != 0)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getVisibilityIcon(item.visibility),
size: 14,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
_getVisibilityText(item.visibility).tr(),
style: TextStyle(
fontSize: 12,
color:
Theme.of(context).colorScheme.secondary,
),
),
],
).padding(top: 2, bottom: 2),
if (item.type == 1)
_ArticlePostDisplay(
item: item,
isFullPost: isFullPost,
)
else ...[
if (item.title?.isNotEmpty ?? false)
Text(
item.title!,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
if (item.description?.isNotEmpty ?? false)
Text(
item.description!,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
).padding(bottom: 8),
if (item.content?.isNotEmpty ?? false)
MarkdownTextContent(
content: item.content!,
linesMargin:
item.type == 0
? EdgeInsets.only(bottom: 8)
: null,
attachments: item.attachments,
),
],
// Render tags and categories if they exist
if (item.tags.isNotEmpty || item.categories.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.tags.isNotEmpty)
Wrap(
children: [
for (final tag in item.tags)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(Symbols.label, size: 13),
Text(
tag.name ?? '#${tag.slug}',
).fontSize(13),
],
),
onTap: () {},
),
],
),
if (item.categories.isNotEmpty)
Wrap(
children: [
for (final category in item.categories)
InkWell(
child: Row(
spacing: 4,
children: [
const Icon(
Symbols.category,
size: 13,
),
Text(
category.name ??
'#${category.slug}',
).fontSize(13),
],
),
onTap: () {},
),
],
),
],
),
// Show truncation hint if post is truncated
if (item.isTruncated && !isFullPost && item.type != 1)
_PostTruncateHint().padding(
bottom:
(item.attachments.isNotEmpty ||
item.repliedPost != null ||
item.forwardedPost != null)
? 8
: null,
),
if ((item.repliedPost != null ||
item.forwardedPost != null) &&
showReferencePost)
_buildReferencePost(context, item),
if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList(
files: item.attachments,
disableConstraint: isFullPost,
maxWidth: math.min(
MediaQuery.of(context).size.width * 0.85,
kWideScreenWidth - 160,
),
),
// Render embed links
if (item.meta?['embeds'] != null)
...((item.meta!['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link')
.map(
(embedData) => EmbedLinkWidget(
link: SnEmbedLink.fromJson(
embedData as Map<String, dynamic>,
),
maxWidth: math.min(
MediaQuery.of(context).size.width * 0.85,
kWideScreenWidth - 160,
),
margin: EdgeInsets.only(top: 8),
),
)),
],
),
onTap: () {
if (isOpenable) {
context.pushNamed(
'postDetail',
pathParameters: {'id': item.id},
);
}
},
),
),
],
), ),
Row( ],
).padding(horizontal: renderingPadding.horizontal, bottom: 4),
if (!isFullPost && item.type == 1)
Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: EdgeInsets.only(
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
top: 4,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Replies count button Align(
Padding( alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 52, right: 12), child: Badge(
child: ActionChip( label: Text('postArticle').tr(),
avatar: Icon(Symbols.reply, size: 16), backgroundColor: Theme.of(context).colorScheme.primary,
label: Text( textColor: Theme.of(context).colorScheme.onPrimary,
(item.repliesCount > 0)
? 'repliesCount'.plural(item.repliesCount)
: 'reply'.tr(),
),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
onPressed: () {
if (isOpenable) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: item),
);
}
},
), ),
), ),
// Reactions list const Gap(4),
Expanded( if (item.title != null)
child: PostReactionList( Text(
parentId: item.id, item.title!,
reactions: item.reactionsCount, style: Theme.of(context).textTheme.titleMedium!.copyWith(
padding: EdgeInsets.zero, fontWeight: FontWeight.bold,
onReact: (symbol, attitude, delta) { ),
final reactionsCount = Map<String, int>.from(
item.reactionsCount,
);
reactionsCount[symbol] =
(reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(
item.copyWith(reactionsCount: reactionsCount),
);
},
), ),
), if (item.description != null)
Text(
item.description!,
style: Theme.of(context).textTheme.bodyMedium,
)
else
MarkdownTextContent(content: '${item.content!}...'),
], ],
), ),
], )
), else if (item.content?.isNotEmpty ?? false)
); Padding(
} padding: EdgeInsets.only(
left: renderingPadding.horizontal,
return ContextMenuWidget( right: renderingPadding.horizontal,
menuProvider: (_) {
return Menu(
children: [
if (isAuthor)
MenuAction(
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context
.pushNamed('postEdit', pathParameters: {'id': item.id})
.then((value) {
if (value != null) {
onRefresh?.call();
}
});
},
),
if (isAuthor)
MenuAction(
title: 'delete'.tr(),
image: MenuImage.icon(Symbols.delete),
callback: () {
showConfirmAlert(
'deletePostHint'.tr(),
'deletePost'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.watch(apiClientProvider);
client
.delete('/sphere/posts/${item.id}')
.catchError((err) {
showErrorAlert(err);
return err;
})
.then((_) {
onRefresh?.call();
});
}
});
},
),
if (isAuthor) MenuSeparator(),
MenuAction(
title: 'copyLink'.tr(),
image: MenuImage.icon(Symbols.link),
callback: () {
Clipboard.setData(
ClipboardData(text: 'https://solsynth.dev/posts/${item.id}'),
);
},
), ),
MenuAction( child: MarkdownTextContent(
title: 'reply'.tr(), content: item.content!,
image: MenuImage.icon(Symbols.reply), isSelectable: isTextSelectable,
callback: () {
context.pushNamed(
'postCompose',
extra: PostComposeInitialState(replyingTo: item),
);
},
), ),
MenuAction( ),
title: 'forward'.tr(), if (item.attachments.isNotEmpty)
image: MenuImage.icon(Symbols.forward), CloudFileList(
callback: () { files: item.attachments,
context.pushNamed( padding: EdgeInsets.symmetric(
'postCompose', horizontal: renderingPadding.horizontal,
extra: PostComposeInitialState(forwardingTo: item), vertical: 4,
);
},
), ),
MenuSeparator(), ),
MenuAction( if (isShowReference)
title: 'share'.tr(), _buildReferencePost(context, item, renderingPadding),
image: MenuImage.icon(Symbols.share), Gap(renderingPadding.vertical),
callback: () { ],
showShareSheetLink(
context: context,
link: '${ref.read(serverUrlProvider)}/posts/${item.id}',
title: 'sharePost'.tr(),
toSystem: true,
);
},
),
MenuAction(
title: 'abuseReport'.tr(),
image: MenuImage.icon(Symbols.flag),
callback: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'post/${item.id}',
);
},
),
],
);
},
child: Material(
color: hasBackground ? Colors.transparent : backgroundColor,
child: child,
),
); );
} }
} }
Widget _buildReferencePost(BuildContext context, SnPost item) { Widget _buildReferencePost(
BuildContext context,
SnPost item,
EdgeInsets renderingPadding,
) {
final referencePost = item.repliedPost ?? item.forwardedPost; final referencePost = item.repliedPost ?? item.forwardedPost;
if (referencePost == null) return const SizedBox.shrink(); if (referencePost == null) return const SizedBox.shrink();
final isReply = item.repliedPost != null; final isReply = item.repliedPost != null;
return Container( return Container(
margin: const EdgeInsets.only(bottom: 8), padding: EdgeInsets.symmetric(
padding: const EdgeInsets.all(12), horizontal: renderingPadding.horizontal,
vertical: 8,
),
margin: EdgeInsets.only(
top: 8,
left: renderingPadding.vertical,
right: renderingPadding.vertical,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),

View File

@@ -45,11 +45,13 @@ class PostItemCreator extends HookConsumerWidget {
title: 'edit'.tr(), title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () { callback: () {
context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) { context
if (value != null) { .pushNamed('postEdit', pathParameters: {'id': item.id})
onRefresh?.call(); .then((value) {
} if (value != null) {
}); onRefresh?.call();
}
});
}, },
), ),
MenuAction( MenuAction(
@@ -80,7 +82,10 @@ class PostItemCreator extends HookConsumerWidget {
image: MenuImage.icon(Symbols.link), image: MenuImage.icon(Symbols.link),
callback: () { callback: () {
// Copy post link to clipboard // Copy post link to clipboard
context.pushNamed('postDetail', pathParameters: {'id': item.id}); context.pushNamed(
'postDetail',
pathParameters: {'id': item.id},
);
}, },
), ),
], ],
@@ -198,7 +203,8 @@ class PostItemCreator extends HookConsumerWidget {
files: item.attachments, files: item.attachments,
maxWidth: MediaQuery.of(context).size.width * 0.85, maxWidth: MediaQuery.of(context).size.width * 0.85,
minWidth: MediaQuery.of(context).size.width * 0.9, minWidth: MediaQuery.of(context).size.width * 0.9,
).padding(top: 8), padding: EdgeInsets.only(top: 8),
),
// Reference post indicator // Reference post indicator
if (item.repliedPost != null || item.forwardedPost != null) if (item.repliedPost != null || item.forwardedPost != null)
@@ -211,7 +217,7 @@ class PostItemCreator extends HookConsumerWidget {
size: 16, size: 16,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
const SizedBox(width: 4), const Gap(4),
Text( Text(
item.repliedPost != null item.repliedPost != null
? 'repliedTo'.tr() ? 'repliedTo'.tr()

View File

@@ -99,7 +99,7 @@ class PostRepliesList extends HookConsumerWidget {
item: data.items[index], item: data.items[index],
backgroundColor: backgroundColor:
backgroundColor ?? (isWide ? Colors.transparent : null), backgroundColor ?? (isWide ? Colors.transparent : null),
showReferencePost: false, isShowReference: false,
), ),
const Divider(height: 1), const Divider(height: 1),
], ],