Team Color, image compression for attendance verification, improved wherebouts
This commit is contained in:
@@ -31,6 +31,10 @@ class AppScaffold extends ConsumerWidget {
|
||||
},
|
||||
orElse: () => 'User',
|
||||
);
|
||||
final avatarUrl = profileAsync.maybeWhen(
|
||||
data: (profile) => profile?.avatarUrl,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
final isStandard = role == 'standard';
|
||||
final location = GoRouterState.of(context).uri.toString();
|
||||
@@ -75,7 +79,11 @@ class AppScaffold extends ConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ProfileAvatar(fullName: displayName, radius: 16),
|
||||
ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(displayName),
|
||||
const SizedBox(width: 4),
|
||||
@@ -91,7 +99,11 @@ class AppScaffold extends ConsumerWidget {
|
||||
IconButton(
|
||||
tooltip: 'Profile',
|
||||
onPressed: () => context.go('/profile'),
|
||||
icon: ProfileAvatar(fullName: displayName, radius: 16),
|
||||
icon: ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Sign out',
|
||||
|
||||
@@ -8,11 +8,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/attendance_provider.dart';
|
||||
import '../providers/profile_provider.dart';
|
||||
import '../services/face_verification.dart' as face;
|
||||
import '../services/image_compress_service.dart';
|
||||
import '../theme/m3_motion.dart';
|
||||
import '../widgets/qr_verification_dialog.dart';
|
||||
|
||||
/// Phases of the full-screen face verification overlay.
|
||||
enum _Phase { liveness, downloading, matching, success, failed, cancelled }
|
||||
enum _Phase {
|
||||
liveness,
|
||||
downloading,
|
||||
matching,
|
||||
saving,
|
||||
success,
|
||||
failed,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
/// Result returned from the overlay.
|
||||
class FaceVerificationResult {
|
||||
@@ -170,14 +179,19 @@ class _FaceVerificationOverlayState
|
||||
final score = await face.compareFaces(result.imageBytes, enrolledBytes);
|
||||
|
||||
if (score >= 0.60) {
|
||||
// Success!
|
||||
// Success! Transition to saving phase for compress + upload.
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.saving;
|
||||
_statusText = 'Compressing & saving photo...';
|
||||
});
|
||||
await _compressAndUpload(result.imageBytes, 'verified');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.success;
|
||||
_statusText =
|
||||
'Face verified!\n${(score * 100).toStringAsFixed(0)}% match';
|
||||
});
|
||||
await _uploadResult(result.imageBytes, 'verified');
|
||||
await Future.delayed(const Duration(milliseconds: 1200));
|
||||
if (mounted) {
|
||||
Navigator.of(
|
||||
@@ -201,13 +215,18 @@ class _FaceVerificationOverlayState
|
||||
} else {
|
||||
// All attempts exhausted
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.saving;
|
||||
_statusText = 'Compressing & saving photo...';
|
||||
});
|
||||
await _compressAndUpload(result.imageBytes, 'unverified');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.failed;
|
||||
_statusText =
|
||||
'Face did not match after ${widget.maxAttempts} attempts\n'
|
||||
'${(score * 100).toStringAsFixed(0)}% similarity';
|
||||
});
|
||||
await _uploadResult(result.imageBytes, 'unverified');
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
if (mounted) {
|
||||
Navigator.of(
|
||||
@@ -218,16 +237,17 @@ class _FaceVerificationOverlayState
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _uploadResult(Uint8List bytes, String status) async {
|
||||
Future<void> _compressAndUpload(Uint8List bytes, String status) async {
|
||||
if (!widget.uploadAttendanceResult || widget.attendanceLogId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final compressed = await ImageCompressService.compress(bytes);
|
||||
await ref
|
||||
.read(attendanceControllerProvider)
|
||||
.uploadVerification(
|
||||
attendanceId: widget.attendanceLogId!,
|
||||
bytes: bytes,
|
||||
bytes: compressed,
|
||||
fileName: 'verification.jpg',
|
||||
status: status,
|
||||
);
|
||||
@@ -280,6 +300,8 @@ class _FaceVerificationOverlayState
|
||||
? Colors.green
|
||||
: _phase == _Phase.failed
|
||||
? colors.error
|
||||
: _phase == _Phase.saving
|
||||
? colors.tertiary
|
||||
: colors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -304,6 +326,7 @@ class _FaceVerificationOverlayState
|
||||
_phase == _Phase.liveness ||
|
||||
_phase == _Phase.downloading ||
|
||||
_phase == _Phase.matching;
|
||||
final isSaving = _phase == _Phase.saving;
|
||||
final isSuccess = _phase == _Phase.success;
|
||||
final isFailed = _phase == _Phase.failed;
|
||||
|
||||
@@ -312,6 +335,8 @@ class _FaceVerificationOverlayState
|
||||
ringColor = Colors.green;
|
||||
} else if (isFailed) {
|
||||
ringColor = colors.error;
|
||||
} else if (isSaving) {
|
||||
ringColor = colors.tertiary;
|
||||
} else {
|
||||
ringColor = colors.primary;
|
||||
}
|
||||
@@ -324,7 +349,9 @@ class _FaceVerificationOverlayState
|
||||
children: [
|
||||
// Pulsing ring
|
||||
ScaleTransition(
|
||||
scale: isActive ? _pulseAnim : const AlwaysStoppedAnimation(1.0),
|
||||
scale: isActive || isSaving
|
||||
? _pulseAnim
|
||||
: const AlwaysStoppedAnimation(1.0),
|
||||
child: Container(
|
||||
width: 180,
|
||||
height: 180,
|
||||
@@ -355,6 +382,8 @@ class _FaceVerificationOverlayState
|
||||
? Icons.check_circle_rounded
|
||||
: isFailed
|
||||
? Icons.error_rounded
|
||||
: isSaving
|
||||
? Icons.cloud_upload_rounded
|
||||
: Icons.face_rounded,
|
||||
key: ValueKey(_phase),
|
||||
size: 64,
|
||||
@@ -400,6 +429,14 @@ class _FaceVerificationOverlayState
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
);
|
||||
}
|
||||
if (_phase == _Phase.saving) {
|
||||
return LinearProgressIndicator(
|
||||
value: null,
|
||||
backgroundColor: colors.tertiary.withValues(alpha: 0.12),
|
||||
color: colors.tertiary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
);
|
||||
}
|
||||
return LinearProgressIndicator(
|
||||
value: null,
|
||||
backgroundColor: colors.primary.withValues(alpha: 0.12),
|
||||
|
||||
Reference in New Issue
Block a user