Post share card

This commit is contained in:
2025-08-11 22:18:35 +08:00
parent 2f43073172
commit cf355a95fd
6 changed files with 206 additions and 45 deletions

View File

@@ -793,5 +793,10 @@
"joinedAt": "Joined at {}", "joinedAt": "Joined at {}",
"searchAccounts": "Search accounts...", "searchAccounts": "Search accounts...",
"webFeeds": "Web Feeds", "webFeeds": "Web Feeds",
"polls": "Polls" "polls": "Polls",
"sharePostSlogan": "Explore more on the Solar Network",
"filesListAdditional": {
"one": "+{} file remaining",
"other": "+{} files remaining"
}
} }

View File

@@ -791,5 +791,10 @@
"joinedAt": "加入于 {}", "joinedAt": "加入于 {}",
"searchAccounts": "搜索帐号……", "searchAccounts": "搜索帐号……",
"webFeeds": "订阅源", "webFeeds": "订阅源",
"polls": "投票" "polls": "投票",
"sharePostSlogan": "加入 Solar Network 以便探索更多",
"filesListAdditional": {
"one": "+{} 个文件被折叠",
"other": "+{} 个文件被折叠"
}
} }

View File

@@ -31,6 +31,7 @@ class CloudFileList extends HookConsumerWidget {
final bool disableZoomIn; final bool disableZoomIn;
final bool disableConstraint; final bool disableConstraint;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool isColumn;
const CloudFileList({ const CloudFileList({
super.key, super.key,
required this.files, required this.files,
@@ -40,6 +41,7 @@ class CloudFileList extends HookConsumerWidget {
this.disableZoomIn = false, this.disableZoomIn = false,
this.disableConstraint = false, this.disableConstraint = false,
this.padding, this.padding,
this.isColumn = false,
}); });
double calculateAspectRatio() { double calculateAspectRatio() {
@@ -63,6 +65,74 @@ class CloudFileList extends HookConsumerWidget {
); );
if (files.isEmpty) return const SizedBox.shrink(); if (files.isEmpty) return const SizedBox.shrink();
if (isColumn) {
final children = <Widget>[];
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) { if (files.length == 1) {
final isImage = files.first.mimeType?.startsWith('image') ?? false; final isImage = files.first.mimeType?.startsWith('image') ?? false;
final isAudio = files.first.mimeType?.startsWith('audio') ?? false; final isAudio = files.first.mimeType?.startsWith('audio') ?? false;

View File

@@ -23,7 +23,6 @@ import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_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:path_provider/path_provider.dart' show getTemporaryDirectory; import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
import 'package:relative_time/relative_time.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -103,13 +102,13 @@ class PostActionableItem extends HookConsumerWidget {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: SizedBox( child: SizedBox(
width: 520, width: 520,
height: 640,
child: PostItemScreenshot(item: item, isFullPost: isFullPost), child: PostItemScreenshot(item: item, isFullPost: isFullPost),
), ),
), ),
), ),
context: context, context: context,
pixelRatio: MediaQuery.of(context).devicePixelRatio, pixelRatio: MediaQuery.of(context).devicePixelRatio,
delay: const Duration(seconds: 1),
) )
.then((Uint8List? image) async { .then((Uint8List? image) async {
if (image == null) return; if (image == null) return;
@@ -468,7 +467,8 @@ class PostItem extends HookConsumerWidget {
translationSection: translationSection, translationSection: translationSection,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
), ),
if (isShowReference) ReferencedPostWidget(item: item), if (isShowReference)
ReferencedPostWidget(item: item, renderingPadding: renderingPadding),
if (item.repliesCount > 0 && isEmbedReply) if (item.repliesCount > 0 && isEmbedReply)
PostReplyPreview( PostReplyPreview(
parent: item, parent: item,

View File

@@ -1,9 +1,12 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/widgets/post/post_shared.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 { class PostItemScreenshot extends ConsumerWidget {
final SnPost item; final SnPost item;
@@ -31,15 +34,22 @@ class PostItemScreenshot extends ConsumerWidget {
.map((e) => e.key) .map((e) => e.key)
.last; .last;
return Column( final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
return Material(
elevation: 0,
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Gap(renderingPadding.vertical),
PostHeader( PostHeader(
item: item, item: item,
isFullPost: isFullPost, isFullPost: isFullPost,
isInteractive: false, isInteractive: false,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
isRelativeTime: false,
trailing: trailing:
mostReaction != null mostReaction != null
? Row( ? Row(
@@ -65,8 +75,61 @@ class PostItemScreenshot extends ConsumerWidget {
isInteractive: false, isInteractive: false,
), ),
if (isShowReference) if (isShowReference)
ReferencedPostWidget(item: item, isInteractive: false), 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: [
const Text(
'Solar Network',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Text(
'sharePostSlogan',
style: TextStyle(fontSize: 12),
).tr().opacity(0.9),
], ],
),
),
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),
),
],
),
),
],
),
); );
} }
} }

View File

@@ -348,11 +348,13 @@ class PostTruncateHint extends StatelessWidget {
class ReferencedPostWidget extends StatelessWidget { class ReferencedPostWidget extends StatelessWidget {
final SnPost item; final SnPost item;
final bool isInteractive; final bool isInteractive;
final EdgeInsets renderingPadding;
const ReferencedPostWidget({ const ReferencedPostWidget({
super.key, super.key,
required this.item, required this.item,
this.isInteractive = true, this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero,
}); });
@override @override
@@ -363,8 +365,15 @@ class ReferencedPostWidget extends StatelessWidget {
final isReply = item.repliedPost != null; final isReply = item.repliedPost != null;
final content = Container( final content = Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: EdgeInsets.symmetric(
margin: const EdgeInsets.only(top: 8), 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),
@@ -522,6 +531,7 @@ class PostHeader extends StatelessWidget {
final Widget? trailing; final Widget? trailing;
final bool isInteractive; final bool isInteractive;
final EdgeInsets renderingPadding; final EdgeInsets renderingPadding;
final bool isRelativeTime;
const PostHeader({ const PostHeader({
super.key, super.key,
@@ -530,6 +540,7 @@ class PostHeader extends StatelessWidget {
this.trailing, this.trailing,
this.isInteractive = true, this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero, this.renderingPadding = EdgeInsets.zero,
this.isRelativeTime = true,
}); });
@override @override
@@ -569,15 +580,21 @@ class PostHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
isFullPost !isFullPost && isRelativeTime
? (item.publishedAt ?? item.createdAt)!.formatSystem() ? (item.publishedAt ?? item.createdAt)!.formatRelative(
: (item.publishedAt ?? item.createdAt)!.formatRelative(
context, context,
), )
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
).fontSize(10), ).fontSize(10),
if (item.editedAt != null) if (item.editedAt != null)
Text( Text(
'editedAt'.tr(args: [item.editedAt!.formatSystem()]), 'editedAt'.tr(
args: [
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
).fontSize(10), ).fontSize(10),
if (item.visibility != 0) if (item.visibility != 0)
Text( Text(
@@ -711,6 +728,7 @@ class PostBody extends ConsumerWidget {
if (item.attachments.isNotEmpty && item.type != 1) if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList( CloudFileList(
files: item.attachments, files: item.attachments,
isColumn: !isInteractive,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal, horizontal: renderingPadding.horizontal,
vertical: 4, vertical: 4,