Here is the best solution I have found to crop uploaded image and save back to Firebase storage, only using the FF interface, without needing to create a separate code branch.
Please note, this example works, but is still rough. FF gives me random compile errors, but it works.
Feel free to add to it. See more at end of this post.
Dependency: https://pub.dev/packages/crop_your_image
Steps:
Please note, this example works, but is still rough. FF gives me random compile errors, but it works.
Feel free to add to it. See more at end of this post.
Dependency: https://pub.dev/packages/crop_your_image
Steps:
- On a page create a button or icon with the Upload Media to Firebase Action.
- Create a Local/App State variable and name it croppedImage type ImagePath:
- Create a Custom Widget name it PhotoCropUI add the parameters and dependency as shown below in screen shot:
Add the below code to the widget
// Automatic FlutterFlow imports import '/backend/backend.dart'; import '/flutter_flow/flutter_flow_theme.dart'; import '/flutter_flow/flutter_flow_util.dart'; import '/custom_code/widgets/index.dart'; // Imports other custom widgets import '/custom_code/actions/index.dart'; // Imports custom actions import '/flutter_flow/custom_functions.dart'; // Imports custom functions import 'package:flutter/material.dart'; // Begin custom widget code // DO NOT REMOVE OR MODIFY THE CODE ABOVE! //https://github.com/chooyan-eng/crop_your_image import '/flutter_flow/flutter_flow_widgets.dart'; import 'package:crop_your_image/crop_your_image.dart'; import 'package:google_fonts/google_fonts.dart'; import '/auth/auth_util.dart'; import '/backend/firebase_storage/storage.dart'; class PhotoCropUI extends StatefulWidget { const PhotoCropUI({ Key? key, this.width, this.height, this.imageFile, this.top, this.right, this.bottom, this.left, this.callBackAction, }) : super(key: key); final double? width; final double? height; final FFUploadedFile? imageFile; final double? top; final double? right; final double? bottom; final double? left; final Future<dynamic> Function()? callBackAction; @override _PhotoCropUIState createState() => _PhotoCropUIState(); } class _PhotoCropUIState extends State<PhotoCropUI> { bool loading = false; final _crop_controller = CropController(); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: widget.width ?? double.infinity, height: widget.height ?? 555, child: Center( child: Crop( image: Uint8List.fromList(widget.imageFile!.bytes!), controller: _crop_controller, onCropped: (image) async { // do something with image data // FFAppState().croppedImage = ''; //Future<String?>; downloadUrls; final path = _getStoragePath( _firebasePathPrefix(), widget.imageFile!.name!, false, 0); uploadData(path, image).then((value) { FFAppState().croppedImage = value!; print('image cropped'); widget.callBackAction!.call(); loading = false; }); // add error handling here }, aspectRatio: 1 / 1, // initialSize: 0.5, // initialArea: Rect.fromLTWH(240, 212, 800, 600),\ //initialAreaBuilder: (rect) => Rect.fromLTRB(rect.left + 80, rect.top + 80, rect.right - 80, rect.bottom - 80), withCircleUi: true, baseColor: Color.fromARGB(255, 0, 3, 22), maskColor: Colors.white.withAlpha(100), radius: 20, onMoved: (newRect) { // do something with current cropping area. }, onStatusChanged: (status) { // do something with current CropStatus }, cornerDotBuilder: (size, edgeAlignment) => const DotControl(color: Color.fromARGB(255, 206, 18, 56)), interactive: true, // fixArea: true, ))), Padding( padding: EdgeInsetsDirectional.fromSTEB(8, 5, 8, 5), child: FFButtonWidget( onPressed: () async { if (!loading) { loading = true; print('Button pressed ...'); await Future.delayed(const Duration(milliseconds: 1555), () { _crop_controller.crop(); }); //widget.loading = true; } }, showLoadingIndicator: true, text: 'Crop', options: FFButtonOptions( width: 250, height: 50, padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0), iconPadding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0), color: FlutterFlowTheme.of(context).primaryColor, textStyle: FlutterFlowTheme.of(context).subtitle2.override( fontFamily: 'Lexend', color: Colors.white, fontSize: 16, fontWeight: FontWeight.normal, useGoogleFonts: GoogleFonts.asMap().containsKey( FlutterFlowTheme.of(context).subtitle2Family), ), borderSide: BorderSide( color: Colors.transparent, width: 0, ), borderRadius: BorderRadius.circular(100), ), ), ), ]); } String _getStoragePath( String? pathPrefix, String filePath, bool isVideo, [ int? index, ]) { pathPrefix ??= _firebasePathPrefix(); pathPrefix = _removeTrailingSlash(pathPrefix); final timestamp = DateTime.now().microsecondsSinceEpoch; final prefix = 'cropped-'; // Workaround fixed by https://github.com/flutter/plugins/pull/3685 // (not yet in stable). final ext = isVideo ? 'mp4' : filePath.split('.').last; final indexStr = index != null ? '_$index' : ''; return '$pathPrefix/$prefix$timestamp$indexStr.$ext'; } String? _removeTrailingSlash(String? path) => path != null && path.endsWith('/') ? path.substring(0, path.length - 1) : path; String _firebasePathPrefix() => 'users/$currentUserUid/uploads'; }
From here it is up to you, I did the following:
Created a Bottom Sheet define a parameter on the bottom sheet - uploadedFile Type Uploaded File (bytes).
On the page you created in STEP 1 Upload Button's action add Open Bottom Sheet action after the Upload Media to Firebase Action. Set the uploadedFile parameter to Uploaded Local File
The widget will save the new Firebase Storage URL to the app/local state variable croppedImage you created in STEP2
Lastly, use the widget parameter callBackAction (as show in screenshot above) to save the image URL back to a Firestore record.
Lastly, use the widget parameter callBackAction (as show in screenshot above) to save the image URL back to a Firestore record.
Set your Firebase imagePath field to the croppedImage local/app state variable.
I also add a condition to check if croppedImage is "Is set and not empty" before saving.
I plan on making the storagePath a parameter, this example saves the cropped file to the user's storage path using code from the FF SelectedMedia class found here:
/lib/flutter_flow/upload_data.dart
Also, I added a FF button and it is using styling from my app. These should be parameters on the custom widget. For now you can change the style of the button in the widget code above, here is an excerpt:
/lib/flutter_flow/upload_data.dart
Also, I added a FF button and it is using styling from my app. These should be parameters on the custom widget. For now you can change the style of the button in the widget code above, here is an excerpt:
options: FFButtonOptions(
height: 50,
padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
iconPadding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
color: FlutterFlowTheme.of(context).accentGreen,
textStyle: FlutterFlowTheme.of(context).subtitle2.override(
fontFamily: 'Lexend',
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.normal,
useGoogleFonts: GoogleFonts.asMap().containsKey(
FlutterFlowTheme.of(context).subtitle2Family),
),
borderSide: BorderSide(
color: Colors.transparent,
width: 0,
),
borderRadius: BorderRadius.circular(100),
),In addition, I need to add parameters for the crop_your_image package options, if you need to change these in the meantime edit them in the custom widget code:
withCircleUi: true
aspectRatio: 4 / 3,
baseColor: Colors.blue.shade900,
maskColor: Colors.white.withAlpha(100),
radius: 20,
NOTE: On locking the aspect ratio of the crop.
Comment out this option if you want to lock the aspectRatio, it took me a while to figure this out.
Rect.fromLTRB(rect.left + 80,
rect.top + 80, rect.right - 80, rect.bottom - 80),
JH
JM
HK
Liked by Joshua and 6 others
4 comments
uhhhhh. I guess some users would like to see that as a marketplace item or even implemented in FF 😊🦾 Nice job!
Great work.
Do you need to upload it before the crop ?
I want to implement it using my own backend API.
Do you need to upload it before the crop ?
I want to implement it using my own backend API.
Post a comment