sedikit penyesuaian
This commit is contained in:
parent
53255d8d43
commit
c8de09b83b
|
|
@ -20,7 +20,6 @@ class ApiUrl {
|
|||
static String get imageBaseUrl => _cachedImageBaseUrl ?? baseUrl.replaceAll('/api', '/storage/');
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await ApiConfigService.clearConfig();
|
||||
await getBaseUrl();
|
||||
await getImageBaseUrl();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class ApiConfigService {
|
|||
static const String _selectedPresetKey = 'selected_preset';
|
||||
|
||||
static const Map<String, String> presets = {
|
||||
'current_ip': 'http://192.168.110.17:8000/api',
|
||||
'current_ip': 'http://192.168.1.3:8000/api',
|
||||
'hostname': 'http://LAPTOP-I0SUKSKL:8000/api',
|
||||
'emulator': 'http://10.0.2.2:8000/api',
|
||||
'custom': '',
|
||||
|
|
|
|||
|
|
@ -105,7 +105,12 @@ class PresensiHistoryModel {
|
|||
if (json['jam_masuk'] != null && json['jam_pulang'] != null) {
|
||||
try {
|
||||
final masuk = DateTime.parse("2000-01-01 ${json['jam_masuk']}");
|
||||
final pulang = DateTime.parse("2000-01-01 ${json['jam_pulang']}");
|
||||
DateTime pulang = DateTime.parse("2000-01-01 ${json['jam_pulang']}");
|
||||
|
||||
if (pulang.isBefore(masuk)) {
|
||||
pulang = pulang.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
final selisih = pulang.difference(masuk);
|
||||
hitungTotalJam = '${selisih.inHours}j ${selisih.inMinutes.remainder(60)}m';
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
|||
import '../repositories/face_repository.dart';
|
||||
import '../core/utils/camera_utils.dart';
|
||||
|
||||
enum VideoRecordingState { idle, recording, uploading, success, error }
|
||||
enum VideoRecordingState { idle, ready, recording, uploading, success, error }
|
||||
|
||||
enum FaceDetectionStatus { noFace, detected }
|
||||
|
||||
|
|
@ -17,10 +17,10 @@ class FaceProvider with ChangeNotifier {
|
|||
final FaceDetector _faceDetector = FaceDetector(
|
||||
options: FaceDetectorOptions(
|
||||
enableClassification: false,
|
||||
enableLandmarks: false,
|
||||
enableLandmarks: true,
|
||||
enableContours: false,
|
||||
enableTracking: false,
|
||||
performanceMode: FaceDetectorMode.fast,
|
||||
enableTracking: true,
|
||||
performanceMode: FaceDetectorMode.accurate,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -40,10 +40,12 @@ class FaceProvider with ChangeNotifier {
|
|||
double _brightness = 0.0;
|
||||
bool _isBrightnessOk = false;
|
||||
double _stepProgress = 0.0;
|
||||
double _rightAngleSign = 0.0;
|
||||
int _consecutiveValidPosedFrames = 0;
|
||||
Timer? _progressTimer;
|
||||
|
||||
static const double _frontDuration = 4.0;
|
||||
static const double _sideDuration = 3.0;
|
||||
static const double _sideDuration = 6.0; // Tambahan jeda ekstra untuk menoleh yang rapi
|
||||
static const double _tickInterval = 100;
|
||||
|
||||
VideoRecordingState get recordingState => _recordingState;
|
||||
|
|
@ -66,31 +68,46 @@ class FaceProvider with ChangeNotifier {
|
|||
isFaceDetected && _isPoseValid && _isDistanceOk && _isBrightnessOk;
|
||||
|
||||
String get qualityMessage {
|
||||
if (_recordingState == VideoRecordingState.ready) return 'Liveliness Lulus! Tekan tombol Rekam di bawah.';
|
||||
if (_recordingState == VideoRecordingState.recording) {
|
||||
if (_currentStep == EnrollmentStep.front) return 'Tahan posisi hadap depan...';
|
||||
if (_currentStep == EnrollmentStep.right) return 'Mulai toleh ke kanan perlahan...';
|
||||
return 'Mulai toleh arah sebaliknya perlahan...';
|
||||
}
|
||||
|
||||
if (!isFaceDetected) return 'Arahkan wajah ke dalam bingkai';
|
||||
if (_faceRatio < 0.20) return 'Terlalu jauh, dekatkan wajah ke kamera';
|
||||
if (_faceRatio > 0.70) return 'Terlalu dekat, mundur sedikit';
|
||||
if (!_isBrightnessOk) return 'Pencahayaan kurang, cari tempat lebih terang';
|
||||
if (!_isBrightnessOk) return 'Pencahayaan kurang, cari terang';
|
||||
if (!_isPoseValid) {
|
||||
switch (_currentStep) {
|
||||
case EnrollmentStep.front:
|
||||
return 'Hadapkan wajah ke depan';
|
||||
return 'UJI: Hadap depan (Ideal: <12, Skg: ${_headEulerAngleY.toStringAsFixed(0)})';
|
||||
case EnrollmentStep.right:
|
||||
return 'Toleh ke kanan';
|
||||
return 'UJI: Toleh kanan/kiri (Minimal 12 derajat, Skg: ${_headEulerAngleY.toStringAsFixed(0)})';
|
||||
case EnrollmentStep.left:
|
||||
return 'Toleh ke kiri';
|
||||
return 'UJI: Toleh arah sebaliknya (Minimal 12 derajat, Skg: ${_headEulerAngleY.toStringAsFixed(0)})';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String get stepLabel {
|
||||
if (_recordingState == VideoRecordingState.idle) {
|
||||
switch (_currentStep) {
|
||||
case EnrollmentStep.front: return 'Tes Liveliness: Depan';
|
||||
case EnrollmentStep.right: return 'Tes Liveliness: Toleh Kanan';
|
||||
case EnrollmentStep.left: return 'Tes Liveliness: Toleh Kiri';
|
||||
}
|
||||
}
|
||||
|
||||
switch (_currentStep) {
|
||||
case EnrollmentStep.front:
|
||||
return 'Hadap Depan';
|
||||
return 'Merekam: Hadap Depan';
|
||||
case EnrollmentStep.right:
|
||||
return 'Toleh Kanan';
|
||||
return 'Merekam: Toleh Kanan';
|
||||
case EnrollmentStep.left:
|
||||
return 'Toleh Kiri';
|
||||
return 'Merekam: Toleh Kiri';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +126,8 @@ class FaceProvider with ChangeNotifier {
|
|||
_brightness = 0.0;
|
||||
_isBrightnessOk = false;
|
||||
_stepProgress = 0.0;
|
||||
_rightAngleSign = 0.0;
|
||||
_consecutiveValidPosedFrames = 0;
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer = null;
|
||||
notifyListeners();
|
||||
|
|
@ -143,13 +162,14 @@ class FaceProvider with ChangeNotifier {
|
|||
return;
|
||||
}
|
||||
|
||||
if (allChecksValid) {
|
||||
final duration = _currentStep == EnrollmentStep.front
|
||||
? _frontDuration
|
||||
: _sideDuration;
|
||||
final increment = (_tickInterval / 1000) / duration;
|
||||
_stepProgress += increment;
|
||||
}
|
||||
// Fase 2: Hardware Android Camera2 pada banyak device mematikan aliran Stream Gambar
|
||||
// ketika Stream Video berjalan (freeze). Bypass strict MLKit, andalkan jeda waktu murni.
|
||||
final duration = _currentStep == EnrollmentStep.front
|
||||
? _frontDuration
|
||||
: _sideDuration;
|
||||
final increment = (_tickInterval / 1000) / duration;
|
||||
|
||||
_stepProgress += increment;
|
||||
|
||||
if (_stepProgress >= 1.0) {
|
||||
_advanceStep(controller);
|
||||
|
|
@ -166,6 +186,7 @@ class FaceProvider with ChangeNotifier {
|
|||
_currentStep = EnrollmentStep.right;
|
||||
_stepProgress = 0.0;
|
||||
_isPoseValid = false;
|
||||
_rightAngleSign = 0.0;
|
||||
_message = 'Toleh Kanan';
|
||||
break;
|
||||
case EnrollmentStep.right:
|
||||
|
|
@ -256,15 +277,41 @@ class FaceProvider with ChangeNotifier {
|
|||
|
||||
switch (_currentStep) {
|
||||
case EnrollmentStep.front:
|
||||
_isPoseValid = _headEulerAngleY.abs() < 15;
|
||||
_isPoseValid = _headEulerAngleY.abs() < 12;
|
||||
break;
|
||||
case EnrollmentStep.right:
|
||||
_isPoseValid = _headEulerAngleY < -20;
|
||||
_isPoseValid = _headEulerAngleY.abs() >= 12;
|
||||
if (_isPoseValid) {
|
||||
_rightAngleSign = _headEulerAngleY.sign;
|
||||
}
|
||||
break;
|
||||
case EnrollmentStep.left:
|
||||
_isPoseValid = _headEulerAngleY > 20;
|
||||
_isPoseValid = _headEulerAngleY.abs() >= 12 &&
|
||||
(_rightAngleSign == 0.0 || _headEulerAngleY.sign != _rightAngleSign);
|
||||
break;
|
||||
}
|
||||
|
||||
// Auto Advance Phase 1
|
||||
if (_recordingState == VideoRecordingState.idle && allChecksValid) {
|
||||
_consecutiveValidPosedFrames++;
|
||||
if (_consecutiveValidPosedFrames > 5) {
|
||||
_consecutiveValidPosedFrames = 0;
|
||||
if (_currentStep == EnrollmentStep.front) {
|
||||
_currentStep = EnrollmentStep.right;
|
||||
_isPoseValid = false;
|
||||
_rightAngleSign = 0.0;
|
||||
} else if (_currentStep == EnrollmentStep.right) {
|
||||
_currentStep = EnrollmentStep.left;
|
||||
_isPoseValid = false;
|
||||
} else if (_currentStep == EnrollmentStep.left) {
|
||||
_recordingState = VideoRecordingState.ready;
|
||||
_currentStep = EnrollmentStep.front; // Reset for recording phase
|
||||
_isPoseValid = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_consecutiveValidPosedFrames = 0;
|
||||
}
|
||||
} else {
|
||||
_faceDetectionStatus = FaceDetectionStatus.noFace;
|
||||
_isPoseValid = false;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,16 @@ class SetupCheckProvider extends ChangeNotifier {
|
|||
bool _isLoading = false;
|
||||
bool _hasFace = false;
|
||||
bool _hasSignature = false;
|
||||
bool _hasError = false;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
bool get hasFace => _hasFace;
|
||||
bool get hasSignature => _hasSignature;
|
||||
bool get hasError => _hasError;
|
||||
|
||||
Future<void> checkSetup() async {
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
|
|
@ -27,13 +30,18 @@ class SetupCheckProvider extends ChangeNotifier {
|
|||
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;
|
||||
_hasFace = faceData['is_registered'] == true && faceData['status'] != 'rejected';
|
||||
|
||||
if (signatureData['error'] == true) {
|
||||
_hasError = true;
|
||||
_hasSignature = true;
|
||||
} else {
|
||||
_hasSignature = signatureData['success'] == true && signatureData['data'] != null;
|
||||
}
|
||||
} catch (e) {
|
||||
_hasFace = false;
|
||||
_hasSignature = false;
|
||||
_hasError = true;
|
||||
_hasFace = true;
|
||||
_hasSignature = true;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
|
@ -44,5 +52,6 @@ class SetupCheckProvider extends ChangeNotifier {
|
|||
_isLoading = false;
|
||||
_hasFace = false;
|
||||
_hasSignature = false;
|
||||
_hasError = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../services/api_client.dart';
|
||||
|
||||
class SignatureRepository {
|
||||
|
|
@ -11,9 +12,14 @@ class SignatureRepository {
|
|||
if (response.statusCode == 200 && response.data['success'] == true) {
|
||||
return {'success': true, 'data': response.data['data']};
|
||||
}
|
||||
return {'success': false, 'message': response.data['message'] ?? 'Gagal'};
|
||||
return {'success': false, 'data': null, 'message': response.data['message'] ?? 'Gagal'};
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
return {'success': false, 'data': null, 'message': 'Belum ada tanda tangan'};
|
||||
}
|
||||
return {'success': false, 'error': true, 'message': e.message ?? 'Gagal koneksi ke server'};
|
||||
} catch (e) {
|
||||
return {'success': false, 'message': e.toString()};
|
||||
return {'success': false, 'error': true, 'message': e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||
if (tipe.contains('lembur')) return Icons.schedule_rounded;
|
||||
if (tipe.contains('presensi')) return Icons.fingerprint_rounded;
|
||||
if (tipe.contains('izin')) return Icons.description_rounded;
|
||||
if (tipe.contains('cuti')) return Icons.beach_access_rounded;
|
||||
if (tipe.contains('poin')) return Icons.stars_rounded;
|
||||
return Icons.notifications_outlined;
|
||||
}
|
||||
|
|
@ -41,6 +42,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||
if (tipe.contains('lembur')) return const Color(0xFFf59e0b);
|
||||
if (tipe.contains('poin')) return const Color(0xFFe11d48);
|
||||
if (tipe.contains('izin')) return const Color(0xFF8b5cf6);
|
||||
if (tipe.contains('cuti')) return const Color(0xFF06b6d4);
|
||||
if (tipe.contains('presensi')) return const Color(0xFF06b6d4);
|
||||
return const Color(0xFF6366f1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
CameraController? _cameraController;
|
||||
bool _isCameraInitialized = false;
|
||||
CameraDescription? _frontCamera;
|
||||
bool _successHandled = false;
|
||||
|
||||
late AnimationController _pulseController;
|
||||
late Animation<double> _pulseAnimation;
|
||||
|
|
@ -58,7 +59,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
|
||||
_cameraController = CameraController(
|
||||
_frontCamera!,
|
||||
ResolutionPreset.medium,
|
||||
ResolutionPreset.high,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: Platform.isAndroid
|
||||
? ImageFormatGroup.nv21
|
||||
|
|
@ -72,8 +73,10 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
_cameraController!.startImageStream((CameraImage image) {
|
||||
if (!mounted) return;
|
||||
final provider = context.read<FaceProvider>();
|
||||
if (provider.recordingState == VideoRecordingState.idle ||
|
||||
provider.recordingState == VideoRecordingState.recording) {
|
||||
var isIdle = provider.recordingState == VideoRecordingState.idle;
|
||||
var isReady = provider.recordingState == VideoRecordingState.ready;
|
||||
var isRecording = provider.recordingState == VideoRecordingState.recording;
|
||||
if (isIdle || isReady || isRecording) {
|
||||
provider.processCameraImage(
|
||||
image,
|
||||
_frontCamera!,
|
||||
|
|
@ -100,25 +103,6 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _cameraController!.stopImageStream();
|
||||
} catch (_) {}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
_cameraController!.startImageStream((CameraImage image) {
|
||||
if (!mounted) return;
|
||||
final p = context.read<FaceProvider>();
|
||||
if (p.recordingState == VideoRecordingState.recording) {
|
||||
p.processCameraImage(
|
||||
image,
|
||||
_frontCamera!,
|
||||
CameraUtils.rotationIntToImageRotation(
|
||||
_frontCamera!.sensorOrientation),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
HapticFeedback.mediumImpact();
|
||||
await provider.startRecording(_cameraController!);
|
||||
}
|
||||
|
|
@ -146,8 +130,19 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
),
|
||||
body: Consumer<FaceProvider>(
|
||||
builder: (context, provider, child) {
|
||||
// Refresh status dari server saat enrollment berhasil
|
||||
if (provider.recordingState == VideoRecordingState.success && !_successHandled) {
|
||||
_successHandled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
context.read<SetupCheckProvider>().checkSetup();
|
||||
}
|
||||
});
|
||||
}
|
||||
final bool isIdle =
|
||||
provider.recordingState == VideoRecordingState.idle;
|
||||
final bool isReady =
|
||||
provider.recordingState == VideoRecordingState.ready;
|
||||
final bool isRecording =
|
||||
provider.recordingState == VideoRecordingState.recording;
|
||||
final bool isUploading =
|
||||
|
|
@ -487,8 +482,8 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
),
|
||||
),
|
||||
|
||||
// Idle state — tombol mulai rekam
|
||||
if (isIdle)
|
||||
// Idle state — tombol mulai rekam (hanya muncul/aktif saat ready)
|
||||
if (isIdle || isReady)
|
||||
Column(
|
||||
children: [
|
||||
if (provider.qualityMessage.isNotEmpty)
|
||||
|
|
@ -497,7 +492,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
child: Text(
|
||||
provider.qualityMessage,
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: provider.isFaceDetected
|
||||
color: provider.isFaceDetected || isReady
|
||||
? AppTheme.statusYellow
|
||||
: Colors.white70,
|
||||
),
|
||||
|
|
@ -505,12 +500,12 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
),
|
||||
),
|
||||
CustomButton(
|
||||
text: "Mulai Rekam",
|
||||
text: isReady ? "Mulai Rekam Dataset" : "Selesaikan Tes Anti-Palsu",
|
||||
icon: Icons.videocam,
|
||||
backgroundColor: provider.allChecksValid
|
||||
backgroundColor: isReady
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.primaryDark.withOpacity(0.5),
|
||||
onPressed: provider.allChecksValid
|
||||
onPressed: isReady
|
||||
? () => _handleStartRecording(provider)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -523,23 +518,33 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
padding: const EdgeInsets.only(top: 24),
|
||||
child: CustomButton(
|
||||
text: widget.isOnboarding
|
||||
? "Lanjut: Tanda Tangan"
|
||||
? (context.read<SetupCheckProvider>().hasSignature
|
||||
? "Mulai Gunakan Aplikasi"
|
||||
: "Lanjut: Tanda Tangan")
|
||||
: "Selesai",
|
||||
icon: widget.isOnboarding
|
||||
? Icons.arrow_forward
|
||||
? (context.read<SetupCheckProvider>().hasSignature
|
||||
? Icons.home
|
||||
: Icons.arrow_forward)
|
||||
: Icons.check,
|
||||
backgroundColor: AppTheme.statusGreen,
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
if (widget.isOnboarding) {
|
||||
final hasSignature = context.read<SetupCheckProvider>().hasSignature;
|
||||
context.read<SetupCheckProvider>().reset();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
const SignatureScreen(isOnboarding: true),
|
||||
),
|
||||
);
|
||||
|
||||
if (hasSignature) {
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
const SignatureScreen(isOnboarding: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
|
@ -580,6 +585,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
icon: Icons.refresh,
|
||||
backgroundColor: AppTheme.primaryOrange,
|
||||
onPressed: () {
|
||||
setState(() => _successHandled = false);
|
||||
provider.reset();
|
||||
_restartCameraStream();
|
||||
},
|
||||
|
|
@ -765,6 +771,7 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
String _getStatusText(FaceProvider provider) {
|
||||
switch (provider.recordingState) {
|
||||
case VideoRecordingState.idle:
|
||||
case VideoRecordingState.ready:
|
||||
return '';
|
||||
case VideoRecordingState.recording:
|
||||
return '';
|
||||
|
|
@ -781,6 +788,8 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
switch (provider.recordingState) {
|
||||
case VideoRecordingState.idle:
|
||||
return Icons.face;
|
||||
case VideoRecordingState.ready:
|
||||
return Icons.check_circle_outline;
|
||||
case VideoRecordingState.recording:
|
||||
return Icons.videocam;
|
||||
case VideoRecordingState.uploading:
|
||||
|
|
@ -796,6 +805,8 @@ class _FaceEnrollmentScreenState extends State<FaceEnrollmentScreen>
|
|||
switch (provider.recordingState) {
|
||||
case VideoRecordingState.idle:
|
||||
return Colors.white;
|
||||
case VideoRecordingState.ready:
|
||||
return AppTheme.statusGreen;
|
||||
case VideoRecordingState.recording:
|
||||
return Colors.white;
|
||||
case VideoRecordingState.uploading:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ class _FaceTestScreenState extends State<FaceTestScreen> {
|
|||
return 'Siap — mulai verifikasi';
|
||||
}
|
||||
|
||||
Color get _resultColor {
|
||||
if (_statusText == 'APPROVED') return AppTheme.statusGreen;
|
||||
if (_statusText == 'PENDING') return AppTheme.statusYellow;
|
||||
return AppTheme.statusRed;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -261,9 +267,7 @@ class _FaceTestScreenState extends State<FaceTestScreen> {
|
|||
|
||||
ScannerOverlay(
|
||||
borderColor: _showResult
|
||||
? (_verified == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed)
|
||||
? _resultColor
|
||||
: _allChecksValid
|
||||
? AppTheme.statusGreen
|
||||
: _isFaceDetected
|
||||
|
|
@ -300,16 +304,16 @@ class _FaceTestScreenState extends State<FaceTestScreen> {
|
|||
_isVerifying
|
||||
? "Memverifikasi wajah..."
|
||||
: _showResult
|
||||
? (_verified == true
|
||||
? (_statusText == 'APPROVED'
|
||||
? "Wajah Terverifikasi ✓"
|
||||
: "Wajah Tidak Cocok ✗")
|
||||
: _statusText == 'PENDING'
|
||||
? "Verifikasi Meragukan ⚠️"
|
||||
: "Wajah Tidak Cocok ✗")
|
||||
: _qualityMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTheme.bodyLarge.copyWith(
|
||||
color: _showResult
|
||||
? (_verified == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed)
|
||||
? _resultColor
|
||||
: _allChecksValid
|
||||
? AppTheme.statusGreen
|
||||
: Colors.white,
|
||||
|
|
@ -373,31 +377,28 @@ class _FaceTestScreenState extends State<FaceTestScreen> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: (_verified == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed)
|
||||
.withOpacity(0.1),
|
||||
color: _resultColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
_verified == true
|
||||
_statusText == 'APPROVED'
|
||||
? Icons.check_circle
|
||||
: Icons.cancel,
|
||||
color: _verified == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed,
|
||||
: _statusText == 'PENDING'
|
||||
? Icons.warning_amber_rounded
|
||||
: Icons.cancel,
|
||||
color: _resultColor,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_verified == true
|
||||
_statusText == 'APPROVED'
|
||||
? "Verifikasi Berhasil"
|
||||
: "Verifikasi Gagal",
|
||||
: _statusText == 'PENDING'
|
||||
? "Verifikasi Diragukan"
|
||||
: "Verifikasi Gagal",
|
||||
style: AppTheme.heading3.copyWith(
|
||||
color: _verified == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed,
|
||||
color: _resultColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -422,15 +423,15 @@ class _FaceTestScreenState extends State<FaceTestScreen> {
|
|||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_confidence! >= 0.85
|
||||
? "Sangat cocok (≥ 85%)"
|
||||
: _confidence! >= 0.65
|
||||
? "Cukup cocok (65-85%)"
|
||||
: "Tidak cocok (< 65%)",
|
||||
_confidence! >= 0.75
|
||||
? "Sangat cocok (≥ 75%)"
|
||||
: _confidence! >= 0.55
|
||||
? "Meragukan (55-74%)"
|
||||
: "Tidak cocok (< 55%)",
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: _confidence! >= 0.85
|
||||
color: _confidence! >= 0.75
|
||||
? AppTheme.statusGreen
|
||||
: _confidence! >= 0.65
|
||||
: _confidence! >= 0.55
|
||||
? AppTheme.statusYellow
|
||||
: AppTheme.statusRed,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -325,14 +325,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
icon: Icons.face,
|
||||
label: "Registrasi Wajah",
|
||||
onTap: () => Navigator.pushNamed(context, '/onboarding/face-enrollment'),
|
||||
trailingText: context.watch<FaceProvider>().faceStatus2['is_registered'] == true
|
||||
? "Terdaftar"
|
||||
: "Belum Terdaftar",
|
||||
trailingColor: context.watch<FaceProvider>().faceStatus2['is_registered'] == true
|
||||
? AppTheme.statusGreen
|
||||
: AppTheme.statusRed,
|
||||
trailingText: (() {
|
||||
final status = context.watch<FaceProvider>().faceStatus2['status'];
|
||||
if (status == 'verified') return 'Terverifikasi';
|
||||
if (status == 'pending') return 'Menunggu HRD';
|
||||
if (status == 'rejected') return 'Ditolak (Ulangi)';
|
||||
return 'Belum Terdaftar';
|
||||
})(),
|
||||
trailingColor: (() {
|
||||
final status = context.watch<FaceProvider>().faceStatus2['status'];
|
||||
if (status == 'verified') return AppTheme.statusGreen;
|
||||
if (status == 'pending') return AppTheme.statusYellow;
|
||||
if (status == 'rejected') return AppTheme.statusRed;
|
||||
return AppTheme.textSecondary;
|
||||
})(),
|
||||
),
|
||||
if (context.watch<FaceProvider>().faceStatus2['is_registered'] == true)
|
||||
if (context.watch<FaceProvider>().faceStatus2['status'] == 'verified')
|
||||
_buildActionItem(
|
||||
icon: Icons.face_retouching_natural,
|
||||
label: "Test Pengenalan Wajah",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class SignatureScreen extends StatefulWidget {
|
|||
class _SignatureScreenState extends State<SignatureScreen> {
|
||||
late HandSignatureControl _signatureController;
|
||||
bool _isScrollable = true;
|
||||
final GlobalKey _signatureKey = GlobalKey();
|
||||
|
||||
|
||||
@override
|
||||
|
|
@ -57,15 +58,11 @@ class _SignatureScreenState extends State<SignatureScreen> {
|
|||
|
||||
Future<Uint8List?> _captureCanvas() async {
|
||||
try {
|
||||
final byteData = await _signatureController.toImage(
|
||||
width: 1080,
|
||||
height: 720,
|
||||
color: const Color(0xFF1E293B),
|
||||
background: Colors.white,
|
||||
fit: true,
|
||||
border: 20,
|
||||
format: ImageByteFormat.png,
|
||||
);
|
||||
final boundary = _signatureKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||
if (boundary == null) return null;
|
||||
|
||||
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||
final byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
|
||||
if (byteData == null) return null;
|
||||
return byteData.buffer.asUint8List();
|
||||
|
|
@ -279,14 +276,18 @@ class _SignatureScreenState extends State<SignatureScreen> {
|
|||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm - 1),
|
||||
child: RepaintBoundary(
|
||||
child: HandSignature(
|
||||
control: _signatureController,
|
||||
color: const Color(0xFF1E293B),
|
||||
width: 2.5,
|
||||
maxWidth: 5.5,
|
||||
type: SignatureDrawType.shape,
|
||||
onPointerDown: () => setState(() => _isScrollable = false),
|
||||
onPointerUp: () => setState(() => _isScrollable = true),
|
||||
key: _signatureKey,
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: HandSignature(
|
||||
control: _signatureController,
|
||||
color: const Color(0xFF1E293B),
|
||||
width: 2.5,
|
||||
maxWidth: 5.5,
|
||||
type: SignatureDrawType.shape,
|
||||
onPointerDown: () => setState(() => _isScrollable = false),
|
||||
onPointerUp: () => setState(() => _isScrollable = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,10 @@ class FcmService {
|
|||
_messaging.onTokenRefresh.listen((t) => _repository.saveDeviceToken(t));
|
||||
|
||||
FirebaseMessaging.onMessage.listen((msg) {
|
||||
if (context.mounted) {
|
||||
_showBanner(context, msg);
|
||||
context.read<NotificationProvider>().fetchUnreadCount();
|
||||
final currentContext = navigatorKey.currentContext;
|
||||
if (currentContext != null && currentContext.mounted) {
|
||||
_showBanner(currentContext, msg);
|
||||
currentContext.read<NotificationProvider>().fetchUnreadCount();
|
||||
}
|
||||
_showSystemNotification(msg);
|
||||
});
|
||||
|
|
@ -81,10 +82,12 @@ class FcmService {
|
|||
if (tipe == null) return Icons.notifications_outlined;
|
||||
if (tipe.contains('disetujui')) return Icons.check_circle_rounded;
|
||||
if (tipe.contains('ditolak')) return Icons.cancel_rounded;
|
||||
if (tipe.contains('proses')) return Icons.hourglass_top_rounded;
|
||||
if (tipe.contains('pengumuman')) return Icons.campaign_rounded;
|
||||
if (tipe.contains('lembur')) return Icons.schedule_rounded;
|
||||
if (tipe.contains('presensi')) return Icons.fingerprint_rounded;
|
||||
if (tipe.contains('izin')) return Icons.description_rounded;
|
||||
if (tipe.contains('cuti')) return Icons.beach_access_rounded;
|
||||
if (tipe.contains('poin')) return Icons.stars_rounded;
|
||||
return Icons.notifications_outlined;
|
||||
}
|
||||
|
|
@ -96,6 +99,9 @@ class FcmService {
|
|||
if (tipe.contains('pengumuman')) return AppTheme.primaryBlue;
|
||||
if (tipe.contains('lembur')) return const Color(0xFFf59e0b);
|
||||
if (tipe.contains('poin')) return const Color(0xFFe11d48);
|
||||
if (tipe.contains('izin')) return const Color(0xFF8b5cf6);
|
||||
if (tipe.contains('cuti')) return const Color(0xFF06b6d4);
|
||||
if (tipe.contains('presensi')) return const Color(0xFF06b6d4);
|
||||
return const Color(0xFF6366f1);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue