Crop image for profile picture, background

This commit is contained in:
LittleSheep 2025-05-05 21:48:02 +08:00
parent f266968644
commit 937e249b87
13 changed files with 182 additions and 51 deletions

View File

@ -1,4 +1,6 @@
PODS: PODS:
- croppy (0.0.1):
- Flutter
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.9): - DKImagePickerController/Core (4.3.9):
@ -171,6 +173,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -215,6 +218,8 @@ SPEC REPOS:
- SwiftyGif - SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
croppy:
:path: ".symlinks/plugins/croppy/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
@ -259,6 +264,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60

View File

@ -1,6 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:croppy/croppy.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -100,6 +101,7 @@ class IslandApp extends HookConsumerWidget {
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
localizationsDelegates: [ localizationsDelegates: [
...context.localizationDelegates, ...context.localizationDelegates,
CroppyLocalizations.delegate,
], // this contains the cupertino one ], // this contains the cupertino one
locale: context.locale, locale: context.locale,
builder: (context, child) { builder: (context, child) {

View File

@ -29,6 +29,7 @@ class AppRouter extends RootStackRouter {
page: EditPublisherRoute.page, page: EditPublisherRoute.page,
path: '/account/me/publishers/:id/edit', path: '/account/me/publishers/:id/edit',
), ),
AutoRoute(page: AccountProfileRoute.page, path: '/account/:name'),
AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'), AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'),
AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),

View File

@ -31,54 +31,61 @@ class AccountScreen extends HookConsumerWidget {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Card( GestureDetector(
child: Column( child: Card(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
if (user.value?.profile.background != null) children: [
ClipRRect( if (user.value?.profile.background != null)
borderRadius: BorderRadius.only( ClipRRect(
topLeft: Radius.circular(8), borderRadius: BorderRadius.only(
topRight: Radius.circular(8), topLeft: Radius.circular(8),
), topRight: Radius.circular(8),
child: AspectRatio( ),
aspectRatio: 16 / 7, child: AspectRatio(
child: CloudFileWidget( aspectRatio: 16 / 7,
item: user.value!.profile.background!, child: CloudFileWidget(
fit: BoxFit.cover, item: user.value!.profile.background!,
fit: BoxFit.cover,
),
), ),
), ),
), Row(
Row( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, spacing: 16,
spacing: 16, children: [
children: [ ProfilePictureWidget(
ProfilePictureWidget( fileId: user.value?.profile.pictureId,
fileId: user.value?.profile.pictureId, radius: 24,
radius: 24, ),
), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( spacing: 4,
spacing: 4, crossAxisAlignment: CrossAxisAlignment.baseline,
crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic,
textBaseline: TextBaseline.alphabetic, children: [
children: [ Text(user.value!.nick).bold().fontSize(16),
Text(user.value!.nick).bold().fontSize(16), Text('@${user.value!.name}'),
Text('@${user.value!.name}'), ],
], ),
), Text(
Text( user.value!.profile.bio ?? 'No description yet.',
user.value!.profile.bio ?? 'No description yet.', ),
), ],
], ),
), ],
], ).padding(horizontal: 16, vertical: 16),
).padding(horizontal: 16, vertical: 16), ],
], ),
), ).padding(horizontal: 8),
).padding(horizontal: 8), onTap: () {
context.router.push(
AccountProfileRoute(name: user.value!.name),
);
},
),
const Gap(8), const Gap(8),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,

View File

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -176,10 +177,23 @@ class EditPublisherScreen extends HookConsumerWidget {
final background = useState<SnCloudFile?>(null); final background = useState<SnCloudFile?>(null);
void setPicture(String position) async { void setPicture(String position) async {
final result = await ref var result = await ref
.read(imagePickerProvider) .read(imagePickerProvider)
.pickImage(source: ImageSource.gallery); .pickImage(source: ImageSource.gallery);
if (result == null) return; 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; submitting.value = true;
try { try {

View File

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -25,10 +26,23 @@ class UpdateProfileScreen extends HookConsumerWidget {
final submitting = useState(false); final submitting = useState(false);
void updateProfilePicture(String position) async { void updateProfilePicture(String position) async {
final result = await ref var result = await ref
.read(imagePickerProvider) .read(imagePickerProvider)
.pickImage(source: ImageSource.gallery); .pickImage(source: ImageSource.gallery);
if (result == null) return; 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; submitting.value = true;
try { try {

View File

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -244,10 +245,23 @@ class EditChatScreen extends HookConsumerWidget {
}, [chat]); }, [chat]);
void setPicture(String position) async { void setPicture(String position) async {
final result = await ref var result = await ref
.read(imagePickerProvider) .read(imagePickerProvider)
.pickImage(source: ImageSource.gallery); .pickImage(source: ImageSource.gallery);
if (result == null) return; 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; submitting.value = true;
try { try {

View File

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:croppy/croppy.dart' show CropAspectRatio;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -158,10 +159,23 @@ class EditRealmScreen extends HookConsumerWidget {
}, [realm]); }, [realm]);
void setPicture(String position) async { void setPicture(String position) async {
final result = await ref var result = await ref
.read(imagePickerProvider) .read(imagePickerProvider)
.pickImage(source: ImageSource.gallery); .pickImage(source: ImageSource.gallery);
if (result == null) return; 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; submitting.value = true;
try { try {

View File

@ -1,11 +1,43 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; 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:cross_file/cross_file.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:tus_client_dart/tus_client_dart.dart'; import 'package:tus_client_dart/tus_client_dart.dart';
Future<XFile?> cropImage(
BuildContext context, {
required XFile image,
List<CropAspectRatio?>? 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<SnCloudFile?> putMediaToCloud({ Completer<SnCloudFile?> putMediaToCloud({
required dynamic fileData, // Can be XFile or List<int> (Uint8List) required dynamic fileData, // Can be XFile or List<int> (Uint8List)
required String atk, required String atk,

View File

@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
croppy
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -217,6 +217,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
cassowary:
dependency: transitive
description:
name: cassowary
sha256: f304452beaf93b9349daaeeda23f853578c9dd8674c06c6100fda0319c46b967
url: "https://pub.dev"
source: hosted
version: "0.4.3"
chalkdart: chalkdart:
dependency: transitive dependency: transitive
description: description:
@ -297,6 +305,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" 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: cross_file:
dependency: "direct main" dependency: "direct main"
description: description:
@ -449,6 +465,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
equatable:
dependency: transitive
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
expandable: expandable:
dependency: transitive dependency: transitive
description: description:

View File

@ -90,6 +90,7 @@ dependencies:
collection: ^1.19.1 collection: ^1.19.1
flutter_expandable_fab: ^2.5.0 flutter_expandable_fab: ^2.5.0
markdown_editor_plus: ^0.2.15 markdown_editor_plus: ^0.2.15
croppy: ^1.3.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
croppy
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)