582 lines
21 KiB
Dart
582 lines
21 KiB
Dart
import 'package:camera/camera.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import '../../core/error/exceptions.dart';
|
|
|
|
class OvalCameraCapturePage extends StatefulWidget {
|
|
final bool isLogin;
|
|
final Future<void> Function(File image) onCapture;
|
|
|
|
const OvalCameraCapturePage({
|
|
super.key,
|
|
this.isLogin = true,
|
|
required this.onCapture,
|
|
});
|
|
|
|
@override
|
|
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
|
}
|
|
|
|
class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
|
CameraController? _cameraController;
|
|
bool _isCameraInitialized = false;
|
|
String? _errorMessage;
|
|
bool _isSuccess = false;
|
|
bool _isLoading = false;
|
|
Timer? _timer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializeCamera();
|
|
}
|
|
|
|
Future<void> _initializeCamera() async {
|
|
try {
|
|
setState(() {
|
|
_errorMessage = null;
|
|
_isCameraInitialized = false;
|
|
});
|
|
|
|
// Dispose existing controller if any
|
|
await _cameraController?.dispose();
|
|
_cameraController = null;
|
|
|
|
// Get available cameras
|
|
final cameras = await availableCameras();
|
|
|
|
// Check if cameras list is available
|
|
if (cameras.isEmpty) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
|
_isCameraInitialized = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Try to find front camera, fallback to first available camera
|
|
CameraDescription? selectedCamera;
|
|
try {
|
|
selectedCamera = cameras.firstWhere(
|
|
(cam) => cam.lensDirection == CameraLensDirection.front,
|
|
);
|
|
} catch (e) {
|
|
// If no front camera found, use the first available camera
|
|
if (cameras.isNotEmpty) {
|
|
selectedCamera = cameras.first;
|
|
} else {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
|
_isCameraInitialized = false;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
_cameraController = CameraController(
|
|
selectedCamera,
|
|
ResolutionPreset.medium,
|
|
enableAudio: false,
|
|
imageFormatGroup: ImageFormatGroup.jpeg,
|
|
);
|
|
|
|
await _cameraController!.initialize();
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
_isCameraInitialized = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
_startScan();
|
|
} on CameraException catch (e) {
|
|
if (!mounted) return;
|
|
String errorMessage;
|
|
switch (e.code) {
|
|
case 'CameraAccessDenied':
|
|
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
|
break;
|
|
case 'CameraAccessDeniedWithoutPrompt':
|
|
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
|
break;
|
|
case 'CameraAccessRestricted':
|
|
errorMessage = "الوصول إلى الكاميرا مقيد";
|
|
break;
|
|
case 'AudioAccessDenied':
|
|
errorMessage = "تم رفض الوصول إلى الميكروفون";
|
|
break;
|
|
default:
|
|
errorMessage = "خطأ في تهيئة الكاميرا: ${e.description ?? 'خطأ غير معروف'}";
|
|
}
|
|
setState(() {
|
|
_errorMessage = errorMessage;
|
|
_isCameraInitialized = false;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_errorMessage = "حدث خطأ غير متوقع أثناء تهيئة الكاميرا";
|
|
_isCameraInitialized = false;
|
|
});
|
|
print("Error initializing camera: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _startScan() async {
|
|
try {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
// Simulate scanning delay
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
if (!mounted ||
|
|
_cameraController == null ||
|
|
!_cameraController!.value.isInitialized) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = "الكاميرا غير مهيأة. يرجى المحاولة مرة أخرى";
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
final xFile = await _cameraController!.takePicture();
|
|
final file = File(xFile.path);
|
|
|
|
// Check if file exists and is readable
|
|
if (!await file.exists()) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = "فشل حفظ الصورة. يرجى المحاولة مرة أخرى";
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Call the onCapture callback which may throw exceptions
|
|
await widget.onCapture(file);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSuccess = true;
|
|
_isLoading = false;
|
|
});
|
|
|
|
// Auto-close after 2 seconds
|
|
Future.delayed(const Duration(seconds: 2), () {
|
|
if (mounted) {
|
|
Navigator.of(context).pop(true);
|
|
}
|
|
});
|
|
}
|
|
} on ServerException catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = e.message;
|
|
});
|
|
}
|
|
} on NetworkException catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = e.message;
|
|
});
|
|
}
|
|
} on CameraException catch (e) {
|
|
if (mounted) {
|
|
String errorMessage;
|
|
switch (e.code) {
|
|
case 'captureAlreadyActive':
|
|
errorMessage = "جاري التقاط صورة بالفعل. يرجى الانتظار";
|
|
break;
|
|
case 'pictureTakingInProgress':
|
|
errorMessage = "جاري التقاط صورة. يرجى الانتظار";
|
|
break;
|
|
default:
|
|
errorMessage = "فشل التقاط الصورة: ${e.description ?? 'خطأ غير معروف'}";
|
|
}
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = errorMessage;
|
|
});
|
|
}
|
|
} on FileSystemException catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = "فشل حفظ الصورة. يرجى التحقق من مساحة التخزين";
|
|
});
|
|
}
|
|
print("File system error: $e");
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى";
|
|
});
|
|
}
|
|
print("Unexpected error in _startScan: $e");
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_cameraController?.dispose();
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Color(0xff000000),
|
|
|
|
body:
|
|
// Show error screen only if camera failed to initialize
|
|
_errorMessage != null && !_isCameraInitialized
|
|
? Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.camera_alt_outlined,
|
|
size: 64,
|
|
color: Colors.white70,
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
_errorMessage!,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 24),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: _initializeCamera,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Color(0xffE8001A),
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
child: Text("إعادة المحاولة"),
|
|
),
|
|
SizedBox(width: 16),
|
|
OutlinedButton(
|
|
onPressed: () {
|
|
if (mounted) {
|
|
Navigator.of(context).pop(false);
|
|
}
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
side: BorderSide(color: Colors.white70),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
child: Text("إلغاء"),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: _isCameraInitialized && _cameraController != null
|
|
? Stack(
|
|
children: [
|
|
SizedBox(height: MediaQuery.of(context).size.height),
|
|
|
|
// Camera Preview
|
|
Positioned(
|
|
child: Center(child: CameraPreview(_cameraController!)),
|
|
),
|
|
// Oval overlay with dimmed background
|
|
Positioned.fill(
|
|
child: CustomPaint(painter: _OvalOverlayPainter()),
|
|
),
|
|
|
|
// Top Text
|
|
Positioned(
|
|
top: 100,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Text(
|
|
widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
fontFamily:
|
|
'Cairo', // Assuming Cairo font based on Arabic text
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Bottom Text and Logo
|
|
Positioned(
|
|
bottom: 80,
|
|
left: 0,
|
|
right: 0,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_isSuccess
|
|
? (widget.isLogin
|
|
? "تم تسجيل دخولك بنجاح"
|
|
: "تم تسجيل خروجك بنجاح")
|
|
: _isLoading
|
|
? (widget.isLogin
|
|
? "يتم تسجيل الدخول ..."
|
|
: "يتم تسجيل الخروج ...")
|
|
: (widget.isLogin
|
|
? "جاهز للتقاط الصورة"
|
|
: "جاهز للتقاط الصورة"),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
// Logo
|
|
SizedBox(
|
|
height: _isSuccess ? 80 : 60,
|
|
width: _isSuccess ? 80 : 60,
|
|
child: SvgPicture.asset(
|
|
_isSuccess
|
|
? 'assets/images/logSuccess.svg'
|
|
: 'assets/images/logLoading.svg',
|
|
// ignore: deprecated_member_use
|
|
color: Colors.white,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Error overlay (shown when error occurs during scanning)
|
|
if (_errorMessage != null && _isCameraInitialized)
|
|
Positioned.fill(
|
|
child: Container(
|
|
color: Colors.black.withOpacity(0.7),
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline,
|
|
size: 64,
|
|
color: Colors.red,
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
_errorMessage!,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 24),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_errorMessage = null;
|
|
_isLoading = false;
|
|
});
|
|
_startScan();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Color(0xffE8001A),
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
child: Text("إعادة المحاولة"),
|
|
),
|
|
SizedBox(width: 16),
|
|
OutlinedButton(
|
|
onPressed: () {
|
|
if (mounted) {
|
|
Navigator.of(context).pop(false);
|
|
}
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
side: BorderSide(color: Colors.white70),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
child: Text("إلغاء"),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// // Capture button
|
|
// Positioned(
|
|
// bottom: 60,
|
|
// left: 0,
|
|
// right: 0,
|
|
// child: Center(
|
|
// child: GestureDetector(
|
|
// onTap: (){},
|
|
// child: Container(
|
|
// width: 72,
|
|
// height: 72,
|
|
// decoration: BoxDecoration(
|
|
// shape: BoxShape.circle,
|
|
// color: Colors.white,
|
|
// boxShadow: [
|
|
// BoxShadow(
|
|
// color: Colors.black26,
|
|
// blurRadius: 8,
|
|
// offset: Offset(0, 4),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// child: Icon(Icons.camera_alt, color: Color(0xffE8001A), size: 36),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
],
|
|
)
|
|
: Center(
|
|
child: CircularProgressIndicator(color: Color(0xffE8001A)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OvalOverlayPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final width = size.width * 0.75;
|
|
final height = size.height * 0.4;
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final ovalRect = Rect.fromCenter(
|
|
center: center,
|
|
width: width,
|
|
height: height,
|
|
);
|
|
|
|
// Create a path for the whole screen
|
|
final screenPath =
|
|
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
|
// Create a path for the oval
|
|
final ovalPath = Path()..addOval(ovalRect);
|
|
// Subtract the oval from the screen path
|
|
final overlayPath = Path.combine(
|
|
PathOperation.difference,
|
|
screenPath,
|
|
ovalPath,
|
|
);
|
|
|
|
// Draw the dimmed area outside the oval with gradient
|
|
final gradient = LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Color.fromARGB(255, 41, 41, 41), // top dark gray
|
|
Color.fromARGB(255, 0, 20, 15), // bottom deep green
|
|
],
|
|
);
|
|
final paint =
|
|
Paint()
|
|
..shader = gradient.createShader(
|
|
Rect.fromLTWH(0, 0, size.width, size.height),
|
|
)
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawPath(overlayPath, paint);
|
|
|
|
// Draw glowing circles effect (like AppBackground) - drawn after overlay
|
|
// Top circle - positioned similar to AppBackground (top: -250, left: 100, right: -200)
|
|
final topCircleCenter = Offset(size.width * 0.3, -250);
|
|
final topCircleRadius = 150.0;
|
|
// Draw multiple circles with different opacities for spread effect (spreadRadius: 160)
|
|
for (int i = 0; i < 5; i++) {
|
|
final spreadPaint =
|
|
Paint()
|
|
..color = Color.fromARGB(69 ~/ (i + 1), 62, 254, 190)
|
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, 140 - (i * 20));
|
|
canvas.drawCircle(
|
|
topCircleCenter,
|
|
topCircleRadius + (i * 30),
|
|
spreadPaint,
|
|
);
|
|
}
|
|
|
|
// Bottom circle - positioned similar to AppBackground (bottom: 100, left: -140, right: -120)
|
|
final bottomCircleCenter = Offset(size.width * 0.2, size.height + 100);
|
|
final bottomCircleRadius = 160.0;
|
|
// Draw multiple circles with different opacities for spread effect (spreadRadius: 60)
|
|
for (int i = 0; i < 5; i++) {
|
|
final spreadPaint =
|
|
Paint()
|
|
..color = Color.fromARGB(83 ~/ (i + 1), 62, 254, 190)
|
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, 180 - (i * 25));
|
|
canvas.drawCircle(
|
|
bottomCircleCenter,
|
|
bottomCircleRadius + (i * 40),
|
|
spreadPaint,
|
|
);
|
|
}
|
|
|
|
// Draw oval border
|
|
final borderPaint =
|
|
Paint()
|
|
..color = Colors.greenAccent
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 4;
|
|
canvas.drawOval(ovalRect, borderPaint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|