Files
pfm-ocr/lib/features/camera/camera_screen.dart
T

325 lines
12 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import '../../core/location/location_service.dart';
import '../../core/utils/exif_helper.dart';
import '../../config/app_config.dart';
import 'image_preview_screen.dart';
import 'scan_mode_provider.dart';
import 'camera_drawer.dart';
class CameraScreen extends ConsumerStatefulWidget {
const CameraScreen({super.key});
@override
ConsumerState<CameraScreen> createState() => CameraScreenState();
}
class CameraScreenState extends ConsumerState<CameraScreen> {
bool _isCapturing = false;
Position? _currentPosition;
StreamSubscription<Position>? _locationSubscription;
@override
void initState() {
super.initState();
_preFetchLocation();
}
@override
void dispose() {
_locationSubscription?.cancel();
super.dispose();
}
Future<void> _preFetchLocation() async {
try {
final pos = await LocationService.determinePosition();
if (mounted) {
setState(() {
_currentPosition = pos;
});
}
// Live location updates
_locationSubscription = LocationService.getPositionStream().listen(
(Position freshPos) {
if (mounted) {
setState(() {
_currentPosition = freshPos;
});
}
},
);
} catch (_) {}
}
Future<void> _takePicture() async {
if (_isCapturing) return;
final ImagePicker picker = ImagePicker();
try {
final XFile? image = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 2048,
maxHeight: 2048,
imageQuality: 85,
);
if (image != null) {
setState(() {
_isCapturing = true;
});
// Use pre-fetched location if available, otherwise fetch quick position
Position? position = _currentPosition;
if (position == null) {
try {
position = await LocationService.determinePosition().timeout(const Duration(seconds: 2));
} catch (_) {}
}
setState(() {
_isCapturing = false;
});
navigateToPreview(image.path, latitude: position?.latitude, longitude: position?.longitude);
}
} catch (e) {
debugPrint('Error taking picture: $e');
}
}
Future<void> _pickFromGallery() async {
if (_isCapturing) return;
final ImagePicker picker = ImagePicker();
try {
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 2048,
maxHeight: 2048,
imageQuality: 85,
);
if (image != null) {
setState(() {
_isCapturing = true;
});
Position? position = _currentPosition;
try {
// Attempt to extract GPS from EXIF tags
final exifGps = await ExifHelper.extractGps(image.path);
if (exifGps != null) {
position = Position(
latitude: exifGps['latitude']!,
longitude: exifGps['longitude']!,
timestamp: DateTime.now(),
accuracy: 0.0,
altitude: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
altitudeAccuracy: 0.0,
headingAccuracy: 0.0,
);
} else if (position == null) {
position = await LocationService.determinePosition().timeout(const Duration(seconds: 2));
}
} catch (_) {}
setState(() {
_isCapturing = false;
});
navigateToPreview(image.path, latitude: position?.latitude, longitude: position?.longitude);
}
} catch (e) {
debugPrint('Error picking image from gallery: $e');
}
}
void navigateToPreview(String imagePath, {double? latitude, double? longitude}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImagePreviewScreen(
imagePath: imagePath,
latitude: latitude,
longitude: longitude,
),
),
);
}
@override
Widget build(BuildContext context) {
final activeMode = ref.watch(scanModeProvider);
return Scaffold(
backgroundColor: AppConfig.backgroundColor,
appBar: AppBar(
title: Text(activeMode == 'DO' ? 'Pemindai Dokumen OCR' : 'Pemindai Produk PFM'),
),
drawer: const CameraDrawer(),
body: SafeArea(
child: Padding(
padding: AppConfig.screenPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Upper Illustration Card
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppConfig.borderRadiusXL),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Padding(
padding: AppConfig.paddingXL,
child: SingleChildScrollView(
child: activeMode == 'DO'
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppConfig.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.document_scanner,
size: 48,
color: AppConfig.primaryColor,
),
),
const SizedBox(height: AppConfig.spaceXL),
Text(
'Pemindai DO',
style: AppConfig.h3,
),
const SizedBox(height: AppConfig.spaceMedium),
Text(
'Ambil foto lembar Delivery Order (DO) Prima Fresh Mart untuk mengekstrak data PO, SO, DO, dan item barang secara otomatis.',
style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: AppConfig.spaceXL),
_buildGuidelineRow(Icons.lightbulb_outline, 'Pastikan pencahayaan cukup dan terang.'),
const SizedBox(height: AppConfig.spaceSmall),
_buildGuidelineRow(Icons.crop_free, 'Posisikan seluruh halaman dokumen di dalam foto.'),
const SizedBox(height: AppConfig.spaceSmall),
_buildGuidelineRow(Icons.camera_enhance_outlined, 'Pegang HP dengan stabil agar tidak buram.'),
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppConfig.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.inventory_2_outlined,
size: 48,
color: AppConfig.primaryColor,
),
),
const SizedBox(height: AppConfig.spaceXL),
Text(
'Pemindai Produk',
style: AppConfig.h3,
),
const SizedBox(height: AppConfig.spaceMedium),
Text(
'Ambil foto kemasan produk Prima Fresh Mart untuk mengidentifikasi SKU, nama item, dan mendeteksi tanggal kadaluwarsa secara otomatis.',
style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: AppConfig.spaceXL),
_buildGuidelineRow(Icons.lightbulb_outline, 'Pastikan kode SKU atau teks kadaluwarsa terlihat jelas.'),
const SizedBox(height: AppConfig.spaceSmall),
_buildGuidelineRow(Icons.center_focus_strong, 'Posisikan produk di tengah kamera.'),
const SizedBox(height: AppConfig.spaceSmall),
_buildGuidelineRow(Icons.flash_on, 'Gunakan pencahayaan yang cukup agar tidak buram.'),
],
),
),
),
),
),
const SizedBox(height: AppConfig.spaceXL),
// Action Buttons
ElevatedButton.icon(
onPressed: _isCapturing ? null : _takePicture,
icon: const Icon(Icons.camera_alt),
label: const Text('Ambil Foto Kamera'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
textStyle: AppConfig.bodyLarge.copyWith(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: AppConfig.spaceMedium),
OutlinedButton.icon(
onPressed: _isCapturing ? null : _pickFromGallery,
icon: const Icon(Icons.photo_library),
label: const Text('Pilih Dari Galeri'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
side: const BorderSide(color: AppConfig.primaryColor),
foregroundColor: AppConfig.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
),
textStyle: AppConfig.bodyLarge.copyWith(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: AppConfig.spaceMedium),
TextButton.icon(
onPressed: _isCapturing ? null : () => context.push('/documents'),
icon: const Icon(Icons.history_edu),
label: const Text('Riwayat Dokumen'),
style: TextButton.styleFrom(
foregroundColor: AppConfig.textSecondary,
),
),
],
),
),
),
);
}
Widget _buildGuidelineRow(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 18, color: AppConfig.secondaryColor),
const SizedBox(width: AppConfig.spaceSmall),
Expanded(
child: Text(
text,
style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary),
),
),
],
);
}
}