325 lines
12 KiB
Dart
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),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|