diff --git a/lib/src/camera_display.dart b/lib/src/camera_display.dart index 5882127..8e80a3e 100644 --- a/lib/src/camera_display.dart +++ b/lib/src/camera_display.dart @@ -1,432 +1,433 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:camera/camera.dart'; -import 'package:image_crop/image_crop.dart'; -import 'package:custom_gallery_display/src/entities/app_theme.dart'; -import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; -import 'package:custom_gallery_display/src/utilities/enum.dart'; -import 'package:custom_gallery_display/src/video_layout/record_count.dart'; -import 'package:custom_gallery_display/src/video_layout/record_fade_animation.dart'; -import 'package:custom_gallery_display/src/entities/selected_image_details.dart'; -import 'package:custom_gallery_display/src/entities/tabs_texts.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:photo_manager/photo_manager.dart'; - -class CustomCameraDisplay extends StatefulWidget { - final bool selectedVideo; - final AppTheme appTheme; - final TabsTexts tapsNames; - final bool enableCamera; - final bool enableVideo; - final VoidCallback moveToVideoScreen; - final ValueNotifier selectedCameraImage; - final ValueNotifier redDeleteText; - final ValueChanged replacingTabBar; - final ValueNotifier clearVideoRecord; - final AsyncValueSetter? sendRequestFunction; - final ValueNotifier videoRecordFile; - - const CustomCameraDisplay({ - Key? key, - required this.appTheme, - required this.tapsNames, - required this.sendRequestFunction, - required this.videoRecordFile, - required this.selectedCameraImage, - required this.enableCamera, - required this.enableVideo, - required this.redDeleteText, - required this.selectedVideo, - required this.replacingTabBar, - required this.clearVideoRecord, - required this.moveToVideoScreen, - }) : super(key: key); - - @override - CustomCameraDisplayState createState() => CustomCameraDisplayState(); -} - -class CustomCameraDisplayState extends State { - ValueNotifier startVideoCount = ValueNotifier(false); - - bool initializeDone = false; - bool allPermissionsAccessed = true; - - List? cameras; - late CameraController controller; - - final cropKey = GlobalKey(); - - Flash currentFlashMode = Flash.auto; - late Widget videoStatusAnimation; - int selectedCamera = 0; - - @override - void dispose() { - startVideoCount.dispose(); - controller.dispose(); - super.dispose(); - } - - @override - void initState() { - videoStatusAnimation = const SizedBox(); - _initializeCamera(); - - super.initState(); - } - - Future _initializeCamera() async { - try { - PermissionState state = await PhotoManager.requestPermissionExtend(); - if (!state.hasAccess || !state.isAuth) { - allPermissionsAccessed = false; - return; - } - allPermissionsAccessed = true; - cameras = await availableCameras(); - if (!mounted) return; - controller = CameraController( - cameras![0], - ResolutionPreset.high, - enableAudio: true, - ); - await controller.initialize(); - initializeDone = true; - } catch (e) { - allPermissionsAccessed = false; - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Material( - color: widget.appTheme.primaryColor, - child: allPermissionsAccessed - ? (initializeDone ? buildBody() : loadingProgress()) - : failedPermissions(), - ); - } - - Widget failedPermissions() { - return Center( - child: Text( - widget.tapsNames.acceptAllPermissions, - style: TextStyle(color: widget.appTheme.focusColor), - ), - ); - } - - Center loadingProgress() { - return Center( - child: CircularProgressIndicator( - color: widget.appTheme.focusColor, - strokeWidth: 1, - ), - ); - } - - Widget buildBody() { - Color whiteColor = widget.appTheme.primaryColor; - File? selectedImage = widget.selectedCameraImage.value; - double previewHeight = MediaQuery.of(context).size.height / 2; - return Column( - children: [ - appBar(), - Flexible( - child: Stack( - children: [ - if (selectedImage == null) ...[ - SizedBox( - width: double.infinity, - child: CameraPreview(controller), - ), - ] else ...[ - Align( - alignment: Alignment.topCenter, - child: Container( - color: whiteColor, - height: previewHeight + 10, - width: double.infinity, - child: buildCrop(selectedImage), - ), - ) - ], - buildFlashIcons(), - buildPickImageContainer(whiteColor, context), - ], - ), - ), - ], - ); - } - - Align buildPickImageContainer(Color whiteColor, BuildContext context) { - double previewHeight = MediaQuery.of(context).size.height / 2; - - return Align( - alignment: Alignment.bottomCenter, - child: Container( - height: previewHeight - 100, - color: whiteColor, - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 1.0), - child: Align( - alignment: Alignment.topCenter, - child: RecordCount( - appTheme: widget.appTheme, - startVideoCount: startVideoCount, - makeProgressRed: widget.redDeleteText, - clearVideoRecord: widget.clearVideoRecord, - ), - ), - ), - const Spacer(), - Stack( - alignment: Alignment.topCenter, - children: [ - Container( - padding: const EdgeInsets.all(60), - child: Align( - alignment: Alignment.center, - child: cameraButton(context), - ), - ), - Positioned(bottom: 120, child: videoStatusAnimation), - ], - ), - const Spacer(), - ], - ), - ), - ); - } - - Align buildFlashIcons() { - return Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - setState(() { - currentFlashMode = currentFlashMode == Flash.off - ? Flash.auto - : (currentFlashMode == Flash.auto ? Flash.on : Flash.off); - }); - currentFlashMode == Flash.on - ? controller.setFlashMode(FlashMode.torch) - : currentFlashMode == Flash.off - ? controller.setFlashMode(FlashMode.off) - : controller.setFlashMode(FlashMode.auto); - }, - icon: Icon( - currentFlashMode == Flash.on - ? Icons.flash_on_rounded - : (currentFlashMode == Flash.auto - ? Icons.flash_auto_rounded - : Icons.flash_off_rounded), - color: Colors.white), - ), - ); - } - - CustomCrop buildCrop(File selectedImage) { - String path = selectedImage.path; - bool isThatVideo = path.contains("mp4", path.length - 5); - return CustomCrop( - image: selectedImage, - isThatImage: !isThatVideo, - key: cropKey, - alwaysShowGrid: true, - paintColor: widget.appTheme.primaryColor, - ); - } - - AppBar appBar() { - Color whiteColor = widget.appTheme.primaryColor; - Color blackColor = widget.appTheme.focusColor; - File? selectedImage = widget.selectedCameraImage.value; - return AppBar( - backgroundColor: whiteColor, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.clear_rounded, color: blackColor, size: 30), - onPressed: () { - Navigator.of(context).maybePop(null); - }, - ), - actions: [ - AnimatedSwitcher( - duration: const Duration(seconds: 1), - switchInCurve: Curves.easeIn, - child: IconButton( - icon: const Icon(Icons.arrow_forward_rounded, - color: Colors.blue, size: 30), - onPressed: () async { - if (widget.videoRecordFile.value != null && - !widget.clearVideoRecord.value) { - Uint8List byte = - await widget.videoRecordFile.value!.readAsBytes(); - SelectedByte selectedByte = SelectedByte( - isThatImage: false, - selectedFile: widget.videoRecordFile.value!, - selectedByte: byte, - ); - SelectedImagesDetails details = SelectedImagesDetails( - multiSelectionMode: false, - selectedFiles: [selectedByte], - aspectRatio: 1.0, - ); - if (!mounted) return; - if (widget.sendRequestFunction != null) { - await widget.sendRequestFunction!(details); - } else { - Navigator.of(context).maybePop(details); - } - } else if (selectedImage != null) { - File? croppedByte = await cropImage(selectedImage); - File img = croppedByte ?? selectedImage; - Uint8List byte = await img.readAsBytes(); - - SelectedByte selectedByte = SelectedByte( - isThatImage: true, - selectedFile: img, - selectedByte: byte, - ); - - SelectedImagesDetails details = SelectedImagesDetails( - selectedFiles: [selectedByte], - multiSelectionMode: false, - aspectRatio: 1.0, - ); - if (!mounted) return; - if (widget.sendRequestFunction != null) { - await widget.sendRequestFunction!(details); - } else { - Navigator.of(context).maybePop(details); - } - } - }, - ), - ), - ], - ); - } - - Future cropImage(File imageFile) async { - await ImageCrop.requestPermissions(); - final scale = cropKey.currentState!.scale; - final area = cropKey.currentState!.area; - if (area == null) { - return null; - } - final sample = await ImageCrop.sampleImage( - file: imageFile, - preferredSize: (2000 / scale).round(), - ); - final File file = await ImageCrop.cropImage( - file: sample, - area: area, - ); - sample.delete(); - return file; - } - - GestureDetector cameraButton(BuildContext context) { - Color whiteColor = widget.appTheme.primaryColor; - return GestureDetector( - onTap: widget.enableCamera ? onPress : null, - onLongPress: widget.enableVideo ? onLongTap : null, - onLongPressUp: widget.enableVideo ? onLongTapUp : onPress, - child: CircleAvatar( - backgroundColor: Colors.grey[400], - radius: 40, - child: CircleAvatar( - radius: 24, - backgroundColor: whiteColor, - ), - ), - ); - } - - onPress() async { - try { - if (!widget.selectedVideo) { - final image = await controller.takePicture(); - File selectedImage = File(image.path); - setState(() { - widget.selectedCameraImage.value = selectedImage; - widget.replacingTabBar(true); - }); - } else { - setState(() { - videoStatusAnimation = buildFadeAnimation(); - }); - } - } catch (e) { - if (kDebugMode) print(e); - } - } - - onLongTap() { - controller.startVideoRecording(); - widget.moveToVideoScreen(); - setState(() { - startVideoCount.value = true; - }); - } - - onLongTapUp() async { - setState(() { - startVideoCount.value = false; - widget.replacingTabBar(true); - }); - XFile video = await controller.stopVideoRecording(); - widget.videoRecordFile.value = File(video.path); - } - - RecordFadeAnimation buildFadeAnimation() { - return RecordFadeAnimation(child: buildMessage()); - } - - Widget buildMessage() { - return Stack( - alignment: Alignment.topCenter, - children: [ - Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - color: Color.fromARGB(255, 54, 53, 53), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - child: Row( - children: [ - Text( - widget.tapsNames.holdButtonText, - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - ], - ), - ), - ), - const Align( - alignment: Alignment.bottomCenter, - child: Center( - child: Icon( - Icons.arrow_drop_down_rounded, - color: Color.fromARGB(255, 49, 49, 49), - size: 65, - ), - ), - ), - ], - ); - } -} +import 'dart:async'; +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:image_crop/image_crop.dart'; +import 'package:custom_gallery_display/src/entities/app_theme.dart'; +import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; +import 'package:custom_gallery_display/src/utilities/enum.dart'; +import 'package:custom_gallery_display/src/video_layout/record_count.dart'; +import 'package:custom_gallery_display/src/video_layout/record_fade_animation.dart'; +import 'package:custom_gallery_display/src/entities/selected_image_details.dart'; +import 'package:custom_gallery_display/src/entities/tabs_texts.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; + +class CustomCameraDisplay extends StatefulWidget { + final bool selectedVideo; + final AppTheme appTheme; + final TabsTexts tapsNames; + final bool enableCamera; + final bool enableVideo; + final VoidCallback moveToVideoScreen; + final ValueNotifier selectedCameraImage; + final ValueNotifier redDeleteText; + final ValueChanged replacingTabBar; + final ValueNotifier clearVideoRecord; + final AsyncValueSetter? sendRequestFunction; + final ValueNotifier videoRecordFile; + + const CustomCameraDisplay({ + Key? key, + required this.appTheme, + required this.tapsNames, + required this.sendRequestFunction, + required this.videoRecordFile, + required this.selectedCameraImage, + required this.enableCamera, + required this.enableVideo, + required this.redDeleteText, + required this.selectedVideo, + required this.replacingTabBar, + required this.clearVideoRecord, + required this.moveToVideoScreen, + }) : super(key: key); + + @override + CustomCameraDisplayState createState() => CustomCameraDisplayState(); +} + +class CustomCameraDisplayState extends State { + ValueNotifier startVideoCount = ValueNotifier(false); + + bool initializeDone = false; + bool allPermissionsAccessed = true; + + List? cameras; + late CameraController controller; + + final cropKey = GlobalKey(); + + Flash currentFlashMode = Flash.auto; + late Widget videoStatusAnimation; + int selectedCamera = 0; + + @override + void dispose() { + startVideoCount.dispose(); + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + videoStatusAnimation = const SizedBox(); + _initializeCamera(); + + super.initState(); + } + + Future _initializeCamera() async { + try { + PermissionState state = await PhotoManager.requestPermissionExtend(); + if (!state.hasAccess || !state.isAuth) { + allPermissionsAccessed = false; + return; + } + allPermissionsAccessed = true; + cameras = await availableCameras(); + if (!mounted) return; + controller = CameraController( + cameras![0], + ResolutionPreset.high, + enableAudio: true, + ); + await controller.initialize(); + initializeDone = true; + } catch (e) { + allPermissionsAccessed = false; + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Material( + color: widget.appTheme.primaryColor, + child: allPermissionsAccessed + ? (initializeDone ? buildBody() : loadingProgress()) + : failedPermissions(), + ); + } + + Widget failedPermissions() { + return Center( + child: Text( + widget.tapsNames.acceptAllPermissions, + style: TextStyle(color: widget.appTheme.focusColor), + ), + ); + } + + Center loadingProgress() { + return Center( + child: CircularProgressIndicator( + color: widget.appTheme.focusColor, + strokeWidth: 1, + ), + ); + } + + Widget buildBody() { + Color whiteColor = widget.appTheme.primaryColor; + File? selectedImage = widget.selectedCameraImage.value; + double previewHeight = MediaQuery.of(context).size.height; + return Column( + children: [ + appBar(), + Flexible( + child: Stack( + children: [ + if (selectedImage == null) ...[ + SizedBox( + width: double.infinity, + child: CameraPreview(controller), + ), + ] else ...[ + Align( + alignment: Alignment.topCenter, + child: Container( + color: whiteColor, + height: previewHeight + 10, + width: double.infinity, + child: buildCrop(selectedImage), + ), + ) + ], + buildFlashIcons(), + buildPickImageContainer(whiteColor, context), + ], + ), + ), + ], + ); + } + + Align buildPickImageContainer(Color whiteColor, BuildContext context) { + double previewHeight = MediaQuery.of(context).size.height; + + return Align( + alignment: Alignment.bottomCenter, + child: Container( + height: previewHeight - 100, + color: Colors.transparent, + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(top: 1.0), + child: Align( + alignment: Alignment.topCenter, + child: RecordCount( + appTheme: widget.appTheme, + startVideoCount: startVideoCount, + makeProgressRed: widget.redDeleteText, + clearVideoRecord: widget.clearVideoRecord, + ), + ), + ), + // const Spacer(), + Stack( + alignment: Alignment.topCenter, + children: [ + Container( + child: Align( + alignment: Alignment.center, + child: cameraButton(context), + ), + ), + Positioned(bottom: 20, child: videoStatusAnimation), + ], + ), + SizedBox( + height: 20, + ) + ], + ), + ), + ); + } + + Align buildFlashIcons() { + return Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () { + setState(() { + currentFlashMode = currentFlashMode == Flash.off + ? Flash.auto + : (currentFlashMode == Flash.auto ? Flash.on : Flash.off); + }); + currentFlashMode == Flash.on + ? controller.setFlashMode(FlashMode.torch) + : currentFlashMode == Flash.off + ? controller.setFlashMode(FlashMode.off) + : controller.setFlashMode(FlashMode.auto); + }, + icon: Icon( + currentFlashMode == Flash.on + ? Icons.flash_on_rounded + : (currentFlashMode == Flash.auto + ? Icons.flash_auto_rounded + : Icons.flash_off_rounded), + color: Colors.white), + ), + ); + } + + CustomCrop buildCrop(File selectedImage) { + String path = selectedImage.path; + bool isThatVideo = path.contains("mp4", path.length - 5); + return CustomCrop( + image: selectedImage, + isThatImage: !isThatVideo, + key: cropKey, + alwaysShowGrid: true, + paintColor: widget.appTheme.primaryColor, + ); + } + + AppBar appBar() { + Color whiteColor = widget.appTheme.primaryColor; + Color blackColor = widget.appTheme.focusColor; + File? selectedImage = widget.selectedCameraImage.value; + return AppBar( + backgroundColor: whiteColor, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.clear_rounded, color: blackColor, size: 30), + onPressed: () { + Navigator.of(context).maybePop(null); + }, + ), + actions: [ + AnimatedSwitcher( + duration: const Duration(seconds: 1), + switchInCurve: Curves.easeIn, + child: IconButton( + icon: const Icon(Icons.arrow_forward_rounded, + color: Colors.blue, size: 30), + onPressed: () async { + if (widget.videoRecordFile.value != null && + !widget.clearVideoRecord.value) { + Uint8List byte = + await widget.videoRecordFile.value!.readAsBytes(); + SelectedByte selectedByte = SelectedByte( + isThatImage: false, + selectedFile: widget.videoRecordFile.value!, + selectedByte: byte, + ); + SelectedImagesDetails details = SelectedImagesDetails( + multiSelectionMode: false, + selectedFiles: [selectedByte], + aspectRatio: 1.0, + ); + if (!mounted) return; + if (widget.sendRequestFunction != null) { + await widget.sendRequestFunction!(details); + } else { + Navigator.of(context).maybePop(details); + } + } else if (selectedImage != null) { + File? croppedByte = await cropImage(selectedImage); + File img = croppedByte ?? selectedImage; + Uint8List byte = await img.readAsBytes(); + + SelectedByte selectedByte = SelectedByte( + isThatImage: true, + selectedFile: img, + selectedByte: byte, + ); + + SelectedImagesDetails details = SelectedImagesDetails( + selectedFiles: [selectedByte], + multiSelectionMode: false, + aspectRatio: 1.0, + ); + if (!mounted) return; + if (widget.sendRequestFunction != null) { + await widget.sendRequestFunction!(details); + } else { + Navigator.of(context).maybePop(details); + } + } + }, + ), + ), + ], + ); + } + + Future cropImage(File imageFile) async { + await ImageCrop.requestPermissions(); + final scale = cropKey.currentState!.scale; + final area = cropKey.currentState!.area; + if (area == null) { + return null; + } + final sample = await ImageCrop.sampleImage( + file: imageFile, + preferredSize: (2000 / scale).round(), + ); + final File file = await ImageCrop.cropImage( + file: sample, + area: area, + ); + sample.delete(); + return file; + } + + GestureDetector cameraButton(BuildContext context) { + Color whiteColor = widget.appTheme.primaryColor; + return GestureDetector( + onTap: widget.enableCamera ? onPress : null, + onLongPress: widget.enableVideo ? onLongTap : null, + onLongPressUp: widget.enableVideo ? onLongTapUp : onPress, + child: CircleAvatar( + backgroundColor: Colors.grey[400], + radius: 40, + child: CircleAvatar( + radius: 25, + backgroundColor: whiteColor, + ), + ), + ); + } + + onPress() async { + try { + if (!widget.selectedVideo) { + final image = await controller.takePicture(); + File selectedImage = File(image.path); + setState(() { + widget.selectedCameraImage.value = selectedImage; + widget.replacingTabBar(true); + }); + } else { + setState(() { + videoStatusAnimation = buildFadeAnimation(); + }); + } + } catch (e) { + if (kDebugMode) print(e); + } + } + + onLongTap() { + controller.startVideoRecording(); + widget.moveToVideoScreen(); + setState(() { + startVideoCount.value = true; + }); + } + + onLongTapUp() async { + setState(() { + startVideoCount.value = false; + widget.replacingTabBar(true); + }); + XFile video = await controller.stopVideoRecording(); + widget.videoRecordFile.value = File(video.path); + } + + RecordFadeAnimation buildFadeAnimation() { + return RecordFadeAnimation(child: buildMessage()); + } + + Widget buildMessage() { + return Stack( + alignment: Alignment.topCenter, + children: [ + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + color: Color.fromARGB(255, 54, 53, 53), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Row( + children: [ + Text( + widget.tapsNames.holdButtonText, + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), + ), + ), + const Align( + alignment: Alignment.bottomCenter, + child: Center( + child: Icon( + Icons.arrow_drop_down_rounded, + color: Color.fromARGB(255, 49, 49, 49), + size: 65, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/crop_image_view.dart b/lib/src/crop_image_view.dart index bfdf8e8..9c08f5d 100644 --- a/lib/src/crop_image_view.dart +++ b/lib/src/crop_image_view.dart @@ -1,180 +1,180 @@ -import 'dart:io'; -import 'package:custom_gallery_display/src/custom_expand_icon.dart'; -import 'package:custom_gallery_display/src/entities/app_theme.dart'; -import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; -import 'package:flutter/material.dart'; - -class CropImageView extends StatefulWidget { - final ValueNotifier> cropKey; - final ValueNotifier> indexOfSelectedImages; - - final ValueNotifier multiSelectionMode; - final ValueNotifier expandImage; - final ValueNotifier expandHeight; - final ValueNotifier expandImageView; - - /// To avoid lag when you interacting with image when it expanded - final ValueNotifier enableVerticalTapping; - final ValueNotifier selectedImage; - - final VoidCallback clearMultiImages; - - final AppTheme appTheme; - final ValueNotifier noDuration; - final Color whiteColor; - final double? topPosition; - - const CropImageView({ - Key? key, - required this.indexOfSelectedImages, - required this.cropKey, - required this.multiSelectionMode, - required this.expandImage, - required this.expandHeight, - required this.clearMultiImages, - required this.expandImageView, - required this.enableVerticalTapping, - required this.selectedImage, - required this.appTheme, - required this.noDuration, - required this.whiteColor, - this.topPosition, - }) : super(key: key); - - @override - State createState() => _CropImageViewState(); -} - -class _CropImageViewState extends State { - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.enableVerticalTapping, - builder: (context, bool enableTappingValue, child) => GestureDetector( - onVerticalDragUpdate: enableTappingValue && widget.topPosition != null - ? (details) { - widget.expandImageView.value = true; - widget.expandHeight.value = details.globalPosition.dy - 56; - setState(() => widget.noDuration.value = true); - } - : null, - onVerticalDragEnd: enableTappingValue && widget.topPosition != null - ? (details) { - widget.expandHeight.value = - widget.expandHeight.value > 260 ? 360 : 0; - if (widget.topPosition == -360) { - widget.enableVerticalTapping.value = true; - } - if (widget.topPosition == 0) { - widget.enableVerticalTapping.value = false; - } - setState(() => widget.noDuration.value = false); - } - : null, - child: ValueListenableBuilder( - valueListenable: widget.selectedImage, - builder: (context, File? selectedImageValue, child) { - if (selectedImageValue != null) { - return showSelectedImage(context, selectedImageValue); - } else { - return Container(key: GlobalKey(debugLabel: "do not have")); - } - }, - ), - ), - ); - } - - Container showSelectedImage(BuildContext context, File selectedImageValue) { - double width = MediaQuery.of(context).size.width; - return Container( - key: GlobalKey(debugLabel: "have image"), - color: widget.whiteColor, - height: 360, - width: width, - child: ValueListenableBuilder( - valueListenable: widget.multiSelectionMode, - builder: (context, bool multiSelectionModeValue, child) => Stack( - children: [ - ValueListenableBuilder( - valueListenable: widget.expandImage, - builder: (context, bool expandImageValue, child) => - cropImageWidget(selectedImageValue, expandImageValue), - ), - if (widget.topPosition != null) ...[ - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: GestureDetector( - onTap: () { - if (multiSelectionModeValue) widget.clearMultiImages(); - setState(() { - widget.multiSelectionMode.value = - !multiSelectionModeValue; - }); - }, - child: Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: multiSelectionModeValue - ? Colors.blue - : const Color.fromARGB(165, 58, 58, 58), - border: Border.all( - color: const Color.fromARGB(45, 250, 250, 250), - ), - shape: BoxShape.circle, - ), - child: const Center( - child: Icon(Icons.copy, color: Colors.white, size: 17), - ), - ), - ), - ), - ), - ], - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: GestureDetector( - onTap: () { - setState(() { - widget.expandImage.value = !widget.expandImage.value; - }); - }, - child: Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: const Color.fromARGB(165, 58, 58, 58), - border: Border.all( - color: const Color.fromARGB(45, 250, 250, 250), - ), - shape: BoxShape.circle, - ), - child: const CustomExpandIcon(), - ), - ), - ), - ), - ], - ), - ), - ); - } - - Widget cropImageWidget(File selectedImageValue, bool expandImageValue) { - GlobalKey cropKey = widget.cropKey.value; - String path = selectedImageValue.path; - bool isThatVideo = path.contains("mp4", path.length - 5); - return CustomCrop( - image: selectedImageValue, - isThatImage: !isThatVideo, - key: cropKey, - paintColor: widget.appTheme.primaryColor, - aspectRatio: expandImageValue ? 6 / 8 : 1.0, - ); - } -} +import 'dart:io'; +import 'package:custom_gallery_display/src/custom_expand_icon.dart'; +import 'package:custom_gallery_display/src/entities/app_theme.dart'; +import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; +import 'package:flutter/material.dart'; + +class CropImageView extends StatefulWidget { + final ValueNotifier> cropKey; + final ValueNotifier> indexOfSelectedImages; + + final ValueNotifier multiSelectionMode; + final ValueNotifier expandImage; + final ValueNotifier expandHeight; + final ValueNotifier expandImageView; + + /// To avoid lag when you interacting with image when it expanded + final ValueNotifier enableVerticalTapping; + final ValueNotifier selectedImage; + + final VoidCallback clearMultiImages; + + final AppTheme appTheme; + final ValueNotifier noDuration; + final Color whiteColor; + final double? topPosition; + + const CropImageView({ + Key? key, + required this.indexOfSelectedImages, + required this.cropKey, + required this.multiSelectionMode, + required this.expandImage, + required this.expandHeight, + required this.clearMultiImages, + required this.expandImageView, + required this.enableVerticalTapping, + required this.selectedImage, + required this.appTheme, + required this.noDuration, + required this.whiteColor, + this.topPosition, + }) : super(key: key); + + @override + State createState() => _CropImageViewState(); +} + +class _CropImageViewState extends State { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.enableVerticalTapping, + builder: (context, bool enableTappingValue, child) => GestureDetector( + onVerticalDragUpdate: enableTappingValue && widget.topPosition != null + ? (details) { + widget.expandImageView.value = true; + widget.expandHeight.value = details.globalPosition.dy - 56; + setState(() => widget.noDuration.value = true); + } + : null, + onVerticalDragEnd: enableTappingValue && widget.topPosition != null + ? (details) { + widget.expandHeight.value = + widget.expandHeight.value > 260 ? 360 : 0; + if (widget.topPosition == -360) { + widget.enableVerticalTapping.value = true; + } + if (widget.topPosition == 0) { + widget.enableVerticalTapping.value = false; + } + setState(() => widget.noDuration.value = false); + } + : null, + child: ValueListenableBuilder( + valueListenable: widget.selectedImage, + builder: (context, File? selectedImageValue, child) { + if (selectedImageValue != null) { + return showSelectedImage(context, selectedImageValue); + } else { + return Container(key: GlobalKey(debugLabel: "do not have")); + } + }, + ), + ), + ); + } + + Container showSelectedImage(BuildContext context, File selectedImageValue) { + double width = MediaQuery.of(context).size.width; + return Container( + key: GlobalKey(debugLabel: "have image"), + color: widget.whiteColor, + height: 360, + width: width, + child: ValueListenableBuilder( + valueListenable: widget.multiSelectionMode, + builder: (context, bool multiSelectionModeValue, child) => Stack( + children: [ + ValueListenableBuilder( + valueListenable: widget.expandImage, + builder: (context, bool expandImageValue, child) => + cropImageWidget(selectedImageValue, expandImageValue), + ), + if (widget.topPosition != null) ...[ + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: GestureDetector( + onTap: () { + if (multiSelectionModeValue) widget.clearMultiImages(); + setState(() { + widget.multiSelectionMode.value = + !multiSelectionModeValue; + }); + }, + child: Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: multiSelectionModeValue + ? Colors.blue + : const Color.fromARGB(165, 58, 58, 58), + border: Border.all( + color: const Color.fromARGB(45, 250, 250, 250), + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon(Icons.copy, color: Colors.white, size: 17), + ), + ), + ), + ), + ), + ], + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: GestureDetector( + onTap: () { + setState(() { + widget.expandImage.value = !widget.expandImage.value; + }); + }, + child: Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: const Color.fromARGB(165, 58, 58, 58), + border: Border.all( + color: const Color.fromARGB(45, 250, 250, 250), + ), + shape: BoxShape.circle, + ), + child: const CustomExpandIcon(), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget cropImageWidget(File selectedImageValue, bool expandImageValue) { + GlobalKey cropKey = widget.cropKey.value; + String path = selectedImageValue.path; + bool isThatVideo = path.contains("mp4", path.length - 5); + return CustomCrop( + image: selectedImageValue, + isThatImage: !isThatVideo, + key: cropKey, + paintColor: widget.appTheme.primaryColor, + aspectRatio: expandImageValue ? 6 / 8 : 1.0, + ); + } +} diff --git a/lib/src/custom_expand_icon.dart b/lib/src/custom_expand_icon.dart index 5e0330d..a4d3e98 100644 --- a/lib/src/custom_expand_icon.dart +++ b/lib/src/custom_expand_icon.dart @@ -1,42 +1,42 @@ -import 'package:flutter/material.dart'; -import 'dart:math' as math; - -class CustomExpandIcon extends StatelessWidget { - const CustomExpandIcon({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Padding( - padding: const EdgeInsets.all(5.0), - child: Align( - alignment: Alignment.topRight, - child: Transform.rotate( - angle: 180 * math.pi / 250, - child: const Icon( - Icons.arrow_back_ios_rounded, - color: Colors.white, - size: 12, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Align( - alignment: Alignment.bottomLeft, - child: Transform.rotate( - angle: 180 * math.pi / 255, - child: const Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.white, - size: 12, - ), - ), - ), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class CustomExpandIcon extends StatelessWidget { + const CustomExpandIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.topRight, + child: Transform.rotate( + angle: 180 * math.pi / 250, + child: const Icon( + Icons.arrow_back_ios_rounded, + color: Colors.white, + size: 12, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.bottomLeft, + child: Transform.rotate( + angle: 180 * math.pi / 255, + child: const Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.white, + size: 12, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/custom_gallery_display.dart b/lib/src/custom_gallery_display.dart index f806394..4b4c193 100644 --- a/lib/src/custom_gallery_display.dart +++ b/lib/src/custom_gallery_display.dart @@ -1,459 +1,459 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:custom_gallery_display/custom_gallery_display.dart'; -import 'package:custom_gallery_display/src/camera_display.dart'; -import 'package:custom_gallery_display/src/images_view_page.dart'; -import 'package:custom_gallery_display/src/utilities/enum.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -class CustomGalleryDisplay extends StatefulWidget { - final DisplaySource displaySource; - final bool multiSelection; - final GalleryDisplaySettings? galleryDisplaySettings; - final AsyncValueSetter onDone; - final PickerSource pickerSource; - final bool cropImage; - final bool showImagePreview; - - const CustomGalleryDisplay.normalDisplay({ - this.displaySource = DisplaySource.gallery, - this.multiSelection = false, - this.galleryDisplaySettings, - this.pickerSource = PickerSource.image, - required this.onDone, - super.key, - }) : showImagePreview = false, - cropImage = false; - const CustomGalleryDisplay.instagramDisplay({ - this.displaySource = DisplaySource.gallery, - this.multiSelection = false, - this.galleryDisplaySettings, - this.pickerSource = PickerSource.image, - this.cropImage = true, - required this.onDone, - super.key, - }) : showImagePreview = true; - - @override - CustomGalleryDisplayState createState() => CustomGalleryDisplayState(); -} - -class CustomGalleryDisplayState extends State - with TickerProviderStateMixin { - final pageController = ValueNotifier(PageController()); - final clearVideoRecord = ValueNotifier(false); - final redDeleteText = ValueNotifier(false); - final selectedPage = ValueNotifier(SelectedPage.left); - ValueNotifier> multiSelectedImage = ValueNotifier([]); - final multiSelectionMode = ValueNotifier(false); - final showDeleteText = ValueNotifier(false); - final selectedVideo = ValueNotifier(false); - final ValueNotifier videoRecordFile = ValueNotifier(null); - - bool showGallery = true; - ValueNotifier selectedCameraImage = ValueNotifier(null); - late bool cropImage; - late AppTheme appTheme; - late TabsTexts tapsNames; - late bool showImagePreview; - - final isImagesReady = ValueNotifier(false); - final currentPage = ValueNotifier(0); - final lastPage = ValueNotifier(0); - - late Color whiteColor; - late Color blackColor; - late GalleryDisplaySettings imagePickerDisplay; - - late bool enableCamera; - late bool enableVideo; - - late bool showInternalVideos; - late bool showInternalImages; - AsyncValueSetter? sendRequestFunction; - late SliverGridDelegateWithFixedCrossAxisCount gridDelegate; - late bool showTabBar; - late bool cameraVideoOnlyEnabled; - late bool showAllTabs; - - @override - void initState() { - _initializeVariables(); - super.initState(); - } - - _initializeVariables() { - imagePickerDisplay = - widget.galleryDisplaySettings ?? GalleryDisplaySettings(); - appTheme = imagePickerDisplay.appTheme ?? AppTheme(); - tapsNames = imagePickerDisplay.tabsTexts ?? TabsTexts(); - cropImage = widget.cropImage; - showImagePreview = cropImage || widget.showImagePreview; - gridDelegate = imagePickerDisplay.gridDelegate; - - showInternalImages = widget.pickerSource != PickerSource.video; - showInternalVideos = widget.pickerSource != PickerSource.image; - sendRequestFunction = widget.onDone; - showGallery = widget.displaySource != DisplaySource.camera; - bool notGallery = widget.displaySource != DisplaySource.gallery; - - enableCamera = showInternalImages && notGallery; - enableVideo = showInternalVideos && notGallery; - bool cameraAndVideoEnabled = enableCamera && enableVideo; - - showTabBar = (cameraAndVideoEnabled) || - (showGallery && enableVideo) || - (showGallery && enableCamera); - - cameraVideoOnlyEnabled = - cameraAndVideoEnabled && widget.displaySource == DisplaySource.camera; - showAllTabs = cameraAndVideoEnabled && showGallery; - whiteColor = appTheme.primaryColor; - blackColor = appTheme.focusColor; - } - - @override - void dispose() { - showDeleteText.dispose(); - selectedVideo.dispose(); - selectedPage.dispose(); - selectedCameraImage.dispose(); - pageController.dispose(); - clearVideoRecord.dispose(); - redDeleteText.dispose(); - multiSelectionMode.dispose(); - multiSelectedImage.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return tabController(); - } - - Widget tapBarMessage(bool isThatDeleteText) { - Color deleteColor = redDeleteText.value ? Colors.red : appTheme.focusColor; - return Center( - child: Padding( - padding: const EdgeInsets.all(14.0), - child: GestureDetector( - onTap: () async { - if (isThatDeleteText) { - setState(() { - if (!redDeleteText.value) { - redDeleteText.value = true; - } else { - selectedCameraImage.value = null; - clearVideoRecord.value = true; - showDeleteText.value = false; - redDeleteText.value = false; - videoRecordFile.value = null; - } - }); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (isThatDeleteText) - Icon(Icons.arrow_back_ios_rounded, - color: deleteColor, size: 15), - Text( - isThatDeleteText - ? tapsNames.deletingText - : tapsNames.limitingText, - style: TextStyle( - fontSize: 14, - color: deleteColor, - fontWeight: FontWeight.w500), - ), - ], - ), - ), - ), - ); - } - - Widget clearSelectedImages() { - return Center( - child: Padding( - padding: const EdgeInsets.all(14.0), - child: GestureDetector( - onTap: () async { - setState(() { - multiSelectionMode.value = !multiSelectionMode.value; - multiSelectedImage.value.clear(); - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - tapsNames.clearImagesText, - style: TextStyle( - fontSize: 14, - color: appTheme.focusColor, - fontWeight: FontWeight.w500), - ), - ], - ), - ), - ), - ); - } - - replacingDeleteWidget(bool showDeleteText) { - this.showDeleteText.value = showDeleteText; - } - - moveToVideo() { - setState(() { - selectedPage.value = SelectedPage.right; - selectedVideo.value = true; - }); - } - - DefaultTabController tabController() { - return DefaultTabController( - length: 2, child: Material(color: whiteColor, child: safeArea())); - } - - SafeArea safeArea() { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: ValueListenableBuilder( - valueListenable: pageController, - builder: (context, PageController pageControllerValue, child) => - PageView( - controller: pageControllerValue, - dragStartBehavior: DragStartBehavior.start, - physics: const NeverScrollableScrollPhysics(), - children: [ - if (showGallery) imagesViewPage(), - if (enableCamera || enableVideo) cameraPage(), - ], - ), - ), - ), - if (multiSelectedImage.value.length < 10) ...[ - ValueListenableBuilder( - valueListenable: multiSelectionMode, - builder: (context, bool multiSelectionModeValue, child) { - if (enableVideo || enableCamera) { - if (!showImagePreview) { - if (multiSelectionModeValue) { - return clearSelectedImages(); - } else { - return buildTabBar(); - } - } else { - return Visibility( - visible: !multiSelectionModeValue, - child: buildTabBar(), - ); - } - } else { - return multiSelectionModeValue - ? clearSelectedImages() - : const SizedBox(); - } - }, - ) - ] else ...[ - tapBarMessage(false) - ], - ], - ), - ); - } - - ValueListenableBuilder cameraPage() { - return ValueListenableBuilder( - valueListenable: selectedVideo, - builder: (context, bool selectedVideoValue, child) => CustomCameraDisplay( - appTheme: appTheme, - selectedCameraImage: selectedCameraImage, - tapsNames: tapsNames, - enableCamera: enableCamera, - enableVideo: enableVideo, - videoRecordFile: videoRecordFile, - replacingTabBar: replacingDeleteWidget, - sendRequestFunction: sendRequestFunction, - clearVideoRecord: clearVideoRecord, - redDeleteText: redDeleteText, - moveToVideoScreen: moveToVideo, - selectedVideo: selectedVideoValue, - ), - ); - } - - void clearMultiImages() { - setState(() { - multiSelectedImage.value.clear(); - multiSelectionMode.value = false; - }); - } - - ImagesViewPage imagesViewPage() { - return ImagesViewPage( - appTheme: appTheme, - clearMultiImages: clearMultiImages, - gridDelegate: gridDelegate, - multiSelectionMode: multiSelectionMode, - blackColor: blackColor, - showImagePreview: showImagePreview, - tabsTexts: tapsNames, - multiSelectedImages: multiSelectedImage, - whiteColor: whiteColor, - cropImage: cropImage, - sendRequestFunction: sendRequestFunction, - multiSelection: widget.multiSelection, - showInternalVideos: showInternalVideos, - showInternalImages: showInternalImages, - ); - } - - ValueListenableBuilder buildTabBar() { - return ValueListenableBuilder( - valueListenable: showDeleteText, - builder: (context, bool showDeleteTextValue, child) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeInOutQuart, - child: showTabBar - ? (showDeleteTextValue ? tapBarMessage(true) : tabBar()) - : const SizedBox(), - ), - ); - } - - Widget tabBar() { - double widthOfScreen = MediaQuery.of(context).size.width; - int divideNumber = showAllTabs ? 3 : 2; - double widthOfTab = widthOfScreen / divideNumber; - return ValueListenableBuilder( - valueListenable: selectedPage, - builder: (context, SelectedPage selectedPageValue, child) { - Color photoColor = - selectedPageValue == SelectedPage.center ? blackColor : Colors.grey; - return Stack( - alignment: Alignment.bottomLeft, - children: [ - Row( - children: [ - if (showGallery) galleryTabBar(widthOfTab, selectedPageValue), - if (enableCamera) photoTabBar(widthOfTab, photoColor), - if (enableVideo) videoTabBar(widthOfTab), - ], - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOutQuad, - right: selectedPageValue == SelectedPage.center - ? widthOfTab - : (selectedPageValue == SelectedPage.right - ? 0 - : (divideNumber == 2 ? widthOfTab : widthOfScreen / 1.5)), - child: Container(height: 1, width: widthOfTab, color: blackColor), - ), - ], - ); - }, - ); - } - - GestureDetector galleryTabBar( - double widthOfTab, SelectedPage selectedPageValue) { - return GestureDetector( - onTap: () { - setState(() { - centerPage(numPage: 0, selectedPage: SelectedPage.left); - }); - }, - child: SizedBox( - width: widthOfTab, - height: 40, - child: Center( - child: Text( - tapsNames.galleryText, - style: TextStyle( - color: selectedPageValue == SelectedPage.left - ? blackColor - : Colors.grey, - fontSize: 14, - fontWeight: FontWeight.w500), - ), - ), - ), - ); - } - - GestureDetector photoTabBar(double widthOfTab, Color textColor) { - return GestureDetector( - onTap: () => centerPage( - numPage: cameraVideoOnlyEnabled ? 0 : 1, - selectedPage: - cameraVideoOnlyEnabled ? SelectedPage.left : SelectedPage.center), - child: SizedBox( - width: widthOfTab, - height: 40, - child: Center( - child: Text( - tapsNames.photoText, - style: TextStyle( - color: textColor, fontSize: 14, fontWeight: FontWeight.w500), - ), - ), - ), - ); - } - - centerPage({required int numPage, required SelectedPage selectedPage}) { - if (!enableVideo && numPage == 1) selectedPage = SelectedPage.right; - - setState(() { - this.selectedPage.value = selectedPage; - pageController.value.animateToPage(numPage, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOutQuad); - selectedVideo.value = false; - }); - } - - GestureDetector videoTabBar(double widthOfTab) { - return GestureDetector( - onTap: () { - setState( - () { - pageController.value.animateToPage(cameraVideoOnlyEnabled ? 0 : 1, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOutQuad); - selectedPage.value = SelectedPage.right; - selectedVideo.value = true; - }, - ); - }, - child: SizedBox( - width: widthOfTab, - height: 40, - child: ValueListenableBuilder( - valueListenable: selectedVideo, - builder: (context, bool selectedVideoValue, child) => Center( - child: Text( - tapsNames.videoText, - style: TextStyle( - fontSize: 14, - color: selectedVideoValue ? blackColor : Colors.grey, - fontWeight: FontWeight.w500), - ), - ), - ), - ), - ); - } -} +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:custom_gallery_display/custom_gallery_display.dart'; +import 'package:custom_gallery_display/src/camera_display.dart'; +import 'package:custom_gallery_display/src/images_view_page.dart'; +import 'package:custom_gallery_display/src/utilities/enum.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class CustomGalleryDisplay extends StatefulWidget { + final DisplaySource displaySource; + final bool multiSelection; + final GalleryDisplaySettings? galleryDisplaySettings; + final AsyncValueSetter onDone; + final PickerSource pickerSource; + final bool cropImage; + final bool showImagePreview; + + const CustomGalleryDisplay.normalDisplay({ + this.displaySource = DisplaySource.gallery, + this.multiSelection = false, + this.galleryDisplaySettings, + this.pickerSource = PickerSource.image, + required this.onDone, + super.key, + }) : showImagePreview = false, + cropImage = false; + const CustomGalleryDisplay.instagramDisplay({ + this.displaySource = DisplaySource.gallery, + this.multiSelection = false, + this.galleryDisplaySettings, + this.pickerSource = PickerSource.image, + this.cropImage = true, + required this.onDone, + super.key, + }) : showImagePreview = true; + + @override + CustomGalleryDisplayState createState() => CustomGalleryDisplayState(); +} + +class CustomGalleryDisplayState extends State + with TickerProviderStateMixin { + final pageController = ValueNotifier(PageController()); + final clearVideoRecord = ValueNotifier(false); + final redDeleteText = ValueNotifier(false); + final selectedPage = ValueNotifier(SelectedPage.left); + ValueNotifier> multiSelectedImage = ValueNotifier([]); + final multiSelectionMode = ValueNotifier(false); + final showDeleteText = ValueNotifier(false); + final selectedVideo = ValueNotifier(false); + final ValueNotifier videoRecordFile = ValueNotifier(null); + + bool showGallery = true; + ValueNotifier selectedCameraImage = ValueNotifier(null); + late bool cropImage; + late AppTheme appTheme; + late TabsTexts tapsNames; + late bool showImagePreview; + + final isImagesReady = ValueNotifier(false); + final currentPage = ValueNotifier(0); + final lastPage = ValueNotifier(0); + + late Color whiteColor; + late Color blackColor; + late GalleryDisplaySettings imagePickerDisplay; + + late bool enableCamera; + late bool enableVideo; + + late bool showInternalVideos; + late bool showInternalImages; + AsyncValueSetter? sendRequestFunction; + late SliverGridDelegateWithFixedCrossAxisCount gridDelegate; + late bool showTabBar; + late bool cameraVideoOnlyEnabled; + late bool showAllTabs; + + @override + void initState() { + _initializeVariables(); + super.initState(); + } + + _initializeVariables() { + imagePickerDisplay = + widget.galleryDisplaySettings ?? GalleryDisplaySettings(); + appTheme = imagePickerDisplay.appTheme ?? AppTheme(); + tapsNames = imagePickerDisplay.tabsTexts ?? TabsTexts(); + cropImage = widget.cropImage; + showImagePreview = cropImage || widget.showImagePreview; + gridDelegate = imagePickerDisplay.gridDelegate; + + showInternalImages = widget.pickerSource != PickerSource.video; + showInternalVideos = widget.pickerSource != PickerSource.image; + sendRequestFunction = widget.onDone; + showGallery = widget.displaySource != DisplaySource.camera; + bool notGallery = widget.displaySource != DisplaySource.gallery; + + enableCamera = showInternalImages && notGallery; + enableVideo = showInternalVideos && notGallery; + bool cameraAndVideoEnabled = enableCamera && enableVideo; + + showTabBar = (cameraAndVideoEnabled) || + (showGallery && enableVideo) || + (showGallery && enableCamera); + + cameraVideoOnlyEnabled = + cameraAndVideoEnabled && widget.displaySource == DisplaySource.camera; + showAllTabs = cameraAndVideoEnabled && showGallery; + whiteColor = appTheme.primaryColor; + blackColor = appTheme.focusColor; + } + + @override + void dispose() { + showDeleteText.dispose(); + selectedVideo.dispose(); + selectedPage.dispose(); + selectedCameraImage.dispose(); + pageController.dispose(); + clearVideoRecord.dispose(); + redDeleteText.dispose(); + multiSelectionMode.dispose(); + multiSelectedImage.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return tabController(); + } + + Widget tapBarMessage(bool isThatDeleteText) { + Color deleteColor = redDeleteText.value ? Colors.red : appTheme.focusColor; + return Center( + child: Padding( + padding: const EdgeInsets.all(14.0), + child: GestureDetector( + onTap: () async { + if (isThatDeleteText) { + setState(() { + if (!redDeleteText.value) { + redDeleteText.value = true; + } else { + selectedCameraImage.value = null; + clearVideoRecord.value = true; + showDeleteText.value = false; + redDeleteText.value = false; + videoRecordFile.value = null; + } + }); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isThatDeleteText) + Icon(Icons.arrow_back_ios_rounded, + color: deleteColor, size: 15), + Text( + isThatDeleteText + ? tapsNames.deletingText + : tapsNames.limitingText, + style: TextStyle( + fontSize: 14, + color: deleteColor, + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ), + ); + } + + Widget clearSelectedImages() { + return Center( + child: Padding( + padding: const EdgeInsets.all(14.0), + child: GestureDetector( + onTap: () async { + setState(() { + multiSelectionMode.value = !multiSelectionMode.value; + multiSelectedImage.value.clear(); + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + tapsNames.clearImagesText, + style: TextStyle( + fontSize: 14, + color: appTheme.focusColor, + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ), + ); + } + + replacingDeleteWidget(bool showDeleteText) { + this.showDeleteText.value = showDeleteText; + } + + moveToVideo() { + setState(() { + selectedPage.value = SelectedPage.right; + selectedVideo.value = true; + }); + } + + DefaultTabController tabController() { + return DefaultTabController( + length: 2, child: Material(color: whiteColor, child: safeArea())); + } + + SafeArea safeArea() { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ValueListenableBuilder( + valueListenable: pageController, + builder: (context, PageController pageControllerValue, child) => + PageView( + controller: pageControllerValue, + dragStartBehavior: DragStartBehavior.start, + physics: const NeverScrollableScrollPhysics(), + children: [ + if (showGallery) imagesViewPage(), + if (enableCamera || enableVideo) cameraPage(), + ], + ), + ), + ), + if (multiSelectedImage.value.length < 10) ...[ + ValueListenableBuilder( + valueListenable: multiSelectionMode, + builder: (context, bool multiSelectionModeValue, child) { + if (enableVideo || enableCamera) { + if (!showImagePreview) { + if (multiSelectionModeValue) { + return clearSelectedImages(); + } else { + return buildTabBar(); + } + } else { + return Visibility( + visible: !multiSelectionModeValue, + child: buildTabBar(), + ); + } + } else { + return multiSelectionModeValue + ? clearSelectedImages() + : const SizedBox(); + } + }, + ) + ] else ...[ + tapBarMessage(false) + ], + ], + ), + ); + } + + ValueListenableBuilder cameraPage() { + return ValueListenableBuilder( + valueListenable: selectedVideo, + builder: (context, bool selectedVideoValue, child) => CustomCameraDisplay( + appTheme: appTheme, + selectedCameraImage: selectedCameraImage, + tapsNames: tapsNames, + enableCamera: enableCamera, + enableVideo: enableVideo, + videoRecordFile: videoRecordFile, + replacingTabBar: replacingDeleteWidget, + sendRequestFunction: sendRequestFunction, + clearVideoRecord: clearVideoRecord, + redDeleteText: redDeleteText, + moveToVideoScreen: moveToVideo, + selectedVideo: selectedVideoValue, + ), + ); + } + + void clearMultiImages() { + setState(() { + multiSelectedImage.value.clear(); + multiSelectionMode.value = false; + }); + } + + ImagesViewPage imagesViewPage() { + return ImagesViewPage( + appTheme: appTheme, + clearMultiImages: clearMultiImages, + gridDelegate: gridDelegate, + multiSelectionMode: multiSelectionMode, + blackColor: blackColor, + showImagePreview: showImagePreview, + tabsTexts: tapsNames, + multiSelectedImages: multiSelectedImage, + whiteColor: whiteColor, + cropImage: cropImage, + sendRequestFunction: sendRequestFunction, + multiSelection: widget.multiSelection, + showInternalVideos: showInternalVideos, + showInternalImages: showInternalImages, + ); + } + + ValueListenableBuilder buildTabBar() { + return ValueListenableBuilder( + valueListenable: showDeleteText, + builder: (context, bool showDeleteTextValue, child) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeInOutQuart, + child: showTabBar + ? (showDeleteTextValue ? tapBarMessage(true) : tabBar()) + : const SizedBox(), + ), + ); + } + + Widget tabBar() { + double widthOfScreen = MediaQuery.of(context).size.width; + int divideNumber = showAllTabs ? 3 : 2; + double widthOfTab = widthOfScreen / divideNumber; + return ValueListenableBuilder( + valueListenable: selectedPage, + builder: (context, SelectedPage selectedPageValue, child) { + Color photoColor = + selectedPageValue == SelectedPage.center ? blackColor : Colors.grey; + return Stack( + alignment: Alignment.bottomLeft, + children: [ + Row( + children: [ + if (showGallery) galleryTabBar(widthOfTab, selectedPageValue), + if (enableCamera) photoTabBar(widthOfTab, photoColor), + if (enableVideo) videoTabBar(widthOfTab), + ], + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOutQuad, + right: selectedPageValue == SelectedPage.center + ? widthOfTab + : (selectedPageValue == SelectedPage.right + ? 0 + : (divideNumber == 2 ? widthOfTab : widthOfScreen / 1.5)), + child: Container(height: 1, width: widthOfTab, color: blackColor), + ), + ], + ); + }, + ); + } + + GestureDetector galleryTabBar( + double widthOfTab, SelectedPage selectedPageValue) { + return GestureDetector( + onTap: () { + setState(() { + centerPage(numPage: 0, selectedPage: SelectedPage.left); + }); + }, + child: SizedBox( + width: widthOfTab, + height: 40, + child: Center( + child: Text( + tapsNames.galleryText, + style: TextStyle( + color: selectedPageValue == SelectedPage.left + ? blackColor + : Colors.grey, + fontSize: 14, + fontWeight: FontWeight.w500), + ), + ), + ), + ); + } + + GestureDetector photoTabBar(double widthOfTab, Color textColor) { + return GestureDetector( + onTap: () => centerPage( + numPage: cameraVideoOnlyEnabled ? 0 : 1, + selectedPage: + cameraVideoOnlyEnabled ? SelectedPage.left : SelectedPage.center), + child: SizedBox( + width: widthOfTab, + height: 40, + child: Center( + child: Text( + tapsNames.photoText, + style: TextStyle( + color: textColor, fontSize: 14, fontWeight: FontWeight.w500), + ), + ), + ), + ); + } + + centerPage({required int numPage, required SelectedPage selectedPage}) { + if (!enableVideo && numPage == 1) selectedPage = SelectedPage.right; + + setState(() { + this.selectedPage.value = selectedPage; + pageController.value.animateToPage(numPage, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOutQuad); + selectedVideo.value = false; + }); + } + + GestureDetector videoTabBar(double widthOfTab) { + return GestureDetector( + onTap: () { + setState( + () { + pageController.value.animateToPage(cameraVideoOnlyEnabled ? 0 : 1, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOutQuad); + selectedPage.value = SelectedPage.right; + selectedVideo.value = true; + }, + ); + }, + child: SizedBox( + width: widthOfTab, + height: 40, + child: ValueListenableBuilder( + valueListenable: selectedVideo, + builder: (context, bool selectedVideoValue, child) => Center( + child: Text( + tapsNames.videoText, + style: TextStyle( + fontSize: 14, + color: selectedVideoValue ? blackColor : Colors.grey, + fontWeight: FontWeight.w500), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/custom_packages/crop_image/crop_image.dart b/lib/src/custom_packages/crop_image/crop_image.dart index c84f82c..a8bcd7e 100644 --- a/lib/src/custom_packages/crop_image/crop_image.dart +++ b/lib/src/custom_packages/crop_image/crop_image.dart @@ -1,701 +1,701 @@ -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; - -const _kCropGridColumnCount = 3; -const _kCropGridRowCount = 3; -const _kCropGridColor = Color.fromRGBO(0xd0, 0xd0, 0xd0, 0.9); -const _kCropHandleSize = 0.0; -const _kCropHandleHitSize = 48.0; - -enum _CropAction { none, moving, cropping, scaling } - -enum _CropHandleSide { none, topLeft, topRight, bottomLeft, bottomRight } - -class CustomCrop extends StatefulWidget { - final File image; - final double? aspectRatio; - final double maximumScale; - - final bool alwaysShowGrid; - final Color? paintColor; - final ImageErrorListener? onImageError; - final ValueChanged? scrollCustomList; - final bool isThatImage; - - const CustomCrop({ - Key? key, - this.aspectRatio, - this.paintColor, - this.scrollCustomList, - this.maximumScale = 2.0, - this.alwaysShowGrid = false, - this.isThatImage = true, - this.onImageError, - required this.image, - }) : super(key: key); - - @override - State createState() => CustomCropState(); - - static CustomCropState? of(BuildContext context) => - context.findAncestorStateOfType(); -} - -class CustomCropState extends State - with TickerProviderStateMixin, Drag { - final _surfaceKey = GlobalKey(); - - late final AnimationController _activeController; - late final AnimationController _settleController; - - double _scale = 1.0; - double _ratio = 1.0; - Rect _view = Rect.zero; - Rect _area = Rect.zero; - Offset _lastFocalPoint = Offset.zero; - _CropAction _action = _CropAction.none; - _CropHandleSide _handle = _CropHandleSide.none; - - late double _startScale; - late Rect _startView; - late Tween _viewTween; - late Tween _scaleTween; - - ImageStream? _imageStream; - ui.Image? _image; - ImageStreamListener? _imageListener; - - double get scale => _area.shortestSide / _scale; - - Rect? get area => _view.isEmpty - ? null - : Rect.fromLTWH( - _area.left * _view.width / _scale - _view.left, - _area.top * _view.height / _scale - _view.top, - _area.width * _view.width / _scale, - _area.height * _view.height / _scale, - ); - bool get _isEnabled => _view.isEmpty == false && _image != null; - - final Map _maxAreaWidthMap = {}; - - int pointers = 0; - - @override - void initState() { - super.initState(); - - _activeController = AnimationController( - vsync: this, - value: widget.alwaysShowGrid ? 1.0 : 0.0, - )..addListener(() => setState(() {})); - _settleController = AnimationController(vsync: this) - ..addListener(_settleAnimationChanged); - } - - @override - void dispose() { - final listener = _imageListener; - if (listener != null) { - _imageStream?.removeListener(listener); - } - _activeController.dispose(); - _settleController.dispose(); - - super.dispose(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _getImage(); - } - - @override - void didUpdateWidget(CustomCrop oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.image != oldWidget.image) { - _getImage(); - } else if (widget.aspectRatio != oldWidget.aspectRatio) { - _area = _calculateDefaultArea( - viewWidth: _view.width, - viewHeight: _view.height, - imageWidth: _image?.width, - imageHeight: _image?.height, - ); - } - if (widget.alwaysShowGrid != oldWidget.alwaysShowGrid) { - if (widget.alwaysShowGrid) { - _activate(); - } else { - _deactivate(); - } - } - } - - void _getImage({bool force = false}) { - if (widget.isThatImage) { - final oldImageStream = _imageStream; - FileImage image = FileImage(widget.image, scale: 1.0); - final newImageStream = - image.resolve(createLocalImageConfiguration(context)); - _imageStream = newImageStream; - if (newImageStream.key != oldImageStream?.key || force) { - final oldImageListener = _imageListener; - if (oldImageListener != null) { - oldImageStream?.removeListener(oldImageListener); - } - final newImageListener = - ImageStreamListener(_updateImage, onError: widget.onImageError); - _imageListener = newImageListener; - newImageStream.addListener(newImageListener); - } - } - } - - @override - Widget build(BuildContext context) { - double width = MediaQuery.of(context).size.width; - return SizedBox( - width: width, - child: ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: Listener( - onPointerDown: (event) => pointers++, - onPointerUp: (event) => pointers = 0, - child: GestureDetector( - key: _surfaceKey, - behavior: HitTestBehavior.opaque, - onScaleStart: _isEnabled ? _handleScaleStart : null, - onScaleUpdate: _isEnabled ? _handleScaleUpdate : null, - onScaleEnd: _isEnabled ? _handleScaleEnd : null, - child: AnimatedBuilder( - builder: (context, child) { - if (widget.isThatImage) { - return buildCustomPaint(); - } else { - return _DisplayVideo(selectedFile: widget.image); - } - }, - animation: _activeController, - ), - ), - ), - ), - ); - } - - void _handleScaleStart(ScaleStartDetails details) { - if (widget.scrollCustomList != null) widget.scrollCustomList!(true); - - _activate(); - _settleController.stop(canceled: false); - _lastFocalPoint = details.focalPoint; - _action = _CropAction.none; - _handle = _hitCropHandle(_getLocalPoint(details.focalPoint)); - _startScale = _scale; - _startView = _view; - } - - void _handleScaleUpdate(ScaleUpdateDetails details) { - if (_action == _CropAction.none) { - if (_handle == _CropHandleSide.none) { - _action = pointers == 2 ? _CropAction.scaling : _CropAction.moving; - } else { - _action = _CropAction.cropping; - } - } - if (_action == _CropAction.cropping) { - final boundaries = _boundaries; - if (boundaries == null) return; - } else if (_action == _CropAction.moving) { - final image = _image; - if (image == null) return; - - final delta = details.focalPoint - _lastFocalPoint; - _lastFocalPoint = details.focalPoint; - - setState(() { - _view = _view.translate( - delta.dx / (image.width * _scale * _ratio), - delta.dy / (image.height * _scale * _ratio), - ); - }); - } else if (_action == _CropAction.scaling) { - final image = _image; - final boundaries = _boundaries; - if (image == null || boundaries == null) return; - - setState(() { - _scale = _startScale * details.scale; - - final dx = boundaries.width * - (1.0 - details.scale) / - (image.width * _scale * _ratio); - final dy = boundaries.height * - (1.0 - details.scale) / - (image.height * _scale * _ratio); - _view = Rect.fromLTWH( - _startView.left + dx / 2, - _startView.top + dy / 2, - _startView.width, - _startView.height, - ); - }); - } - } - - void _handleScaleEnd(ScaleEndDetails details) { - if (widget.scrollCustomList != null) widget.scrollCustomList!(false); - - _deactivate(); - final minimumScale = _minimumScale; - if (minimumScale == null) return; - - final targetScale = _scale.clamp(minimumScale, _maximumScale); - _scaleTween = Tween( - begin: _scale, - end: targetScale, - ); - - _startView = _view; - _viewTween = RectTween( - begin: _view, - end: _getViewInBoundaries(targetScale), - ); - - _settleController.value = 0.0; - _settleController.animateTo( - 1.0, - curve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 350), - ); - } - - Rect _getViewInBoundaries(double scale) => - Offset( - max( - min(_view.left, _area.left * _view.width / scale), - _area.right * _view.width / scale - 1.0, - ), - max( - min( - _view.top, - _area.top * _view.height / scale, - ), - _area.bottom * _view.height / scale - 1.0), - ) & - _view.size; - - double get _maximumScale => widget.maximumScale; - - double? get _minimumScale { - final boundaries = _boundaries; - final image = _image; - if (boundaries == null || image == null) { - return null; - } - - final scaleX = boundaries.width * _area.width / (image.width * _ratio); - final scaleY = boundaries.height * _area.height / (image.height * _ratio); - return min(_maximumScale, max(scaleX, scaleY)); - } - - Widget buildCustomPaint() { - return CustomPaint( - painter: _CropPainter( - image: _image, - ratio: _ratio, - view: _view, - area: _area, - scale: _scale, - active: _activeController.value, - paintColor: widget.paintColor ?? Colors.white, - ), - ); - } - - void _activate() { - _activeController.animateTo( - 1.0, - curve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 250), - ); - } - - void _deactivate() { - if (widget.alwaysShowGrid == false) { - _activeController.animateTo( - 0.0, - curve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 250), - ); - } - } - - Size? get _boundaries { - final context = _surfaceKey.currentContext; - if (context == null) return null; - - final box = context.findRenderObject() as RenderBox; - final size = box.size; - - return size - const Offset(_kCropHandleSize, _kCropHandleSize) as Size; - } - - Offset? _getLocalPoint(Offset point) { - final context = _surfaceKey.currentContext; - if (context == null) return null; - - final box = context.findRenderObject() as RenderBox; - - return box.globalToLocal(point); - } - - void _settleAnimationChanged() { - setState(() { - _scale = _scaleTween.transform(_settleController.value); - final nextView = _viewTween.transform(_settleController.value); - if (nextView != null) { - _view = nextView; - } - }); - } - - Rect _calculateDefaultArea({ - required int? imageWidth, - required int? imageHeight, - required double viewWidth, - required double viewHeight, - }) { - if (imageWidth == null || imageHeight == null) return Rect.zero; - - double height; - double width; - if ((widget.aspectRatio ?? 1.0) < 1) { - height = 1.0; - width = - ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) / - imageWidth / - viewWidth; - if (width > 1.0) { - width = 1.0; - height = (imageWidth * viewWidth * width) / - (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0)); - } - } else { - width = 1.0; - height = (imageWidth * viewWidth * width) / - (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0)); - if (height > 1.0) { - height = 1.0; - width = - ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) / - imageWidth / - viewWidth; - } - } - final aspectRatio = _maxAreaWidthMap[widget.aspectRatio]; - if (aspectRatio != null) { - _maxAreaWidthMap[aspectRatio] = width; - } - ui.Rect rect = - Rect.fromLTWH((1.0 - width) / 2, (1.0 - height) / 2, width, height); - return rect; - } - - void _updateImage(ImageInfo imageInfo, bool synchronousCall) { - final boundaries = _boundaries; - if (boundaries == null) return; - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final image = imageInfo.image; - - setState(() { - _image = image; - _scale = imageInfo.scale; - _ratio = max( - boundaries.width / image.width, - boundaries.height / image.height, - ); - - final viewWidth = boundaries.width / (image.width * _scale * _ratio); - final viewHeight = boundaries.height / (image.height * _scale * _ratio); - _area = _calculateDefaultArea( - viewWidth: viewWidth, - viewHeight: viewHeight, - imageWidth: image.width, - imageHeight: image.height, - ); - _view = Rect.fromLTWH( - (viewWidth - 1.0) / 2, - (viewHeight - 1.0) / 2, - viewWidth, - viewHeight, - ); - }); - }); - - WidgetsBinding.instance.ensureVisualUpdate(); - } - - _CropHandleSide _hitCropHandle(Offset? localPoint) { - final boundaries = _boundaries; - if (localPoint == null || boundaries == null) { - return _CropHandleSide.none; - } - - final viewRect = Rect.fromLTWH( - boundaries.width * _area.left, - boundaries.height * _area.top, - boundaries.width * _area.width, - boundaries.height * _area.height, - ).deflate(_kCropHandleSize / 2); - - if (Rect.fromLTWH( - viewRect.left - _kCropHandleHitSize / 2, - viewRect.top - _kCropHandleHitSize / 2, - _kCropHandleHitSize, - _kCropHandleHitSize, - ).contains(localPoint)) { - return _CropHandleSide.topLeft; - } - - if (Rect.fromLTWH( - viewRect.right - _kCropHandleHitSize / 2, - viewRect.top - _kCropHandleHitSize / 2, - _kCropHandleHitSize, - _kCropHandleHitSize, - ).contains(localPoint)) { - return _CropHandleSide.topRight; - } - - if (Rect.fromLTWH( - viewRect.left - _kCropHandleHitSize / 2, - viewRect.bottom - _kCropHandleHitSize / 2, - _kCropHandleHitSize, - _kCropHandleHitSize, - ).contains(localPoint)) { - return _CropHandleSide.bottomLeft; - } - - if (Rect.fromLTWH( - viewRect.right - _kCropHandleHitSize / 2, - viewRect.bottom - _kCropHandleHitSize / 2, - _kCropHandleHitSize, - _kCropHandleHitSize, - ).contains(localPoint)) { - return _CropHandleSide.bottomRight; - } - - return _CropHandleSide.none; - } -} - -class _CropPainter extends CustomPainter { - final ui.Image? image; - final Rect view; - final double ratio; - final Rect area; - final double scale; - final double active; - final Color paintColor; - - _CropPainter({ - required this.image, - required this.view, - required this.ratio, - required this.area, - required this.scale, - required this.active, - required this.paintColor, - }); - - @override - bool shouldRepaint(_CropPainter oldDelegate) { - return oldDelegate.image != image || - oldDelegate.view != view || - oldDelegate.ratio != ratio || - oldDelegate.area != area || - oldDelegate.active != active || - oldDelegate.scale != scale; - } - - @override - void paint(Canvas canvas, Size size) { - final rect = Rect.fromLTWH( - _kCropHandleSize / 2, - _kCropHandleSize / 2, - size.width - _kCropHandleSize, - size.height - _kCropHandleSize, - ); - canvas.save(); - canvas.translate(rect.left, rect.top); - - final paint = Paint()..isAntiAlias = false; - - final image = this.image; - if (image != null) { - final src = Rect.fromLTWH( - 0.0, - 0.0, - image.width.toDouble(), - image.height.toDouble(), - ); - final dst = Rect.fromLTWH( - view.left * image.width * scale * ratio, - view.top * image.height * scale * ratio, - image.width * scale * ratio, - image.height * scale * ratio, - ); - canvas.save(); - canvas.clipRect(Rect.fromLTWH(0.0, 0.0, rect.width, rect.height)); - canvas.drawImageRect(image, src, dst, paint); - canvas.restore(); - } - - paint.color = paintColor; - - final boundaries = Rect.fromLTWH( - rect.width * area.left, - rect.height * area.top, - rect.width * area.width, - rect.height * area.height, - ); - canvas.drawRect(Rect.fromLTRB(0.0, 0.0, rect.width, boundaries.top), paint); - canvas.drawRect( - Rect.fromLTRB(0.0, boundaries.bottom, rect.width, rect.height), paint); - canvas.drawRect( - Rect.fromLTRB(0.0, boundaries.top, boundaries.left, boundaries.bottom), - paint); - canvas.drawRect( - Rect.fromLTRB( - boundaries.right, boundaries.top, rect.width, boundaries.bottom), - paint); - - if (boundaries.isEmpty == false) { - _drawGrid(canvas, boundaries); - } - - canvas.restore(); - } - - void _drawGrid(Canvas canvas, Rect boundaries) { - if (active == 0.0) return; - - final paint = Paint() - ..isAntiAlias = false - ..color = _kCropGridColor.withOpacity(_kCropGridColor.opacity * active) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.0; - - final path = Path() - ..moveTo(boundaries.left, boundaries.top) - ..lineTo(boundaries.right, boundaries.top) - ..lineTo(boundaries.right, boundaries.bottom) - ..lineTo(boundaries.left, boundaries.bottom) - ..lineTo(boundaries.left, boundaries.top); - for (var column = 1; column < _kCropGridColumnCount; column++) { - path - ..moveTo( - boundaries.left + column * boundaries.width / _kCropGridColumnCount, - boundaries.top) - ..lineTo( - boundaries.left + column * boundaries.width / _kCropGridColumnCount, - boundaries.bottom); - } - - for (var row = 1; row < _kCropGridRowCount; row++) { - path - ..moveTo(boundaries.left, - boundaries.top + row * boundaries.height / _kCropGridRowCount) - ..lineTo(boundaries.right, - boundaries.top + row * boundaries.height / _kCropGridRowCount); - } - - canvas.drawPath(path, paint); - } -} - -class _DisplayVideo extends StatefulWidget { - final File selectedFile; - const _DisplayVideo({Key? key, required this.selectedFile}) : super(key: key); - - @override - State<_DisplayVideo> createState() => _DisplayVideoState(); -} - -class _DisplayVideoState extends State<_DisplayVideo> { - late VideoPlayerController controller; - late Future initializeVideoPlayerFuture; - bool showPlayIcon = false; - @override - void initState() { - super.initState(); - - controller = VideoPlayerController.file(widget.selectedFile); - initializeVideoPlayerFuture = controller.initialize(); - controller.setLooping(true); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: initializeVideoPlayerFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return GestureDetector( - onTap: () { - setState(() { - if (controller.value.isPlaying) { - controller.pause(); - showPlayIcon = true; - } else { - controller.play(); - showPlayIcon = false; - } - }); - }, - child: Stack( - alignment: Alignment.center, - children: [ - AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: VideoPlayer(controller), - ), - Align( - alignment: Alignment.center, - child: Visibility( - visible: showPlayIcon, - child: Icon( - controller.value.isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - color: Colors.white, - size: 60, - ), - ), - ) - ], - ), - ); - } else { - return const Center( - child: CircularProgressIndicator(strokeWidth: 1), - ); - } - }, - ); - } -} +import 'dart:io'; +import 'dart:math'; +import 'dart:ui' as ui; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +const _kCropGridColumnCount = 3; +const _kCropGridRowCount = 3; +const _kCropGridColor = Color.fromRGBO(0xd0, 0xd0, 0xd0, 0.9); +const _kCropHandleSize = 0.0; +const _kCropHandleHitSize = 48.0; + +enum _CropAction { none, moving, cropping, scaling } + +enum _CropHandleSide { none, topLeft, topRight, bottomLeft, bottomRight } + +class CustomCrop extends StatefulWidget { + final File image; + final double? aspectRatio; + final double maximumScale; + + final bool alwaysShowGrid; + final Color? paintColor; + final ImageErrorListener? onImageError; + final ValueChanged? scrollCustomList; + final bool isThatImage; + + const CustomCrop({ + Key? key, + this.aspectRatio, + this.paintColor, + this.scrollCustomList, + this.maximumScale = 2.0, + this.alwaysShowGrid = false, + this.isThatImage = true, + this.onImageError, + required this.image, + }) : super(key: key); + + @override + State createState() => CustomCropState(); + + static CustomCropState? of(BuildContext context) => + context.findAncestorStateOfType(); +} + +class CustomCropState extends State + with TickerProviderStateMixin, Drag { + final _surfaceKey = GlobalKey(); + + late final AnimationController _activeController; + late final AnimationController _settleController; + + double _scale = 1.0; + double _ratio = 1.0; + Rect _view = Rect.zero; + Rect _area = Rect.zero; + Offset _lastFocalPoint = Offset.zero; + _CropAction _action = _CropAction.none; + _CropHandleSide _handle = _CropHandleSide.none; + + late double _startScale; + late Rect _startView; + late Tween _viewTween; + late Tween _scaleTween; + + ImageStream? _imageStream; + ui.Image? _image; + ImageStreamListener? _imageListener; + + double get scale => _area.shortestSide / _scale; + + Rect? get area => _view.isEmpty + ? null + : Rect.fromLTWH( + _area.left * _view.width / _scale - _view.left, + _area.top * _view.height / _scale - _view.top, + _area.width * _view.width / _scale, + _area.height * _view.height / _scale, + ); + bool get _isEnabled => _view.isEmpty == false && _image != null; + + final Map _maxAreaWidthMap = {}; + + int pointers = 0; + + @override + void initState() { + super.initState(); + + _activeController = AnimationController( + vsync: this, + value: widget.alwaysShowGrid ? 1.0 : 0.0, + )..addListener(() => setState(() {})); + _settleController = AnimationController(vsync: this) + ..addListener(_settleAnimationChanged); + } + + @override + void dispose() { + final listener = _imageListener; + if (listener != null) { + _imageStream?.removeListener(listener); + } + _activeController.dispose(); + _settleController.dispose(); + + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _getImage(); + } + + @override + void didUpdateWidget(CustomCrop oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.image != oldWidget.image) { + _getImage(); + } else if (widget.aspectRatio != oldWidget.aspectRatio) { + _area = _calculateDefaultArea( + viewWidth: _view.width, + viewHeight: _view.height, + imageWidth: _image?.width, + imageHeight: _image?.height, + ); + } + if (widget.alwaysShowGrid != oldWidget.alwaysShowGrid) { + if (widget.alwaysShowGrid) { + _activate(); + } else { + _deactivate(); + } + } + } + + void _getImage({bool force = false}) { + if (widget.isThatImage) { + final oldImageStream = _imageStream; + FileImage image = FileImage(widget.image, scale: 1.0); + final newImageStream = + image.resolve(createLocalImageConfiguration(context)); + _imageStream = newImageStream; + if (newImageStream.key != oldImageStream?.key || force) { + final oldImageListener = _imageListener; + if (oldImageListener != null) { + oldImageStream?.removeListener(oldImageListener); + } + final newImageListener = + ImageStreamListener(_updateImage, onError: widget.onImageError); + _imageListener = newImageListener; + newImageStream.addListener(newImageListener); + } + } + } + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + return SizedBox( + width: width, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: Listener( + onPointerDown: (event) => pointers++, + onPointerUp: (event) => pointers = 0, + child: GestureDetector( + key: _surfaceKey, + behavior: HitTestBehavior.opaque, + onScaleStart: _isEnabled ? _handleScaleStart : null, + onScaleUpdate: _isEnabled ? _handleScaleUpdate : null, + onScaleEnd: _isEnabled ? _handleScaleEnd : null, + child: AnimatedBuilder( + builder: (context, child) { + if (widget.isThatImage) { + return buildCustomPaint(); + } else { + return _DisplayVideo(selectedFile: widget.image); + } + }, + animation: _activeController, + ), + ), + ), + ), + ); + } + + void _handleScaleStart(ScaleStartDetails details) { + if (widget.scrollCustomList != null) widget.scrollCustomList!(true); + + _activate(); + _settleController.stop(canceled: false); + _lastFocalPoint = details.focalPoint; + _action = _CropAction.none; + _handle = _hitCropHandle(_getLocalPoint(details.focalPoint)); + _startScale = _scale; + _startView = _view; + } + + void _handleScaleUpdate(ScaleUpdateDetails details) { + if (_action == _CropAction.none) { + if (_handle == _CropHandleSide.none) { + _action = pointers == 2 ? _CropAction.scaling : _CropAction.moving; + } else { + _action = _CropAction.cropping; + } + } + if (_action == _CropAction.cropping) { + final boundaries = _boundaries; + if (boundaries == null) return; + } else if (_action == _CropAction.moving) { + final image = _image; + if (image == null) return; + + final delta = details.focalPoint - _lastFocalPoint; + _lastFocalPoint = details.focalPoint; + + setState(() { + _view = _view.translate( + delta.dx / (image.width * _scale * _ratio), + delta.dy / (image.height * _scale * _ratio), + ); + }); + } else if (_action == _CropAction.scaling) { + final image = _image; + final boundaries = _boundaries; + if (image == null || boundaries == null) return; + + setState(() { + _scale = _startScale * details.scale; + + final dx = boundaries.width * + (1.0 - details.scale) / + (image.width * _scale * _ratio); + final dy = boundaries.height * + (1.0 - details.scale) / + (image.height * _scale * _ratio); + _view = Rect.fromLTWH( + _startView.left + dx / 2, + _startView.top + dy / 2, + _startView.width, + _startView.height, + ); + }); + } + } + + void _handleScaleEnd(ScaleEndDetails details) { + if (widget.scrollCustomList != null) widget.scrollCustomList!(false); + + _deactivate(); + final minimumScale = _minimumScale; + if (minimumScale == null) return; + + final targetScale = _scale.clamp(minimumScale, _maximumScale); + _scaleTween = Tween( + begin: _scale, + end: targetScale, + ); + + _startView = _view; + _viewTween = RectTween( + begin: _view, + end: _getViewInBoundaries(targetScale), + ); + + _settleController.value = 0.0; + _settleController.animateTo( + 1.0, + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 350), + ); + } + + Rect _getViewInBoundaries(double scale) => + Offset( + max( + min(_view.left, _area.left * _view.width / scale), + _area.right * _view.width / scale - 1.0, + ), + max( + min( + _view.top, + _area.top * _view.height / scale, + ), + _area.bottom * _view.height / scale - 1.0), + ) & + _view.size; + + double get _maximumScale => widget.maximumScale; + + double? get _minimumScale { + final boundaries = _boundaries; + final image = _image; + if (boundaries == null || image == null) { + return null; + } + + final scaleX = boundaries.width * _area.width / (image.width * _ratio); + final scaleY = boundaries.height * _area.height / (image.height * _ratio); + return min(_maximumScale, max(scaleX, scaleY)); + } + + Widget buildCustomPaint() { + return CustomPaint( + painter: _CropPainter( + image: _image, + ratio: _ratio, + view: _view, + area: _area, + scale: _scale, + active: _activeController.value, + paintColor: widget.paintColor ?? Colors.white, + ), + ); + } + + void _activate() { + _activeController.animateTo( + 1.0, + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 250), + ); + } + + void _deactivate() { + if (widget.alwaysShowGrid == false) { + _activeController.animateTo( + 0.0, + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 250), + ); + } + } + + Size? get _boundaries { + final context = _surfaceKey.currentContext; + if (context == null) return null; + + final box = context.findRenderObject() as RenderBox; + final size = box.size; + + return size - const Offset(_kCropHandleSize, _kCropHandleSize) as Size; + } + + Offset? _getLocalPoint(Offset point) { + final context = _surfaceKey.currentContext; + if (context == null) return null; + + final box = context.findRenderObject() as RenderBox; + + return box.globalToLocal(point); + } + + void _settleAnimationChanged() { + setState(() { + _scale = _scaleTween.transform(_settleController.value); + final nextView = _viewTween.transform(_settleController.value); + if (nextView != null) { + _view = nextView; + } + }); + } + + Rect _calculateDefaultArea({ + required int? imageWidth, + required int? imageHeight, + required double viewWidth, + required double viewHeight, + }) { + if (imageWidth == null || imageHeight == null) return Rect.zero; + + double height; + double width; + if ((widget.aspectRatio ?? 1.0) < 1) { + height = 1.0; + width = + ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) / + imageWidth / + viewWidth; + if (width > 1.0) { + width = 1.0; + height = (imageWidth * viewWidth * width) / + (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0)); + } + } else { + width = 1.0; + height = (imageWidth * viewWidth * width) / + (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0)); + if (height > 1.0) { + height = 1.0; + width = + ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) / + imageWidth / + viewWidth; + } + } + final aspectRatio = _maxAreaWidthMap[widget.aspectRatio]; + if (aspectRatio != null) { + _maxAreaWidthMap[aspectRatio] = width; + } + ui.Rect rect = + Rect.fromLTWH((1.0 - width) / 2, (1.0 - height) / 2, width, height); + return rect; + } + + void _updateImage(ImageInfo imageInfo, bool synchronousCall) { + final boundaries = _boundaries; + if (boundaries == null) return; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final image = imageInfo.image; + + setState(() { + _image = image; + _scale = imageInfo.scale; + _ratio = max( + boundaries.width / image.width, + boundaries.height / image.height, + ); + + final viewWidth = boundaries.width / (image.width * _scale * _ratio); + final viewHeight = boundaries.height / (image.height * _scale * _ratio); + _area = _calculateDefaultArea( + viewWidth: viewWidth, + viewHeight: viewHeight, + imageWidth: image.width, + imageHeight: image.height, + ); + _view = Rect.fromLTWH( + (viewWidth - 1.0) / 2, + (viewHeight - 1.0) / 2, + viewWidth, + viewHeight, + ); + }); + }); + + WidgetsBinding.instance.ensureVisualUpdate(); + } + + _CropHandleSide _hitCropHandle(Offset? localPoint) { + final boundaries = _boundaries; + if (localPoint == null || boundaries == null) { + return _CropHandleSide.none; + } + + final viewRect = Rect.fromLTWH( + boundaries.width * _area.left, + boundaries.height * _area.top, + boundaries.width * _area.width, + boundaries.height * _area.height, + ).deflate(_kCropHandleSize / 2); + + if (Rect.fromLTWH( + viewRect.left - _kCropHandleHitSize / 2, + viewRect.top - _kCropHandleHitSize / 2, + _kCropHandleHitSize, + _kCropHandleHitSize, + ).contains(localPoint)) { + return _CropHandleSide.topLeft; + } + + if (Rect.fromLTWH( + viewRect.right - _kCropHandleHitSize / 2, + viewRect.top - _kCropHandleHitSize / 2, + _kCropHandleHitSize, + _kCropHandleHitSize, + ).contains(localPoint)) { + return _CropHandleSide.topRight; + } + + if (Rect.fromLTWH( + viewRect.left - _kCropHandleHitSize / 2, + viewRect.bottom - _kCropHandleHitSize / 2, + _kCropHandleHitSize, + _kCropHandleHitSize, + ).contains(localPoint)) { + return _CropHandleSide.bottomLeft; + } + + if (Rect.fromLTWH( + viewRect.right - _kCropHandleHitSize / 2, + viewRect.bottom - _kCropHandleHitSize / 2, + _kCropHandleHitSize, + _kCropHandleHitSize, + ).contains(localPoint)) { + return _CropHandleSide.bottomRight; + } + + return _CropHandleSide.none; + } +} + +class _CropPainter extends CustomPainter { + final ui.Image? image; + final Rect view; + final double ratio; + final Rect area; + final double scale; + final double active; + final Color paintColor; + + _CropPainter({ + required this.image, + required this.view, + required this.ratio, + required this.area, + required this.scale, + required this.active, + required this.paintColor, + }); + + @override + bool shouldRepaint(_CropPainter oldDelegate) { + return oldDelegate.image != image || + oldDelegate.view != view || + oldDelegate.ratio != ratio || + oldDelegate.area != area || + oldDelegate.active != active || + oldDelegate.scale != scale; + } + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH( + _kCropHandleSize / 2, + _kCropHandleSize / 2, + size.width - _kCropHandleSize, + size.height - _kCropHandleSize, + ); + canvas.save(); + canvas.translate(rect.left, rect.top); + + final paint = Paint()..isAntiAlias = false; + + final image = this.image; + if (image != null) { + final src = Rect.fromLTWH( + 0.0, + 0.0, + image.width.toDouble(), + image.height.toDouble(), + ); + final dst = Rect.fromLTWH( + view.left * image.width * scale * ratio, + view.top * image.height * scale * ratio, + image.width * scale * ratio, + image.height * scale * ratio, + ); + canvas.save(); + canvas.clipRect(Rect.fromLTWH(0.0, 0.0, rect.width, rect.height)); + canvas.drawImageRect(image, src, dst, paint); + canvas.restore(); + } + + paint.color = paintColor; + + final boundaries = Rect.fromLTWH( + rect.width * area.left, + rect.height * area.top, + rect.width * area.width, + rect.height * area.height, + ); + canvas.drawRect(Rect.fromLTRB(0.0, 0.0, rect.width, boundaries.top), paint); + canvas.drawRect( + Rect.fromLTRB(0.0, boundaries.bottom, rect.width, rect.height), paint); + canvas.drawRect( + Rect.fromLTRB(0.0, boundaries.top, boundaries.left, boundaries.bottom), + paint); + canvas.drawRect( + Rect.fromLTRB( + boundaries.right, boundaries.top, rect.width, boundaries.bottom), + paint); + + if (boundaries.isEmpty == false) { + _drawGrid(canvas, boundaries); + } + + canvas.restore(); + } + + void _drawGrid(Canvas canvas, Rect boundaries) { + if (active == 0.0) return; + + final paint = Paint() + ..isAntiAlias = false + ..color = _kCropGridColor.withOpacity(_kCropGridColor.opacity * active) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + + final path = Path() + ..moveTo(boundaries.left, boundaries.top) + ..lineTo(boundaries.right, boundaries.top) + ..lineTo(boundaries.right, boundaries.bottom) + ..lineTo(boundaries.left, boundaries.bottom) + ..lineTo(boundaries.left, boundaries.top); + for (var column = 1; column < _kCropGridColumnCount; column++) { + path + ..moveTo( + boundaries.left + column * boundaries.width / _kCropGridColumnCount, + boundaries.top) + ..lineTo( + boundaries.left + column * boundaries.width / _kCropGridColumnCount, + boundaries.bottom); + } + + for (var row = 1; row < _kCropGridRowCount; row++) { + path + ..moveTo(boundaries.left, + boundaries.top + row * boundaries.height / _kCropGridRowCount) + ..lineTo(boundaries.right, + boundaries.top + row * boundaries.height / _kCropGridRowCount); + } + + canvas.drawPath(path, paint); + } +} + +class _DisplayVideo extends StatefulWidget { + final File selectedFile; + const _DisplayVideo({Key? key, required this.selectedFile}) : super(key: key); + + @override + State<_DisplayVideo> createState() => _DisplayVideoState(); +} + +class _DisplayVideoState extends State<_DisplayVideo> { + late VideoPlayerController controller; + late Future initializeVideoPlayerFuture; + bool showPlayIcon = false; + @override + void initState() { + super.initState(); + + controller = VideoPlayerController.file(widget.selectedFile); + initializeVideoPlayerFuture = controller.initialize(); + controller.setLooping(true); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return GestureDetector( + onTap: () { + setState(() { + if (controller.value.isPlaying) { + controller.pause(); + showPlayIcon = true; + } else { + controller.play(); + showPlayIcon = false; + } + }); + }, + child: Stack( + alignment: Alignment.center, + children: [ + AspectRatio( + aspectRatio: controller.value.aspectRatio, + child: VideoPlayer(controller), + ), + Align( + alignment: Alignment.center, + child: Visibility( + visible: showPlayIcon, + child: Icon( + controller.value.isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: Colors.white, + size: 60, + ), + ), + ) + ], + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(strokeWidth: 1), + ); + } + }, + ); + } +} diff --git a/lib/src/custom_packages/crop_image/owner_of_this_package.dart b/lib/src/custom_packages/crop_image/owner_of_this_package.dart index a637fc4..092214c 100644 --- a/lib/src/custom_packages/crop_image/owner_of_this_package.dart +++ b/lib/src/custom_packages/crop_image/owner_of_this_package.dart @@ -1 +1 @@ -/// image_crop => "https://pub.dev/packages/image_crop" +/// image_crop => "https://pub.dev/packages/image_crop" diff --git a/lib/src/custom_packages/defenition b/lib/src/custom_packages/defenition index 7aa9cb2..f8a71a2 100644 --- a/lib/src/custom_packages/defenition +++ b/lib/src/custom_packages/defenition @@ -1,4 +1,4 @@ -custom packages it's not belong to me, -it's external packages but i want to make a lot of changes on them, -and i faces some issues when trying to customize them in their files, +custom packages it's not belong to me, +it's external packages but i want to make a lot of changes on them, +and i faces some issues when trying to customize them in their files, so i mentioned the owner of each package in his file (owner_of_this_package.dart) \ No newline at end of file diff --git a/lib/src/custom_route.dart b/lib/src/custom_route.dart index 37ffdc1..8565cd0 100644 --- a/lib/src/custom_route.dart +++ b/lib/src/custom_route.dart @@ -1,47 +1,47 @@ -import 'package:flutter/material.dart'; - -class HeroDialogRoute extends PageRoute { - final Color? backgroundColor; - final bool maintainStates; - - HeroDialogRoute({ - this.backgroundColor, - this.maintainStates = true, - required WidgetBuilder builder, - RouteSettings? settings, - bool fullscreenDialog = false, - }) : _builder = builder, - super(settings: settings, fullscreenDialog: fullscreenDialog); - - final WidgetBuilder _builder; - - @override - bool get opaque => false; - - @override - bool get barrierDismissible => true; - - @override - Duration get transitionDuration => const Duration(milliseconds: 300); - - @override - bool get maintainState => maintainStates; - - @override - Color get barrierColor => backgroundColor ?? Colors.white; - - @override - Widget buildTransitions(BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { - return child; - } - - @override - Widget buildPage(BuildContext context, Animation animation, - Animation secondaryAnimation) { - return _builder(context); - } - - @override - String get barrierLabel => 'Popup dialog open'; -} +import 'package:flutter/material.dart'; + +class HeroDialogRoute extends PageRoute { + final Color? backgroundColor; + final bool maintainStates; + + HeroDialogRoute({ + this.backgroundColor, + this.maintainStates = true, + required WidgetBuilder builder, + RouteSettings? settings, + bool fullscreenDialog = false, + }) : _builder = builder, + super(settings: settings, fullscreenDialog: fullscreenDialog); + + final WidgetBuilder _builder; + + @override + bool get opaque => false; + + @override + bool get barrierDismissible => true; + + @override + Duration get transitionDuration => const Duration(milliseconds: 300); + + @override + bool get maintainState => maintainStates; + + @override + Color get barrierColor => backgroundColor ?? Colors.white; + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + return child; + } + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return _builder(context); + } + + @override + String get barrierLabel => 'Popup dialog open'; +} diff --git a/lib/src/entities/app_theme.dart b/lib/src/entities/app_theme.dart index 78d87f1..57bb851 100644 --- a/lib/src/entities/app_theme.dart +++ b/lib/src/entities/app_theme.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; - -class AppTheme { - final Color primaryColor; - final Color focusColor; - final Color shimmerBaseColor; - final Color shimmerHighlightColor; - - AppTheme({ - this.primaryColor = Colors.white, - this.focusColor = Colors.black, - this.shimmerBaseColor = const Color.fromARGB(255, 185, 185, 185), - this.shimmerHighlightColor = const Color.fromARGB(255, 209, 209, 209), - }); -} +import 'package:flutter/material.dart'; + +class AppTheme { + final Color primaryColor; + final Color focusColor; + final Color shimmerBaseColor; + final Color shimmerHighlightColor; + + AppTheme({ + this.primaryColor = Colors.white, + this.focusColor = Colors.black, + this.shimmerBaseColor = const Color.fromARGB(255, 185, 185, 185), + this.shimmerHighlightColor = const Color.fromARGB(255, 209, 209, 209), + }); +} diff --git a/lib/src/entities/image_picker_display.dart b/lib/src/entities/image_picker_display.dart index 48a936f..e5ea57a 100644 --- a/lib/src/entities/image_picker_display.dart +++ b/lib/src/entities/image_picker_display.dart @@ -1,16 +1,16 @@ -import 'package:custom_gallery_display/custom_gallery_display.dart'; -import 'package:flutter/material.dart'; - -/// [GalleryDisplaySettings] When you make ImageSource from the camera these settings will be disabled because they belong to the gallery. -class GalleryDisplaySettings { - AppTheme? appTheme; - TabsTexts? tabsTexts; - SliverGridDelegateWithFixedCrossAxisCount gridDelegate; - - GalleryDisplaySettings({ - this.appTheme, - this.tabsTexts, - this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, crossAxisSpacing: 1.7, mainAxisSpacing: 1.5), - }); -} +import 'package:custom_gallery_display/custom_gallery_display.dart'; +import 'package:flutter/material.dart'; + +/// [GalleryDisplaySettings] When you make ImageSource from the camera these settings will be disabled because they belong to the gallery. +class GalleryDisplaySettings { + AppTheme? appTheme; + TabsTexts? tabsTexts; + SliverGridDelegateWithFixedCrossAxisCount gridDelegate; + + GalleryDisplaySettings({ + this.appTheme, + this.tabsTexts, + this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, crossAxisSpacing: 1.7, mainAxisSpacing: 1.5), + }); +} diff --git a/lib/src/entities/selected_image_details.dart b/lib/src/entities/selected_image_details.dart index cd66d8c..a14dc55 100644 --- a/lib/src/entities/selected_image_details.dart +++ b/lib/src/entities/selected_image_details.dart @@ -1,27 +1,27 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -class SelectedImagesDetails { - List selectedFiles; - double aspectRatio; - bool multiSelectionMode; - - SelectedImagesDetails({ - required this.selectedFiles, - required this.aspectRatio, - required this.multiSelectionMode, - }); -} - -class SelectedByte { - File selectedFile; - Uint8List selectedByte; - - bool isThatImage; - SelectedByte({ - required this.isThatImage, - required this.selectedFile, - required this.selectedByte, - }); -} +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +class SelectedImagesDetails { + List selectedFiles; + double aspectRatio; + bool multiSelectionMode; + + SelectedImagesDetails({ + required this.selectedFiles, + required this.aspectRatio, + required this.multiSelectionMode, + }); +} + +class SelectedByte { + File selectedFile; + Uint8List selectedByte; + + bool isThatImage; + SelectedByte({ + required this.isThatImage, + required this.selectedFile, + required this.selectedByte, + }); +} diff --git a/lib/src/entities/tabs_texts.dart b/lib/src/entities/tabs_texts.dart index 28be000..e84836b 100644 --- a/lib/src/entities/tabs_texts.dart +++ b/lib/src/entities/tabs_texts.dart @@ -1,25 +1,25 @@ -class TabsTexts { - final String videoText; - final String photoText; - final String galleryText; - final String deletingText; - final String limitingText; - final String holdButtonText; - final String clearImagesText; - final String notFoundingCameraText; - final String noImagesFounded; - final String acceptAllPermissions; - - TabsTexts({ - this.videoText = "VIDEO", - this.photoText = "PHOTO", - this.clearImagesText = "Clear selected images", - this.galleryText = "GALLERY", - this.deletingText = "DELETE", - this.limitingText = "The limit is 10 photos or videos.", - this.notFoundingCameraText = "No secondary camera found", - this.holdButtonText = "Press and hold to record", - this.noImagesFounded = "There is no images", - this.acceptAllPermissions = "Failed! accept all access permissions.", - }); -} +class TabsTexts { + final String videoText; + final String photoText; + final String galleryText; + final String deletingText; + final String limitingText; + final String holdButtonText; + final String clearImagesText; + final String notFoundingCameraText; + final String noImagesFounded; + final String acceptAllPermissions; + + TabsTexts({ + this.videoText = "VIDEO", + this.photoText = "PHOTO", + this.clearImagesText = "Clear selected images", + this.galleryText = "GALLERY", + this.deletingText = "DELETE", + this.limitingText = "The limit is 10 photos or videos.", + this.notFoundingCameraText = "No secondary camera found", + this.holdButtonText = "Press and hold to record", + this.noImagesFounded = "There is no images", + this.acceptAllPermissions = "Failed! accept all access permissions.", + }); +} diff --git a/lib/src/image.dart b/lib/src/image.dart index d185338..2ef8d04 100644 --- a/lib/src/image.dart +++ b/lib/src/image.dart @@ -1,48 +1,48 @@ -import 'dart:typed_data'; -import 'package:custom_gallery_display/custom_gallery_display.dart'; -import 'package:flutter/material.dart'; - -class MemoryImageDisplay extends StatefulWidget { - final Uint8List imageBytes; - final AppTheme appTheme; - - const MemoryImageDisplay( - {Key? key, required this.imageBytes, required this.appTheme}) - : super(key: key); - - @override - State createState() => _NetworkImageDisplayState(); -} - -class _NetworkImageDisplayState extends State { - @override - void didChangeDependencies() { - precacheImage(MemoryImage(widget.imageBytes), context); - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return buildOctoImage(); - } - - Widget buildOctoImage() { - return Container( - width: double.infinity, - color: widget.appTheme.shimmerBaseColor, - child: Image.memory( - widget.imageBytes, - errorBuilder: (context, url, error) => buildError(), - fit: BoxFit.cover, - width: double.infinity, - ), - ); - } - - SizedBox buildError() { - return SizedBox( - width: double.infinity, - child: Icon(Icons.warning_amber_rounded, - color: widget.appTheme.focusColor)); - } -} +import 'dart:typed_data'; +import 'package:custom_gallery_display/custom_gallery_display.dart'; +import 'package:flutter/material.dart'; + +class MemoryImageDisplay extends StatefulWidget { + final Uint8List imageBytes; + final AppTheme appTheme; + + const MemoryImageDisplay( + {Key? key, required this.imageBytes, required this.appTheme}) + : super(key: key); + + @override + State createState() => _NetworkImageDisplayState(); +} + +class _NetworkImageDisplayState extends State { + @override + void didChangeDependencies() { + precacheImage(MemoryImage(widget.imageBytes), context); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return buildOctoImage(); + } + + Widget buildOctoImage() { + return Container( + width: double.infinity, + color: widget.appTheme.shimmerBaseColor, + child: Image.memory( + widget.imageBytes, + errorBuilder: (context, url, error) => buildError(), + fit: BoxFit.cover, + width: double.infinity, + ), + ); + } + + SizedBox buildError() { + return SizedBox( + width: double.infinity, + child: Icon(Icons.warning_amber_rounded, + color: widget.appTheme.focusColor)); + } +} diff --git a/lib/src/images_view_page.dart b/lib/src/images_view_page.dart index db9dc7a..fbcf515 100644 --- a/lib/src/images_view_page.dart +++ b/lib/src/images_view_page.dart @@ -1,749 +1,884 @@ -import 'dart:io'; - -import 'package:image_crop/image_crop.dart'; -import 'package:custom_gallery_display/custom_gallery_display.dart'; -import 'package:custom_gallery_display/src/crop_image_view.dart'; -import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; -import 'package:custom_gallery_display/src/image.dart'; -import 'package:custom_gallery_display/src/multi_selection_mode.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:shimmer/shimmer.dart'; - -class ImagesViewPage extends StatefulWidget { - final ValueNotifier> multiSelectedImages; - final ValueNotifier multiSelectionMode; - final TabsTexts tabsTexts; - final bool cropImage; - final bool multiSelection; - final bool showInternalVideos; - final bool showInternalImages; - final AsyncValueSetter? sendRequestFunction; - - /// To avoid lag when you interacting with image when it expanded - final AppTheme appTheme; - final VoidCallback clearMultiImages; - final Color whiteColor; - final Color blackColor; - final bool showImagePreview; - final SliverGridDelegateWithFixedCrossAxisCount gridDelegate; - const ImagesViewPage({ - Key? key, - required this.multiSelectedImages, - required this.multiSelectionMode, - required this.clearMultiImages, - required this.sendRequestFunction, - required this.appTheme, - required this.tabsTexts, - required this.whiteColor, - required this.cropImage, - required this.multiSelection, - required this.showInternalVideos, - required this.showInternalImages, - required this.blackColor, - required this.showImagePreview, - required this.gridDelegate, - }) : super(key: key); - - @override - State createState() => _ImagesViewPageState(); -} - -class _ImagesViewPageState extends State - with - TickerProviderStateMixin, - AutomaticKeepAliveClientMixin { - final ValueNotifier>> _mediaList = - ValueNotifier([]); - - ValueNotifier> allImages = ValueNotifier([]); - final ValueNotifier> scaleOfCropsKeys = ValueNotifier([]); - final ValueNotifier> areaOfCropsKeys = ValueNotifier([]); - - ValueNotifier selectedImage = ValueNotifier(null); - ValueNotifier> indexOfSelectedImages = ValueNotifier([]); - - ScrollController scrollController = ScrollController(); - - final expandImage = ValueNotifier(false); - final expandHeight = ValueNotifier(0.0); - final moveAwayHeight = ValueNotifier(0.0); - final expandImageView = ValueNotifier(false); - - final isImagesReady = ValueNotifier(false); - final currentPage = ValueNotifier(0); - final lastPage = ValueNotifier(0); - - /// To avoid lag when you interacting with image when it expanded - final enableVerticalTapping = ValueNotifier(false); - final cropKey = ValueNotifier(GlobalKey()); - bool noPaddingForGridView = false; - - double scrollPixels = 0.0; - bool isScrolling = false; - bool noImages = false; - final noDuration = ValueNotifier(false); - int indexOfLatestImage = -1; - - @override - void dispose() { - _mediaList.dispose(); - allImages.dispose(); - scrollController.dispose(); - isImagesReady.dispose(); - currentPage.dispose(); - lastPage.dispose(); - expandImage.dispose(); - expandHeight.dispose(); - moveAwayHeight.dispose(); - expandImageView.dispose(); - enableVerticalTapping.dispose(); - cropKey.dispose(); - noDuration.dispose(); - selectedImage.dispose(); - scaleOfCropsKeys.dispose(); - areaOfCropsKeys.dispose(); - indexOfSelectedImages.dispose(); - super.dispose(); - } - - late Widget forBack; - @override - void initState() { - _fetchNewMedia(currentPageValue: 0); - super.initState(); - } - - bool _handleScrollEvent(ScrollNotification scroll, - {required int currentPageValue, required int lastPageValue}) { - if (scroll.metrics.pixels / scroll.metrics.maxScrollExtent > 0.33 && - currentPageValue != lastPageValue) { - _fetchNewMedia(currentPageValue: currentPageValue); - return true; - } - return false; - } - - _fetchNewMedia({required int currentPageValue}) async { - lastPage.value = currentPageValue; - PermissionState result = await PhotoManager.requestPermissionExtend(); - if (result.isAuth) { - RequestType type = widget.showInternalVideos && widget.showInternalImages - ? RequestType.common - : (widget.showInternalImages ? RequestType.image : RequestType.video); - - List albums = - await PhotoManager.getAssetPathList(onlyAll: true, type: type); - if (albums.isEmpty) { - WidgetsBinding.instance - .addPostFrameCallback((_) => setState(() => noImages = true)); - return; - } else if (noImages) { - noImages = false; - } - List media = - await albums[0].getAssetListPaged(page: currentPageValue, size: 60); - List> temp = []; - List imageTemp = []; - - for (int i = 0; i < media.length; i++) { - FutureBuilder gridViewImage = - await getImageGallery(media, i); - File? image = await highQualityImage(media, i); - temp.add(gridViewImage); - imageTemp.add(image); - } - _mediaList.value.addAll(temp); - allImages.value.addAll(imageTemp); - selectedImage.value ??= allImages.value[0]; - currentPage.value++; - isImagesReady.value = true; - WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {})); - } else { - await PhotoManager.requestPermissionExtend(); - PhotoManager.openSetting(); - } - } - - Future> getImageGallery( - List media, int i) async { - bool highResolution = widget.gridDelegate.crossAxisCount <= 3; - FutureBuilder futureBuilder = FutureBuilder( - future: media[i].thumbnailDataWithSize(highResolution - ? const ThumbnailSize(350, 350) - : const ThumbnailSize(200, 200)), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Uint8List? image = snapshot.data; - if (image != null) { - return Container( - color: const Color.fromARGB(255, 189, 189, 189), - child: Stack( - children: [ - Positioned.fill( - child: MemoryImageDisplay( - imageBytes: image, appTheme: widget.appTheme), - ), - if (media[i].type == AssetType.video) - const Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only(right: 5, bottom: 5), - child: Icon( - Icons.slow_motion_video_rounded, - color: Colors.white, - ), - ), - ), - ], - ), - ); - } - } - return const SizedBox(); - }, - ); - return futureBuilder; - } - - Future highQualityImage(List media, int i) async => - media[i].file; - - @override - Widget build(BuildContext context) { - super.build(context); - return noImages - ? Column( - children: [ - appBar(), - Flexible( - child: Center( - child: Text( - widget.tabsTexts.noImagesFounded, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ), - ], - ) - : buildGridView(); - } - - ValueListenableBuilder buildGridView() { - return ValueListenableBuilder( - valueListenable: isImagesReady, - builder: (context, bool isImagesReadyValue, child) { - if (isImagesReadyValue) { - return ValueListenableBuilder( - valueListenable: _mediaList, - builder: (context, List> mediaListValue, - child) { - return ValueListenableBuilder( - valueListenable: lastPage, - builder: (context, int lastPageValue, child) => - ValueListenableBuilder( - valueListenable: currentPage, - builder: (context, int currentPageValue, child) { - if (!widget.showImagePreview) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - normalAppBar(), - Flexible( - child: normalGridView(mediaListValue, - currentPageValue, lastPageValue)), - ], - ); - } else { - return instagramGridView( - mediaListValue, currentPageValue, lastPageValue); - } - }, - ), - ); - }, - ); - } else { - return loadingWidget(); - } - }, - ); - } - - Widget loadingWidget() { - return SingleChildScrollView( - child: Column( - children: [ - appBar(), - Shimmer.fromColors( - baseColor: widget.appTheme.shimmerBaseColor, - highlightColor: widget.appTheme.shimmerHighlightColor, - child: Column( - children: [ - if (widget.showImagePreview) ...[ - Container( - color: const Color(0xff696969), - height: 360, - width: double.infinity), - const SizedBox(height: 1), - ], - Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.gridDelegate.crossAxisSpacing), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - primary: false, - gridDelegate: widget.gridDelegate, - itemBuilder: (context, index) { - return Container( - color: const Color(0xff696969), - width: double.infinity); - }, - itemCount: 40, - ), - ), - ], - ), - ), - ], - ), - ); - } - - AppBar appBar() { - return AppBar( - backgroundColor: widget.appTheme.primaryColor, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.clear_rounded, - color: widget.appTheme.focusColor, size: 30), - onPressed: () { - Navigator.of(context).maybePop(null); - }, - ), - ); - } - - Widget normalAppBar() { - double width = MediaQuery.of(context).size.width; - return Container( - color: widget.whiteColor, - height: 56, - width: width, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [existButton(), const Spacer(), doneButton()], - ), - ); - } - - IconButton existButton() { - return IconButton( - icon: Icon(Icons.clear_rounded, color: widget.blackColor, size: 30), - onPressed: () { - Navigator.of(context).maybePop(null); - }, - ); - } - - Widget doneButton() { - return ValueListenableBuilder( - valueListenable: indexOfSelectedImages, - builder: (context, List indexOfSelectedImagesValue, child) => - IconButton( - icon: const Icon(Icons.arrow_forward_rounded, - color: Colors.blue, size: 30), - onPressed: () async { - double aspect = expandImage.value ? 6 / 8 : 1.0; - if (widget.multiSelectionMode.value && widget.multiSelection) { - if (areaOfCropsKeys.value.length != - widget.multiSelectedImages.value.length) { - scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); - areaOfCropsKeys.value.add(cropKey.value.currentState?.area); - } else { - if (indexOfLatestImage != -1) { - scaleOfCropsKeys.value[indexOfLatestImage] = - cropKey.value.currentState?.scale; - areaOfCropsKeys.value[indexOfLatestImage] = - cropKey.value.currentState?.area; - } - } - - List selectedBytes = []; - for (int i = 0; i < widget.multiSelectedImages.value.length; i++) { - File currentImage = widget.multiSelectedImages.value[i]; - String path = currentImage.path; - bool isThatVideo = path.contains("mp4", path.length - 5); - File? croppedImage = !isThatVideo && widget.cropImage - ? await cropImage(currentImage, indexOfCropImage: i) - : null; - File image = croppedImage ?? currentImage; - Uint8List byte = await image.readAsBytes(); - SelectedByte img = SelectedByte( - isThatImage: !isThatVideo, - selectedFile: image, - selectedByte: byte, - ); - selectedBytes.add(img); - } - if (selectedBytes.isNotEmpty) { - SelectedImagesDetails details = SelectedImagesDetails( - selectedFiles: selectedBytes, - multiSelectionMode: true, - aspectRatio: aspect, - ); - if (!mounted) return; - if (widget.sendRequestFunction != null) { - await widget.sendRequestFunction!(details); - } else { - Navigator.of(context).maybePop(details); - } - } - } else { - File? image = selectedImage.value; - if (image == null) return; - String path = image.path; - - bool isThatVideo = path.contains("mp4", path.length - 5); - File? croppedImage = !isThatVideo && widget.cropImage - ? await cropImage(image) - : null; - File img = croppedImage ?? image; - Uint8List byte = await img.readAsBytes(); - - SelectedByte selectedByte = SelectedByte( - isThatImage: !isThatVideo, - selectedFile: img, - selectedByte: byte, - ); - SelectedImagesDetails details = SelectedImagesDetails( - multiSelectionMode: false, - aspectRatio: aspect, - selectedFiles: [selectedByte], - ); - if (!mounted) return; - if (widget.sendRequestFunction != null) { - await widget.sendRequestFunction!(details); - } else { - Navigator.of(context).maybePop(details); - } - } - }, - ), - ); - } - - Widget normalGridView(List> mediaListValue, - int currentPageValue, int lastPageValue) { - return NotificationListener( - onNotification: (ScrollNotification notification) { - _handleScrollEvent(notification, - currentPageValue: currentPageValue, lastPageValue: lastPageValue); - return true; - }, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.gridDelegate.crossAxisSpacing), - child: GridView.builder( - gridDelegate: widget.gridDelegate, - itemBuilder: (context, index) { - return buildImage(mediaListValue, index); - }, - itemCount: mediaListValue.length, - ), - ), - ); - } - - ValueListenableBuilder buildImage( - List> mediaListValue, int index) { - return ValueListenableBuilder( - valueListenable: selectedImage, - builder: (context, File? selectedImageValue, child) { - return ValueListenableBuilder( - valueListenable: allImages, - builder: (context, List allImagesValue, child) { - return ValueListenableBuilder( - valueListenable: widget.multiSelectedImages, - builder: (context, List selectedImagesValue, child) { - FutureBuilder mediaList = mediaListValue[index]; - File? image = allImagesValue[index]; - if (image != null) { - bool imageSelected = selectedImagesValue.contains(image); - List multiImages = selectedImagesValue; - return Stack( - children: [ - gestureDetector(image, index, mediaList), - if (selectedImageValue == image) - gestureDetector(image, index, blurContainer()), - MultiSelectionMode( - image: image, - multiSelectionMode: widget.multiSelectionMode, - imageSelected: imageSelected, - multiSelectedImage: multiImages, - ), - ], - ); - } else { - return const SizedBox(); - } - }, - ); - }, - ); - }, - ); - } - - Container blurContainer() { - return Container( - width: double.infinity, - color: const Color.fromARGB(184, 234, 234, 234), - height: double.maxFinite, - ); - } - - Widget gestureDetector(File image, int index, Widget childWidget) { - return ValueListenableBuilder( - valueListenable: widget.multiSelectionMode, - builder: (context, bool multipleValue, child) => ValueListenableBuilder( - valueListenable: widget.multiSelectedImages, - builder: (context, List selectedImagesValue, child) => - GestureDetector( - onTap: () => onTapImage(image, selectedImagesValue, index), - onLongPress: () { - if (widget.multiSelection) { - widget.multiSelectionMode.value = true; - } - }, - onLongPressUp: () { - if (multipleValue) { - selectionImageCheck(image, selectedImagesValue, index, - enableCopy: true); - expandImageView.value = false; - moveAwayHeight.value = 0; - - enableVerticalTapping.value = false; - setState(() => noPaddingForGridView = true); - } else { - onTapImage(image, selectedImagesValue, index); - } - }, - child: childWidget), - ), - ); - } - - onTapImage(File image, List selectedImagesValue, int index) { - setState(() { - if (widget.multiSelectionMode.value) { - bool close = selectionImageCheck(image, selectedImagesValue, index); - if (close) return; - } - selectedImage.value = image; - expandImageView.value = false; - moveAwayHeight.value = 0; - enableVerticalTapping.value = false; - noPaddingForGridView = true; - }); - } - - bool selectionImageCheck( - File image, List multiSelectionValue, int index, - {bool enableCopy = false}) { - if (multiSelectionValue.contains(image) && selectedImage.value == image) { - setState(() { - int indexOfImage = - multiSelectionValue.indexWhere((element) => element == image); - multiSelectionValue.removeAt(indexOfImage); - if (multiSelectionValue.isNotEmpty && - indexOfImage < scaleOfCropsKeys.value.length) { - indexOfSelectedImages.value.remove(index); - - scaleOfCropsKeys.value.removeAt(indexOfImage); - areaOfCropsKeys.value.removeAt(indexOfImage); - indexOfLatestImage = -1; - } - }); - - return true; - } else { - if (multiSelectionValue.length < 10) { - setState(() { - if (!multiSelectionValue.contains(image)) { - multiSelectionValue.add(image); - if (multiSelectionValue.length > 1) { - scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); - areaOfCropsKeys.value.add(cropKey.value.currentState?.area); - indexOfSelectedImages.value.add(index); - } - } else if (areaOfCropsKeys.value.length != - multiSelectionValue.length) { - scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); - areaOfCropsKeys.value.add(cropKey.value.currentState?.area); - } - if (widget.showImagePreview && multiSelectionValue.contains(image)) { - int index = - multiSelectionValue.indexWhere((element) => element == image); - if (indexOfLatestImage != -1) { - scaleOfCropsKeys.value[indexOfLatestImage] = - cropKey.value.currentState?.scale; - areaOfCropsKeys.value[indexOfLatestImage] = - cropKey.value.currentState?.area; - } - indexOfLatestImage = index; - } - - if (enableCopy) selectedImage.value = image; - }); - } - return false; - } - } - - Future cropImage(File imageFile, {int? indexOfCropImage}) async { - await ImageCrop.requestPermissions(); - final double? scale; - final Rect? area; - if (indexOfCropImage == null) { - scale = cropKey.value.currentState?.scale; - area = cropKey.value.currentState?.area; - } else { - scale = scaleOfCropsKeys.value[indexOfCropImage]; - area = areaOfCropsKeys.value[indexOfCropImage]; - } - - if (area == null || scale == null) return null; - - final sample = await ImageCrop.sampleImage( - file: imageFile, - preferredSize: (2000 / scale).round(), - ); - - final File file = await ImageCrop.cropImage( - file: sample, - area: area, - ); - sample.delete(); - return file; - } - - void clearMultiImages() { - setState(() { - widget.multiSelectedImages.value = []; - widget.clearMultiImages(); - indexOfSelectedImages.value.clear(); - scaleOfCropsKeys.value.clear(); - areaOfCropsKeys.value.clear(); - }); - } - - Widget instagramGridView(List> mediaListValue, - int currentPageValue, int lastPageValue) { - return ValueListenableBuilder( - valueListenable: expandHeight, - builder: (context, double expandedHeightValue, child) { - return ValueListenableBuilder( - valueListenable: moveAwayHeight, - builder: (context, double moveAwayHeightValue, child) => - ValueListenableBuilder( - valueListenable: expandImageView, - builder: (context, bool expandImageValue, child) { - double a = expandedHeightValue - 360; - double expandHeightV = a < 0 ? a : 0; - double moveAwayHeightV = - moveAwayHeightValue < 360 ? moveAwayHeightValue * -1 : -360; - double topPosition = - expandImageValue ? expandHeightV : moveAwayHeightV; - enableVerticalTapping.value = !(topPosition == 0); - double padding = 2; - if (scrollPixels < 416) { - double pixels = 416 - scrollPixels; - padding = pixels >= 58 ? pixels + 2 : 58; - } else if (expandImageValue) { - padding = 58; - } else if (noPaddingForGridView) { - padding = 58; - } else { - padding = topPosition + 418; - } - int duration = noDuration.value ? 0 : 250; - - return Stack( - children: [ - Padding( - padding: EdgeInsets.only(top: padding), - child: NotificationListener( - onNotification: (ScrollNotification notification) { - expandImageView.value = false; - moveAwayHeight.value = scrollController.position.pixels; - scrollPixels = scrollController.position.pixels; - setState(() { - isScrolling = true; - noPaddingForGridView = false; - noDuration.value = false; - if (notification is ScrollEndNotification) { - expandHeight.value = - expandedHeightValue > 240 ? 360 : 0; - isScrolling = false; - } - }); - - _handleScrollEvent(notification, - currentPageValue: currentPageValue, - lastPageValue: lastPageValue); - return true; - }, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.gridDelegate.crossAxisSpacing), - child: GridView.builder( - gridDelegate: widget.gridDelegate, - controller: scrollController, - itemBuilder: (context, index) { - return buildImage(mediaListValue, index); - }, - itemCount: mediaListValue.length, - ), - ), - ), - ), - AnimatedPositioned( - top: topPosition, - duration: Duration(milliseconds: duration), - child: Column( - children: [ - normalAppBar(), - CropImageView( - cropKey: cropKey, - indexOfSelectedImages: indexOfSelectedImages, - selectedImage: selectedImage, - appTheme: widget.appTheme, - multiSelectionMode: widget.multiSelectionMode, - enableVerticalTapping: enableVerticalTapping, - expandHeight: expandHeight, - expandImage: expandImage, - expandImageView: expandImageView, - noDuration: noDuration, - clearMultiImages: clearMultiImages, - topPosition: topPosition, - whiteColor: widget.whiteColor, - ), - ], - ), - ), - ], - ); - }, - ), - ); - }, - ); - } - - @override - bool get wantKeepAlive => true; -} +import 'dart:io'; + +import 'package:image_crop/image_crop.dart'; +import 'package:custom_gallery_display/custom_gallery_display.dart'; +import 'package:custom_gallery_display/src/crop_image_view.dart'; +import 'package:custom_gallery_display/src/custom_packages/crop_image/crop_image.dart'; +import 'package:custom_gallery_display/src/image.dart'; +import 'package:custom_gallery_display/src/multi_selection_mode.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:shimmer/shimmer.dart'; + +class ImagesViewPage extends StatefulWidget { + final ValueNotifier> multiSelectedImages; + final ValueNotifier multiSelectionMode; + final TabsTexts tabsTexts; + final bool cropImage; + final bool multiSelection; + final bool showInternalVideos; + final bool showInternalImages; + final AsyncValueSetter? sendRequestFunction; + + /// To avoid lag when you interacting with image when it expanded + final AppTheme appTheme; + final VoidCallback clearMultiImages; + final Color whiteColor; + final Color blackColor; + final bool showImagePreview; + final SliverGridDelegateWithFixedCrossAxisCount gridDelegate; + + const ImagesViewPage({ + Key? key, + required this.multiSelectedImages, + required this.multiSelectionMode, + required this.clearMultiImages, + required this.sendRequestFunction, + required this.appTheme, + required this.tabsTexts, + required this.whiteColor, + required this.cropImage, + required this.multiSelection, + required this.showInternalVideos, + required this.showInternalImages, + required this.blackColor, + required this.showImagePreview, + required this.gridDelegate, + }) : super(key: key); + + @override + State createState() => _ImagesViewPageState(); +} + +class _ImagesViewPageState extends State + with + TickerProviderStateMixin, + AutomaticKeepAliveClientMixin { + final ValueNotifier>> _mediaList = + ValueNotifier([]); + + ValueNotifier> allImages = ValueNotifier([]); + final ValueNotifier> scaleOfCropsKeys = ValueNotifier([]); + final ValueNotifier> areaOfCropsKeys = ValueNotifier([]); + + ValueNotifier selectedImage = ValueNotifier(null); + ValueNotifier> indexOfSelectedImages = ValueNotifier([]); + final GlobalKey _keyLoader = new GlobalKey(); + + ScrollController scrollController = ScrollController(); + + final expandImage = ValueNotifier(false); + final expandHeight = ValueNotifier(0.0); + final moveAwayHeight = ValueNotifier(0.0); + final expandImageView = ValueNotifier(false); + + final isImagesReady = ValueNotifier(false); + final currentPage = ValueNotifier(0); + final lastPage = ValueNotifier(0); + + /// To avoid lag when you interacting with image when it expanded + final enableVerticalTapping = ValueNotifier(false); + final cropKey = ValueNotifier(GlobalKey()); + bool noPaddingForGridView = false; + + double scrollPixels = 0.0; + bool isScrolling = false; + bool noImages = false; + final noDuration = ValueNotifier(false); + int indexOfLatestImage = -1; + + @override + void dispose() { + _mediaList.dispose(); + allImages.dispose(); + scrollController.dispose(); + isImagesReady.dispose(); + currentPage.dispose(); + lastPage.dispose(); + expandImage.dispose(); + expandHeight.dispose(); + moveAwayHeight.dispose(); + expandImageView.dispose(); + enableVerticalTapping.dispose(); + cropKey.dispose(); + noDuration.dispose(); + selectedImage.dispose(); + scaleOfCropsKeys.dispose(); + areaOfCropsKeys.dispose(); + indexOfSelectedImages.dispose(); + super.dispose(); + } + + late Widget forBack; + + @override + void initState() { + _fetchNewMedia(currentPageValue: 0); + super.initState(); + } + + bool _handleScrollEvent(ScrollNotification scroll, + {required int currentPageValue, required int lastPageValue}) { + if (scroll.metrics.pixels / scroll.metrics.maxScrollExtent > 0.33 && + currentPageValue != lastPageValue) { + _fetchNewMedia(currentPageValue: currentPageValue); + return true; + } + return false; + } + + _fetchNewMedia({required int currentPageValue}) async { + lastPage.value = currentPageValue; + PermissionState result = await PhotoManager.requestPermissionExtend(); + if (result.isAuth) { + RequestType type = widget.showInternalVideos && widget.showInternalImages + ? RequestType.common + : (widget.showInternalImages ? RequestType.image : RequestType.video); + + List albums = + await PhotoManager.getAssetPathList(onlyAll: true, type: type); + if (albums.isEmpty) { + WidgetsBinding.instance + .addPostFrameCallback((_) => setState(() => noImages = true)); + return; + } else if (noImages) { + noImages = false; + } + List media = + await albums[0].getAssetListPaged(page: currentPageValue, size: 60); + List> temp = []; + List imageTemp = []; + + for (int i = 0; i < media.length; i++) { + FutureBuilder gridViewImage = + await getImageGallery(media, i); + File? image = await highQualityImage(media, i); + temp.add(gridViewImage); + imageTemp.add(image); + } + _mediaList.value.addAll(temp); + allImages.value.addAll(imageTemp); + selectedImage.value ??= allImages.value[0]; + currentPage.value++; + isImagesReady.value = true; + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {})); + } else { + await PhotoManager.requestPermissionExtend(); + PhotoManager.openSetting(); + } + } + + Future> getImageGallery(List media, + int i) async { + bool highResolution = widget.gridDelegate.crossAxisCount <= 3; + FutureBuilder futureBuilder = FutureBuilder( + future: media[i].thumbnailDataWithSize(highResolution + ? const ThumbnailSize(350, 350) + : const ThumbnailSize(200, 200)), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Uint8List? image = snapshot.data; + if (image != null) { + return Container( + color: const Color.fromARGB(255, 189, 189, 189), + child: Stack( + children: [ + Positioned.fill( + child: MemoryImageDisplay( + imageBytes: image, appTheme: widget.appTheme), + ), + if (media[i].type == AssetType.video) + const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(right: 5, bottom: 5), + child: Icon( + Icons.slow_motion_video_rounded, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + } + return const SizedBox(); + }, + ); + return futureBuilder; + } + + Future highQualityImage(List media, int i) async => + media[i].file; + + @override + Widget build(BuildContext context) { + super.build(context); + return noImages + ? Column( + children: [ + appBar(), + Flexible( + child: Center( + child: Text( + widget.tabsTexts.noImagesFounded, + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold,color: Colors.white), + ), + ), + ), + ], + ) + : buildGridView(); + } + + ValueListenableBuilder buildGridView() { + return ValueListenableBuilder( + valueListenable: isImagesReady, + builder: (context, bool isImagesReadyValue, child) { + if (isImagesReadyValue) { + return ValueListenableBuilder( + valueListenable: _mediaList, + builder: (context, List> mediaListValue, + child) { + return ValueListenableBuilder( + valueListenable: lastPage, + builder: (context, int lastPageValue, child) => + ValueListenableBuilder( + valueListenable: currentPage, + builder: (context, int currentPageValue, child) { + if (!widget.showImagePreview) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + normalAppBar(), + Flexible( + child: normalGridView(mediaListValue, + currentPageValue, lastPageValue)), + ], + ); + } else { + return instagramGridView( + mediaListValue, currentPageValue, lastPageValue); + } + }, + ), + ); + }, + ); + } else { + return loadingWidget(); + } + }, + ); + } + + Widget loadingWidget() { + return SingleChildScrollView( + child: Column( + children: [ + appBar(), + Shimmer.fromColors( + baseColor: widget.appTheme.shimmerBaseColor, + highlightColor: widget.appTheme.shimmerHighlightColor, + child: Column( + children: [ + if (widget.showImagePreview) ...[ + Container( + color: const Color(0xff696969), + height: 360, + width: double.infinity), + const SizedBox(height: 1), + ], + Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.gridDelegate.crossAxisSpacing), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + primary: false, + gridDelegate: widget.gridDelegate, + itemBuilder: (context, index) { + return Container( + color: const Color(0xff696969), + width: double.infinity); + }, + itemCount: 40, + ), + ), + ], + ), + ), + ], + ), + ); + } + + AppBar appBar() { + return AppBar( + backgroundColor: widget.appTheme.primaryColor, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.clear_rounded, + color: widget.appTheme.focusColor, size: 30,), + onPressed: () { + Navigator.of(context).maybePop(null); + }, + ), + ); + } + + Widget normalAppBar() { + double width = MediaQuery + .of(context) + .size + .width; + return Container( + color: widget.whiteColor, + height: 56, + width: width, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + existButton(), + Text("Add Post", style: TextStyle(color: Colors.white,fontWeight: FontWeight.w700,fontSize: 20),), + doneButton() + ], + ), + ); + } + + IconButton existButton() { + return IconButton( + icon: Icon(Icons.clear_rounded, color: widget.blackColor, size: 30), + onPressed: () { + Navigator.of(context).maybePop(null); + }, + ); + } + + Widget doneButton() { + return ValueListenableBuilder( + valueListenable: indexOfSelectedImages, + builder: (context, List indexOfSelectedImagesValue, child) => + IconButton( + icon: const Icon(Icons.arrow_forward_rounded, + color: Colors.white, size: 30), + onPressed: () async { + DialogBuilder(context).showLoadingIndicator(); + double aspect = expandImage.value ? 6 / 8 : 1.0; + + if (widget.multiSelectionMode.value && widget.multiSelection) { + if (areaOfCropsKeys.value.length != + widget.multiSelectedImages.value.length) { + scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); + areaOfCropsKeys.value.add(cropKey.value.currentState?.area); + + } else { + if (indexOfLatestImage != -1) { + scaleOfCropsKeys.value[indexOfLatestImage] = + cropKey.value.currentState?.scale; + areaOfCropsKeys.value[indexOfLatestImage] = + cropKey.value.currentState?.area; + + } + } + + List selectedBytes = []; + for (int i = 0; i < + widget.multiSelectedImages.value.length; i++) { + File currentImage = widget.multiSelectedImages.value[i]; + String path = currentImage.path; + bool isThatVideo = path.contains("mp4", path.length - 5); + File? croppedImage = !isThatVideo && widget.cropImage + ? await cropImage(currentImage, indexOfCropImage: i) + : null; + File image = croppedImage ?? currentImage; + Uint8List byte = await image.readAsBytes(); + SelectedByte img = SelectedByte( + isThatImage: !isThatVideo, + selectedFile: image, + selectedByte: byte, + ); + selectedBytes.add(img); + } + if (selectedBytes.isNotEmpty) { + SelectedImagesDetails details = SelectedImagesDetails( + selectedFiles: selectedBytes, + multiSelectionMode: true, + aspectRatio: aspect, + ); + if (!mounted) return; + if (widget.sendRequestFunction != null) { + await widget.sendRequestFunction!(details); + + } else { + Navigator.of(context).maybePop(details); + + } + } + + } else { + File? image = selectedImage.value; + if (image == null) return; + String path = image.path; + + bool isThatVideo = path.contains("mp4", path.length - 5); + File? croppedImage = !isThatVideo && widget.cropImage + ? await cropImage(image) + : null; + File img = croppedImage ?? image; + Uint8List byte = await img.readAsBytes(); + + SelectedByte selectedByte = SelectedByte( + isThatImage: !isThatVideo, + selectedFile: img, + selectedByte: byte, + ); + SelectedImagesDetails details = SelectedImagesDetails( + multiSelectionMode: false, + aspectRatio: aspect, + selectedFiles: [selectedByte], + ); + if (!mounted) return; + if (widget.sendRequestFunction != null) { + await widget.sendRequestFunction!(details); + } else { + Navigator.of(context).maybePop(details); + } + + } + // DialogBuilder(context).hideOpenDialog(); + + }, + ), + ); + } + + Widget normalGridView(List> mediaListValue, + int currentPageValue, int lastPageValue) { + return NotificationListener( + onNotification: (ScrollNotification notification) { + _handleScrollEvent(notification, + currentPageValue: currentPageValue, lastPageValue: lastPageValue); + return true; + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.gridDelegate.crossAxisSpacing), + child: GridView.builder( + gridDelegate: widget.gridDelegate, + itemBuilder: (context, index) { + return buildImage(mediaListValue, index); + }, + itemCount: mediaListValue.length, + ), + ), + ); + } + + ValueListenableBuilder buildImage( + List> mediaListValue, int index) { + return ValueListenableBuilder( + valueListenable: selectedImage, + builder: (context, File? selectedImageValue, child) { + return ValueListenableBuilder( + valueListenable: allImages, + builder: (context, List allImagesValue, child) { + return ValueListenableBuilder( + valueListenable: widget.multiSelectedImages, + builder: (context, List selectedImagesValue, child) { + FutureBuilder mediaList = mediaListValue[index]; + File? image = allImagesValue[index]; + if (image != null) { + bool imageSelected = selectedImagesValue.contains(image); + List multiImages = selectedImagesValue; + return Stack( + children: [ + gestureDetector(image, index, mediaList), + if (selectedImageValue == image) + gestureDetector(image, index, blurContainer()), + MultiSelectionMode( + image: image, + multiSelectionMode: widget.multiSelectionMode, + imageSelected: imageSelected, + multiSelectedImage: multiImages, + ), + ], + ); + } else { + return const SizedBox(); + } + }, + ); + }, + ); + }, + ); + } + + Container blurContainer() { + return Container( + width: double.infinity, + color: const Color.fromARGB(184, 234, 234, 234), + height: double.maxFinite, + ); + } + + Widget gestureDetector(File image, int index, Widget childWidget) { + return ValueListenableBuilder( + valueListenable: widget.multiSelectionMode, + builder: (context, bool multipleValue, child) => + ValueListenableBuilder( + valueListenable: widget.multiSelectedImages, + builder: (context, List selectedImagesValue, child) => + GestureDetector( + onTap: () => onTapImage(image, selectedImagesValue, index), + onLongPress: () { + if (widget.multiSelection) { + widget.multiSelectionMode.value = true; + } + }, + onLongPressUp: () { + if (multipleValue) { + selectionImageCheck(image, selectedImagesValue, index, + enableCopy: true); + expandImageView.value = false; + moveAwayHeight.value = 0; + + enableVerticalTapping.value = false; + setState(() => noPaddingForGridView = true); + } else { + onTapImage(image, selectedImagesValue, index); + } + }, + child: childWidget), + ), + ); + } + + onTapImage(File image, List selectedImagesValue, int index) { + setState(() { + if (widget.multiSelectionMode.value) { + bool close = selectionImageCheck(image, selectedImagesValue, index); + if (close) return; + } + selectedImage.value = image; + expandImageView.value = false; + moveAwayHeight.value = 0; + enableVerticalTapping.value = false; + noPaddingForGridView = true; + }); + } + + bool selectionImageCheck(File image, List multiSelectionValue, + int index, + {bool enableCopy = false}) { + if (multiSelectionValue.contains(image) && selectedImage.value == image) { + setState(() { + int indexOfImage = + multiSelectionValue.indexWhere((element) => element == image); + multiSelectionValue.removeAt(indexOfImage); + if (multiSelectionValue.isNotEmpty && + indexOfImage < scaleOfCropsKeys.value.length) { + indexOfSelectedImages.value.remove(index); + + scaleOfCropsKeys.value.removeAt(indexOfImage); + areaOfCropsKeys.value.removeAt(indexOfImage); + indexOfLatestImage = -1; + } + }); + + return true; + } else { + if (multiSelectionValue.length < 10) { + setState(() { + if (!multiSelectionValue.contains(image)) { + multiSelectionValue.add(image); + if (multiSelectionValue.length > 1) { + scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); + areaOfCropsKeys.value.add(cropKey.value.currentState?.area); + indexOfSelectedImages.value.add(index); + } + } else if (areaOfCropsKeys.value.length != + multiSelectionValue.length) { + scaleOfCropsKeys.value.add(cropKey.value.currentState?.scale); + areaOfCropsKeys.value.add(cropKey.value.currentState?.area); + } + if (widget.showImagePreview && multiSelectionValue.contains(image)) { + int index = + multiSelectionValue.indexWhere((element) => element == image); + if (indexOfLatestImage != -1) { + scaleOfCropsKeys.value[indexOfLatestImage] = + cropKey.value.currentState?.scale; + areaOfCropsKeys.value[indexOfLatestImage] = + cropKey.value.currentState?.area; + } + indexOfLatestImage = index; + } + + if (enableCopy) selectedImage.value = image; + }); + } + return false; + } + } + + Future cropImage(File imageFile, {int? indexOfCropImage}) async { + await ImageCrop.requestPermissions(); + final double? scale; + final Rect? area; + if (indexOfCropImage == null) { + scale = cropKey.value.currentState?.scale; + area = cropKey.value.currentState?.area; + } else { + scale = scaleOfCropsKeys.value[indexOfCropImage]; + area = areaOfCropsKeys.value[indexOfCropImage]; + } + + if (area == null || scale == null) return null; + + final sample = await ImageCrop.sampleImage( + file: imageFile, + preferredSize: (2000 / scale).round(), + ); + + final File file = await ImageCrop.cropImage( + file: sample, + area: area, + ); + sample.delete(); + return file; + } + + void clearMultiImages() { + setState(() { + widget.multiSelectedImages.value = []; + widget.clearMultiImages(); + indexOfSelectedImages.value.clear(); + scaleOfCropsKeys.value.clear(); + areaOfCropsKeys.value.clear(); + }); + } + + Widget instagramGridView(List> mediaListValue, + int currentPageValue, int lastPageValue) { + return ValueListenableBuilder( + valueListenable: expandHeight, + builder: (context, double expandedHeightValue, child) { + return ValueListenableBuilder( + valueListenable: moveAwayHeight, + builder: (context, double moveAwayHeightValue, child) => + ValueListenableBuilder( + valueListenable: expandImageView, + builder: (context, bool expandImageValue, child) { + double a = expandedHeightValue - 360; + double expandHeightV = a < 0 ? a : 0; + double moveAwayHeightV = + moveAwayHeightValue < 360 ? moveAwayHeightValue * -1 : -360; + double topPosition = + expandImageValue ? expandHeightV : moveAwayHeightV; + enableVerticalTapping.value = !(topPosition == 0); + double padding = 2; + if (scrollPixels < 416) { + double pixels = 416 - scrollPixels; + padding = pixels >= 58 ? pixels + 2 : 58; + } else if (expandImageValue) { + padding = 58; + } else if (noPaddingForGridView) { + padding = 58; + } else { + padding = topPosition + 418; + } + int duration = noDuration.value ? 0 : 250; + + return Stack( + children: [ + Padding( + padding: EdgeInsets.only(top: padding), + child: NotificationListener( + onNotification: (ScrollNotification notification) { + expandImageView.value = false; + moveAwayHeight.value = + scrollController.position.pixels; + scrollPixels = scrollController.position.pixels; + setState(() { + isScrolling = true; + noPaddingForGridView = false; + noDuration.value = false; + if (notification is ScrollEndNotification) { + expandHeight.value = + expandedHeightValue > 240 ? 360 : 0; + isScrolling = false; + } + }); + + _handleScrollEvent(notification, + currentPageValue: currentPageValue, + lastPageValue: lastPageValue); + return true; + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.gridDelegate + .crossAxisSpacing), + child: GridView.builder( + gridDelegate: widget.gridDelegate, + controller: scrollController, + itemBuilder: (context, index) { + return buildImage(mediaListValue, index); + }, + itemCount: mediaListValue.length, + ), + ), + ), + ), + AnimatedPositioned( + top: topPosition, + duration: Duration(milliseconds: duration), + child: Column( + children: [ + normalAppBar(), + CropImageView( + cropKey: cropKey, + indexOfSelectedImages: indexOfSelectedImages, + selectedImage: selectedImage, + appTheme: widget.appTheme, + multiSelectionMode: widget.multiSelectionMode, + enableVerticalTapping: enableVerticalTapping, + expandHeight: expandHeight, + expandImage: expandImage, + expandImageView: expandImageView, + noDuration: noDuration, + clearMultiImages: clearMultiImages, + topPosition: topPosition, + whiteColor: widget.whiteColor, + ), + ], + ), + ), + ], + ); + }, + ), + ); + }, + ); + } + + @override + bool get wantKeepAlive => true; + +} +class Dialogs { + static Future showLoadingDialog( + BuildContext context, GlobalKey key) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return new WillPopScope( + onWillPop: () async => false, + child: SimpleDialog( + key: key, + backgroundColor: Colors.black54, + children: [ + Center( + child: Column(children: [ + CircularProgressIndicator(), + SizedBox(height: 10,), + Text("Please Wait....",style: TextStyle(color: Colors.blueAccent),) + ]), + ) + ])); + }); + } +} + + +class DialogBuilder { + DialogBuilder(this.context); + + final BuildContext context; + + void showLoadingIndicator() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)) + ), + backgroundColor: Colors.black87, + content: LoadingIndicator( + text: "text" + ), + ) + ); + }, + ); + } + + void hideOpenDialog() { + Navigator.of(context).pop(); + } +} +class LoadingIndicator extends StatelessWidget{ + LoadingIndicator({this.text = ''}); + + final String text; + + @override + Widget build(BuildContext context) { + var displayedText = text; + + return Container( + padding: EdgeInsets.all(16), + color: Colors.black87, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _getLoadingIndicator(), + _getHeading(context), + + ] + ) + ); + } + + Padding _getLoadingIndicator() { + return Padding( + child: Container( + child: CircularProgressIndicator( + strokeWidth: 3 + ), + width: 32, + height: 32 + ), + padding: EdgeInsets.only(bottom: 16) + ); + } + + Widget _getHeading(context) { + return + Padding( + child: Text( + 'Please wait …', + style: TextStyle( + color: Colors.white, + fontSize: 16 + ), + textAlign: TextAlign.center, + ), + padding: EdgeInsets.only(bottom: 4) + ); + } + +} \ No newline at end of file diff --git a/lib/src/multi_selection_mode.dart b/lib/src/multi_selection_mode.dart index 0a5e134..3b6fd06 100644 --- a/lib/src/multi_selection_mode.dart +++ b/lib/src/multi_selection_mode.dart @@ -1,53 +1,53 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -class MultiSelectionMode extends StatelessWidget { - final ValueNotifier multiSelectionMode; - final bool imageSelected; - final List multiSelectedImage; - - final File image; - const MultiSelectionMode({ - Key? key, - required this.image, - required this.imageSelected, - required this.multiSelectedImage, - required this.multiSelectionMode, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: multiSelectionMode, - builder: (context, bool multiSelectionModeValue, child) => Visibility( - visible: multiSelectionModeValue, - child: Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.all(3), - child: Container( - height: 25, - width: 25, - decoration: BoxDecoration( - color: imageSelected - ? Colors.blue - : const Color.fromARGB(115, 222, 222, 222), - border: Border.all(color: Colors.white), - shape: BoxShape.circle, - ), - child: imageSelected - ? Center( - child: Text( - "${multiSelectedImage.indexOf(image) + 1}", - style: const TextStyle(color: Colors.white), - ), - ) - : const SizedBox(), - ), - ), - ), - ), - ); - } -} +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class MultiSelectionMode extends StatelessWidget { + final ValueNotifier multiSelectionMode; + final bool imageSelected; + final List multiSelectedImage; + + final File image; + const MultiSelectionMode({ + Key? key, + required this.image, + required this.imageSelected, + required this.multiSelectedImage, + required this.multiSelectionMode, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: multiSelectionMode, + builder: (context, bool multiSelectionModeValue, child) => Visibility( + visible: multiSelectionModeValue, + child: Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + height: 25, + width: 25, + decoration: BoxDecoration( + color: imageSelected + ? Colors.blue + : const Color.fromARGB(115, 222, 222, 222), + border: Border.all(color: Colors.white), + shape: BoxShape.circle, + ), + child: imageSelected + ? Center( + child: Text( + "${multiSelectedImage.indexOf(image) + 1}", + style: const TextStyle(color: Colors.white), + ), + ) + : const SizedBox(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/utilities/enum.dart b/lib/src/utilities/enum.dart index 5573e8a..0b59ce0 100644 --- a/lib/src/utilities/enum.dart +++ b/lib/src/utilities/enum.dart @@ -1,3 +1,3 @@ -enum Flash { off, auto, on } - -enum SelectedPage { left, center, right } +enum Flash { off, auto, on } + +enum SelectedPage { left, center, right } diff --git a/lib/src/utilities/enum_image_source.dart b/lib/src/utilities/enum_image_source.dart index 9a9d8e5..6058a9f 100644 --- a/lib/src/utilities/enum_image_source.dart +++ b/lib/src/utilities/enum_image_source.dart @@ -1,3 +1,3 @@ -enum DisplaySource { camera, gallery, both } - -enum PickerSource { image, video, both } +enum DisplaySource { camera, gallery, both } + +enum PickerSource { image, video, both } diff --git a/lib/src/utilities/typedef.dart b/lib/src/utilities/typedef.dart index 2babc2a..7b7380b 100644 --- a/lib/src/utilities/typedef.dart +++ b/lib/src/utilities/typedef.dart @@ -1,2 +1,2 @@ -typedef CustomThreeAsyncValueSetter = A Function(B value, C value2); -typedef CustomTwoAsyncValueSetter = Future Function(B value); +typedef CustomThreeAsyncValueSetter = A Function(B value, C value2); +typedef CustomTwoAsyncValueSetter = Future Function(B value); diff --git a/lib/src/video_layout/record_count.dart b/lib/src/video_layout/record_count.dart index 2601a85..b10fcd4 100644 --- a/lib/src/video_layout/record_count.dart +++ b/lib/src/video_layout/record_count.dart @@ -1,147 +1,147 @@ -import 'package:custom_gallery_display/custom_gallery_display.dart'; -import 'package:flutter/material.dart'; - -class RecordCount extends StatefulWidget { - final ValueNotifier startVideoCount; - final ValueNotifier makeProgressRed; - final ValueNotifier clearVideoRecord; - final AppTheme appTheme; - - const RecordCount({ - Key? key, - required this.appTheme, - required this.startVideoCount, - required this.makeProgressRed, - required this.clearVideoRecord, - }) : super(key: key); - - @override - RecordCountState createState() => RecordCountState(); -} - -class RecordCountState extends State - with TickerProviderStateMixin { - late AnimationController controller; - double opacityLevel = 1.0; - bool isPlaying = false; - - String get countText { - Duration count = controller.duration! * controller.value; - if (controller.isDismissed) { - return '0:00'; - } else { - return '${(count.inMinutes % 60).toString().padLeft(1, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}'; - } - } - - double progress = 0; - - @override - void initState() { - super.initState(); - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 60), - ); - - controller.addListener(() { - if (controller.isAnimating) { - setState(() { - progress = controller.value; - }); - } else { - setState(() { - progress = 0; - isPlaying = false; - }); - } - }); - } - - @override - void didUpdateWidget(RecordCount oldWidget) { - if (widget.startVideoCount.value) { - controller.forward(from: controller.value == 1.0 ? 0 : controller.value); - setState(() { - isPlaying = true; - opacityLevel = opacityLevel == 0 ? 1.0 : 0.0; - }); - } else { - if (widget.clearVideoRecord.value) { - widget.clearVideoRecord.value = false; - controller.reset(); - setState(() { - isPlaying = false; - }); - } else { - controller.stop(); - setState(() { - isPlaying = false; - }); - } - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LinearProgressIndicator( - color: widget.makeProgressRed.value - ? Colors.red - : widget.appTheme.focusColor, - backgroundColor: Colors.transparent, - value: progress, - minHeight: 3, - ), - Visibility( - visible: widget.startVideoCount.value, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: Padding( - padding: const EdgeInsets.only(top: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedOpacity( - opacity: opacityLevel, - duration: const Duration(seconds: 1), - child: const Icon(Icons.fiber_manual_record_rounded, - color: Colors.red, size: 10), - onEnd: () { - if (isPlaying) { - setState( - () => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0); - } - }, - ), - const SizedBox(width: 5), - AnimatedBuilder( - animation: controller, - builder: (context, child) => Text( - countText, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: widget.appTheme.focusColor, - ), - ), - ), - ], - ), - ), - ), - ], - ); - } -} +import 'package:custom_gallery_display/custom_gallery_display.dart'; +import 'package:flutter/material.dart'; + +class RecordCount extends StatefulWidget { + final ValueNotifier startVideoCount; + final ValueNotifier makeProgressRed; + final ValueNotifier clearVideoRecord; + final AppTheme appTheme; + + const RecordCount({ + Key? key, + required this.appTheme, + required this.startVideoCount, + required this.makeProgressRed, + required this.clearVideoRecord, + }) : super(key: key); + + @override + RecordCountState createState() => RecordCountState(); +} + +class RecordCountState extends State + with TickerProviderStateMixin { + late AnimationController controller; + double opacityLevel = 1.0; + bool isPlaying = false; + + String get countText { + Duration count = controller.duration! * controller.value; + if (controller.isDismissed) { + return '0:00'; + } else { + return '${(count.inMinutes % 60).toString().padLeft(1, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}'; + } + } + + double progress = 0; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 60), + ); + + controller.addListener(() { + if (controller.isAnimating) { + setState(() { + progress = controller.value; + }); + } else { + setState(() { + progress = 0; + isPlaying = false; + }); + } + }); + } + + @override + void didUpdateWidget(RecordCount oldWidget) { + if (widget.startVideoCount.value) { + controller.forward(from: controller.value == 1.0 ? 0 : controller.value); + setState(() { + isPlaying = true; + opacityLevel = opacityLevel == 0 ? 1.0 : 0.0; + }); + } else { + if (widget.clearVideoRecord.value) { + widget.clearVideoRecord.value = false; + controller.reset(); + setState(() { + isPlaying = false; + }); + } else { + controller.stop(); + setState(() { + isPlaying = false; + }); + } + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LinearProgressIndicator( + color: widget.makeProgressRed.value + ? Colors.red + : widget.appTheme.focusColor, + backgroundColor: Colors.transparent, + value: progress, + minHeight: 3, + ), + Visibility( + visible: widget.startVideoCount.value, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Padding( + padding: const EdgeInsets.only(top: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedOpacity( + opacity: opacityLevel, + duration: const Duration(seconds: 1), + child: const Icon(Icons.fiber_manual_record_rounded, + color: Colors.red, size: 10), + onEnd: () { + if (isPlaying) { + setState( + () => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0); + } + }, + ), + const SizedBox(width: 5), + AnimatedBuilder( + animation: controller, + builder: (context, child) => Text( + countText, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: widget.appTheme.focusColor, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/video_layout/record_fade_animation.dart b/lib/src/video_layout/record_fade_animation.dart index 88abe53..6aa45b2 100644 --- a/lib/src/video_layout/record_fade_animation.dart +++ b/lib/src/video_layout/record_fade_animation.dart @@ -1,57 +1,57 @@ -import 'package:flutter/material.dart'; - -class RecordFadeAnimation extends StatefulWidget { - const RecordFadeAnimation({Key? key, required this.child}) : super(key: key); - - final Widget child; - - @override - RecordFadeAnimationState createState() => RecordFadeAnimationState(); -} - -class RecordFadeAnimationState extends State - with TickerProviderStateMixin { - late AnimationController _controller; - late final Animation _animation = CurvedAnimation( - parent: _controller, - curve: Curves.fastOutSlowIn, - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void initState() { - _controller = - AnimationController(duration: const Duration(seconds: 1), vsync: this); - _controller.addListener(() async { - if (_controller.isCompleted) { - await Future.delayed(const Duration(seconds: 3)).then((value) { - _controller.reverse(); - }); - } - }); - super.initState(); - } - - @override - void didUpdateWidget(RecordFadeAnimation oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.child != widget.child) { - _controller.forward(from: 0.0); - } - } - - @override - Widget build(BuildContext context) { - return Center( - child: ScaleTransition( - scale: _animation, - child: widget.child, - ), - ); - } -} +import 'package:flutter/material.dart'; + +class RecordFadeAnimation extends StatefulWidget { + const RecordFadeAnimation({Key? key, required this.child}) : super(key: key); + + final Widget child; + + @override + RecordFadeAnimationState createState() => RecordFadeAnimationState(); +} + +class RecordFadeAnimationState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late final Animation _animation = CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + _controller = + AnimationController(duration: const Duration(seconds: 1), vsync: this); + _controller.addListener(() async { + if (_controller.isCompleted) { + await Future.delayed(const Duration(seconds: 3)).then((value) { + _controller.reverse(); + }); + } + }); + super.initState(); + } + + @override + void didUpdateWidget(RecordFadeAnimation oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.child != widget.child) { + _controller.forward(from: 0.0); + } + } + + @override + Widget build(BuildContext context) { + return Center( + child: ScaleTransition( + scale: _animation, + child: widget.child, + ), + ); + } +}