diff --git a/lib/provider/camera_controller_provider.dart b/lib/provider/camera_controller_provider.dart new file mode 100644 index 0000000..0dc0c42 --- /dev/null +++ b/lib/provider/camera_controller_provider.dart @@ -0,0 +1,38 @@ +import 'package:camera/camera.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +final cameraControllerProvider = + StateNotifierProvider( + (ref) => CameraControllerNotifier()); + +class CameraControllerNotifier extends StateNotifier { + CameraControllerNotifier() : super(null); + + Future initializeCamera(CameraDescription camera) async { + final controller = CameraController(camera, ResolutionPreset.medium); + await controller.initialize(); + state = controller; + } + + Future takePicture() async { + if (state == null || !state!.value.isInitialized) { + throw Exception('Camera is not initialized'); + } + + final path = p.join( + (await getTemporaryDirectory()).path, + '${DateTime.now()}.png', + ); + + await state!.takePicture(); + return path; + } + + @override + void dispose() { + state?.dispose(); + super.dispose(); + } +} diff --git a/lib/screen/presensi/camera_page.dart b/lib/screen/presensi/camera_page.dart new file mode 100644 index 0000000..a4f1551 --- /dev/null +++ b/lib/screen/presensi/camera_page.dart @@ -0,0 +1,92 @@ +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class CameraPage extends StatefulWidget { + const CameraPage({Key? key}) : super(key: key); + + @override + State createState() => _CameraPageState(); +} + +class _CameraPageState extends State { + CameraController? controller; + String imagePath = ""; + List? cameras; + + @override + void initState() { + super.initState(); + initializeCamera(); + } + + Future initializeCamera() async { + // Initialize cameras + WidgetsFlutterBinding.ensureInitialized(); + try { + cameras = await availableCameras(); + if (cameras != null && cameras!.isNotEmpty) { + // Use the first available camera + controller = CameraController(cameras![0], ResolutionPreset.max); + await controller?.initialize(); + if (mounted) { + setState(() {}); + } + } + } catch (e) { + print('Error initializing camera: $e'); + } + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (controller == null || !controller!.value.isInitialized) { + return Center(child: CircularProgressIndicator()); + } + + return Scaffold( + body: SafeArea( + child: Center( + child: Column( + children: [ + SizedBox(height: 50), + Container( + width: 200, + height: 200, + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: CameraPreview(controller!), + ), + ), + TextButton( + onPressed: () async { + try { + final image = await controller!.takePicture(); + setState(() { + imagePath = image.path; + }); + } catch (e) { + print('Error taking picture: $e'); + } + }, + child: Text("Take Photo"), + ), + if (imagePath.isNotEmpty) + Container( + width: 300, + height: 300, + child: Image.file(File(imagePath)), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen/presensi/presensi_selfie_screen.dart b/lib/screen/presensi/presensi_selfie_screen.dart index 931884c..78d1249 100644 --- a/lib/screen/presensi/presensi_selfie_screen.dart +++ b/lib/screen/presensi/presensi_selfie_screen.dart @@ -1,14 +1,17 @@ import 'dart:convert'; +import 'package:camera/camera.dart'; +import 'package:camera_web/camera_web.dart'; import 'dart:io'; - import 'package:absensi_sas/repository/googleapis_repository.dart'; import 'package:absensi_sas/screen/presensi/presensi_clock_in_provider.dart'; import 'package:absensi_sas/screen/presensi/presensi_clock_out_provider.dart'; import 'package:absensi_sas/widget/custom_drawer.dart'; import 'package:dart_nominatim/dart_nominatim.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; // import 'package:geocoding/geocoding.dart'; // import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -20,6 +23,7 @@ import 'package:mobkit_dashed_border/mobkit_dashed_border.dart'; import '../../app/constant.dart'; import '../../app/route.dart'; +import '../../provider/camera_controller_provider.dart'; import '../../provider/current_check_distance_provider.dart'; import '../../provider/current_check_jam_presensi_provider.dart'; import '../../provider/current_user_provider.dart'; @@ -27,12 +31,15 @@ import '../../widget/custom_dialog_presensi_selfie_sukses.dart'; import '../../widget/real_date.dart'; import '../../widget/real_time.dart'; import '../../widget/sankbar_widget.dart'; +import 'camera_page.dart'; import 'check_distance_provider.dart'; import 'check_presensi_jam_provider.dart'; import 'googleapis_provider.dart'; import 'presensi_selfie_upload_area.dart'; import 'dart:io' as io; +import 'presensi_selfie_upload_area_web.dart'; + class PresensiSelfieScreen extends HookConsumerWidget { const PresensiSelfieScreen({super.key}); @@ -56,10 +63,15 @@ class PresensiSelfieScreen extends HookConsumerWidget { final fileEkstension = useState(""); final fileSize = useState(0); final fileName = useState(""); + List cameras; Location location = new Location(); LocationData _locationData; + final controller = ref.watch(cameraControllerProvider); + final isInited = useState(false); + final imageUrl = useState(null); + getBase64() async { // List imageBytes = await fileData.value?.readAsBytes(); if (fileData.value != null) { @@ -79,38 +91,50 @@ class PresensiSelfieScreen extends HookConsumerWidget { } final ImagePicker _picker = ImagePicker(); - pickImage() async { - final XFile? pickedFile = await _picker.pickImage( - source: ImageSource.camera, - // maxWidth set untuk width image - maxWidth: 640, - // maxHeight set untuk width image - maxHeight: 480); - if (pickedFile != null) { - final tmpFile = FilePickerResult([ - PlatformFile( - name: pickedFile.name, - size: await pickedFile.length(), - path: pickedFile.path, - ) - ]); - if (await pickedFile.length() > 10000000) { - SanckbarWidget(context, "File tidak boleh lebih dari 10 MB", - snackbarType.warning); - } else { - fileData.value = pickedFile; - isImage.value = true; - fileEkstension.value = tmpFile.files.single.extension ?? ""; - DateTime now = new DateTime.now(); - fileName.value = - "IMG-${now.year}${now.month}${now.day}.${tmpFile.files.single.extension ?? ''}"; - } - // final Directory appDocumentsDir = - // await getApplicationDocumentsDirectory(); - // print(appDocumentsDir); - // await pickedFile!.saveTo(appDocumentsDir.path); - await getBase64(); + pickImage() async { + if (kIsWeb) { + try { + final path = + await ref.read(cameraControllerProvider.notifier).takePicture(); + imageUrl.value = path; + } catch (e) { + SanckbarWidget( + context, "Failed to take picture", snackbarType.warning); + } + } else { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.camera, + // maxWidth set untuk width image + maxWidth: 640, + // maxHeight set untuk width image + maxHeight: 480); + if (pickedFile != null) { + final tmpFile = FilePickerResult([ + PlatformFile( + name: pickedFile.name, + size: await pickedFile.length(), + path: pickedFile.path, + ) + ]); + if (await pickedFile.length() > 10000000) { + SanckbarWidget(context, "File tidak boleh lebih dari 10 MB", + snackbarType.warning); + } else { + fileData.value = pickedFile; + isImage.value = true; + fileEkstension.value = tmpFile.files.single.extension ?? ""; + DateTime now = new DateTime.now(); + fileName.value = + "IMG-${now.year}${now.month}${now.day}.${tmpFile.files.single.extension ?? ''}"; + } + + // final Directory appDocumentsDir = + // await getApplicationDocumentsDirectory(); + // print(appDocumentsDir); + // await pickedFile!.saveTo(appDocumentsDir.path); + await getBase64(); + } } } @@ -172,7 +196,7 @@ class PresensiSelfieScreen extends HookConsumerWidget { isLoadingAddressUserLocation.value = true; // Mendapatkan posisi pengguna // LocationPermission permission = await Geolocator.requestPermission(); - final permission = await location.hasPermission(); + final permission = await location.hasPermission(); if (permission == PermissionStatus.denied) { isLoadingAddressUserLocation.value = false; @@ -757,13 +781,48 @@ class PresensiSelfieScreen extends HookConsumerWidget { ? Center( child: CircularProgressIndicator(), ) - : PresensiSelfieUploadAreaWidget( - isLoading: isLoadingAddressUserLocation, - isImage: isImage, - fileData: fileData, - fileDataBase64: fileDataBase64, - pickFile: pickFile, - pickImage: pickImage, + : InkWell( + onTap: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + CameraPage(), + ), + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon( + // Icons.upload_outlined, + // color: Constant.textOrange, + // ), + + Image.asset( + 'images/camera_selfie.png', // Path gambar untuk "Check In" + width: Constant.getActualXPhone( + context: context, x: 24), + height: Constant.getActualYPhone( + context: context, y: 24), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 4, + ), + ), + Text( + 'Upload File', + style: Constant.titleH2_400_12( + context: context) + .copyWith( + fontWeight: FontWeight.w600, + color: Constant.textOrange), + ) + ], + ), ), ] else ...[ // gambar icon presensi clock out @@ -774,7 +833,7 @@ class PresensiSelfieScreen extends HookConsumerWidget { ? Center( child: CircularProgressIndicator(), ) - : PresensiSelfieUploadAreaWidget( + : PresensiSelfieUploadAreaWebWidget( isLoading: isLoadingAddressUserLocation, isImage: isImage, fileData: fileData, diff --git a/lib/screen/presensi/presensi_selfie_upload_area_web.dart b/lib/screen/presensi/presensi_selfie_upload_area_web.dart new file mode 100644 index 0000000..9d3b6fa --- /dev/null +++ b/lib/screen/presensi/presensi_selfie_upload_area_web.dart @@ -0,0 +1,198 @@ +import 'dart:io'; + +import '../../app/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; + +class PresensiSelfieUploadAreaWebWidget extends StatelessWidget { + const PresensiSelfieUploadAreaWebWidget({ + super.key, + required this.isImage, + required this.fileData, + required this.fileDataBase64, + required this.pickFile, + required this.pickImage, + required this.isLoading, + }); + + final ValueNotifier isImage; + final ValueNotifier isLoading; + final ValueNotifier fileData; + final ValueNotifier fileDataBase64; + final Function pickImage; + final Function pickFile; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: !isLoading.value + ? () { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + height: Constant.getActualYPhone(context: context, y: 200), + padding: EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Column( + // children: [ + // IconButton( + // onPressed: () { + // Navigator.pop(context); + // pickFile(); + // }, + // icon: Icon( + // Icons.folder_copy_rounded, + // size: 50, + // color: Constant.textOrange, + // )), + // Text( + // "Browse a file", + // style: Constant.titleH2_400_12(context: context) + // .copyWith( + // fontWeight: FontWeight.w600, + // color: Constant.textBlack, + // ), + // ) + // ], + // ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.pop(context); + pickImage(); + }, + icon: Icon( + Icons.add_a_photo_rounded, + size: 50, + color: Constant.textOrange, + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, y: 10), + ), + Text( + "Take a picture", + style: Constant.titleH2_400_12(context: context) + .copyWith( + fontWeight: FontWeight.w600, + color: Constant.textBlack, + ), + ) + ], + ), + ], + ), + ); + }, + ); + } + : null, + child: Stack( + alignment: AlignmentDirectional.topEnd, + children: [ + Container( + width: Constant.getActualXPhone(context: context, x: 390), + height: Constant.getActualYPhone( + context: context, y: isImage.value ? 200 : 83), + decoration: BoxDecoration( + color: Constant.bgUploadFile, + borderRadius: BorderRadius.circular(8), + ), + child: Builder(builder: (context) { + final String? mime = lookupMimeType(fileData.value?.path ?? ""); + return Semantics( + label: 'image_picker_example_picked_image', + child: (mime != null + ? (mime.startsWith('image/')) + ? Image.network( + // image: AssetImage(photo.value!.path), + fileData.value!.path, + frameBuilder: (context, child, frame, + wasSynchronouslyLoaded) { + return (wasSynchronouslyLoaded) + ? Center( + child: Text("Loadinga"), + ) + : Container( + child: child, + ); + }, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text( + 'This image type is not supported')); + }, + ) + : Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.file_present_rounded, + size: 40, + color: Constant.textOrange, + ), + Text( + fileData.value?.name ?? '', + style: + Constant.titleH2_400(context: context), + ) + ], + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon( + // Icons.upload_outlined, + // color: Constant.textOrange, + // ), + + Image.asset( + 'images/camera_selfie.png', // Path gambar untuk "Check In" + width: Constant.getActualXPhone( + context: context, x: 24), + height: Constant.getActualYPhone( + context: context, y: 24), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 4, + ), + ), + Text( + 'Upload File', + style: Constant.titleH2_400_12(context: context) + .copyWith( + fontWeight: FontWeight.w600, + color: Constant.textOrange), + ) + ], + ))); + }), + ), + if (fileData.value != null && !isLoading.value) + IconButton( + onPressed: () { + fileData.value = null; + fileDataBase64.value = ''; + isImage.value = false; + }, + icon: Icon(Icons.cancel_outlined)), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c2081c8..e859bc0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + camera: + dependency: "direct main" + description: + name: camera + sha256: "26ff41045772153f222ffffecba711a206f670f5834d40ebf5eed3811692f167" + url: "https://pub.dev" + source: hosted + version: "0.11.0+2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "7cd93578ad201dcc6bb5810451fb00d76a86bab9b68dceb68b8cbd7038ac5846" + url: "https://pub.dev" + source: hosted + version: "0.6.8+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "7c28969a975a7eb2349bc2cb2dfe3ad218a33dba9968ecfb181ce08c87486655" + url: "https://pub.dev" + source: hosted + version: "0.9.17+3" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + url: "https://pub.dev" + source: hosted + version: "2.8.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" characters: dependency: transitive description: @@ -262,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.6" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + url: "https://pub.dev" + source: hosted + version: "1.1.3" flutter_launcher_icons: dependency: "direct main" description: @@ -777,7 +825,7 @@ packages: source: hosted version: "3.3.2" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" @@ -1069,6 +1117,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1238,5 +1294,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 41f6d2e..05ca396 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,10 @@ dependencies: image_picker_web: ^4.0.0 dart_nominatim: ^1.0.1 location: ^5.0.0 + # camera_web: ^0.3.5 + camera: ^0.11.0+2 + flutter_image_compress: ^1.1.0 + path: ^1.8.3 dev_dependencies: flutter_test: