From cf355a95fd37132228da92f3e0dfc7f1aa8e915d Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 11 Aug 2025 22:18:35 +0800 Subject: [PATCH] :sparkles: Post share card --- assets/i18n/en-US.json | 7 +- assets/i18n/zh-CN.json | 7 +- .../content/cloud_file_collection.dart | 70 ++++++++++ lib/widgets/post/post_item.dart | 6 +- lib/widgets/post/post_item_screenshot.dart | 129 +++++++++++++----- lib/widgets/post/post_shared.dart | 32 ++++- 6 files changed, 206 insertions(+), 45 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 9dbb98b..2b54eb8 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -793,5 +793,10 @@ "joinedAt": "Joined at {}", "searchAccounts": "Search accounts...", "webFeeds": "Web Feeds", - "polls": "Polls" + "polls": "Polls", + "sharePostSlogan": "Explore more on the Solar Network", + "filesListAdditional": { + "one": "+{} file remaining", + "other": "+{} files remaining" + } } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 7e98cc1..a39c30a 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -791,5 +791,10 @@ "joinedAt": "加入于 {}", "searchAccounts": "搜索帐号……", "webFeeds": "订阅源", - "polls": "投票" + "polls": "投票", + "sharePostSlogan": "加入 Solar Network 以便探索更多", + "filesListAdditional": { + "one": "+{} 个文件被折叠", + "other": "+{} 个文件被折叠" + } } diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index 16c594e..936bdb2 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -31,6 +31,7 @@ class CloudFileList extends HookConsumerWidget { final bool disableZoomIn; final bool disableConstraint; final EdgeInsets? padding; + final bool isColumn; const CloudFileList({ super.key, required this.files, @@ -40,6 +41,7 @@ class CloudFileList extends HookConsumerWidget { this.disableZoomIn = false, this.disableConstraint = false, this.padding, + this.isColumn = false, }); double calculateAspectRatio() { @@ -63,6 +65,74 @@ class CloudFileList extends HookConsumerWidget { ); if (files.isEmpty) return const SizedBox.shrink(); + + if (isColumn) { + final children = []; + const maxFiles = 2; + final filesToShow = files.take(maxFiles).toList(); + + for (var i = 0; i < filesToShow.length; i++) { + final file = filesToShow[i]; + final isImage = file.mimeType?.startsWith('image') ?? false; + final isAudio = file.mimeType?.startsWith('audio') ?? false; + final widgetItem = ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: _CloudFileListEntry( + file: file, + heroTag: heroTags[i], + isImage: isImage, + disableZoomIn: disableZoomIn, + onTap: () { + if (!isImage) { + return; + } + if (!disableZoomIn) { + context.pushTransparentRoute( + CloudFileZoomIn(item: file, heroTag: heroTags[i]), + rootNavigator: true, + ); + } + }, + ), + ); + + Widget item; + if (isAudio) { + item = SizedBox(height: 120, child: widgetItem); + } else { + item = AspectRatio( + aspectRatio: file.fileMeta?['ratio'] as double? ?? 1.0, + child: widgetItem, + ); + } + children.add(item); + if (i < filesToShow.length - 1) { + children.add(const Gap(8)); + } + } + + if (files.length > maxFiles) { + children.add(const Gap(8)); + children.add( + Text( + 'filesListAdditional'.plural(files.length - filesToShow.length), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ); + } + + return Padding( + padding: padding ?? EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + ); + } if (files.length == 1) { final isImage = files.first.mimeType?.startsWith('image') ?? false; final isAudio = files.first.mimeType?.startsWith('audio') ?? false; diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 43fa56d..385b832 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -23,7 +23,6 @@ import 'package:island/widgets/safety/abuse_report_helper.dart'; import 'package:island/widgets/share/share_sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; -import 'package:relative_time/relative_time.dart'; import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -103,13 +102,13 @@ class PostActionableItem extends HookConsumerWidget { textDirection: TextDirection.ltr, child: SizedBox( width: 520, - height: 640, child: PostItemScreenshot(item: item, isFullPost: isFullPost), ), ), ), context: context, pixelRatio: MediaQuery.of(context).devicePixelRatio, + delay: const Duration(seconds: 1), ) .then((Uint8List? image) async { if (image == null) return; @@ -468,7 +467,8 @@ class PostItem extends HookConsumerWidget { translationSection: translationSection, renderingPadding: renderingPadding, ), - if (isShowReference) ReferencedPostWidget(item: item), + if (isShowReference) + ReferencedPostWidget(item: item, renderingPadding: renderingPadding), if (item.repliesCount > 0 && isEmbedReply) PostReplyPreview( parent: item, diff --git a/lib/widgets/post/post_item_screenshot.dart b/lib/widgets/post/post_item_screenshot.dart index 59833a1..eeea50a 100644 --- a/lib/widgets/post/post_item_screenshot.dart +++ b/lib/widgets/post/post_item_screenshot.dart @@ -1,9 +1,12 @@ import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/widgets/post/post_shared.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:styled_widget/styled_widget.dart'; class PostItemScreenshot extends ConsumerWidget { final SnPost item; @@ -31,42 +34,102 @@ class PostItemScreenshot extends ConsumerWidget { .map((e) => e.key) .last; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PostHeader( - item: item, - isFullPost: isFullPost, - isInteractive: false, - renderingPadding: renderingPadding, - trailing: - mostReaction != null - ? Row( + final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; + + return Material( + elevation: 0, + color: Theme.of(context).colorScheme.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Gap(renderingPadding.vertical), + PostHeader( + item: item, + isFullPost: isFullPost, + isInteractive: false, + renderingPadding: renderingPadding, + isRelativeTime: false, + trailing: + mostReaction != null + ? Row( + children: [ + Text( + kReactionTemplates[mostReaction]?.icon ?? '', + style: const TextStyle(fontSize: 20), + ), + const Gap(4), + Text( + 'x${item.reactionsCount[mostReaction]}', + style: const TextStyle(fontSize: 11), + ), + ], + ) + : null, + ), + PostBody( + item: item, + renderingPadding: renderingPadding, + isFullPost: isFullPost, + isTextSelectable: false, + isInteractive: false, + ), + if (isShowReference) + ReferencedPostWidget( + item: item, + isInteractive: false, + renderingPadding: renderingPadding, + ), + Container( + color: Theme.of(context).colorScheme.surfaceContainerLow, + margin: const EdgeInsets.only(top: 8), + padding: EdgeInsets.symmetric( + horizontal: renderingPadding.horizontal, + vertical: 4, + ), + child: Row( + children: [ + SizedBox( + width: 44, + height: 44, + child: Image.asset( + 'assets/icons/icon${isDark ? '-dark' : ''}.png', + width: 40, + height: 40, + ), + ).padding(vertical: 8, right: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - kReactionTemplates[mostReaction]?.icon ?? '', - style: const TextStyle(fontSize: 20), - ), - const Gap(4), - Text( - 'x${item.reactionsCount[mostReaction]}', - style: const TextStyle(fontSize: 11), + const Text( + 'Solar Network', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), ), + const Text( + 'sharePostSlogan', + style: TextStyle(fontSize: 12), + ).tr().opacity(0.9), ], - ) - : null, - ), - PostBody( - item: item, - renderingPadding: renderingPadding, - isFullPost: isFullPost, - isTextSelectable: false, - isInteractive: false, - ), - if (isShowReference) - ReferencedPostWidget(item: item, isInteractive: false), - ], + ), + ), + QrImageView( + data: 'https://solian.app/posts/${item.id}', + version: QrVersions.auto, + size: 60, + errorCorrectionLevel: QrErrorCorrectLevel.M, + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context).colorScheme.onSurface, + padding: const EdgeInsets.all(8), + ), + ], + ), + ), + ], + ), ); } } diff --git a/lib/widgets/post/post_shared.dart b/lib/widgets/post/post_shared.dart index 8fde695..8e0d1d8 100644 --- a/lib/widgets/post/post_shared.dart +++ b/lib/widgets/post/post_shared.dart @@ -348,11 +348,13 @@ class PostTruncateHint extends StatelessWidget { class ReferencedPostWidget extends StatelessWidget { final SnPost item; final bool isInteractive; + final EdgeInsets renderingPadding; const ReferencedPostWidget({ super.key, required this.item, this.isInteractive = true, + this.renderingPadding = EdgeInsets.zero, }); @override @@ -363,8 +365,15 @@ class ReferencedPostWidget extends StatelessWidget { final isReply = item.repliedPost != null; final content = Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - margin: const EdgeInsets.only(top: 8), + padding: EdgeInsets.symmetric( + horizontal: renderingPadding.horizontal, + vertical: 8, + ), + margin: EdgeInsets.only( + top: 8, + left: renderingPadding.vertical, + right: renderingPadding.vertical, + ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), borderRadius: BorderRadius.circular(12), @@ -522,6 +531,7 @@ class PostHeader extends StatelessWidget { final Widget? trailing; final bool isInteractive; final EdgeInsets renderingPadding; + final bool isRelativeTime; const PostHeader({ super.key, @@ -530,6 +540,7 @@ class PostHeader extends StatelessWidget { this.trailing, this.isInteractive = true, this.renderingPadding = EdgeInsets.zero, + this.isRelativeTime = true, }); @override @@ -569,15 +580,21 @@ class PostHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - isFullPost - ? (item.publishedAt ?? item.createdAt)!.formatSystem() - : (item.publishedAt ?? item.createdAt)!.formatRelative( + !isFullPost && isRelativeTime + ? (item.publishedAt ?? item.createdAt)!.formatRelative( context, - ), + ) + : (item.publishedAt ?? item.createdAt)!.formatSystem(), ).fontSize(10), if (item.editedAt != null) Text( - 'editedAt'.tr(args: [item.editedAt!.formatSystem()]), + 'editedAt'.tr( + args: [ + !isFullPost && isRelativeTime + ? item.editedAt!.formatRelative(context) + : item.editedAt!.formatSystem(), + ], + ), ).fontSize(10), if (item.visibility != 0) Text( @@ -711,6 +728,7 @@ class PostBody extends ConsumerWidget { if (item.attachments.isNotEmpty && item.type != 1) CloudFileList( files: item.attachments, + isColumn: !isInteractive, padding: EdgeInsets.symmetric( horizontal: renderingPadding.horizontal, vertical: 4,