Alur
This commit is contained in:
parent
4091624ec4
commit
664acb389e
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,7 @@ class ApiUrl {
|
||||||
return _cachedImageBaseUrl!;
|
return _cachedImageBaseUrl!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get baseUrl => dotenv.env['API_BASE_URL'] ?? _cachedBaseUrl ?? 'http://192.168.1.3:8000/api';
|
static String get baseUrl => _cachedBaseUrl ?? dotenv.env['API_BASE_URL'] ?? 'http://192.168.1.4:8000/api';
|
||||||
static String get imageBaseUrl => _cachedImageBaseUrl ?? baseUrl.replaceAll('/api', '/storage/');
|
static String get imageBaseUrl => _cachedImageBaseUrl ?? baseUrl.replaceAll('/api', '/storage/');
|
||||||
|
|
||||||
static Future<void> initialize() async {
|
static Future<void> initialize() async {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class ApiConfigService {
|
||||||
static const String _selectedPresetKey = 'selected_preset';
|
static const String _selectedPresetKey = 'selected_preset';
|
||||||
|
|
||||||
static const Map<String, String> presets = {
|
static const Map<String, String> presets = {
|
||||||
'current_ip': 'http://192.168.1.7:8000/api',
|
'current_ip': 'http://192.168.1.4:8000/api',
|
||||||
'hostname': 'http://LAPTOP-I0SUKSKL:8000/api',
|
'hostname': 'http://LAPTOP-I0SUKSKL:8000/api',
|
||||||
'emulator': 'http://10.0.2.2:8000/api',
|
'emulator': 'http://10.0.2.2:8000/api',
|
||||||
'custom': '',
|
'custom': '',
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'providers/poin_provider.dart';
|
||||||
import 'providers/signature_provider.dart';
|
import 'providers/signature_provider.dart';
|
||||||
import 'providers/surat_izin_provider.dart';
|
import 'providers/surat_izin_provider.dart';
|
||||||
import 'providers/notification_provider.dart';
|
import 'providers/notification_provider.dart';
|
||||||
|
import 'providers/setup_check_provider.dart';
|
||||||
|
|
||||||
import 'screens/splash_screen.dart';
|
import 'screens/splash_screen.dart';
|
||||||
import 'screens/auth/login_screen.dart';
|
import 'screens/auth/login_screen.dart';
|
||||||
|
|
@ -36,6 +37,7 @@ import 'screens/profile/face_test_screen.dart';
|
||||||
import 'screens/profile/signature_screen.dart';
|
import 'screens/profile/signature_screen.dart';
|
||||||
import 'screens/profile/edit_profile_screen.dart';
|
import 'screens/profile/edit_profile_screen.dart';
|
||||||
import 'screens/documents/surat_izin_screen.dart';
|
import 'screens/documents/surat_izin_screen.dart';
|
||||||
|
import 'screens/onboarding/onboarding_check_screen.dart';
|
||||||
|
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'core/cache_manager.dart';
|
import 'core/cache_manager.dart';
|
||||||
|
|
@ -72,6 +74,7 @@ class MyApp extends StatelessWidget {
|
||||||
ChangeNotifierProvider(create: (_) => SignatureProvider()),
|
ChangeNotifierProvider(create: (_) => SignatureProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => SuratIzinProvider()),
|
ChangeNotifierProvider(create: (_) => SuratIzinProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => NotificationProvider()),
|
ChangeNotifierProvider(create: (_) => NotificationProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => SetupCheckProvider()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'MPG HRIS',
|
title: 'MPG HRIS',
|
||||||
|
|
@ -98,6 +101,7 @@ class MyApp extends StatelessWidget {
|
||||||
'/pengajuan/izin': (context) => const IzinFormScreen(),
|
'/pengajuan/izin': (context) => const IzinFormScreen(),
|
||||||
'/pengajuan/lembur': (context) => const LemburFormScreen(),
|
'/pengajuan/lembur': (context) => const LemburFormScreen(),
|
||||||
'/onboarding/face-enrollment': (context) => const FaceEnrollmentScreen(),
|
'/onboarding/face-enrollment': (context) => const FaceEnrollmentScreen(),
|
||||||
|
'/onboarding/check': (context) => const OnboardingCheckScreen(),
|
||||||
'/poin/usage': (context) => const PointUsageScreen(),
|
'/poin/usage': (context) => const PointUsageScreen(),
|
||||||
'/poin/history': (context) => const PoinHistoryScreen(),
|
'/poin/history': (context) => const PoinHistoryScreen(),
|
||||||
'/notification': (context) => const NotificationScreen(),
|
'/notification': (context) => const NotificationScreen(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui' show Rect;
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||||
|
|
@ -7,6 +8,17 @@ import '../core/utils/camera_utils.dart';
|
||||||
|
|
||||||
enum EnrollmentStep { depan, kanan, kiri, bawah, selesai }
|
enum EnrollmentStep { depan, kanan, kiri, bawah, selesai }
|
||||||
|
|
||||||
|
// Status feedback real-time ke UI untuk panduan user
|
||||||
|
enum FaceStatus {
|
||||||
|
noFace, // Tidak ada wajah terdeteksi
|
||||||
|
tooFar, // Wajah terlalu jauh (kotak kecil)
|
||||||
|
tooClose, // Wajah terlalu dekat (kotak besar)
|
||||||
|
outOfFrame, // Wajah tidak di dalam area scan
|
||||||
|
wrongPose, // Wajah ada & di frame, tapi pose salah
|
||||||
|
holding, // Pose benar, sedang menghitung mundur
|
||||||
|
ready, // Siap capture (internal, langsung ambil foto)
|
||||||
|
}
|
||||||
|
|
||||||
class FaceProvider with ChangeNotifier {
|
class FaceProvider with ChangeNotifier {
|
||||||
final FaceRepository _repository = FaceRepository();
|
final FaceRepository _repository = FaceRepository();
|
||||||
final FaceDetector _faceDetector = FaceDetector(
|
final FaceDetector _faceDetector = FaceDetector(
|
||||||
|
|
@ -23,6 +35,12 @@ class FaceProvider with ChangeNotifier {
|
||||||
bool _isProcessing = false;
|
bool _isProcessing = false;
|
||||||
bool _isUploading = false;
|
bool _isUploading = false;
|
||||||
String _instructionText = "Hadapkan wajah lurus ke depan";
|
String _instructionText = "Hadapkan wajah lurus ke depan";
|
||||||
|
FaceStatus _faceStatus = FaceStatus.noFace;
|
||||||
|
|
||||||
|
// Hold timer: pose harus dipertahankan selama ini sebelum capture
|
||||||
|
static const int _holdDurationMs = 1500;
|
||||||
|
DateTime? _poseValidSince;
|
||||||
|
double _holdProgress = 0.0; // 0.0 - 1.0
|
||||||
|
|
||||||
File? _fotoDepan;
|
File? _fotoDepan;
|
||||||
File? _fotoKanan;
|
File? _fotoKanan;
|
||||||
|
|
@ -32,6 +50,8 @@ class FaceProvider with ChangeNotifier {
|
||||||
EnrollmentStep get currentStep => _currentStep;
|
EnrollmentStep get currentStep => _currentStep;
|
||||||
bool get isUploading => _isUploading;
|
bool get isUploading => _isUploading;
|
||||||
String get instructionText => _instructionText;
|
String get instructionText => _instructionText;
|
||||||
|
FaceStatus get faceStatus => _faceStatus;
|
||||||
|
double get holdProgress => _holdProgress;
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
_currentStep = EnrollmentStep.depan;
|
_currentStep = EnrollmentStep.depan;
|
||||||
|
|
@ -42,10 +62,22 @@ class FaceProvider with ChangeNotifier {
|
||||||
_fotoBawah = null;
|
_fotoBawah = null;
|
||||||
_isProcessing = false;
|
_isProcessing = false;
|
||||||
_isUploading = false;
|
_isUploading = false;
|
||||||
|
_faceStatus = FaceStatus.noFace;
|
||||||
|
_poseValidSince = null;
|
||||||
|
_holdProgress = 0.0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> processCameraImage(CameraImage image, CameraDescription camera, InputImageRotation rotation) async {
|
Future<void> processCameraImage(
|
||||||
|
CameraImage image,
|
||||||
|
CameraDescription camera,
|
||||||
|
InputImageRotation rotation, {
|
||||||
|
// Ukuran kotak scan dalam koordinat layar (0.0 - 1.0)
|
||||||
|
double scanBoxLeft = 0.125,
|
||||||
|
double scanBoxRight = 0.875,
|
||||||
|
double scanBoxTop = 0.275,
|
||||||
|
double scanBoxBottom = 0.725,
|
||||||
|
}) async {
|
||||||
if (_isProcessing || _currentStep == EnrollmentStep.selesai) return;
|
if (_isProcessing || _currentStep == EnrollmentStep.selesai) return;
|
||||||
|
|
||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
|
|
@ -59,10 +91,12 @@ class FaceProvider with ChangeNotifier {
|
||||||
|
|
||||||
final List<Face> faces = await _faceDetector.processImage(inputImage);
|
final List<Face> faces = await _faceDetector.processImage(inputImage);
|
||||||
|
|
||||||
if (faces.isNotEmpty) {
|
if (faces.isEmpty) {
|
||||||
final Face face = faces.first;
|
_updateFaceStatus(FaceStatus.noFace);
|
||||||
_checkPoseAndCapture(face, image);
|
_resetHoldTimer();
|
||||||
} else {
|
} else {
|
||||||
|
final Face face = faces.first;
|
||||||
|
_validateAndCheckPose(face, image, scanBoxLeft, scanBoxRight, scanBoxTop, scanBoxBottom);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error processing face: $e");
|
debugPrint("Error processing face: $e");
|
||||||
|
|
@ -71,53 +105,135 @@ class FaceProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkPoseAndCapture(Face face, CameraImage originalImage) async {
|
void _validateAndCheckPose(
|
||||||
|
Face face,
|
||||||
|
CameraImage image,
|
||||||
|
double boxLeft,
|
||||||
|
double boxRight,
|
||||||
|
double boxTop,
|
||||||
|
double boxBottom,
|
||||||
|
) {
|
||||||
|
final imgW = image.width.toDouble();
|
||||||
|
final imgH = image.height.toDouble();
|
||||||
|
|
||||||
|
// Bounding box dari ML Kit (dalam koordinat gambar - KAMERA DEPAN MIRROR)
|
||||||
|
final Rect bb = face.boundingBox;
|
||||||
|
|
||||||
|
// Normalisasi ke 0.0 - 1.0
|
||||||
|
// Untuk kamera depan di Android, X perlu di-flip karena kamera mirror
|
||||||
|
final double faceLeft = 1.0 - (bb.right / imgW);
|
||||||
|
final double faceRight = 1.0 - (bb.left / imgW);
|
||||||
|
final double faceTop = bb.top / imgH;
|
||||||
|
final double faceBottom = bb.bottom / imgH;
|
||||||
|
|
||||||
|
final double faceWidth = faceRight - faceLeft;
|
||||||
|
|
||||||
|
// --- Validasi Jarak (ukuran wajah) ---
|
||||||
|
// Wajah ideal: lebarnya 35% - 65% dari lebar frame
|
||||||
|
const double minFaceRatio = 0.30;
|
||||||
|
const double maxFaceRatio = 0.70;
|
||||||
|
|
||||||
|
if (faceWidth < minFaceRatio) {
|
||||||
|
_updateFaceStatus(FaceStatus.tooFar);
|
||||||
|
_resetHoldTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (faceWidth > maxFaceRatio) {
|
||||||
|
_updateFaceStatus(FaceStatus.tooClose);
|
||||||
|
_resetHoldTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validasi Posisi dalam Kotak Scan ---
|
||||||
|
// Center wajah harus berada di dalam kotak scan (dengan toleransi 10%)
|
||||||
|
final double faceCenterX = (faceLeft + faceRight) / 2;
|
||||||
|
final double faceCenterY = (faceTop + faceBottom) / 2;
|
||||||
|
|
||||||
|
const double tolerance = 0.10;
|
||||||
|
final bool inBox = faceCenterX >= (boxLeft - tolerance) &&
|
||||||
|
faceCenterX <= (boxRight + tolerance) &&
|
||||||
|
faceCenterY >= (boxTop - tolerance) &&
|
||||||
|
faceCenterY <= (boxBottom + tolerance);
|
||||||
|
|
||||||
|
if (!inBox) {
|
||||||
|
_updateFaceStatus(FaceStatus.outOfFrame);
|
||||||
|
_resetHoldTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validasi Pose ---
|
||||||
|
final bool isPoseValid = _isPoseValid(face);
|
||||||
|
|
||||||
|
if (!isPoseValid) {
|
||||||
|
_updateFaceStatus(FaceStatus.wrongPose);
|
||||||
|
_resetHoldTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pose valid: jalankan hold timer ---
|
||||||
|
_tickHoldTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPoseValid(Face face) {
|
||||||
const double thresholdFront = 10.0;
|
const double thresholdFront = 10.0;
|
||||||
const double thresholdSide = 15.0;
|
const double thresholdSide = 15.0;
|
||||||
const double thresholdDown = 5.0;
|
const double thresholdDown = 5.0;
|
||||||
|
|
||||||
double? rotY = face.headEulerAngleY;
|
final double? rotY = face.headEulerAngleY;
|
||||||
double? rotX = face.headEulerAngleX;
|
final double? rotX = face.headEulerAngleX;
|
||||||
|
|
||||||
debugPrint("ROT X (Atas/Bawah): $rotX | ROT Y (Kiri/Kanan): $rotY");
|
if (rotY == null || rotX == null) return false;
|
||||||
|
|
||||||
if (rotY == null || rotX == null) return;
|
|
||||||
|
|
||||||
bool isPoseValid = false;
|
|
||||||
|
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case EnrollmentStep.depan:
|
case EnrollmentStep.depan:
|
||||||
if (rotY.abs() < thresholdFront && rotX.abs() < thresholdFront) {
|
return rotY.abs() < thresholdFront && rotX.abs() < thresholdFront;
|
||||||
isPoseValid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case EnrollmentStep.kanan:
|
case EnrollmentStep.kanan:
|
||||||
if (rotY < -thresholdSide) {
|
return rotY < -thresholdSide;
|
||||||
isPoseValid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case EnrollmentStep.kiri:
|
case EnrollmentStep.kiri:
|
||||||
if (rotY > thresholdSide) {
|
return rotY > thresholdSide;
|
||||||
isPoseValid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case EnrollmentStep.bawah:
|
case EnrollmentStep.bawah:
|
||||||
bool isLookingForward = rotY.abs() < thresholdFront;
|
return rotX < -thresholdDown && rotY.abs() < thresholdFront;
|
||||||
|
|
||||||
if (rotX < -thresholdDown && isLookingForward) {
|
|
||||||
isPoseValid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (isPoseValid) {
|
|
||||||
await _captureImage(originalImage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _captureImage(CameraImage image) async {
|
void _tickHoldTimer() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (_poseValidSince == null) {
|
||||||
|
_poseValidSince = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
final elapsed = now.difference(_poseValidSince!).inMilliseconds;
|
||||||
|
_holdProgress = (elapsed / _holdDurationMs).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
_updateFaceStatus(FaceStatus.holding);
|
||||||
|
|
||||||
|
if (elapsed >= _holdDurationMs) {
|
||||||
|
// Capture!
|
||||||
|
_poseValidSince = null;
|
||||||
|
_holdProgress = 0.0;
|
||||||
|
_captureImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetHoldTimer() {
|
||||||
|
if (_poseValidSince != null || _holdProgress > 0) {
|
||||||
|
_poseValidSince = null;
|
||||||
|
_holdProgress = 0.0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateFaceStatus(FaceStatus status) {
|
||||||
|
if (_faceStatus != status) {
|
||||||
|
_faceStatus = status;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _captureImage() {
|
||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
|
|
||||||
if (_currentStep == EnrollmentStep.depan) {
|
if (_currentStep == EnrollmentStep.depan) {
|
||||||
|
|
@ -134,20 +250,26 @@ class FaceProvider with ChangeNotifier {
|
||||||
_instructionText = "Data lengkap. Mengunggah...";
|
_instructionText = "Data lengkap. Mengunggah...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_faceStatus = FaceStatus.noFace;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
// Cooldown setelah capture agar tidak langsung re-trigger
|
||||||
_isProcessing = false;
|
Future.delayed(const Duration(milliseconds: 800), () {
|
||||||
|
_isProcessing = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveCapturedFile(File file) {
|
void saveCapturedFile(File file) {
|
||||||
if (_fotoDepan == null) _fotoDepan = file;
|
if (_fotoDepan == null) {
|
||||||
else if (_fotoKanan == null) _fotoKanan = file;
|
_fotoDepan = file;
|
||||||
else if (_fotoKiri == null) _fotoKiri = file;
|
} else if (_fotoKanan == null) {
|
||||||
else if (_fotoBawah == null) {
|
_fotoKanan = file;
|
||||||
|
} else if (_fotoKiri == null) {
|
||||||
|
_fotoKiri = file;
|
||||||
|
} else if (_fotoBawah == null) {
|
||||||
_fotoBawah = file;
|
_fotoBawah = file;
|
||||||
if (_currentStep == EnrollmentStep.selesai) {
|
if (_currentStep == EnrollmentStep.selesai) {
|
||||||
submitEnrollment();
|
submitEnrollment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,13 +305,13 @@ class FaceProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _faceStatus = {};
|
Map<String, dynamic> _faceStatus2 = {};
|
||||||
Map<String, dynamic> get faceStatus => _faceStatus;
|
Map<String, dynamic> get faceStatus2 => _faceStatus2;
|
||||||
|
|
||||||
Future<void> loadFaceStatus() async {
|
Future<void> loadFaceStatus() async {
|
||||||
try {
|
try {
|
||||||
final status = await _repository.getFaceStatus();
|
final status = await _repository.getFaceStatus();
|
||||||
_faceStatus = status;
|
_faceStatus2 = status;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error loading face status: $e");
|
debugPrint("Error loading face status: $e");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../repositories/face_repository.dart';
|
||||||
|
import '../repositories/signature_repository.dart';
|
||||||
|
|
||||||
|
class SetupCheckProvider extends ChangeNotifier {
|
||||||
|
final FaceRepository _faceRepository = FaceRepository();
|
||||||
|
final SignatureRepository _signatureRepository = SignatureRepository();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _hasFace = false;
|
||||||
|
bool _hasSignature = false;
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
bool get hasFace => _hasFace;
|
||||||
|
bool get hasSignature => _hasSignature;
|
||||||
|
|
||||||
|
Future<void> checkSetup() async {
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final results = await Future.wait([
|
||||||
|
_faceRepository.getFaceStatus(),
|
||||||
|
_signatureRepository.getActiveSignature(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final faceData = results[0];
|
||||||
|
final signatureData = results[1];
|
||||||
|
|
||||||
|
// Backend mengembalikan: { is_registered: bool, status: 'verified'|'pending'|'not_registered' }
|
||||||
|
// Dianggap sudah punya wajah jika is_registered = true (sudah upload foto, meski pending approval)
|
||||||
|
_hasFace = faceData['is_registered'] == true;
|
||||||
|
_hasSignature = signatureData['success'] == true && signatureData['data'] != null;
|
||||||
|
} catch (e) {
|
||||||
|
_hasFace = false;
|
||||||
|
_hasSignature = false;
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_isLoading = false;
|
||||||
|
_hasFace = false;
|
||||||
|
_hasSignature = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success && mounted) {
|
if (success && mounted) {
|
||||||
Navigator.pushReplacementNamed(context, '/home');
|
Navigator.pushReplacementNamed(context, '/onboarding/check');
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,17 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/face_provider.dart';
|
import '../../providers/face_provider.dart';
|
||||||
|
import '../../providers/setup_check_provider.dart';
|
||||||
import '../../widgets/atoms/custom_button.dart';
|
import '../../widgets/atoms/custom_button.dart';
|
||||||
import '../../widgets/atoms/scanner_overlay.dart';
|
import '../../widgets/atoms/scanner_overlay.dart';
|
||||||
import '../../core/utils/camera_utils.dart';
|
import '../../core/utils/camera_utils.dart';
|
||||||
import '../../core/theme.dart';
|
import '../../core/theme.dart';
|
||||||
|
import '../profile/signature_screen.dart';
|
||||||
|
|
||||||
class FaceEnrollmentScreen extends StatefulWidget {
|
class FaceEnrollmentScreen extends StatefulWidget {
|
||||||
const FaceEnrollmentScreen({super.key});
|
final bool isOnboarding;
|
||||||
|
|
||||||
|
const FaceEnrollmentScreen({this.isOnboarding = false, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FaceEnrollmentScreen> createState() => _FaceEnrollmentScreenState();
|
State<FaceEnrollmentScreen> createState() => _FaceEnrollmentScreenState();
|
||||||
|
|
@ -23,6 +27,13 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
CameraDescription? _frontCamera;
|
CameraDescription? _frontCamera;
|
||||||
int _lastStepIndex = -1;
|
int _lastStepIndex = -1;
|
||||||
|
|
||||||
|
// Koordinat kotak scan (0.0 - 1.0) — harus cocok dengan _ScannerPainter
|
||||||
|
// ScannerOverlay: lebar 75% (0.125 - 0.875), tinggi 45% dari tengah (0.275 - 0.725)
|
||||||
|
static const double _boxLeft = 0.125;
|
||||||
|
static const double _boxRight = 0.875;
|
||||||
|
static const double _boxTop = 0.275;
|
||||||
|
static const double _boxBottom = 0.725;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -60,7 +71,11 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
provider.processCameraImage(
|
provider.processCameraImage(
|
||||||
image,
|
image,
|
||||||
_frontCamera!,
|
_frontCamera!,
|
||||||
CameraUtils.rotationIntToImageRotation(_frontCamera!.sensorOrientation)
|
CameraUtils.rotationIntToImageRotation(_frontCamera!.sensorOrientation),
|
||||||
|
scanBoxLeft: _boxLeft,
|
||||||
|
scanBoxRight: _boxRight,
|
||||||
|
scanBoxTop: _boxTop,
|
||||||
|
scanBoxBottom: _boxBottom,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -75,6 +90,43 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Teks panduan berdasarkan FaceStatus
|
||||||
|
String _getStatusText(FaceProvider provider) {
|
||||||
|
switch (provider.faceStatus) {
|
||||||
|
case FaceStatus.noFace:
|
||||||
|
return provider.instructionText;
|
||||||
|
case FaceStatus.tooFar:
|
||||||
|
return "⬆️ Dekatkan wajah ke kamera";
|
||||||
|
case FaceStatus.tooClose:
|
||||||
|
return "⬇️ Jauhkan wajah dari kamera";
|
||||||
|
case FaceStatus.outOfFrame:
|
||||||
|
return "📦 Arahkan wajah ke dalam kotak";
|
||||||
|
case FaceStatus.wrongPose:
|
||||||
|
return provider.instructionText;
|
||||||
|
case FaceStatus.holding:
|
||||||
|
return "✅ Tahan posisi...";
|
||||||
|
case FaceStatus.ready:
|
||||||
|
return "📸 Mengambil foto...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warna border kotak berdasarkan status
|
||||||
|
Color _getBorderColor(FaceProvider provider) {
|
||||||
|
if (provider.currentStep == EnrollmentStep.selesai) {
|
||||||
|
return AppTheme.statusGreen;
|
||||||
|
}
|
||||||
|
switch (provider.faceStatus) {
|
||||||
|
case FaceStatus.holding:
|
||||||
|
return AppTheme.statusGreen;
|
||||||
|
case FaceStatus.outOfFrame:
|
||||||
|
case FaceStatus.tooFar:
|
||||||
|
case FaceStatus.tooClose:
|
||||||
|
return AppTheme.statusRed;
|
||||||
|
default:
|
||||||
|
return AppTheme.primaryOrange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -97,18 +149,22 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
),
|
),
|
||||||
body: Consumer<FaceProvider>(
|
body: Consumer<FaceProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
// Deteksi step baru → ambil foto
|
||||||
if (provider.currentStep.index > _lastStepIndex && _lastStepIndex != -1) {
|
if (provider.currentStep.index > _lastStepIndex && _lastStepIndex != -1) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
_takePictureAndSave(provider);
|
_takePictureAndSave(provider);
|
||||||
}
|
}
|
||||||
if (_lastStepIndex != provider.currentStep.index) {
|
if (_lastStepIndex != provider.currentStep.index) {
|
||||||
_lastStepIndex = provider.currentStep.index;
|
_lastStepIndex = provider.currentStep.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool isComplete = provider.currentStep == EnrollmentStep.selesai;
|
final bool isComplete = provider.currentStep == EnrollmentStep.selesai;
|
||||||
|
final bool isHolding = provider.faceStatus == FaceStatus.holding;
|
||||||
|
final Color scanColor = _getBorderColor(provider);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
// --- Preview Kamera ---
|
||||||
if (_isCameraInitialized)
|
if (_isCameraInitialized)
|
||||||
SizedBox.expand(
|
SizedBox.expand(
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
|
|
@ -123,11 +179,31 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
else
|
else
|
||||||
const Center(child: CircularProgressIndicator(color: Colors.white)),
|
const Center(child: CircularProgressIndicator(color: Colors.white)),
|
||||||
|
|
||||||
|
// --- Overlay Kotak Scan ---
|
||||||
ScannerOverlay(
|
ScannerOverlay(
|
||||||
borderColor: isComplete ? AppTheme.statusGreen : AppTheme.primaryOrange,
|
borderColor: scanColor,
|
||||||
isScanning: !isComplete && !provider.isUploading,
|
isScanning: !isComplete && !provider.isUploading && !isHolding,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// --- Hold Progress Ring (di tengah kotak scan) ---
|
||||||
|
if (isHolding && !isComplete)
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: provider.holdProgress,
|
||||||
|
strokeWidth: 5,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(AppTheme.statusGreen),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Banner Instruksi Atas ---
|
||||||
Positioned(
|
Positioned(
|
||||||
top: MediaQuery.of(context).padding.top + kToolbarHeight + 20,
|
top: MediaQuery.of(context).padding.top + kToolbarHeight + 20,
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|
@ -139,37 +215,61 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
style: AppTheme.heading3.copyWith(color: Colors.white),
|
style: AppTheme.heading3.copyWith(color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ClipRRect(
|
AnimatedSwitcher(
|
||||||
borderRadius: BorderRadius.circular(20),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: BackdropFilter(
|
child: ClipRRect(
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
key: ValueKey(provider.faceStatus),
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(20),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
child: BackdropFilter(
|
||||||
decoration: BoxDecoration(
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
color: Colors.white.withOpacity(0.15),
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(20),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
border: Border.all(color: Colors.white.withOpacity(0.3)),
|
decoration: BoxDecoration(
|
||||||
),
|
color: isHolding
|
||||||
child: Row(
|
? AppTheme.statusGreen.withOpacity(0.25)
|
||||||
mainAxisSize: MainAxisSize.min,
|
: Colors.white.withOpacity(0.15),
|
||||||
children: [
|
borderRadius: BorderRadius.circular(20),
|
||||||
Icon(
|
border: Border.all(
|
||||||
isComplete ? Icons.check_circle : Icons.face,
|
color: isHolding
|
||||||
color: isComplete ? AppTheme.statusGreen : AppTheme.primaryOrange,
|
? AppTheme.statusGreen.withOpacity(0.6)
|
||||||
size: 20,
|
: Colors.white.withOpacity(0.3),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Flexible(
|
child: Row(
|
||||||
child: Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
provider.instructionText,
|
children: [
|
||||||
textAlign: TextAlign.center,
|
Icon(
|
||||||
style: AppTheme.bodyLarge.copyWith(
|
isComplete
|
||||||
color: isComplete ? AppTheme.statusGreen : Colors.white,
|
? Icons.check_circle
|
||||||
fontWeight: FontWeight.bold
|
: isHolding
|
||||||
|
? Icons.timer
|
||||||
|
: Icons.face,
|
||||||
|
color: isComplete
|
||||||
|
? AppTheme.statusGreen
|
||||||
|
: isHolding
|
||||||
|
? AppTheme.statusGreen
|
||||||
|
: AppTheme.primaryOrange,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
isComplete
|
||||||
|
? provider.instructionText
|
||||||
|
: _getStatusText(provider),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppTheme.bodyLarge.copyWith(
|
||||||
|
color: isComplete
|
||||||
|
? AppTheme.statusGreen
|
||||||
|
: isHolding
|
||||||
|
? AppTheme.statusGreen
|
||||||
|
: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -178,6 +278,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// --- Panel Bawah: Progress Steps & Tombol Selesai ---
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 40,
|
bottom: 40,
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|
@ -216,7 +317,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
children: List.generate(4, (index) {
|
children: List.generate(4, (index) {
|
||||||
final isActive = index == provider.currentStep.index;
|
final isActive = index == provider.currentStep.index;
|
||||||
final isPassed = index < provider.currentStep.index;
|
final isPassed = index < provider.currentStep.index;
|
||||||
|
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
|
@ -242,17 +343,27 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (isComplete)
|
if (isComplete)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 24),
|
padding: const EdgeInsets.only(top: 24),
|
||||||
child: CustomButton(
|
child: CustomButton(
|
||||||
text: "Selesai",
|
text: widget.isOnboarding ? "Lanjut: Tanda Tangan" : "Selesai",
|
||||||
icon: Icons.check,
|
icon: widget.isOnboarding ? Icons.arrow_forward : Icons.check,
|
||||||
backgroundColor: AppTheme.statusGreen,
|
backgroundColor: AppTheme.statusGreen,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
Navigator.pop(context);
|
if (widget.isOnboarding) {
|
||||||
|
context.read<SetupCheckProvider>().reset();
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const SignatureScreen(isOnboarding: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -274,12 +385,16 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen> {
|
||||||
provider.saveCapturedFile(File(file.path));
|
provider.saveCapturedFile(File(file.path));
|
||||||
|
|
||||||
await _cameraController?.startImageStream((CameraImage image) {
|
await _cameraController?.startImageStream((CameraImage image) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
provider.processCameraImage(
|
provider.processCameraImage(
|
||||||
image,
|
image,
|
||||||
_frontCamera!,
|
_frontCamera!,
|
||||||
CameraUtils.rotationIntToImageRotation(_frontCamera!.sensorOrientation)
|
CameraUtils.rotationIntToImageRotation(_frontCamera!.sensorOrientation),
|
||||||
);
|
scanBoxLeft: _boxLeft,
|
||||||
|
scanBoxRight: _boxRight,
|
||||||
|
scanBoxTop: _boxTop,
|
||||||
|
scanBoxBottom: _boxBottom,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error capturing image: $e");
|
debugPrint("Error capturing image: $e");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../core/theme.dart';
|
||||||
|
import '../../providers/setup_check_provider.dart';
|
||||||
|
import 'face_enrollment_screen.dart';
|
||||||
|
import '../profile/signature_screen.dart';
|
||||||
|
|
||||||
|
class OnboardingCheckScreen extends StatefulWidget {
|
||||||
|
const OnboardingCheckScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OnboardingCheckScreen> createState() => _OnboardingCheckScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OnboardingCheckScreenState extends State<OnboardingCheckScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
);
|
||||||
|
_fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
|
||||||
|
_controller.forward();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_runCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runCheck() async {
|
||||||
|
final provider = context.read<SetupCheckProvider>();
|
||||||
|
await provider.checkSetup();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Navigasi berdasarkan hasil pengecekan
|
||||||
|
if (!provider.hasFace) {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const FaceEnrollmentScreen(isOnboarding: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!provider.hasSignature) {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const SignatureScreen(isOnboarding: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pushReplacementNamed(context, '/home');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.primaryDark,
|
||||||
|
body: FadeTransition(
|
||||||
|
opacity: _fadeAnimation,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
boxShadow: AppTheme.shadowMd,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.diamond_outlined,
|
||||||
|
size: 44,
|
||||||
|
color: AppTheme.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingLg),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
|
Text(
|
||||||
|
'Memeriksa kelengkapan akun...',
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -330,14 +330,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
icon: Icons.face,
|
icon: Icons.face,
|
||||||
label: "Registrasi Wajah",
|
label: "Registrasi Wajah",
|
||||||
onTap: () => Navigator.pushNamed(context, '/onboarding/face-enrollment'),
|
onTap: () => Navigator.pushNamed(context, '/onboarding/face-enrollment'),
|
||||||
trailingText: context.watch<FaceProvider>().faceStatus['is_registered'] == true
|
trailingText: context.watch<FaceProvider>().faceStatus2['is_registered'] == true
|
||||||
? "Terdaftar"
|
? "Terdaftar"
|
||||||
: "Belum Terdaftar",
|
: "Belum Terdaftar",
|
||||||
trailingColor: context.watch<FaceProvider>().faceStatus['is_registered'] == true
|
trailingColor: context.watch<FaceProvider>().faceStatus2['is_registered'] == true
|
||||||
? AppTheme.statusGreen
|
? AppTheme.statusGreen
|
||||||
: AppTheme.statusRed,
|
: AppTheme.statusRed,
|
||||||
),
|
),
|
||||||
if (context.watch<FaceProvider>().faceStatus['is_registered'] == true)
|
if (context.watch<FaceProvider>().faceStatus2['is_registered'] == true)
|
||||||
_buildActionItem(
|
_buildActionItem(
|
||||||
icon: Icons.face_retouching_natural,
|
icon: Icons.face_retouching_natural,
|
||||||
label: "Test Pengenalan Wajah",
|
label: "Test Pengenalan Wajah",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import '../../providers/signature_provider.dart';
|
||||||
import '../../widgets/atoms/custom_button.dart';
|
import '../../widgets/atoms/custom_button.dart';
|
||||||
|
|
||||||
class SignatureScreen extends StatefulWidget {
|
class SignatureScreen extends StatefulWidget {
|
||||||
const SignatureScreen({Key? key}) : super(key: key);
|
final bool isOnboarding;
|
||||||
|
|
||||||
|
const SignatureScreen({this.isOnboarding = false, Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SignatureScreen> createState() => _SignatureScreenState();
|
State<SignatureScreen> createState() => _SignatureScreenState();
|
||||||
|
|
@ -114,7 +116,13 @@ class _SignatureScreenState extends State<SignatureScreen> {
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (success) _clearCanvas();
|
if (success) {
|
||||||
|
if (widget.isOnboarding && mounted) {
|
||||||
|
Navigator.pushReplacementNamed(context, '/home');
|
||||||
|
} else {
|
||||||
|
_clearCanvas();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -173,10 +181,12 @@ class _SignatureScreenState extends State<SignatureScreen> {
|
||||||
backgroundColor: AppTheme.bgWhite,
|
backgroundColor: AppTheme.bgWhite,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: IconButton(
|
leading: widget.isOnboarding
|
||||||
icon: const Icon(Icons.arrow_back, color: AppTheme.textPrimary),
|
? const SizedBox.shrink()
|
||||||
onPressed: () => Navigator.pop(context),
|
: IconButton(
|
||||||
),
|
icon: const Icon(Icons.arrow_back, color: AppTheme.textPrimary),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Consumer<SignatureProvider>(
|
body: Consumer<SignatureProvider>(
|
||||||
builder: (context, provider, _) {
|
builder: (context, provider, _) {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderSt
|
||||||
|
|
||||||
final token = CacheManager.authBox.get('auth_token');
|
final token = CacheManager.authBox.get('auth_token');
|
||||||
if (token != null && token.toString().isNotEmpty) {
|
if (token != null && token.toString().isNotEmpty) {
|
||||||
Navigator.pushReplacementNamed(context, '/home');
|
Navigator.pushReplacementNamed(context, '/onboarding/check');
|
||||||
} else {
|
} else {
|
||||||
Navigator.pushReplacementNamed(context, '/login');
|
Navigator.pushReplacementNamed(context, '/login');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
||||||
import '../core/cache_manager.dart';
|
import '../core/cache_manager.dart';
|
||||||
|
import '../core/constants/api_url.dart';
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
late final Dio dio;
|
late final Dio dio;
|
||||||
|
|
||||||
ApiClient() {
|
ApiClient() {
|
||||||
dio = Dio(BaseOptions(
|
dio = Dio(BaseOptions(
|
||||||
baseUrl: dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000/api',
|
baseUrl: ApiUrl.baseUrl,
|
||||||
connectTimeout: const Duration(seconds: 30),
|
connectTimeout: const Duration(seconds: 30),
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -24,8 +24,7 @@ class ApiClient {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[API Request] ${options.method} ${options.path}');
|
print('[API Request] ${options.method} ${options.baseUrl}${options.path}');
|
||||||
print('[API Headers] ${options.headers}');
|
|
||||||
return handler.next(options);
|
return handler.next(options);
|
||||||
},
|
},
|
||||||
onResponse: (response, handler) {
|
onResponse: (response, handler) {
|
||||||
|
|
@ -33,22 +32,12 @@ class ApiClient {
|
||||||
return handler.next(response);
|
return handler.next(response);
|
||||||
},
|
},
|
||||||
onError: (DioException e, handler) {
|
onError: (DioException e, handler) {
|
||||||
String errorMessage = 'Terjadi kesalahan';
|
|
||||||
|
|
||||||
if (e.response != null) {
|
if (e.response != null) {
|
||||||
print('[API Error] ${e.response?.statusCode} ${e.requestOptions.path}');
|
print('[API Error] ${e.response?.statusCode} ${e.requestOptions.path}');
|
||||||
print('[API Error Data] ${e.response?.data}');
|
print('[API Error Data] ${e.response?.data}');
|
||||||
|
|
||||||
if (e.response!.data is Map && e.response!.data['message'] != null) {
|
|
||||||
errorMessage = e.response!.data['message'];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
print('[API Error] Network error: ${e.message}');
|
print('[API Network Error] ${e.type}: ${e.message}');
|
||||||
errorMessage = 'Koneksi gagal. Periksa internet Anda.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[API Error Message] $errorMessage');
|
|
||||||
|
|
||||||
return handler.next(e);
|
return handler.next(e);
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue