diff --git a/lib/repositories/user_repository.dart b/lib/repositories/user_repository.dart index dca5683..33ca781 100644 --- a/lib/repositories/user_repository.dart +++ b/lib/repositories/user_repository.dart @@ -1,10 +1,16 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; import 'package:flutter_base/network/api_client.dart'; import '../models/entities/user/user_entity.dart'; abstract class UserRepository { Future getProfile(); + Future updateProfile({required UserEntity userEntity}); + + Future uploadAvatar(File file); } class UserRepositoryImpl extends UserRepository { @@ -23,4 +29,15 @@ class UserRepositoryImpl extends UserRepository { Future updateProfile({required UserEntity userEntity}) async { return UserEntity.updateProfile(userEntity: userEntity); } + + @override + Future uploadAvatar(File file) async { + FormData.fromMap({ + "file": await MultipartFile.fromFile( + file.path, + ), + }); + await Future.delayed(const Duration(seconds: 2)); + return UserEntity.mockData(); + } } diff --git a/lib/ui/pages/profile/update_avatar/update_avatar_cubit.dart b/lib/ui/pages/profile/update_avatar/update_avatar_cubit.dart index 20b9453..03546ae 100644 --- a/lib/ui/pages/profile/update_avatar/update_avatar_cubit.dart +++ b/lib/ui/pages/profile/update_avatar/update_avatar_cubit.dart @@ -1,17 +1,45 @@ import 'dart:io'; import 'package:equatable/equatable.dart'; +import 'package:flutter_base/global_blocs/user/user_cubit.dart'; import 'package:flutter_base/models/entities/user/user_entity.dart'; import 'package:flutter_base/models/enums/load_status.dart'; +import 'package:flutter_base/repositories/user_repository.dart'; import 'package:flutter_base/ui/pages/profile/update_avatar/update_avatar_navigator.dart'; +import 'package:flutter_base/utils/logger.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; part 'update_avatar_state.dart'; class UpdateAvatarCubit extends Cubit { final UpdateAvatarNavigator navigator; + final UserRepository userRepository; + final UserCubit userCubit; UpdateAvatarCubit({ required this.navigator, + required this.userRepository, + required this.userCubit, }) : super(const UpdateAvatarState()); + + Future updateImage(File file) async { + emit(state.copyWith(updateImageStatus: LoadStatus.loading)); + try { + final result = await userRepository.uploadAvatar(file); + if (result != null) { + userCubit.updateUser(result); + emit(state.copyWith( + updateImageStatus: LoadStatus.success, + image: file, + )); + navigator.showSuccessFlushbar(message: 'Update Avatar Successfully!'); + } else { + emit(state.copyWith(updateImageStatus: LoadStatus.failure)); + navigator.showErrorFlushbar(message: 'Update Avatar Failed!'); + } + } catch (error) { + logger.e(error); + emit(state.copyWith(updateImageStatus: LoadStatus.failure)); + } + } } diff --git a/lib/ui/pages/profile/update_avatar/update_avatar_navigator.dart b/lib/ui/pages/profile/update_avatar/update_avatar_navigator.dart index 0ad7ed7..820666e 100644 --- a/lib/ui/pages/profile/update_avatar/update_avatar_navigator.dart +++ b/lib/ui/pages/profile/update_avatar/update_avatar_navigator.dart @@ -1,5 +1,15 @@ +import 'package:flutter/material.dart'; import 'package:flutter_base/common/app_navigator.dart'; class UpdateAvatarNavigator extends AppNavigator { UpdateAvatarNavigator({required super.context}); + + Future showBottomSheet({required Widget child}) async { + await showModalBottomSheet( + context: context, + builder: (context) { + return child; + }, + ); + } } diff --git a/lib/ui/pages/profile/update_avatar/update_avatar_page.dart b/lib/ui/pages/profile/update_avatar/update_avatar_page.dart index fd05cc3..b9f363f 100644 --- a/lib/ui/pages/profile/update_avatar/update_avatar_page.dart +++ b/lib/ui/pages/profile/update_avatar/update_avatar_page.dart @@ -1,8 +1,15 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_base/global_blocs/user/user_cubit.dart'; +import 'package:flutter_base/models/enums/load_status.dart'; +import 'package:flutter_base/repositories/user_repository.dart'; import 'package:flutter_base/ui/pages/profile/update_avatar/update_avatar_navigator.dart'; +import 'package:flutter_base/ui/widgets/appbar/app_bar_widget.dart'; +import 'package:flutter_base/ui/widgets/divider/app_divider.dart'; import 'package:flutter_base/ui/widgets/images/app_circle_avatar.dart'; +import 'package:flutter_base/ui/widgets/loading/app_loading_indicator.dart'; +import 'package:flutter_base/ui/widgets/picker/app_image_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'update_avatar_cubit.dart'; @@ -16,8 +23,12 @@ class UpdateAvatarPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) { + final userRepo = RepositoryProvider.of(context); + final userCubit = RepositoryProvider.of(context); return UpdateAvatarCubit( navigator: UpdateAvatarNavigator(context: context), + userRepository: userRepo, + userCubit: userCubit, ); }, child: const UpdateAvatarChildPage(), @@ -44,23 +55,27 @@ class _UpdateAvatarChildPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text("Avatar"), + appBar: AppBarWidget( + title: "Avatar", actions: [ BlocBuilder( builder: (context, state) { return _cubit.state.image == null ? TextButton( onPressed: () async { - showOption( - chooseImageCollection: () {}, - chooseImageCamera: () {}); + if (_cubit.state.updateImageStatus == + LoadStatus.loading) return; + showOption(); }, - child: const Text("thay đổi"), + child: const Text("Upload"), ) : TextButton( - onPressed: () async {}, - child: const Text("cập nhật"), + onPressed: () async { + if (_cubit.state.updateImageStatus == + LoadStatus.loading) return; + showOption(); + }, + child: const Text("Update"), ); }, ) @@ -76,72 +91,92 @@ class _UpdateAvatarChildPageState extends State { return Center( child: BlocBuilder( builder: (context, state) { - return state.image == null - ? const AppCircleAvatar(size: Size(400, 400)) - : ClipRRect( - borderRadius: BorderRadius.circular(200), - child: Image.file( - state.image ?? File(''), - width: 400, - height: 400, - fit: BoxFit.cover, - ), - ); + return state.updateImageStatus == LoadStatus.loading + ? const AppCircularProgressIndicator() + : state.image == null + ? const AppCircleAvatar(size: Size(400, 400)) + : ClipRRect( + borderRadius: BorderRadius.circular(200), + child: Image.file( + state.image ?? File(''), + width: 400, + height: 400, + fit: BoxFit.cover, + ), + ); }, ), ); } - Future showOption({ - required Function() chooseImageCollection, - required Function() chooseImageCamera, - }) async { - // AppBottomSheet.show(Container( - // height: 200, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(20), - // color: Colors.white, - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceAround, - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // InkWell( - // onTap: () { - // chooseImageCollection(); - // }, - // child: const Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon( - // Icons.collections, - // size: 60, - // color: Colors.grey, - // ), - // Text( - // "choose from the collection", - // ), - // ], - // ), - // ), - // const InkWell( - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon( - // Icons.photo_camera, - // size: 60, - // color: Colors.grey, - // ), - // Text( - // 'take a photo', - // ), - // ], - // ), - // ), - // ], - // ), - // )); + Future showOption() async { + await _cubit.navigator.showBottomSheet( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.white, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Edit Image", + ), + const AppDivider( + height: 16, + thickness: 1, + ), + ListTile( + onTap: () async { + await onChooseImageFromGallery(); + }, + leading: const Icon( + Icons.collections, + size: 24, + color: Colors.red, + ), + title: const Text( + "Choose from the collection", + ), + ), + ListTile( + onTap: () async { + await onChooseImageFromCamera(); + }, + leading: const Icon( + Icons.photo_camera, + size: 24, + color: Colors.green, + ), + title: const Text( + "Take a photo", + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Future onChooseImageFromGallery() async { + final result = await AppImagePicker.getImageFromGallery(context); + _cubit.navigator.pop(); + if (result != null) { + final file = File(result.path); + await _cubit.updateImage(file); + } + } + + Future onChooseImageFromCamera() async { + final result = await AppImagePicker.getImageFromCamera(context); + _cubit.navigator.pop(); + if (result != null) { + final file = File(result.path); + await _cubit.updateImage(file); + } } @override diff --git a/lib/ui/pages/profile/update_avatar/update_avatar_state.dart b/lib/ui/pages/profile/update_avatar/update_avatar_state.dart index ad4d786..378bcfd 100644 --- a/lib/ui/pages/profile/update_avatar/update_avatar_state.dart +++ b/lib/ui/pages/profile/update_avatar/update_avatar_state.dart @@ -4,15 +4,13 @@ class UpdateAvatarState extends Equatable { final LoadStatus userStatus; final UserEntity? user; final File? image; - final LoadStatus imageCollectionStatus; - final LoadStatus imageCameraStatus; + final LoadStatus updateImageStatus; const UpdateAvatarState({ this.userStatus = LoadStatus.initial, this.user, this.image, - this.imageCollectionStatus = LoadStatus.initial, - this.imageCameraStatus = LoadStatus.initial, + this.updateImageStatus = LoadStatus.initial, }); @override @@ -20,24 +18,20 @@ class UpdateAvatarState extends Equatable { userStatus, user, image, - imageCollectionStatus, - imageCameraStatus, + updateImageStatus, ]; UpdateAvatarState copyWith({ LoadStatus? userStatus, UserEntity? user, File? image, - LoadStatus? imageCollectionStatus, - LoadStatus? imageCameraStatus, + LoadStatus? updateImageStatus, }) { return UpdateAvatarState( userStatus: userStatus ?? this.userStatus, user: user ?? this.user, image: image ?? this.image, - imageCollectionStatus: - imageCollectionStatus ?? this.imageCollectionStatus, - imageCameraStatus: imageCameraStatus ?? this.imageCameraStatus, + updateImageStatus: updateImageStatus ?? this.updateImageStatus, ); } } diff --git a/lib/ui/widgets/appbar/app_bar_widget.dart b/lib/ui/widgets/appbar/app_bar_widget.dart index 800bd5e..dd90daf 100644 --- a/lib/ui/widgets/appbar/app_bar_widget.dart +++ b/lib/ui/widgets/appbar/app_bar_widget.dart @@ -9,12 +9,14 @@ class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { final String title; final bool showBackButton; final VoidCallback? onBackPressed; + final List? actions; const AppBarWidget({ super.key, required this.title, this.showBackButton = true, this.onBackPressed, + this.actions, }); @override @@ -49,6 +51,7 @@ class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { ), ) : const SizedBox(), + actions: actions, ), ); } diff --git a/lib/ui/widgets/divider/app_divider.dart b/lib/ui/widgets/divider/app_divider.dart index 3c4615c..e4bc877 100644 --- a/lib/ui/widgets/divider/app_divider.dart +++ b/lib/ui/widgets/divider/app_divider.dart @@ -6,8 +6,9 @@ class AppDivider extends Divider { super.key, double super.indent = 0, double super.endIndent = 0, + double super.thickness = 0, + double super.height = 0, }) : super( color: AppColors.divider, - height: 1, ); } diff --git a/lib/ui/widgets/picker/app_image_picker.dart b/lib/ui/widgets/picker/app_image_picker.dart new file mode 100644 index 0000000..77e19aa --- /dev/null +++ b/lib/ui/widgets/picker/app_image_picker.dart @@ -0,0 +1,55 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_base/utils/permission_utils.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; + +class AppImagePicker { + static Future getImageFromCamera(BuildContext context) async { + final isPhotosPermissionGranted = + await PermissionUtils.requestPhotosPermission(context); + + final isCameraPermissionGranted = + // ignore: use_build_context_synchronously + await PermissionUtils.requestCameraPermission(context); + + if (isCameraPermissionGranted && isPhotosPermissionGranted) { + XFile? imageFile = await ImagePicker().pickImage( + source: ImageSource.camera, + ); + if (imageFile != null) { + final temp = await getTemporaryDirectory(); + final savePath = '${temp.path}/${imageFile.name}.jpeg'; + final result = await FlutterImageCompress.compressAndGetFile( + imageFile.path, + savePath, + quality: 80, + ); + return result; + } + } + return null; + } + + static Future getImageFromGallery(BuildContext context) async { + final isPhotosPermissionGranted = + await PermissionUtils.requestPhotosPermission(context); + + if (isPhotosPermissionGranted) { + final imageFile = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + if (imageFile != null) { + final temp = await getTemporaryDirectory(); + final savePath = '${temp.path}/${imageFile.name}.jpeg'; + final result = await FlutterImageCompress.compressAndGetFile( + imageFile.path, + savePath, + quality: 80, + ); + return result; + } + } + return null; + } +} diff --git a/pubspec.lock b/pubspec.lock index b028d89..5e5ea48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.7.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.dev" + source: hosted + version: "0.3.3+8" crypto: dependency: transitive description: @@ -305,6 +313,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -334,6 +374,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: f159d2e8c4ed04b8e36994124fd4a5017a0f01e831ae3358c74095c340e9ae5e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: "0756440ddc647696ebc56f393be8c1055e81ef1c1f58986fb1f078af393637d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: fea1e3d71150d03373916b832c49b5c2f56c3e7e13da82a929274a2c6f88251e + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "87d0964ae72ccab1cdfbc32f825ce6b57bf87fd3576a2842bf38189d392163b8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: "1c8cd505be95cb2e0573cdd9d236ac0ba39c166b8df06ef1823f2a17b60e1253" + url: "https://pub.dev" + source: hosted + version: "0.1.4" flutter_inappwebview: dependency: "direct main" description: @@ -419,6 +499,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" flutter_secure_storage: dependency: "direct main" description: @@ -549,6 +637,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.7" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + url: "https://pub.dev" + source: hosted + version: "0.8.9+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + url: "https://pub.dev" + source: hosted + version: "0.8.9+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38" + url: "https://pub.dev" + source: hosted + version: "2.9.4" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -1356,4 +1508,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3547d09..2a4e679 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: photo_view: ^0.14.0 device_info_plus: ^9.1.2 package_info_plus: ^5.0.1 + image_picker: ^1.0.7 + flutter_image_compress: ^2.1.0 #Store flutter_secure_storage: ^9.0.0