From a6ae596cd716dd40a22683e2d511032b46f59112 Mon Sep 17 00:00:00 2001 From: JerraldKim Date: Sat, 7 Sep 2024 13:59:32 +0900 Subject: [PATCH] =?UTF-8?q?-=20Image=20Picker=20=EB=8C=80=EC=8B=A0=20Photo?= =?UTF-8?q?=20Manager=20=EC=82=AC=EC=9A=A9=20-=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EB=94=A9=20=EA=B0=9C=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=EA=B1=BA=20(50=EC=9E=A5=EC=94=A9=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9)=20-=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=8B=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=A1=9C=EB=94=A9=20-=20=ED=94=84?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20'?= =?UTF-8?q?=EC=82=AC=EC=9A=A9'=20=EC=84=A0=ED=83=9D=EC=8B=9C=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=B1=84=ED=83=9D=EB=90=98=EB=A9=B0=20=EA=B0=A4?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=EC=97=90=EC=84=9C=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EC=A7=90=20-=20=ED=94=84=EB=A6=AC=EB=B7=B0=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=EC=84=9C=20'=EB=8B=A4=EC=8B=9C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D'=20=EB=88=84=EB=A5=BC=20=EC=8B=9C=20=ED=94=84?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=A2=85=EB=A3=8C=EB=90=98=EB=A9=B0=20?= =?UTF-8?q?=EA=B0=A4=EB=9F=AC=EB=A6=AC=EC=97=90=20=EC=9E=94=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/feature/ImageConfirmationDialog.dart | 14 +- lib/feature/ImageGalleryScreen.dart | 123 +++++++++++ lib/feature/ImageImporter.dart | 62 +----- lib/feature/ProfilePicVerificationScreen.dart | 208 ++++++++---------- 4 files changed, 228 insertions(+), 179 deletions(-) create mode 100644 lib/feature/ImageGalleryScreen.dart diff --git a/lib/feature/ImageConfirmationDialog.dart b/lib/feature/ImageConfirmationDialog.dart index c84c84f..7d0556a 100644 --- a/lib/feature/ImageConfirmationDialog.dart +++ b/lib/feature/ImageConfirmationDialog.dart @@ -30,7 +30,7 @@ class ImageConfirmationDialog extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Text( '이 사진을 사용하시겠습니까?', - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -41,17 +41,17 @@ class ImageConfirmationDialog extends StatelessWidget { children: [ ElevatedButton( onPressed: () { - Navigator.of(context).pop(); - ref.read(imageFileProvider.notifier).clearImage(); + Navigator.of(context).pop(); // 프리뷰 닫고 갤러리에 남기 }, - child: Text('다시 선택'), + child: const Text('다시 선택'), ), ElevatedButton( onPressed: () { - Navigator.of(context).pop(); - ref.read(imageFileProvider.notifier).state = imageFile; + Navigator.of(context).pop(); // 프리뷰 닫기 + ref.read(imageFileProvider.notifier).state = imageFile; // 선택된 이미지 프로필 사진으로 지정하기 + Navigator.of(context).pop(); // 갤러리에서 나가기 }, - child: Text('사용'), + child: const Text('사용'), ), ], ), diff --git a/lib/feature/ImageGalleryScreen.dart b/lib/feature/ImageGalleryScreen.dart new file mode 100644 index 0000000..83d68f1 --- /dev/null +++ b/lib/feature/ImageGalleryScreen.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:io'; +import 'ImageImporter.dart'; +import 'ImageConfirmationDialog.dart'; + +class ImageGalleryScreen extends StatefulWidget { + final WidgetRef ref; + + ImageGalleryScreen({required this.ref}); + + @override + _ImageGalleryScreenState createState() => _ImageGalleryScreenState(); +} + +class _ImageGalleryScreenState extends State { + List images = []; + ScrollController _scrollController = ScrollController(); + int currentPage = 0; + bool isLoading = false; + bool hasMore = true; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); // 스크롤 리스너 + _fetchPhotos(); // 초기 배치 로드 + } + + Future _fetchPhotos({int limit = 50}) async { + if (isLoading || !hasMore) return; + + setState(() { + isLoading = true; + }); + + // Photo Manager 사용해서 이미지 불러오기 + List newImages = await widget.ref + .read(imagePickerServiceProvider) + .fetchImages(page: currentPage, limit: limit); + + setState(() { + if (newImages.isEmpty) { + hasMore = false; // 더 이상 불러올 이미지 없음 + } else { + images.addAll(newImages); // 리스트에 이미지 추가 + currentPage++; // 다음 페이지로 넘어가기 + } + isLoading = false; + }); + } + + void _onScroll() { + if (_scrollController.position.extentAfter < 300 && !isLoading && hasMore) { + _fetchPhotos(); // 추가 사진 불러오는 트리거 + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('프로필 사진을 고르세요'), + ), + body: GridView.builder( + controller: _scrollController, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, // 한 줄에 이미지 4개 + ), + itemCount: images.length + (hasMore ? 1 : 0), // 로딩 아이콘 들어가는 공간 확보 + itemBuilder: (context, index) { + if (index == images.length) { + // 로딩 아이콘 보여줌 + return const Center(child: CircularProgressIndicator()); + } + + return FutureBuilder( + future: images[index].thumbnailDataWithSize(ThumbnailSize(200, 200)), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.data != null) { + return GestureDetector( + onTap: () async { + File? fullResolutionFile = await images[index].file; + if (fullResolutionFile != null) { + _showImageConfirmationDialog(context, fullResolutionFile); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('이미지를 불러 올 수 없습니다.')), + ); + } + }, + child: Image.memory( + snapshot.data!, + fit: BoxFit.cover, + ), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + }, + ), + ); + } + + void _showImageConfirmationDialog(BuildContext context, File imageFile) { + showDialog( + context: context, + builder: (BuildContext context) { + return ImageConfirmationDialog(imageFile: imageFile, ref: widget.ref); + }, + ); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/feature/ImageImporter.dart b/lib/feature/ImageImporter.dart index 69e60eb..8152c1a 100644 --- a/lib/feature/ImageImporter.dart +++ b/lib/feature/ImageImporter.dart @@ -1,55 +1,25 @@ -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; import 'dart:io'; final imagePickerServiceProvider = Provider((ref) => ImagePickerService()); -final imageFileProvider = -StateNotifierProvider((ref) { +final imageFileProvider = StateNotifierProvider((ref) { final imagePickerService = ref.watch(imagePickerServiceProvider); return ImageFileNotifier(imagePickerService); }); class ImagePickerService { - final ImagePicker _picker = ImagePicker(); - - Future pickImageFromGallery(BuildContext context) async { - try { - final XFile? pickedFile = - await _picker.pickImage(source: ImageSource.gallery); - if (pickedFile != null) { - return File(pickedFile.path); - } else { - _showErrorSnackbar(context, '선택된 사진이 없습니다.'); - return null; - } - } catch (e) { - _showErrorSnackbar(context, '사진 불러오기에 실패했습니다: $e'); - return null; - } - } - - Future pickImageFromCamera(BuildContext context) async { - try { - final XFile? pickedFile = - await _picker.pickImage(source: ImageSource.camera); - if (pickedFile != null) { - return File(pickedFile.path); - } else { - _showErrorSnackbar(context, '찍은 사진이 없습니다.'); - return null; + Future> fetchImages({int page = 0, int limit = 50}) async { + final PermissionState permission = await PhotoManager.requestPermissionExtend(); + if (permission.isAuth) { + List albums = await PhotoManager.getAssetPathList(type: RequestType.image); + if (albums.isNotEmpty) { + final AssetPathEntity album = albums.first; + return await album.getAssetListPaged(page: page, size: limit); } - } catch (e) { - _showErrorSnackbar(context, '사진 찍기에 실패했습니다: $e'); - return null; } - } - - void _showErrorSnackbar(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + return []; } } @@ -58,18 +28,6 @@ class ImageFileNotifier extends StateNotifier { ImageFileNotifier(this._imagePickerService) : super(null); - Future pickImageFromGallery(BuildContext context) async { - final file = await _imagePickerService.pickImageFromGallery(context); - state = file; - return file; // 선택된 파일 return - } - - Future pickImageFromCamera(BuildContext context) async { - final file = await _imagePickerService.pickImageFromCamera(context); - state = file; - return file; // 선택된 파일 return - } - void clearImage() { state = null; } diff --git a/lib/feature/ProfilePicVerificationScreen.dart b/lib/feature/ProfilePicVerificationScreen.dart index 7db001f..656b4d8 100644 --- a/lib/feature/ProfilePicVerificationScreen.dart +++ b/lib/feature/ProfilePicVerificationScreen.dart @@ -1,109 +1,104 @@ import 'dart:io'; +import 'package:blueberry_flutter_template/utils/AppColors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'ImageImporter.dart'; -import 'ImageConfirmationDialog.dart'; // Import the new widget +import 'ImageGalleryScreen.dart'; -class ProfilePicVerificationScreen extends ConsumerWidget { +class ProfilePicVerificationScreen extends ConsumerStatefulWidget { const ProfilePicVerificationScreen({super.key}); static const name = 'ProfilePicVerificationScreen'; @override - Widget build(BuildContext context, WidgetRef ref) { - final imageFile = ref.watch(imageFileProvider); + ConsumerState createState() => _ProfilePicVerificationScreenState(); +} - // 승인 여부 - const bool isAuthorized = false; // true면 승인, false면 미승인 +class _ProfilePicVerificationScreenState extends ConsumerState { + bool isAuthorized = false; // 승인 여부 데이터 + + @override + Widget build(BuildContext context) { + final imageFile = ref.watch(imageFileProvider); // 선택된 사진 파일 watch return Scaffold( + backgroundColor: Colors.brown[100], appBar: AppBar( title: const Text('프로필 사진 페이지'), ), - body: LayoutBuilder( - builder: (context, constraints) { - double squareSize = constraints.maxWidth * 0.8; + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildProfileImage(imageFile), + const SizedBox(height: 20), + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 10), + onPressed: () { + _showImageSourceActionSheet(context); + }, + child: const Text( + '댕댕이 사진 고르기', + style: TextStyle( + color: Colors.brown, + fontSize: 20, + fontWeight: FontWeight.w900), + ), + ), + ], + ), + ), + ); + } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox( - height: 100, - ), - Container( - width: squareSize, - height: squareSize, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(20.0), - border: Border.all( - color: Colors.grey, - width: 4.0, - ), - ), - child: imageFile != null - ? ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: Stack( - children: [ - Image.file( - imageFile, - fit: BoxFit.cover, - width: squareSize, - height: squareSize, - color: isAuthorized - ? null - : Colors.black.withOpacity(0.6), - colorBlendMode: - isAuthorized ? null : BlendMode.darken, - ), - if (!isAuthorized) - Center( - child: Text( - '승인 대기 중입니다 \n조금만 기다려주세요', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - backgroundColor: - Colors.black.withOpacity(0.5), - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ) - : const Center( - child: Icon( - Icons.pets, - size: 200, - color: Colors.white, - ), - ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - _showImageSourceActionSheet(context, ref); - }, - child: const Text( - '사진 수정', - style: TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.w900), + Widget _buildProfileImage(File? imageFile) { + return Container( + width: 400, + height: 400, + decoration: BoxDecoration( + color: Colors.brown.shade400, + borderRadius: BorderRadius.circular(20.0), + border: Border.all(color: Colors.brown.shade200, width: 4.0), + ), + child: imageFile != null + ? ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Stack( + children: [ + Image.file( + imageFile, + fit: BoxFit.cover, + width: 400, + height: 400, + color: isAuthorized ? null : Colors.black.withOpacity(0.5), // 미승인 사진에 대해 블러 처리 + colorBlendMode: isAuthorized ? null : BlendMode.darken, + ), + if (!isAuthorized) + Center( + child: Text( + '승인 대기 중입니다\n잠시만 기다려주세요', + style: TextStyle( + color: Colors.white, + fontSize: 40, + fontWeight: FontWeight.bold, + backgroundColor: Colors.black.withOpacity(0.5), ), + textAlign: TextAlign.center, ), - ], - ), - ); - }, + ), + ], + ), + ) + : Center( + child: Icon( + Icons.pets, + size: 200, + color: Colors.brown.shade100, + ), ), ); } - void _showImageSourceActionSheet(BuildContext context, WidgetRef ref) { + void _showImageSourceActionSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (BuildContext context) { @@ -112,40 +107,14 @@ class ProfilePicVerificationScreen extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ ListTile( + tileColor: Colors.grey[250], leading: const Icon(Icons.photo_library), title: const Text('갤러리에서 선택'), - onTap: () async { - final file = await ref - .read(imageFileProvider.notifier) - .pickImageFromGallery(context); // 선택된 파일 캡처 - Navigator.of(context).pop(); - if (file != null) { - _showImageConfirmationDialog(context, file, ref); - } - }, - ), - ListTile( - leading: const Icon(Icons.camera_alt), - title: const Text('카메라로 찍기'), - onTap: () async { - final file = await ref - .read(imageFileProvider.notifier) - .pickImageFromCamera(context); // 선택된 파일 캡처 + onTap: () { Navigator.of(context).pop(); - if (file != null) { - _showImageConfirmationDialog(context, file, ref); - } + _showImageGallery(context); }, ), - if (ref.read(imageFileProvider) != null) - ListTile( - leading: const Icon(Icons.delete), - title: const Text('사진 삭제'), - onTap: () { - ref.read(imageFileProvider.notifier).clearImage(); - Navigator.of(context).pop(); - }, - ), ], ), ); @@ -153,13 +122,12 @@ class ProfilePicVerificationScreen extends ConsumerWidget { ); } - void _showImageConfirmationDialog( - BuildContext context, File imageFile, WidgetRef ref) { - showDialog( - context: context, - builder: (BuildContext context) { - return ImageConfirmationDialog(imageFile: imageFile, ref: ref); // Use the new widget - }, + void _showImageGallery(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImageGalleryScreen(ref: ref), + ), ); } } \ No newline at end of file