Files
App/lib/widgets/post/post_item_screenshot.dart
2025-12-27 23:41:10 +08:00

282 lines
10 KiB
Dart

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/pods/config.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:island/widgets/content/image.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart';
const kAvailableStickers = {
'angry',
'clap',
'confuse',
'pray',
'thumb_up',
'party',
};
bool _getReactionImageAvailable(String symbol) {
return kAvailableStickers.contains(symbol);
}
Widget _buildReactionIcon(String symbol, double size, {double iconSize = 24}) {
if (_getReactionImageAvailable(symbol)) {
return Image.asset(
'assets/images/stickers/$symbol.png',
width: size,
height: size,
fit: BoxFit.contain,
alignment: Alignment.bottomCenter,
);
} else {
return Text(
kReactionTemplates[symbol]?.icon ?? '',
style: TextStyle(fontSize: iconSize),
);
}
}
class PostItemScreenshot extends ConsumerWidget {
final SnPost item;
final EdgeInsets? padding;
final bool isFullPost;
final bool isShowReference;
const PostItemScreenshot({
super.key,
required this.item,
this.padding,
this.isFullPost = false,
this.isShowReference = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
final mostReaction = item.reactionsCount.isEmpty
? null
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.last;
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(
hideOverlay: true,
item: item,
isFullPost: isFullPost,
isInteractive: false,
renderingPadding: renderingPadding,
isRelativeTime: false,
trailing: mostReaction != null
? Badge(
label: Center(
child: Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
textAlign: TextAlign.center,
),
),
offset: const Offset(4, 20),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.75),
textColor: Theme.of(context).colorScheme.onPrimary,
child: mostReaction.contains('+')
? Consumer(
builder: (context, ref, child) {
final baseUrl = ref.watch(serverUrlProvider);
final stickerUri =
'$baseUrl/sphere/stickers/lookup/$mostReaction/open';
return SizedBox(
width: 28,
height: 28,
child: UniversalImage(
uri: stickerUri,
width: 28,
height: 28,
fit: BoxFit.contain,
).center(),
);
},
)
: _buildReactionIcon(mostReaction, 32).padding(
bottom: _getReactionImageAvailable(mostReaction)
? 2
: 0,
),
)
: null,
),
PostBody(
item: item,
renderingPadding: renderingPadding,
isFullPost: isFullPost,
isRelativeTime: false,
isTextSelectable: false,
isInteractive: false,
hideOverlay: true,
),
if (isShowReference)
ReferencedPostWidget(
item: item,
isInteractive: false,
renderingPadding: renderingPadding,
),
if (item.repliesCount > 0)
Consumer(
builder: (context, ref, child) {
final repliesState = ref.watch(repliesProvider(item.id));
final posts = repliesState.posts;
return Container(
margin: EdgeInsets.only(
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
top: 8,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text(
'repliesCount',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
).plural(item.repliesCount).padding(horizontal: 5),
if (posts.isEmpty && repliesState.loading)
Row(
children: [
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
const Gap(8),
const Text('loading').tr(),
],
).padding(horizontal: 5),
if (posts.isNotEmpty)
...posts.map(
(post) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file:
post.publisher.picture ??
post.publisher.account?.profile.picture,
radius: 12,
).padding(top: 4),
if (post.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: post.content!,
attachments: post.attachments,
).padding(top: 2),
)
else
Expanded(
child:
Text(
'postHasAttachments',
style: const TextStyle(height: 2),
)
.plural(post.attachments.length)
.padding(top: 2),
),
],
),
),
),
],
),
);
},
),
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),
),
],
),
),
],
),
);
}
}