201 lines
6.2 KiB
Dart
201 lines
6.2 KiB
Dart
import 'package:camera/camera.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../domain/entities/quality_status.dart';
|
|
import '../providers/display_config_provider.dart';
|
|
import 'fingerprint_guide_painter.dart';
|
|
|
|
class CameraPreviewWidget extends ConsumerWidget {
|
|
final CameraController? controller;
|
|
final QualityStatus? qualityStatus;
|
|
final int consecutivePasses;
|
|
final bool canCapture;
|
|
|
|
const CameraPreviewWidget({
|
|
super.key,
|
|
required this.controller,
|
|
this.qualityStatus,
|
|
this.consecutivePasses = 0,
|
|
this.canCapture = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final config = ref.watch(displayConfigProvider);
|
|
|
|
// If controller is null or not initialized, show Simulation/Placeholder
|
|
final bool showCamera =
|
|
controller != null && controller!.value.isInitialized;
|
|
|
|
final qs = qualityStatus;
|
|
final isFocusGood = qs?.isFocusGood ?? false;
|
|
final isLightingGood = qs?.isLightingGood ?? false;
|
|
final isPositionGood = qs?.isPositionGood ?? false;
|
|
final overallGood = qs?.canCapture ?? false;
|
|
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
if (showCamera)
|
|
_buildCoveredCameraPreview(context, controller!)
|
|
else
|
|
Container(
|
|
color: Colors.grey.shade900,
|
|
child: const Center(
|
|
child: Text(
|
|
"Camera Unavailable\n(Simulation Mode)",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white54, fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Fingerprint Guide (Painter)
|
|
if (config.showGuide)
|
|
CustomPaint(
|
|
painter: FingerprintGuidePainter(isGoodQuality: overallGood),
|
|
child: Container(),
|
|
),
|
|
|
|
// Icon (Center Guide)
|
|
if (config.showGuide)
|
|
Center(
|
|
child: Icon(
|
|
Icons.fingerprint,
|
|
color: overallGood
|
|
? Colors.greenAccent
|
|
: Colors.white.withValues(alpha: 0.3),
|
|
size: 100,
|
|
),
|
|
),
|
|
|
|
// Quality Indicators (Top)
|
|
if (config.showQualityIndicators)
|
|
Positioned(
|
|
top: 60,
|
|
left: 0,
|
|
right: 0,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildStatusIcon(
|
|
Icons.center_focus_strong,
|
|
"Focus",
|
|
isFocusGood,
|
|
),
|
|
_buildStatusIcon(Icons.wb_sunny, "Light", isLightingGood),
|
|
_buildStatusIcon(Icons.crop_free, "Position", isPositionGood),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Status Text (Bottom)
|
|
if (config.showStatusText)
|
|
Positioned(
|
|
bottom: 120, // Above FAB
|
|
left: 20,
|
|
right: 20,
|
|
child: Column(
|
|
children: [
|
|
if (overallGood && !canCapture)
|
|
Text(
|
|
"Hold Steady... $consecutivePasses/5",
|
|
style: const TextStyle(
|
|
color: Colors.yellowAccent,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
shadows: [Shadow(blurRadius: 4, color: Colors.black)],
|
|
),
|
|
)
|
|
else if (canCapture)
|
|
const Text(
|
|
"READY TO CAPTURE",
|
|
style: TextStyle(
|
|
color: Colors.greenAccent,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
shadows: [Shadow(blurRadius: 4, color: Colors.black)],
|
|
),
|
|
)
|
|
else
|
|
const Text(
|
|
"Adjust Finger",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
shadows: [Shadow(blurRadius: 4, color: Colors.black)],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
if (qs != null && config.showDebugInfo)
|
|
Text(
|
|
"Blur: ${qs.blurScore.toStringAsFixed(0)} | Bright: ${qs.brightness.toStringAsFixed(0)}",
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCoveredCameraPreview(
|
|
BuildContext context,
|
|
CameraController controller,
|
|
) {
|
|
final size = MediaQuery.of(context).size;
|
|
|
|
double scale = 1.0;
|
|
final double screenRatio = size.aspectRatio;
|
|
final double cameraRatio = controller.value.aspectRatio;
|
|
|
|
// Calculate effective camera ratio displayed (swapped if portrait)
|
|
// We assume if screen is portrait, camera preview is also rendered in portrait mode (1/ratio)
|
|
// by the CameraPreview widget.
|
|
final double effectiveCameraRatio = (size.width < size.height)
|
|
? (1 / cameraRatio)
|
|
: cameraRatio;
|
|
|
|
if (screenRatio > effectiveCameraRatio) {
|
|
// Screen is wider than camera preview
|
|
scale = screenRatio / effectiveCameraRatio;
|
|
} else {
|
|
// Screen is taller than camera preview
|
|
scale = effectiveCameraRatio / screenRatio;
|
|
}
|
|
|
|
return Transform.scale(
|
|
scale: scale,
|
|
alignment: Alignment.center,
|
|
child: Center(child: CameraPreview(controller)),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusIcon(IconData icon, String label, bool isGood) {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: isGood
|
|
? Colors.green.withValues(alpha: 0.8)
|
|
: Colors.red.withValues(alpha: 0.8),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: Colors.white, size: 24),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
shadows: [Shadow(blurRadius: 2, color: Colors.black)],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|