💄 Optimize attachment view

This commit is contained in:
LittleSheep 2025-03-02 22:53:14 +08:00
parent 33a4bd7e71
commit 73777fe74e
8 changed files with 203 additions and 157 deletions

View File

@ -752,5 +752,6 @@
"screenAccountBadges": "Badges", "screenAccountBadges": "Badges",
"accountBadges": "Badges", "accountBadges": "Badges",
"accountBadgesDescription": "View and manage your badges.", "accountBadgesDescription": "View and manage your badges.",
"badgeActivated": "Activated badge {}." "badgeActivated": "Activated badge {}.",
"viewDetailedAttachment": "Details"
} }

View File

@ -750,5 +750,6 @@
"screenAccountBadges": "徽章", "screenAccountBadges": "徽章",
"accountBadges": "徽章", "accountBadges": "徽章",
"accountBadgesDescription": "查看并管理你的徽章。", "accountBadgesDescription": "查看并管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。" "badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件详情"
} }

View File

@ -750,5 +750,6 @@
"screenAccountBadges": "徽章", "screenAccountBadges": "徽章",
"accountBadges": "徽章", "accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。", "accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。" "badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
} }

View File

@ -750,5 +750,6 @@
"screenAccountBadges": "徽章", "screenAccountBadges": "徽章",
"accountBadges": "徽章", "accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。", "accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。" "badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
} }

View File

@ -72,34 +72,36 @@ class AccountPopoverCard extends StatelessWidget {
const Gap(8) const Gap(8)
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(16), if (data.badges.isNotEmpty) const Gap(12),
Wrap( if (data.badges.isNotEmpty)
children: data.badges Wrap(
.map( spacing: 4,
(ele) => Tooltip( children: data.badges
richMessage: TextSpan( .map(
children: [ (ele) => Tooltip(
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), richMessage: TextSpan(
if (ele.metadata['title'] != null) children: [
TextSpan( TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
text: '\n${ele.metadata['title']}', if (ele.metadata['title'] != null)
style: const TextStyle(fontWeight: FontWeight.bold), TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
), ),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
), ),
], )
), .toList(),
child: Icon( ).padding(horizontal: 24),
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(8), const Gap(8),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -127,33 +129,33 @@ class AccountPopoverCard extends StatelessWidget {
final SnAccountStatusInfo? status = final SnAccountStatusInfo? status =
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null; snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
return Row( return Row(
children: [ children: [
Icon( Icon(
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey, color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text(
status != null
? status.isOnline
? 'accountStatusOnline'.tr()
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (status != null && !status.isOnline && status.lastSeenAt != null)
Text( Text(
status != null 'accountStatusLastSeen'.tr(args: [
? status.isOnline status.lastSeenAt != null
? 'accountStatusOnline'.tr() ? RelativeTime(context).format(
: 'accountStatusOffline'.tr() status.lastSeenAt!.toLocal(),
: 'loading'.tr(), )
), : 'unknown',
if (status != null && !status.isOnline && status.lastSeenAt != null) ]),
Text( ).padding(left: 6).opacity(0.75),
'accountStatusLastSeen'.tr(args: [ ],
status.lastSeenAt != null ).padding(horizontal: 24);
? RelativeTime(context).format(
status.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
],
).padding(horizontal: 24);
}, },
), ),
// Bottom padding // Bottom padding

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' show max;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
@ -43,6 +44,9 @@ class AttachmentZoomView extends StatefulWidget {
class _AttachmentZoomViewState extends State<AttachmentZoomView> { class _AttachmentZoomViewState extends State<AttachmentZoomView> {
late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0); late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0);
bool _showOverlay = true;
bool _dismissable = true;
void _updatePage() { void _updatePage() {
setState(() { setState(() {
if (_isCompletedDownload) { if (_isCompletedDownload) {
@ -146,7 +150,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: DismissiblePageDismissDirection.none, direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: GestureDetector( child: GestureDetector(
@ -163,6 +167,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
child: PhotoView( child: PhotoView(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
backgroundDecoration: BoxDecoration(color: Colors.transparent), backgroundDecoration: BoxDecoration(color: Colors.transparent),
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
imageProvider: UniversalImage.provider( imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.first.rid), sn.getAttachmentUrl(widget.data.first.rid),
), ),
@ -172,7 +179,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
return PhotoViewGallery.builder( return PhotoViewGallery.builder(
pageController: _pageController, pageController: _pageController,
scrollPhysics: const BouncingScrollPhysics(), enableRotation: true,
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
builder: (context, idx) { builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
@ -197,6 +207,27 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
backgroundDecoration: BoxDecoration(color: Colors.transparent), backgroundDecoration: BoxDecoration(color: Colors.transparent),
); );
}), }),
Positioned(
top: max(MediaQuery.of(context).padding.top, 8),
left: 14,
child: IgnorePointer(
ignoring: !_showOverlay,
child: IconButton(
constraints: const BoxConstraints(),
icon: const Icon(Icons.close),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.surface.withOpacity(0.5),
),
),
onPressed: () {
Navigator.of(context).pop();
},
)
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: IgnorePointer( child: IgnorePointer(
@ -214,7 +245,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
), ),
), ),
), ),
), )
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
Positioned( Positioned(
left: 16, left: 16,
right: 16, right: 16,
@ -318,16 +351,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
]), ]),
style: metaTextStyle, style: metaTextStyle,
).padding(right: 2), ).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null && if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null) item.metadata['exif']?['Model'] != null)
Text( Text(
@ -344,29 +367,44 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
'${item.metadata['width']}x${item.metadata['height']}', '${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle, style: metaTextStyle,
), ),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text(
item.mimetype,
style: metaTextStyle,
),
], ],
), ),
), ),
const Gap(4),
InkWell(
onTap: () {
_showDetail = true;
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data
.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
});
},
child: Text(
'viewDetailedAttachment'.tr(),
style: metaTextStyle.copyWith(decoration: TextDecoration.underline),
),
),
], ],
); );
}), }),
), )
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
), ),
], ],
), ),
), ),
onTap: () {
setState(() => _showOverlay = !_showOverlay);
},
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
if (_showDetail) return; if (_showDetail) return;
if (details.delta.dy <= -40) { if (details.delta.dy <= -20) {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -378,9 +416,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
}); });
} }
}, },
onTap: () {
Navigator.of(context).pop();
},
), ),
); );
} }
@ -480,14 +515,14 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
), ),
tableGap, tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow( ...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [ children: [
TableCell(child: Text(k).padding(right: 16)), TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())), TableCell(child: Text(data.metadata['exif'][k].toString())),
], ],
)) ?? )) ??
[]), []),
], ],
).padding(horizontal: 20, vertical: 8), ).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom),
), ),
), ),
], ],

View File

@ -28,8 +28,7 @@ class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller; final ChatMessageController controller;
final SnChannelMember? otherMember; final SnChannelMember? otherMember;
const ChatMessageInput( const ChatMessageInput({super.key, required this.controller, this.otherMember});
{super.key, required this.controller, this.otherMember});
@override @override
State<ChatMessageInput> createState() => ChatMessageInputState(); State<ChatMessageInput> createState() => ChatMessageInputState();
@ -46,20 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [ modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
final HotKey _newLineHotKey = HotKey( final HotKey _newLineHotKey = HotKey(
key: PhysicalKeyboardKey.enter, key: PhysicalKeyboardKey.enter,
modifiers: [ modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@ -109,8 +100,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear(); _attachments.clear();
_attachments.addAll( _attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value); setState(() => _editingMessage = value);
} }
@ -149,9 +139,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
media.name, media.name,
'messaging', 'messaging',
null, null,
mimetype: media.raw != null && media.type == SnMediaType.image mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
? 'image/png'
: null,
); );
final item = await attach.chunkedUploadParts( final item = await attach.chunkedUploadParts(
@ -183,10 +171,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
widget.controller.sendMessage( widget.controller.sendMessage(
_editingMessage != null ? 'messages.edit' : 'messages.new', _editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text, _contentController.text,
attachments: _attachments attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
relatedId: _editingMessage?.id, relatedId: _editingMessage?.id,
quoteId: _replyingMessage?.id, quoteId: _replyingMessage?.id,
editingMessage: _editingMessage, editingMessage: _editingMessage,
@ -247,15 +232,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress), tween: Tween(begin: 0, end: _progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_isBusy) else if (_isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
Padding( Padding(
padding: _attachments.isNotEmpty padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
? const EdgeInsets.only(top: 8)
: EdgeInsets.zero,
child: PostMediaPendingList( child: PostMediaPendingList(
attachments: _attachments, attachments: _attachments,
isBusy: _isBusy, isBusy: _isBusy,
@ -267,8 +249,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}, },
onUpdateBusy: (state) => setState(() => _isBusy = state), onUpdateBusy: (state) => setState(() => _isBusy = state),
), ),
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate( )
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: _replyingMessage != null child: _replyingMessage != null
@ -289,8 +272,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: Text( child: Text(
_replyingMessage?.body['text'] ?? _replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
'${_replyingMessage?.sender.nick}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -307,8 +289,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8), ).padding(vertical: 8),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
).height(_replyingMessage != null ? 38 : 0, animate: true).animate( )
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .height(_replyingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: _editingMessage != null child: _editingMessage != null
@ -329,8 +312,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: Text( child: Text(
_editingMessage?.body['text'] ?? _editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
'${_editingMessage?.sender.nick}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -348,8 +330,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8), ).padding(vertical: 8),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
).height(_editingMessage != null ? 38 : 0, animate: true).animate( )
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .height(_editingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
constraints: BoxConstraints(minHeight: 56, maxHeight: 240), constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
@ -366,14 +349,11 @@ class ChatMessageInputState extends State<ChatMessageInput> {
? 'fieldChatMessageDirect'.tr(args: [ ? 'fieldChatMessageDirect'.tr(args: [
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}', '@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
]) ])
: 'fieldChatMessage'.tr(args: [ : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
widget.controller.channel?.name ?? 'loading'.tr()
]),
border: InputBorder.none, border: InputBorder.none,
), ),
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) { onSubmitted: (_) {
if (_isBusy) return; if (_isBusy) return;
_sendMessage(); _sendMessage();
@ -388,8 +368,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.mood, Symbols.mood,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
visualDensity: visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
@ -409,8 +388,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.send, Symbols.send,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
visualDensity: visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), ),
@ -427,8 +405,7 @@ class _StickerPicker extends StatelessWidget {
final Function? onDismiss; final Function? onDismiss;
final Function(String)? onInsert; final Function(String)? onInsert;
const _StickerPicker( const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
{this.onDismiss, required this.originalText, this.onInsert});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -439,8 +416,9 @@ class _StickerPicker extends StatelessWidget {
}, },
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: min(360, MediaQuery.of(context).size.width), maxWidth: min(360, MediaQuery.of(context).size.width - 40),
maxHeight: 240), maxHeight: 240,
),
child: Material( child: Material(
elevation: 8, elevation: 8,
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
@ -453,10 +431,8 @@ class _StickerPicker extends StatelessWidget {
return <Widget>[ return <Widget>[
Container( Container(
margin: EdgeInsets.only(bottom: 8), margin: EdgeInsets.only(bottom: 8),
padding: padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
EdgeInsets.symmetric(horizontal: 8, vertical: 4), color: Theme.of(context).colorScheme.surfaceContainerHigh,
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -468,8 +444,7 @@ class _StickerPicker extends StatelessWidget {
), ),
GridView.builder( GridView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
shrinkWrap: true, shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48, maxCrossAxisExtent: 48,
@ -492,8 +467,7 @@ class _StickerPicker extends StatelessWidget {
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: text: ':${element.pack.prefix}${element.alias}:\n',
':${element.pack.prefix}${element.alias}:\n',
style: GoogleFonts.robotoMono()), style: GoogleFonts.robotoMono()),
TextSpan(text: element.name).bold(), TextSpan(text: element.name).bold(),
], ],
@ -502,15 +476,11 @@ class _StickerPicker extends StatelessWidget {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(Radius.circular(8)),
Radius.circular(8)), color: Theme.of(context).colorScheme.surfaceContainerHigh,
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(Radius.circular(8)),
Radius.circular(8)),
child: UniversalImage( child: UniversalImage(
sn.getAttachmentUrl(element.attachment.rid), sn.getAttachmentUrl(element.attachment.rid),
width: 48, width: 48,

View File

@ -6,17 +6,24 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import '../../screens/account/profile_page.dart' show kBadgesMeta;
class PublisherPopoverCard extends StatelessWidget { class PublisherPopoverCard extends StatelessWidget {
final SnPublisher data; final SnPublisher data;
const PublisherPopoverCard({super.key, required this.data}); const PublisherPopoverCard({super.key, required this.data});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -41,6 +48,7 @@ class PublisherPopoverCard extends StatelessWidget {
AccountImage( AccountImage(
content: data.avatar, content: data.avatar,
radius: 20, radius: 20,
borderRadius: data.type == 1 ? 8 : 20,
), ),
Gap(16), Gap(16),
Expanded( Expanded(
@ -68,6 +76,36 @@ class PublisherPopoverCard extends StatelessWidget {
const Gap(8) const Gap(8)
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (user != null && user.badges.isNotEmpty) const Gap(16),
if (user != null && user.badges.isNotEmpty)
Wrap(
spacing: 4,
children: user.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(16), const Gap(16),
Row( Row(
children: [ children: [
@ -108,10 +146,7 @@ class PublisherPopoverCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text('publisherTotalDownvote') Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75),
.tr()
.fontSize(13)
.opacity(0.75),
Text(data.totalDownvote.toString()), Text(data.totalDownvote.toString()),
], ],
), ),