From 230145c265ae9468ce314081318cf5c5c40eb26b Mon Sep 17 00:00:00 2001 From: Hitori <113356370+Hitori638@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:25:18 +0100 Subject: [PATCH 1/3] feat: Add avatar cropping support for Desktop and Web --- lib/pages/settings/settings.dart | 16 +++++-- lib/widgets/avatar_crop_dialog.dart | 72 +++++++++++++++++++++++++++++ pubspec.lock | 8 ++++ pubspec.yaml | 1 + 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 lib/widgets/avatar_crop_dialog.dart diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 2611af782..3d2bb7475 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -16,6 +17,7 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/avatar_crop_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_view.dart'; @@ -139,10 +141,16 @@ class SettingsController extends State { final result = await selectFiles(context, type: FileType.image); final pickedFile = result.firstOrNull; if (pickedFile == null) return; - file = MatrixFile( - bytes: await pickedFile.readAsBytes(), - name: pickedFile.name, - ); + var bytes = await pickedFile.readAsBytes(); + if (PlatformInfos.isDesktop || PlatformInfos.isWeb) { + final cropped = await showDialog( + context: context, + builder: (context) => AvatarCropDialog(image: bytes), + ); + if (cropped == null) return; + bytes = cropped; + } + file = MatrixFile(bytes: bytes, name: pickedFile.name); } final success = await showFutureLoadingDialog( context: context, diff --git a/lib/widgets/avatar_crop_dialog.dart b/lib/widgets/avatar_crop_dialog.dart new file mode 100644 index 000000000..04925f1ba --- /dev/null +++ b/lib/widgets/avatar_crop_dialog.dart @@ -0,0 +1,72 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import 'package:crop_image/crop_image.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; + +class AvatarCropDialog extends StatefulWidget { + final Uint8List image; + + const AvatarCropDialog({super.key, required this.image}); + + @override + AvatarCropDialogController createState() => AvatarCropDialogController(); +} + +class AvatarCropDialogController extends State { + final controller = CropController( + aspectRatio: 1, + defaultCrop: const Rect.fromLTWH(0.1, 0.1, 0.8, 0.8), + ); + + void onCancelAction() => Navigator.of(context).pop(); + + void onCropAction() async { + final image = await controller.croppedBitmap(); + if (mounted) { + final data = await image.toByteData(format: ui.ImageByteFormat.png); + Navigator.of(context).pop(data?.buffer.asUint8List()); + } + } + + @override + Widget build(BuildContext context) => AvatarCropDialogView(this); +} + +class AvatarCropDialogView extends StatelessWidget { + final AvatarCropDialogController controller; + + const AvatarCropDialogView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(L10n.of(context).changeYourAvatar), + content: SizedBox( + width: 400, + height: 400, + child: CropImage( + controller: controller.controller, + image: Image.memory(controller.widget.image), + gridColor: Colors.white, + gridCornerSize: 20, + touchSize: 20, + alwaysShowThirdLines: true, + ), + ), + actions: [ + TextButton( + onPressed: controller.onCancelAction, + child: Text(L10n.of(context).cancel), + ), + TextButton( + onPressed: controller.onCropAction, + child: Text(L10n.of(context).ok), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 78d1b7508..94091a905 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + crop_image: + dependency: "direct main" + description: + name: crop_image + sha256: "27cbce1685a595efee62caab81c98b49b636f765c1da86353f58f5b2bf2775d8" + url: "https://pub.dev" + source: hosted + version: "1.0.17" cross_file: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 99115e514..4202dddf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: blurhash_dart: ^1.2.1 chewie: ^1.13.0 collection: ^1.18.0 + crop_image: ^1.0.17 cross_file: ^0.3.5 cupertino_icons: any desktop_drop: ^0.7.0 From 4ae7545e61ce2574898d6b8253028a83d437debf Mon Sep 17 00:00:00 2001 From: Hitori <113356370+Hitori638@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:50:53 +0000 Subject: [PATCH 2/3] chore: Sort imports --- lib/pages/settings/settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 3d2bb7475..46b182d94 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -16,8 +16,8 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/avatar_crop_dialog.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_view.dart'; From bc9b9f3a920cd1315d33b4ccbb3393b46168c147 Mon Sep 17 00:00:00 2001 From: Hitori <113356370+Hitori638@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:02:39 +0100 Subject: [PATCH 3/3] fix: resolve avoid_void_async lint --- lib/widgets/avatar_crop_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/avatar_crop_dialog.dart b/lib/widgets/avatar_crop_dialog.dart index 04925f1ba..b64494b8d 100644 --- a/lib/widgets/avatar_crop_dialog.dart +++ b/lib/widgets/avatar_crop_dialog.dart @@ -24,7 +24,7 @@ class AvatarCropDialogController extends State { void onCancelAction() => Navigator.of(context).pop(); - void onCropAction() async { + Future onCropAction() async { final image = await controller.croppedBitmap(); if (mounted) { final data = await image.toByteData(format: ui.ImageByteFormat.png);