♻️ 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

@@ -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'),
),