♻️ Better image loading animation and more commonly used blurhash
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
String _getPublisherName() {
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user