reorganize and rename widgets

This commit is contained in:
Henry Hiles 2026-11-09 17:25:02 -05:00
commit d5f29dc344
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 568 additions and 547 deletions

View file

@ -0,0 +1,42 @@
import "package:color_hash/color_hash.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/helpers/mxc_image.dart";
final class const AvatarOrHash(
final Uri? avatar,
final String title, {
final Widget? fallback,
final double dimension = 24,
super.key,
}) extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final box = ColoredBox(
color: ColorHash(title).color,
child: Center(child: Icon(Icons.person, size: dimension / 2)),
);
return SizedBox(
width: dimension,
height: dimension,
child: Center(
child: ClipRRect(
borderRadius: .all(.circular((dimension - 8) / 2.5)),
child: SizedBox.square(
dimension: dimension,
child: avatar == null
? fallback ?? box
: Image(
image: MxcImage(ref, .new(mxc: avatar!, isAvatar: true)),
fit: .cover,
loadingBuilder: (_, child, loadingProgress) =>
loadingProgress == null ? child : fallback ?? box,
errorBuilder: (_, _, _) => fallback ?? box,
),
),
),
),
);
}
}

View file

@ -0,0 +1,39 @@
import "package:material_ui/material_ui.dart";
class const AvatarRing({
required final String tooltip,
required final VoidCallback onTap,
required final Widget child,
final double dimension = 24,
super.key,
}) extends StatelessWidget {
static const ringWidth = 1.5;
@override
Widget build(BuildContext context) => Tooltip(
message: tooltip,
child: Container(
padding: .all(ringWidth),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: .circle,
),
child: ClipOval(
child: SizedBox.square(
dimension: dimension,
child: Stack(
fit: .expand,
children: [
child,
// Over the child, so the ripple shows on top of avatars
Material(
type: .transparency,
child: InkWell(onTap: onTap),
),
],
),
),
),
),
);
}

View file

@ -0,0 +1,86 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_ui/material_ui.dart";
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/helpers/file_dialog_helper.dart";
import "package:nexus/helpers/hooks/saving.dart";
import "package:nexus/main.dart";
import "package:nexus/models/content/message.dart";
/// Shows [child], an avatar, with a button to upload a new one or remove it.
class const EditableAvatar({
required final Widget child,
/// Whether there's an avatar to remove.
required final bool hasAvatar,
/// Called with the uploaded image, or null when the avatar is removed.
required final Future<void> Function(Uri? url) onChanged,
final double dimension = 200,
super.key,
}) extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final saving = Saving.use();
Future<void> save(Future<void> Function() action) =>
saving.run(() => action().onError(showError));
Future<void> pick() async {
final file = await ref
.read(FileDialogHelper.provider)
.pickFile(images: true);
if (file == null || !context.mounted) return;
await save(() async {
final content = await ref
.read(ClientController.provider.notifier)
.uploadMedia(.new(path: file.path, encrypt: false));
if (content case ImageMessageContent(:final url)) {
await onChanged(url);
} else {
throw Exception("The file isn't an image");
}
});
}
return SizedBox.square(
dimension: dimension,
child: Stack(
children: [
child,
if (saving.active) const Center(child: CircularProgressIndicator()),
Positioned(
right: dimension * 0.01,
bottom: dimension * 0.01,
child: Builder(
builder: (context) => IconButton.filled(
tooltip: "Edit avatar",
icon: Icon(Icons.edit),
onPressed: saving.active
? null
: !hasAvatar
? pick
: () => context.showAdaptiveMenu([
PopupMenuItem(
onTap: pick,
child: ListTile(
leading: Icon(Icons.upload),
title: Text("Upload new avatar"),
),
),
PopupMenuItem(
onTap: () => save(() => onChanged(null)),
child: ListTile(
leading: Icon(Icons.delete),
title: Text("Remove avatar"),
),
),
]),
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,14 @@
import "package:material_ui/material_ui.dart";
import "package:nexus/widgets/emoji/mxc_emoji.dart";
class const ImagePackAvatar(final Uri? icon, {super.key})
extends StatelessWidget {
@override
Widget build(BuildContext context) => SizedBox.square(
dimension: 40,
child: switch (icon) {
final icon? => MxcEmoji(icon),
null => Icon(Icons.emoji_emotions),
},
);
}