From 937e249b87ce03c3102466f4be49fa8d576ae45a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 5 May 2025 21:48:02 +0800 Subject: [PATCH] :sparkles: Crop image for profile picture, background --- ios/Podfile.lock | 6 ++ lib/main.dart | 2 + lib/route.dart | 1 + lib/screens/account.dart | 99 +++++++++++++------------ lib/screens/account/me/publishers.dart | 16 +++- lib/screens/account/me/update.dart | 16 +++- lib/screens/chat/chat.dart | 16 +++- lib/screens/realm/realms.dart | 16 +++- lib/services/file.dart | 34 ++++++++- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 24 ++++++ pubspec.yaml | 1 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 182 insertions(+), 51 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 79c8167..d314512 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - croppy (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -171,6 +173,7 @@ PODS: - Flutter DEPENDENCIES: + - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -215,6 +218,8 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: + croppy: + :path: ".symlinks/plugins/croppy/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -259,6 +264,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: + croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 diff --git a/lib/main.dart b/lib/main.dart index 9b40f87..4fefc41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:io'; +import 'package:croppy/croppy.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; @@ -100,6 +101,7 @@ class IslandApp extends HookConsumerWidget { supportedLocales: context.supportedLocales, localizationsDelegates: [ ...context.localizationDelegates, + CroppyLocalizations.delegate, ], // this contains the cupertino one locale: context.locale, builder: (context, child) { diff --git a/lib/route.dart b/lib/route.dart index dc56694..ebb236d 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -29,6 +29,7 @@ class AppRouter extends RootStackRouter { page: EditPublisherRoute.page, path: '/account/me/publishers/:id/edit', ), + AutoRoute(page: AccountProfileRoute.page, path: '/account/:name'), AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'), AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 6c457b2..31b81b3 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -31,54 +31,61 @@ class AccountScreen extends HookConsumerWidget { body: SingleChildScrollView( child: Column( children: [ - Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (user.value?.profile.background != null) - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: AspectRatio( - aspectRatio: 16 / 7, - child: CloudFileWidget( - item: user.value!.profile.background!, - fit: BoxFit.cover, + GestureDetector( + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (user.value?.profile.background != null) + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + child: AspectRatio( + aspectRatio: 16 / 7, + child: CloudFileWidget( + item: user.value!.profile.background!, + fit: BoxFit.cover, + ), ), ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 16, - children: [ - ProfilePictureWidget( - fileId: user.value?.profile.pictureId, - radius: 24, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 4, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text(user.value!.nick).bold().fontSize(16), - Text('@${user.value!.name}'), - ], - ), - Text( - user.value!.profile.bio ?? 'No description yet.', - ), - ], - ), - ], - ).padding(horizontal: 16, vertical: 16), - ], - ), - ).padding(horizontal: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 16, + children: [ + ProfilePictureWidget( + fileId: user.value?.profile.pictureId, + radius: 24, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 4, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(user.value!.nick).bold().fontSize(16), + Text('@${user.value!.name}'), + ], + ), + Text( + user.value!.profile.bio ?? 'No description yet.', + ), + ], + ), + ], + ).padding(horizontal: 16, vertical: 16), + ], + ), + ).padding(horizontal: 8), + onTap: () { + context.router.push( + AccountProfileRoute(name: user.value!.name), + ); + }, + ), const Gap(8), ListTile( minTileHeight: 48, diff --git a/lib/screens/account/me/publishers.dart b/lib/screens/account/me/publishers.dart index 286b817..2ba58fc 100644 --- a/lib/screens/account/me/publishers.dart +++ b/lib/screens/account/me/publishers.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:croppy/croppy.dart' hide cropImage; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -176,10 +177,23 @@ class EditPublisherScreen extends HookConsumerWidget { final background = useState(null); void setPicture(String position) async { - final result = await ref + var result = await ref .read(imagePickerProvider) .pickImage(source: ImageSource.gallery); if (result == null) return; + if (!context.mounted) return; + result = await cropImage( + context, + image: result, + allowedAspectRatios: [ + if (position == 'background') + CropAspectRatio(height: 7, width: 16) + else + CropAspectRatio(height: 1, width: 1), + ], + ); + if (result == null) return; + if (!context.mounted) return; submitting.value = true; try { diff --git a/lib/screens/account/me/update.dart b/lib/screens/account/me/update.dart index 6bceeb4..4d9e262 100644 --- a/lib/screens/account/me/update.dart +++ b/lib/screens/account/me/update.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:croppy/croppy.dart' hide cropImage; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -25,10 +26,23 @@ class UpdateProfileScreen extends HookConsumerWidget { final submitting = useState(false); void updateProfilePicture(String position) async { - final result = await ref + var result = await ref .read(imagePickerProvider) .pickImage(source: ImageSource.gallery); if (result == null) return; + if (!context.mounted) return; + result = await cropImage( + context, + image: result, + allowedAspectRatios: [ + if (position == 'background') + CropAspectRatio(height: 7, width: 16) + else + CropAspectRatio(height: 1, width: 1), + ], + ); + if (result == null) return; + if (!context.mounted) return; submitting.value = true; try { diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 7426164..9c6f011 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:croppy/croppy.dart' hide cropImage; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -244,10 +245,23 @@ class EditChatScreen extends HookConsumerWidget { }, [chat]); void setPicture(String position) async { - final result = await ref + var result = await ref .read(imagePickerProvider) .pickImage(source: ImageSource.gallery); if (result == null) return; + if (!context.mounted) return; + result = await cropImage( + context, + image: result, + allowedAspectRatios: [ + if (position == 'background') + CropAspectRatio(height: 7, width: 16) + else + CropAspectRatio(height: 1, width: 1), + ], + ); + if (result == null) return; + if (!context.mounted) return; submitting.value = true; try { diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 2e05ae4..532897e 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:croppy/croppy.dart' show CropAspectRatio; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -158,10 +159,23 @@ class EditRealmScreen extends HookConsumerWidget { }, [realm]); void setPicture(String position) async { - final result = await ref + var result = await ref .read(imagePickerProvider) .pickImage(source: ImageSource.gallery); if (result == null) return; + if (!context.mounted) return; + result = await cropImage( + context, + image: result, + allowedAspectRatios: [ + if (position == 'background') + CropAspectRatio(height: 7, width: 16) + else + CropAspectRatio(height: 1, width: 1), + ], + ); + if (result == null) return; + if (!context.mounted) return; submitting.value = true; try { diff --git a/lib/services/file.dart b/lib/services/file.dart index f1ad42d..114c1a2 100644 --- a/lib/services/file.dart +++ b/lib/services/file.dart @@ -1,11 +1,43 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; +import 'dart:io'; +import 'dart:ui'; +import 'package:croppy/croppy.dart'; import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:island/models/file.dart'; import 'package:tus_client_dart/tus_client_dart.dart'; +Future cropImage( + BuildContext context, { + required XFile image, + List? allowedAspectRatios, +}) async { + final result = await showMaterialImageCropper( + context, + imageProvider: + kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)), + showLoadingIndicatorOnSubmit: true, + allowedAspectRatios: allowedAspectRatios, + ); + if (result == null) return null; // Cancelled operation + final croppedFile = result.uiImage; + final croppedBytes = await croppedFile.toByteData( + format: ImageByteFormat.png, + ); + if (croppedBytes == null) { + return image; + } + croppedFile.dispose(); + return XFile.fromData( + croppedBytes.buffer.asUint8List(), + path: image.path, + mimeType: image.mimeType, + ); +} + Completer putMediaToCloud({ required dynamic fileData, // Can be XFile or List (Uint8List) required String atk, diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1c913d8..a3f64ab 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + croppy ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 0cced39..2d00503 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + cassowary: + dependency: transitive + description: + name: cassowary + sha256: f304452beaf93b9349daaeeda23f853578c9dd8674c06c6100fda0319c46b967 + url: "https://pub.dev" + source: hosted + version: "0.4.3" chalkdart: dependency: transitive description: @@ -297,6 +305,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + croppy: + dependency: "direct main" + description: + name: croppy + sha256: "2a69059d9ec007b79d6a494854094b2e3c0a4f7ed609cf55a4805c9de9ec171d" + url: "https://pub.dev" + source: hosted + version: "1.3.6" cross_file: dependency: "direct main" description: @@ -449,6 +465,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" expandable: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acf8695..81358fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: collection: ^1.19.1 flutter_expandable_fab: ^2.5.0 markdown_editor_plus: ^0.2.15 + croppy: ^1.3.6 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4df7e22..ecdf13b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + croppy ) set(PLUGIN_BUNDLED_LIBRARIES)