step 10 : slicing scan screen, add pubdev, fungsi camera, crop, rotate angle
This commit is contained in:
@@ -7,9 +7,10 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.scanktpflutter"
|
namespace = "com.example.scanktpflutter"
|
||||||
compileSdk = flutter.compileSdkVersion
|
// compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
compileSdk = 34
|
||||||
|
// ndkVersion = flutter.ndkVersion
|
||||||
|
ndkVersion = "21.1.6352462"
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="scanktpflutter"
|
android:label="scanktpflutter"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:scanktpflutter/screen/home/home_screen.dart';
|
|
||||||
|
|
||||||
|
import '../screen/home/home_screen.dart';
|
||||||
import '../screen/login/login_screen.dart';
|
import '../screen/login/login_screen.dart';
|
||||||
|
import '../screen/scan/scan_screen.dart';
|
||||||
import '../screen/splash/splash_screen.dart';
|
import '../screen/splash/splash_screen.dart';
|
||||||
|
|
||||||
const splashRoute = "/splashRoute";
|
const splashRoute = "/splashRoute";
|
||||||
const loginRoute = "/loginRoute";
|
const loginRoute = "/loginRoute";
|
||||||
const homeRoute = "/homeRoute";
|
const homeRoute = "/homeRoute";
|
||||||
|
const scanRoute = "/scanRoute";
|
||||||
|
|
||||||
class AppRoute {
|
class AppRoute {
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
@@ -43,6 +45,17 @@ class AppRoute {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scan screen
|
||||||
|
if (settings.name == scanRoute) {
|
||||||
|
return MaterialPageRoute(builder: (context) {
|
||||||
|
return MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)),
|
||||||
|
child: ScanScreen(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return MaterialPageRoute(builder: (context) {
|
return MaterialPageRoute(builder: (context) {
|
||||||
return MediaQuery(
|
return MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ class HomeScreen extends HookConsumerWidget {
|
|||||||
y: 48,
|
y: 48,
|
||||||
),
|
),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(scanRoute);
|
||||||
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Constant.bgButton,
|
backgroundColor: Constant.bgButton,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|||||||
354
lib/screen/scan/scan_screen.dart
Normal file
354
lib/screen/scan/scan_screen.dart
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
|
import '../../app/constant.dart';
|
||||||
|
|
||||||
|
class ScanScreen extends HookConsumerWidget {
|
||||||
|
const ScanScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [
|
||||||
|
SystemUiOverlay.bottom,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final cameraController = useState<CameraController?>(null);
|
||||||
|
final initializeControllerFuture = useState<Future<void>?>(null);
|
||||||
|
final capturedImage = useState<XFile?>(null);
|
||||||
|
final croppedImage = useState<File?>(null);
|
||||||
|
final isLoading = useState<bool>(false);
|
||||||
|
final isModalOpen = useState<bool>(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> initializeCamera() async {
|
||||||
|
final cameras = await availableCameras();
|
||||||
|
cameraController.value =
|
||||||
|
CameraController(cameras[0], ResolutionPreset.max);
|
||||||
|
initializeControllerFuture.value = cameraController.value!.initialize();
|
||||||
|
await initializeControllerFuture.value;
|
||||||
|
await cameraController.value!.setFlashMode(FlashMode.off);
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCamera();
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
Future<File> rotateAndCropImage(File imageFile) async {
|
||||||
|
Uint8List imageBytes = await imageFile.readAsBytes();
|
||||||
|
img.Image? original = img.decodeImage(imageBytes);
|
||||||
|
if (original == null) return imageFile;
|
||||||
|
|
||||||
|
img.Image rotated = img.bakeOrientation(original);
|
||||||
|
|
||||||
|
int width = rotated.width;
|
||||||
|
int height = rotated.height;
|
||||||
|
bool isPortrait = height > width;
|
||||||
|
|
||||||
|
int cropWidth, cropHeight;
|
||||||
|
|
||||||
|
if (isPortrait) {
|
||||||
|
cropHeight = (height * 0.7).toInt();
|
||||||
|
cropWidth = (cropHeight ~/ 1.59).toInt();
|
||||||
|
} else {
|
||||||
|
cropWidth = (width * 0.7).toInt();
|
||||||
|
cropHeight = (cropWidth ~/ 1.59).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int left = ((width - cropWidth) ~/ 2).toInt();
|
||||||
|
int top = ((height - cropHeight) ~/ 2).toInt();
|
||||||
|
|
||||||
|
img.Image cropped = img.copyCrop(rotated,
|
||||||
|
x: left, y: top, width: cropWidth, height: cropHeight);
|
||||||
|
|
||||||
|
File croppedFile = File('${imageFile.path}_cropped.jpg');
|
||||||
|
await croppedFile.writeAsBytes(img.encodeJpg(cropped));
|
||||||
|
|
||||||
|
return croppedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> rotateImage(File imageFile) async {
|
||||||
|
Uint8List bytes = await imageFile.readAsBytes();
|
||||||
|
img.Image? image = img.decodeImage(bytes);
|
||||||
|
|
||||||
|
if (image == null) return imageFile;
|
||||||
|
|
||||||
|
img.Image rotated = img.copyRotate(image, angle: -90);
|
||||||
|
|
||||||
|
File rotatedFile = File('${imageFile.path}_rotated.jpg');
|
||||||
|
await rotatedFile.writeAsBytes(img.encodeJpg(rotated));
|
||||||
|
|
||||||
|
return rotatedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showResultModal(BuildContext context) {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
enableDrag: false,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
builder: (context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header dengan tombol close
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Hasil Foto",
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
color: Constant.textBlack,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.black),
|
||||||
|
onPressed: () {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Gambar hasil foto
|
||||||
|
if (croppedImage.value != null)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 300,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.file(
|
||||||
|
croppedImage.value!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Tombol Hapus dan Upload
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// Tombol Hapus
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
capturedImage.value = null;
|
||||||
|
croppedImage.value = null;
|
||||||
|
isModalOpen.value = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
size: 17, color: Colors.white),
|
||||||
|
label: Text(
|
||||||
|
'Hapus',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.textRed,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tombol Upload
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
print('upload baru');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.upload,
|
||||||
|
size: 17, color: Colors.white),
|
||||||
|
label: Text(
|
||||||
|
'Upload',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).whenComplete(() => isModalOpen.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> captureAndCropImage() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
await initializeControllerFuture.value;
|
||||||
|
|
||||||
|
// Ambil gambar dari kamera
|
||||||
|
final image = await cameraController.value!.takePicture();
|
||||||
|
File cropped = await rotateAndCropImage(File(image.path));
|
||||||
|
|
||||||
|
// Rotate gambar setelah cropping
|
||||||
|
File rotatedImage = await rotateImage(cropped);
|
||||||
|
|
||||||
|
// Simpan hasil yang sudah di-crop dan di-rotate
|
||||||
|
capturedImage.value = image;
|
||||||
|
croppedImage.value = rotatedImage;
|
||||||
|
|
||||||
|
showResultModal(context);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error capturing image: $e");
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.5),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
if (cameraController.value != null)
|
||||||
|
FutureBuilder<void>(
|
||||||
|
future: initializeControllerFuture.value,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
return CameraPreview(cameraController.value!);
|
||||||
|
} else {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// bingkai foto
|
||||||
|
Positioned.fill(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: OverlayPainter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// loading
|
||||||
|
if (isLoading.value)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomAppBar(
|
||||||
|
height: Constant.getActualYPhone(
|
||||||
|
context: context,
|
||||||
|
y: 100,
|
||||||
|
),
|
||||||
|
shape: const CircularNotchedRectangle(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
size: 17,
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Kembali',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.textCardGrey,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Constant.bgButton.withOpacity(0.24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// scan
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: captureAndCropImage,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.camera,
|
||||||
|
size: 17,
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Foto',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.bgButton,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Constant.bgButton.withOpacity(0.24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.black.withOpacity(0.6)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final borderPaint = Paint()
|
||||||
|
..color = Colors.yellow
|
||||||
|
..strokeWidth = 4
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
double boxHeight = size.height * 0.7;
|
||||||
|
double boxWidth = boxHeight / 1.59;
|
||||||
|
|
||||||
|
double left = (size.width - boxWidth) / 2;
|
||||||
|
double top = (size.height - boxHeight) / 2;
|
||||||
|
|
||||||
|
Path path = Path()
|
||||||
|
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
|
||||||
|
..addRect(Rect.fromLTWH(left, top, boxWidth, boxHeight))
|
||||||
|
..fillType = PathFillType.evenOdd;
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
canvas.drawRect(Rect.fromLTWH(left, top, boxWidth, boxHeight), borderPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
277
lib/screen/scan/scan_screen1.txt
Normal file
277
lib/screen/scan/scan_screen1.txt
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
|
import '../../app/constant.dart';
|
||||||
|
|
||||||
|
class ScanScreen extends HookConsumerWidget {
|
||||||
|
const ScanScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [
|
||||||
|
SystemUiOverlay.bottom,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final cameraController = useState<CameraController?>(null);
|
||||||
|
final capturedImage = useState<XFile?>(null);
|
||||||
|
final croppedImage = useState<File?>(null);
|
||||||
|
final isLoading = useState<bool>(false);
|
||||||
|
final initializeControllerFuture = useState<Future<void>?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> initializeCamera() async {
|
||||||
|
final cameras = await availableCameras();
|
||||||
|
cameraController.value =
|
||||||
|
CameraController(cameras[0], ResolutionPreset.max);
|
||||||
|
initializeControllerFuture.value = cameraController.value!.initialize();
|
||||||
|
await initializeControllerFuture.value;
|
||||||
|
await cameraController.value!.setFlashMode(FlashMode.off);
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCamera();
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
Future<File> rotateAndCropImage(File imageFile) async {
|
||||||
|
Uint8List imageBytes = await imageFile.readAsBytes();
|
||||||
|
img.Image? original = img.decodeImage(imageBytes);
|
||||||
|
if (original == null) return imageFile;
|
||||||
|
|
||||||
|
img.Image rotated = img.bakeOrientation(original);
|
||||||
|
|
||||||
|
int width = rotated.width;
|
||||||
|
int height = rotated.height;
|
||||||
|
bool isPortrait = height > width;
|
||||||
|
|
||||||
|
int cropWidth, cropHeight;
|
||||||
|
|
||||||
|
if (isPortrait) {
|
||||||
|
cropHeight = (height * 0.7).toInt();
|
||||||
|
cropWidth = (cropHeight ~/ 1.59).toInt();
|
||||||
|
} else {
|
||||||
|
cropWidth = (width * 0.7).toInt();
|
||||||
|
cropHeight = (cropWidth ~/ 1.59).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int left = ((width - cropWidth) ~/ 2).toInt();
|
||||||
|
int top = ((height - cropHeight) ~/ 2).toInt();
|
||||||
|
|
||||||
|
img.Image cropped = img.copyCrop(rotated,
|
||||||
|
x: left, y: top, width: cropWidth, height: cropHeight);
|
||||||
|
|
||||||
|
File croppedFile = File('${imageFile.path}_cropped.jpg');
|
||||||
|
await croppedFile.writeAsBytes(img.encodeJpg(cropped));
|
||||||
|
|
||||||
|
return croppedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> rotateImage(File imageFile) async {
|
||||||
|
Uint8List bytes = await imageFile.readAsBytes();
|
||||||
|
img.Image? image = img.decodeImage(bytes);
|
||||||
|
|
||||||
|
if (image == null) return imageFile;
|
||||||
|
|
||||||
|
img.Image rotated = img.copyRotate(image, angle: -90);
|
||||||
|
|
||||||
|
File rotatedFile = File('${imageFile.path}_rotated.jpg');
|
||||||
|
await rotatedFile.writeAsBytes(img.encodeJpg(rotated));
|
||||||
|
|
||||||
|
return rotatedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> captureAndCropImage() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
await initializeControllerFuture.value;
|
||||||
|
|
||||||
|
// Ambil gambar dari kamera
|
||||||
|
final image = await cameraController.value!.takePicture();
|
||||||
|
File cropped = await rotateAndCropImage(File(image.path));
|
||||||
|
|
||||||
|
// Rotate gambar setelah cropping
|
||||||
|
File rotatedImage = await rotateImage(cropped);
|
||||||
|
|
||||||
|
// Simpan hasil yang sudah di-crop dan di-rotate
|
||||||
|
capturedImage.value = image;
|
||||||
|
croppedImage.value = rotatedImage;
|
||||||
|
} catch (e) {
|
||||||
|
print("Error capturing image: $e");
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
FocusManager.instance.primaryFocus!.unfocus();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: true,
|
||||||
|
backgroundColor: Constant.textBlack,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
if (cameraController.value != null)
|
||||||
|
FutureBuilder<void>(
|
||||||
|
future: initializeControllerFuture.value,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
return CameraPreview(cameraController.value!);
|
||||||
|
} else {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// bingkai foto
|
||||||
|
Positioned.fill(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: OverlayPainter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// hasil foto
|
||||||
|
if (croppedImage.value != null)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.file(
|
||||||
|
croppedImage.value!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// loading
|
||||||
|
if (isLoading.value)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomAppBar(
|
||||||
|
shape: const CircularNotchedRectangle(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
size: 17,
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Kembali',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.textCardGrey,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Constant.bgButton.withOpacity(0.24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Spacer(),
|
||||||
|
|
||||||
|
// clear
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
capturedImage.value = null;
|
||||||
|
croppedImage.value = null;
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.clear,
|
||||||
|
size: 17,
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Reset',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.textRed,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Constant.bgButton.withOpacity(0.24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: Constant.getActualXPhone(context: context, x: 10),
|
||||||
|
),
|
||||||
|
// scan
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: captureAndCropImage,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.camera,
|
||||||
|
size: 17,
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'Foto',
|
||||||
|
style: Constant.cardText(context: context).copyWith(
|
||||||
|
color: Constant.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Constant.bgButton,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Constant.bgButton.withOpacity(0.24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.black.withOpacity(0.6)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final borderPaint = Paint()
|
||||||
|
..color = Colors.yellow
|
||||||
|
..strokeWidth = 4
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
double boxHeight = size.height * 0.7;
|
||||||
|
double boxWidth = boxHeight / 1.59;
|
||||||
|
|
||||||
|
double left = (size.width - boxWidth) / 2;
|
||||||
|
double top = (size.height - boxHeight) / 2;
|
||||||
|
|
||||||
|
Path path = Path()
|
||||||
|
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
|
||||||
|
..addRect(Rect.fromLTWH(left, top, boxWidth, boxHeight))
|
||||||
|
..fillType = PathFillType.evenOdd;
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
canvas.drawRect(Rect.fromLTWH(left, top, boxWidth, boxHeight), borderPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
184
pubspec.lock
184
pubspec.lock
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.2"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -17,6 +25,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
camera:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: camera
|
||||||
|
sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.6"
|
||||||
|
camera_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_android
|
||||||
|
sha256: "007c57cdcace4751014071e3d42f2eb8a64a519254abed35b714223d81d66234"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.10"
|
||||||
|
camera_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_avfoundation
|
||||||
|
sha256: eff7ed630b1ac3994737c790368fe006388ad9f271d7148e432263721e45dc75
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.18+7"
|
||||||
|
camera_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_platform_interface
|
||||||
|
sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.0"
|
||||||
|
camera_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_web
|
||||||
|
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +89,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+2"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -118,6 +182,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.24"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -152,6 +224,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -232,6 +312,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.15"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -256,6 +360,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.3.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.13"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.5"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.3"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -272,6 +432,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.1"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -373,6 +541,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -445,6 +621,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.4 <4.0.0"
|
dart: ">=3.5.4 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.24.0"
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ dependencies:
|
|||||||
equatable: ^2.0.7
|
equatable: ^2.0.7
|
||||||
top_snackbar_flutter: ^3.2.0
|
top_snackbar_flutter: ^3.2.0
|
||||||
jiffy: ^6.3.1
|
jiffy: ^6.3.1
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
camera: ^0.10.6
|
||||||
|
image: ^4.1.3
|
||||||
|
permission_handler: ^11.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
permission_handler_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user