♻️ Better image loading animation and more commonly used blurhash

This commit is contained in:
2026-01-02 18:32:37 +08:00
parent f1f5113b01
commit 78c1a284a5
44 changed files with 2043 additions and 2185 deletions

View File

@@ -24,193 +24,181 @@ class AccountNameplate extends HookConsumerWidget {
final user = ref.watch(accountProvider(name));
return Container(
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
)
: null,
decoration: isOutlined
? BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
)
: null,
margin: padding,
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.transparent,
child: user.when(
data:
(account) =>
account.profile.background != null
? AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
// Background image
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudFileWidget(
item: account.profile.background!,
fit: BoxFit.cover,
),
),
data: (account) => account.profile.background != null
? AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
// Background image
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudFileWidget(
item: account.profile.background!,
fit: BoxFit.cover,
),
),
),
// Gradient overlay for text readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.1),
Colors.transparent,
],
),
// Gradient overlay for text readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.1),
Colors.transparent,
],
),
),
),
),
),
// Content positioned at the bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
file: account.profile.picture,
),
),
// Content positioned at the bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'@${account.name}',
).textColor(Colors.white70),
],
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'@${account.name}',
).textColor(Colors.white70),
],
),
),
),
],
],
),
),
)
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
color:
Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
)
: null,
child: Row(
),
],
),
)
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
decoration: isOutlined
? BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
)
: null,
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(file: account.profile.picture),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text('@${account.name}'),
],
),
AccountName(
account: account,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text('@${account.name}'),
],
),
),
loading:
() => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
],
),
),
child: Row(
children: [
// Loading indicator (equivalent to leading)
const CircularProgressIndicator(),
const SizedBox(width: 16),
// Loading text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('loading').bold().tr(),
const SizedBox(height: 4),
const Text('...'),
],
),
),
],
loading: () => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Loading indicator (equivalent to leading)
const CircularProgressIndicator(),
const SizedBox(width: 16),
// Loading text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('loading').bold().tr(),
const SizedBox(height: 4),
const Text('...'),
],
),
),
),
error:
(error, stackTrace) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
],
),
),
error: (error, stackTrace) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Error icon (equivalent to leading)
const Icon(Symbols.error),
const SizedBox(width: 16),
// Error text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('somethingWentWrong').tr().bold(),
const SizedBox(height: 4),
Text(error.toString()),
],
),
),
child: Row(
children: [
// Error icon (equivalent to leading)
const Icon(Symbols.error),
const SizedBox(width: 16),
// Error text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('somethingWentWrong').tr().bold(),
const SizedBox(height: 4),
Text(error.toString()),
],
),
),
],
),
),
],
),
),
),
),
);

View File

@@ -62,8 +62,8 @@ class AccountPickerSheet extends HookConsumerWidget {
),
),
autofocus: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
@@ -74,23 +74,22 @@ class AccountPickerSheet extends HookConsumerWidget {
);
return searchResult.when(
data:
(accounts) => ListView.builder(
itemCount: accounts.length,
itemBuilder: (context, index) {
final account = accounts[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
title: Text(account.nick),
subtitle: Text('@${account.name}'),
onTap: () => Navigator.of(context).pop(account),
);
},
),
loading:
() => const Center(child: CircularProgressIndicator()),
data: (accounts) => ListView.builder(
itemCount: accounts.length,
itemBuilder: (context, index) {
final account = accounts[index];
return ListTile(
leading: ProfilePictureWidget(
file: account.profile.picture,
),
title: Text(account.nick),
subtitle: Text('@${account.name}'),
onTap: () => Navigator.of(context).pop(account),
);
},
),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
},

View File

@@ -133,10 +133,9 @@ class _ExpandedSection extends StatelessWidget {
},
child: Card(
margin: EdgeInsets.zero,
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
color: Theme.of(
context,
).colorScheme.surfaceContainer,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -144,8 +143,9 @@ class _ExpandedSection extends StatelessWidget {
const Gap(4),
Text(
'Poll',
style:
Theme.of(context).textTheme.bodySmall,
style: Theme.of(
context,
).textTheme.bodySmall,
),
],
),
@@ -160,8 +160,8 @@ class _ExpandedSection extends StatelessWidget {
await showModalBottomSheet<SnWalletFund>(
context: context,
isScrollControlled: true,
builder:
(context) => const ComposeFundSheet(),
builder: (context) =>
const ComposeFundSheet(),
);
if (fund != null) {
onFundSelected(fund);
@@ -169,10 +169,9 @@ class _ExpandedSection extends StatelessWidget {
},
child: Card(
margin: EdgeInsets.zero,
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
color: Theme.of(
context,
).colorScheme.surfaceContainer,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -180,8 +179,9 @@ class _ExpandedSection extends StatelessWidget {
const Gap(4),
Text(
'fund'.tr(),
style:
Theme.of(context).textTheme.bodySmall,
style: Theme.of(
context,
).textTheme.bodySmall,
),
],
),
@@ -192,11 +192,8 @@ class _ExpandedSection extends StatelessWidget {
),
StickerPickerEmbedded(
height: kInputDrawerExpandedHeight,
onPick:
(placeholder) => _insertPlaceholder(
messageController,
placeholder,
),
onPick: (placeholder) =>
_insertPlaceholder(messageController, placeholder),
),
],
),
@@ -373,15 +370,16 @@ class ChatInput extends HookConsumerWidget {
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
),
),
position:
Tween<Offset>(
begin: const Offset(0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
),
),
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
@@ -389,41 +387,40 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
chatSubscribe.isNotEmpty
? Container(
key: const ValueKey('typing-indicator'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Row(
children: [
const Icon(
Symbols.more_horiz,
size: 16,
).padding(horizontal: 8),
const Gap(8),
Expanded(
child: Text(
'typingHint'.plural(
chatSubscribe.length,
args: [
chatSubscribe
.map((x) => x.nick ?? x.account.nick)
.join(', '),
],
),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('typing-indicator-none'),
child: chatSubscribe.isNotEmpty
? Container(
key: const ValueKey('typing-indicator'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Row(
children: [
const Icon(
Symbols.more_horiz,
size: 16,
).padding(horizontal: 8),
const Gap(8),
Expanded(
child: Text(
'typingHint'.plural(
chatSubscribe.length,
args: [
chatSubscribe
.map((x) => x.nick ?? x.account.nick)
.join(', '),
],
),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('typing-indicator-none'),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
@@ -445,41 +442,36 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload:
() => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove:
(delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12)
: const SizedBox.shrink(
key: ValueKey('no-attachments'),
child: attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12)
: const SizedBox.shrink(key: ValueKey('no-attachments')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -501,66 +493,62 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
selectedPoll != null
? Container(
key: const ValueKey('selected-poll'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.how_to_vote,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
selectedPoll!.title ?? 'Poll',
style: Theme.of(context).textTheme.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onPollSelected(null),
tooltip: 'clear'.tr(),
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no-selected-poll'),
child: selectedPoll != null
? Container(
key: const ValueKey('selected-poll'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.how_to_vote,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
selectedPoll!.title ?? 'Poll',
style: Theme.of(context).textTheme.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onPollSelected(null),
tooltip: 'clear'.tr(),
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-selected-poll')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -582,93 +570,88 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
selectedFund != null
? Container(
key: const ValueKey('selected-fund'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
child: selectedFund != null
? Container(
key: const ValueKey('selected-fund'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.currency_exchange,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.currency_exchange,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
const Gap(8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (selectedFund!.message != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
selectedFund!.message!,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontSize: 10,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (selectedFund!.message != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
selectedFund!.message!,
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontSize: 10,
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onFundSelected(null),
tooltip: 'clear'.tr(),
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onFundSelected(null),
tooltip: 'clear'.tr(),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no-selected-fund'),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-selected-fund')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -692,59 +675,57 @@ class ChatInput extends HookConsumerWidget {
},
child:
(messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
messageForwardingTo != null ||
messageEditingTo != null)
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 18,
color:
Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'chatReplyingTo'.tr(
? 'chatReplyingTo'.tr(
args: [
messageReplyingTo
?.sender
@@ -753,60 +734,57 @@ class ChatInput extends HookConsumerWidget {
'unknown'.tr(),
],
)
: messageForwardingTo != null
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
),
),
],
),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
: messageForwardingTo != null
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
),
),
],
),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(context).textTheme.bodySmall!
.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -815,25 +793,19 @@ class ChatInput extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip:
isExpanded.value ? 'collapse'.tr() : 'more'.tr(),
tooltip: isExpanded.value
? 'collapse'.tr()
: 'more'.tr(),
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child:
isExpanded.value
? const Icon(
Symbols.close,
key: ValueKey('close'),
)
: const Icon(
Symbols.add,
key: ValueKey('add'),
),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: isExpanded.value
? const Icon(
Symbols.close,
key: ValueKey('close'),
)
: const Icon(Symbols.add, key: ValueKey('add')),
),
onPressed: () {
isExpanded.value = !isExpanded.value;
@@ -885,46 +857,43 @@ class ChatInput extends HookConsumerWidget {
hintMaxLines: 1,
hintText:
(chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr(
args: [
getValidMembers(
chatRoom.members!,
).map((e) => e.account.nick).join(', '),
],
)
: 'chatMessageHint'.tr(
args: [chatRoom.name!],
),
? 'chatDirectMessageHint'.tr(
args: [
getValidMembers(
chatRoom.members!,
).map((e) => e.account.nick).join(', '),
],
)
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
counterText: messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: 5,
minLines: 1,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
textInputAction:
settings.enterToSend
? TextInputAction.send
: null,
onSubmitted:
settings.enterToSend ? (_) => send() : null,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
textInputAction: settings.enterToSend
? TextInputAction.send
: null,
onSubmitted: settings.enterToSend
? (_) => send()
: null,
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return [];
final chopped = pattern.substring(triggerIndex);
if (chopped.contains(' ')) return [];
@@ -986,9 +955,7 @@ class ChatInput extends HookConsumerWidget {
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(
fileId: sticker.image.id,
),
child: CloudImageWidget(file: sticker.image),
),
);
break;
@@ -1005,8 +972,9 @@ class ChatInput extends HookConsumerWidget {
final text = messageController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
@@ -1053,16 +1021,15 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
isExpanded.value
? _ExpandedSection(
messageController: messageController,
selectedPoll: selectedPoll,
onPollSelected: onPollSelected,
selectedFund: selectedFund,
onFundSelected: onFundSelected,
)
: const SizedBox.shrink(key: ValueKey('collapsed')),
child: isExpanded.value
? _ExpandedSection(
messageController: messageController,
selectedPoll: selectedPoll,
onPollSelected: onPollSelected,
selectedFund: selectedFund,
onFundSelected: onFundSelected,
)
: const SizedBox.shrink(key: ValueKey('collapsed')),
),
],
),

View File

@@ -31,7 +31,7 @@ class MessageListTile extends StatelessWidget {
radius: 20,
backgroundColor: Colors.transparent,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 20,
),
),

View File

@@ -24,12 +24,11 @@ class MessageSenderInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final timestamp =
DateTime.now().difference(createdAt).inDays > 365
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
: DateTime.now().difference(createdAt).inDays > 0
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
final timestamp = DateTime.now().difference(createdAt).inDays > 365
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
: DateTime.now().difference(createdAt).inDays > 0
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
if (isCompact) {
return Row(
@@ -41,7 +40,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 14,
),
),
@@ -69,7 +68,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 14,
),
),
@@ -106,7 +105,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 16,
),
),

View File

@@ -99,15 +99,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@@ -132,15 +132,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(

View File

@@ -22,15 +22,13 @@ class ChatRoomAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final avatarChild = (isDirect && room.picture?.id == null)
final avatarChild = (isDirect && room.picture == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
files: validMembers.map((e) => e.account.profile.picture).toList(),
)
: room.picture?.id == null
: room.picture == null
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
: ProfilePictureWidget(fileId: room.picture?.id);
: ProfilePictureWidget(file: room.picture);
final badgeChild = Badge(
isLabelVisible: summary.when(

View File

@@ -262,10 +262,7 @@ class CheckInActivityWidget extends StatelessWidget {
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: result.account!.profile.picture?.id,
radius: 12,
),
ProfilePictureWidget(file: result.account!.profile.picture, radius: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider);
final String? id = file?.id ?? fileId;
final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {};
final blurHash = meta['blur'] as String?;
final fallback = Icon(
fallbackIcon ?? Symbols.account_circle,
size: radius,
@@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget {
placeholder: fallback,
content: () => UniversalImage(
uri: '$serverUrl/drive/files/$id',
blurHash: blurHash,
fit: BoxFit.cover,
),
);
@@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget {
}
class SplitAvatarWidget extends ConsumerWidget {
final List<String?> filesId;
final List<SnCloudFile?> files;
final double radius;
final IconData fallbackIcon;
final Color? fallbackColor;
const SplitAvatarWidget({
super.key,
required this.filesId,
required this.files,
this.radius = 20,
this.fallbackIcon = Symbols.account_circle,
this.fallbackColor,
@@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (filesId.isEmpty) {
if (files.isEmpty) {
return ProfilePictureWidget(
fileId: null,
file: null,
radius: radius,
fallbackIcon: fallbackIcon,
fallbackColor: fallbackColor,
);
}
if (filesId.length == 1) {
if (files.length == 1) {
return ProfilePictureWidget(
fileId: filesId[0],
file: files[0],
radius: radius,
fallbackIcon: fallbackIcon,
fallbackColor: fallbackColor,
@@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget {
color: Theme.of(context).colorScheme.primaryContainer,
child: Stack(
children: [
if (filesId.length == 2)
if (files.length == 2)
Row(
children: [
Expanded(
child: _buildQuadrant(context, filesId[0], ref, radius),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(context, filesId[1], ref, radius),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
)
else if (filesId.length == 3)
else if (files.length == 3)
Row(
children: [
Column(
children: [
Expanded(
child: _buildQuadrant(context, filesId[0], ref, radius),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(context, filesId[1], ref, radius),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
),
Expanded(
child: _buildQuadrant(context, filesId[2], ref, radius),
child: _buildQuadrant(context, files[2], ref, radius),
),
],
)
@@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget {
child: Row(
children: [
Expanded(
child: _buildQuadrant(
context,
filesId[0],
ref,
radius,
),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(
context,
filesId[1],
ref,
radius,
),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
),
@@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget {
child: Row(
children: [
Expanded(
child: _buildQuadrant(
context,
filesId[2],
ref,
radius,
),
child: _buildQuadrant(context, files[2], ref, radius),
),
Expanded(
child: filesId.length > 4
child: files.length > 4
? Container(
color: Theme.of(
context,
).colorScheme.primaryContainer,
child: Center(
child: Text(
'+${filesId.length - 3}',
'+${files.length - 3}',
style: TextStyle(
fontSize: radius * 0.4,
color: Theme.of(
@@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget {
),
),
)
: _buildQuadrant(
context,
filesId[3],
ref,
radius,
),
: _buildQuadrant(context, files[3], ref, radius),
),
],
),
@@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget {
Widget _buildQuadrant(
BuildContext context,
String? fileId,
SnCloudFile? file,
WidgetRef ref,
double radius,
) {
if (fileId == null) {
if (file == null) {
return Container(
width: radius,
height: radius,
@@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget {
}
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/$fileId';
final uri = '$serverUrl/drive/files/${file.id}';
return SizedBox(
width: radius,

View File

@@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
class UniversalImage extends StatelessWidget {
class UniversalImage extends HookWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final loaded = useState(false);
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
if (isSvgImage) {
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
fit: fit,
width: width,
height: height,
placeholderBuilder:
(BuildContext context) =>
Center(child: CircularProgressIndicator()),
placeholderBuilder: (BuildContext context) =>
Center(child: CircularProgressIndicator()),
);
}
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight =
height != null ? (height! * devicePixelRatio).round() : null;
cacheHeight = height != null
? (height! * devicePixelRatio).round()
: null;
}
return SizedBox(
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
memCacheWidth: cacheWidth,
progressIndicatorBuilder: (context, url, progress) {
return Center(
child: CircularProgressIndicator(value: progress.progress),
child: AnimatedCircularProgressIndicator(
value: progress.progress,
color: Colors.white.withOpacity(0.5),
),
);
},
errorWidget:
(context, url, error) =>
useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
imageBuilder: (context, imageProvider) {
Future(() => loaded.value = true);
return AnimatedOpacity(
opacity: loaded.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Image(
image: imageProvider,
fit: fit,
width: width,
height: height,
),
);
},
errorWidget: (context, url, error) => useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
),
],
),
);
}
}
class AnimatedCircularProgressIndicator extends HookWidget {
final double? value;
final Color? color;
final double strokeWidth;
final Duration duration;
const AnimatedCircularProgressIndicator({
super.key,
this.value,
this.color,
this.strokeWidth = 4.0,
this.duration = const Duration(milliseconds: 200),
});
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(duration: duration);
final animation = useAnimation(
Tween<double>(begin: 0.0, end: value ?? 0.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.linear),
),
);
useEffect(() {
animationController.animateTo(value ?? 0.0);
return null;
}, [value]);
return CircularProgressIndicator(
value: animation,
color: color,
strokeWidth: strokeWidth,
backgroundColor: Colors.transparent,
);
}
}

View File

@@ -41,12 +41,11 @@ class ComposeFormFields extends HookConsumerWidget {
GestureDetector(
onTap: onPublisherTap,
child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
file: state.currentPublisher.value?.picture,
radius: 20,
fallbackIcon:
state.currentPublisher.value == null
? Icons.question_mark
: null,
fallbackIcon: state.currentPublisher.value == null
? Icons.question_mark
: null,
),
),
@@ -98,8 +97,8 @@ class ComposeFormFields extends HookConsumerWidget {
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
@@ -115,8 +114,8 @@ class ComposeFormFields extends HookConsumerWidget {
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Content field
@@ -138,16 +137,17 @@ class ComposeFormFields extends HookConsumerWidget {
),
),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return [];
final chopped = pattern.substring(triggerIndex);
if (chopped.contains(' ')) return [];
@@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget {
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(fileId: sticker.image.id),
child: CloudImageWidget(file: sticker.image),
),
);
break;
@@ -219,8 +219,9 @@ class ComposeFormFields extends HookConsumerWidget {
final text = state.contentController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
@@ -281,8 +282,8 @@ class ArticleComposeFormFields extends StatelessWidget {
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
@@ -297,8 +298,8 @@ class ArticleComposeFormFields extends StatelessWidget {
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Content field (expanded)
@@ -317,8 +318,8 @@ class ArticleComposeFormFields extends StatelessWidget {
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],

View File

@@ -135,10 +135,7 @@ class CompactReferencePost extends StatelessWidget {
Widget _buildProfilePicture(BuildContext context) {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
fileId: post.publisher!.picture?.id,
radius: 16,
);
return ProfilePictureWidget(file: post.publisher!.picture, radius: 16);
}
// Handle actor case
if (post.actor != null) {
@@ -169,7 +166,7 @@ class CompactReferencePost extends StatelessWidget {
}
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: 16);
return ProfilePictureWidget(file: null, radius: 16);
}
String _getDisplayName() {

View File

@@ -412,7 +412,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: currentRealm.picture?.id,
file: currentRealm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),
@@ -428,7 +428,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),
@@ -454,7 +454,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
)
else
ProfilePictureWidget(
fileId: currentRealm.picture?.id,
file: currentRealm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),

View File

@@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget {
}
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getPublisherName() {

View File

@@ -73,96 +73,94 @@ class PostQuickReply extends HookConsumerWidget {
const kInputChipHeight = 54.0;
return publishers.when(
data:
(data) => Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
child: Container(
constraints: BoxConstraints(minHeight: kInputChipHeight),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: currentPublisher.value?.picture?.id,
radius: (kInputChipHeight * 0.5) - 6,
data: (data) => Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
child: Container(
constraints: BoxConstraints(minHeight: kInputChipHeight),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: ProfilePictureWidget(
file: currentPublisher.value?.picture,
radius: (kInputChipHeight * 0.5) - 6,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) {
currentPublisher.value = value;
}
});
},
).padding(right: 12),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) {
currentPublisher.value = value;
}
});
},
).padding(right: 12),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
visualDensity: VisualDensity.compact,
),
style: TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
IconButton(
onPressed: () async {
onLaunch?.call();
final value = await PostComposeSheet.show(
context,
initialState: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
);
if (value != null) onPosted?.call();
},
icon: const Icon(Symbols.launch, size: 20),
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
IconButton(
icon:
submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
],
style: TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
const Gap(8),
IconButton(
onPressed: () async {
onLaunch?.call();
final value = await PostComposeSheet.show(
context,
initialState: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
);
if (value != null) onPosted?.call();
},
icon: const Icon(Symbols.launch, size: 20),
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
IconButton(
icon: submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
],
),
),
),
loading: () => const SizedBox.shrink(),
error: (e, _) => const SizedBox.shrink(),
);

View File

@@ -148,7 +148,7 @@ class PostReplyPreview extends HookConsumerWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
@override
@@ -448,7 +448,7 @@ class ReferencedPostWidget extends StatelessWidget {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
fileId: post.publisher!.picture?.id,
file: post.publisher!.picture,
radius: radius,
);
}
@@ -457,7 +457,7 @@ class ReferencedPostWidget extends StatelessWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getDisplayName(SnPost post) {
@@ -701,7 +701,7 @@ class PostHeader extends HookConsumerWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getDisplayName(SnPost post) {

View File

@@ -21,63 +21,57 @@ class PublisherModal extends HookConsumerWidget {
children: [
Expanded(
child: publishers.when(
data:
(value) =>
value.isEmpty
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'publishersEmpty',
textAlign: TextAlign.center,
).tr().fontSize(17).bold(),
Text(
'publishersEmptyDescription',
textAlign: TextAlign.center,
).tr(),
const Gap(12),
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) =>
const NewPublisherScreen(),
).then((value) {
if (value != null) {
ref.invalidate(
publishersManagedProvider,
);
}
});
},
child: Text('createPublisher').tr(),
),
],
).center(),
)
: SingleChildScrollView(
child: Column(
children: [
for (final publisher in value)
ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () {
Navigator.pop(context, publisher);
},
),
],
),
data: (value) => value.isEmpty
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'publishersEmpty',
textAlign: TextAlign.center,
).tr().fontSize(17).bold(),
Text(
'publishersEmptyDescription',
textAlign: TextAlign.center,
).tr(),
const Gap(12),
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) =>
const NewPublisherScreen(),
).then((value) {
if (value != null) {
ref.invalidate(publishersManagedProvider);
}
});
},
child: Text('createPublisher').tr(),
),
],
).center(),
)
: SingleChildScrollView(
child: Column(
children: [
for (final publisher in value)
ListTile(
leading: ProfilePictureWidget(
file: publisher.picture,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () {
Navigator.pop(context, publisher);
},
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'),
),

View File

@@ -30,17 +30,16 @@ class RealmListTile extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child:
realm.background == null
? const SizedBox.shrink()
: CloudImageWidget(file: realm.background),
child: realm.background == null
? const SizedBox.shrink()
: CloudImageWidget(file: realm.background),
),
),
Positioned(
bottom: -30,
left: 18,
child: ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.group,
radius: 24,
),

View File

@@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),

View File

@@ -776,19 +776,19 @@ class _ChatRoomOption extends HookConsumerWidget {
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: (isDirect && room.picture?.id == null)
child: (isDirect && room.picture == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
files: validMembers
.map((e) => e.account.profile.picture)
.toList(),
radius: 16,
)
: room.picture?.id == null
: room.picture == null
? CircleAvatar(
radius: 16,
child: Text(room.name![0].toUpperCase()),
)
: ProfilePictureWidget(fileId: room.picture?.id, radius: 16),
: ProfilePictureWidget(file: room.picture, radius: 16),
),
const SizedBox(height: 4),
// Chat room name

View File

@@ -75,31 +75,29 @@ class StickerPicker extends HookConsumerWidget {
},
);
},
loading:
() => const SizedBox(
width: 320,
height: 320,
child: Center(child: CircularProgressIndicator()),
),
error:
(err, _) => SizedBox(
width: 360,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
loading: () => const SizedBox(
width: 320,
height: 320,
child: Center(child: CircularProgressIndicator()),
),
error: (err, _) => SizedBox(
width: 360,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
),
),
);
@@ -263,7 +261,7 @@ class _StickersGrid extends StatelessWidget {
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.image.id,
file: sticker.image,
fit: BoxFit.contain,
),
),
@@ -310,31 +308,29 @@ class StickerPickerEmbedded extends HookConsumerWidget {
},
);
},
loading:
() => SizedBox(
width: 320,
height: height ?? 320,
child: const Center(child: CircularProgressIndicator()),
),
error:
(err, _) => SizedBox(
width: 360,
height: height ?? 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
loading: () => SizedBox(
width: 320,
height: height ?? 320,
child: const Center(child: CircularProgressIndicator()),
),
error: (err, _) => SizedBox(
width: 360,
height: height ?? 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
);
}
}
@@ -386,18 +382,16 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color:
selected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainer,
color: selected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border:
selected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
border: selected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
),
margin: const EdgeInsets.only(right: 8),
child: InkWell(
@@ -413,11 +407,11 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
builder: (context, value, _) {
return packs[i].icon != null
? CloudImageWidget(
file: packs[i].icon!,
).clipRRect(all: value)
file: packs[i].icon!,
).clipRRect(all: value)
: CloudImageWidget(
file: packs[i].stickers.firstOrNull?.image,
).clipRRect(all: value);
file: packs[i].stickers.firstOrNull?.image,
).clipRRect(all: value);
},
),
),
@@ -458,18 +452,17 @@ Future<void> showStickerPickerPopover(
offset: offset,
alignment: alignment ?? Alignment.topLeft,
dimBackground: true,
builder:
(ctx) => SizedBox(
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
height: 480,
child: ProviderScope(
child: StickerPicker(
onPick: (ph) {
onPick(ph);
Navigator.of(ctx).maybePop();
},
),
),
builder: (ctx) => SizedBox(
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
height: 480,
child: ProviderScope(
child: StickerPicker(
onPick: (ph) {
onPick(ph);
Navigator.of(ctx).maybePop();
},
),
),
),
);
}