Compare commits
2 Commits
ace2ad0a32
...
bb6f3931ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6f3931ce | ||
|
|
2022245810 |
@@ -10,12 +10,9 @@ class ApiClient {
|
|||||||
ApiClient({required this.dio, this.sharedPreferences}) {
|
ApiClient({required this.dio, this.sharedPreferences}) {
|
||||||
dio.options = BaseOptions(
|
dio.options = BaseOptions(
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
connectTimeout: const Duration(seconds: 30),
|
connectTimeout: const Duration(seconds: 60),
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
headers: {
|
headers: {'Content-Type': 'application/json', 'accept': 'text/plain'},
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'accept': 'text/plain',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add interceptor to add token to requests
|
// Add interceptor to add token to requests
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ abstract class AttendanceRemoteDataSource {
|
|||||||
Future<List<OvertimeDto>> getExtraHours({required String employeeId});
|
Future<List<OvertimeDto>> getExtraHours({required String employeeId});
|
||||||
Future<List<RewardDto>> getRewards({required String employeeId});
|
Future<List<RewardDto>> getRewards({required String employeeId});
|
||||||
Future<List<PunishmentDto>> getPunishments({required String employeeId});
|
Future<List<PunishmentDto>> getPunishments({required String employeeId});
|
||||||
|
Future<AttendanceRecordDto?> getLastRecord({required String employeeId});
|
||||||
|
Future<bool> hasActiveLogin({required String employeeId});
|
||||||
Future<SalaryResponseDto> calculateSalary({
|
Future<SalaryResponseDto> calculateSalary({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required int month,
|
required int month,
|
||||||
@@ -171,12 +173,10 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
if (data is Map<String, dynamic> &&
|
if (data is Map<String, dynamic>) {
|
||||||
data['data'] != null &&
|
final items = (data['data'] ?? data['Data'])?['items'] as List? ?? [];
|
||||||
data['data']['items'] is List) {
|
|
||||||
final items = data['data']['items'] as List;
|
|
||||||
return items.map((e) => AttendanceRecordDto.fromJson(e)).toList();
|
return items.map((e) => AttendanceRecordDto.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -187,15 +187,82 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw ServerException(
|
_handleDioError(e, 'فشل في جلب البيانات');
|
||||||
message: e.message ?? 'Unknown error',
|
rethrow; // Should not reach here due to _handleDioError throwing
|
||||||
statusCode: e.response?.statusCode,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
throw ServerException(message: 'خطأ غير متوقع');
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AttendanceRecordDto?> getLastRecord({
|
||||||
|
required String employeeId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await apiClient.get(
|
||||||
|
'/Attendance',
|
||||||
|
queryParameters: {
|
||||||
|
'IsDeleted': false,
|
||||||
|
'EmployeeId': employeeId,
|
||||||
|
'PageNumber': 1,
|
||||||
|
'PageSize': 1,
|
||||||
|
'SortBy': 'CreatedAt',
|
||||||
|
'SortDescending': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
final data = response.data;
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
final items = (data['data'] ?? data['Data'])?['items'] as List? ?? [];
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
return AttendanceRecordDto.fromJson(items.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw ServerException(
|
||||||
|
message: 'فشل في جلب آخر سجل',
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
_handleDioError(e, 'فشل في جلب آخر سجل');
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> hasActiveLogin({required String employeeId}) async {
|
||||||
|
try {
|
||||||
|
final last = await getLastRecord(employeeId: employeeId);
|
||||||
|
if (last == null) return false;
|
||||||
|
|
||||||
|
// The API enforces a DAILY check ("Employee already logged in today")
|
||||||
|
// So we check: has a login TODAY, regardless of logout status
|
||||||
|
if (last.login != null) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final loginDate = last.login!;
|
||||||
|
if (loginDate.year == now.year &&
|
||||||
|
loginDate.month == now.month &&
|
||||||
|
loginDate.day == now.day) {
|
||||||
|
// Logged in today — if no logout, definitely active
|
||||||
|
// If logout exists, they already completed today's cycle
|
||||||
|
return last.logout == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
// If the check fails, let the user try — the API will reject if needed
|
||||||
|
print('hasActiveLogin check failed: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OvertimeDto>> getExtraHours({required String employeeId}) async {
|
Future<List<OvertimeDto>> getExtraHours({required String employeeId}) async {
|
||||||
try {
|
try {
|
||||||
@@ -204,7 +271,7 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = response.data;
|
final responseData = response.data;
|
||||||
|
|
||||||
if (responseData is Map<String, dynamic>) {
|
if (responseData is Map<String, dynamic>) {
|
||||||
@@ -222,11 +289,10 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw ServerException(
|
_handleDioError(e, 'فشل في جلب البيانات');
|
||||||
message: e.message ?? 'Unknown error',
|
rethrow;
|
||||||
statusCode: e.response?.statusCode,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
throw ServerException(message: 'خطأ غير متوقع');
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +305,7 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = response.data;
|
final responseData = response.data;
|
||||||
if (responseData is Map<String, dynamic>) {
|
if (responseData is Map<String, dynamic>) {
|
||||||
return RewardListResponseDto.fromJson(responseData).items;
|
return RewardListResponseDto.fromJson(responseData).items;
|
||||||
@@ -252,11 +318,10 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw ServerException(
|
_handleDioError(e, 'فشل في جلب المكافآت');
|
||||||
message: e.message ?? 'Unknown error',
|
rethrow;
|
||||||
statusCode: e.response?.statusCode,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
throw ServerException(message: 'خطأ غير متوقع');
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +336,7 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
queryParameters: {'IsDeleted': false, 'EmployeeId': employeeId},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = response.data;
|
final responseData = response.data;
|
||||||
if (responseData is Map<String, dynamic>) {
|
if (responseData is Map<String, dynamic>) {
|
||||||
return PunishmentListResponseDto.fromJson(responseData).items;
|
return PunishmentListResponseDto.fromJson(responseData).items;
|
||||||
@@ -284,11 +349,10 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw ServerException(
|
_handleDioError(e, 'فشل في جلب البيانات');
|
||||||
message: e.message ?? 'Unknown error',
|
rethrow;
|
||||||
statusCode: e.response?.statusCode,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
throw ServerException(message: 'خطأ غير متوقع');
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,17 +373,12 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
print('Salary Response Status: ${response.statusCode}');
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = response.data;
|
final responseData = response.data;
|
||||||
print(
|
|
||||||
'Salary Response Data: $responseData (${responseData.runtimeType})',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (responseData is Map<String, dynamic>) {
|
if (responseData is Map<String, dynamic>) {
|
||||||
return SalaryResponseDto.fromJson(responseData);
|
return SalaryResponseDto.fromJson(responseData);
|
||||||
} else if (responseData is num) {
|
} else if (responseData is num) {
|
||||||
// Handle case where API returns raw number
|
|
||||||
return SalaryResponseDto(
|
return SalaryResponseDto(
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
@@ -327,7 +386,6 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
} else if (responseData is String &&
|
} else if (responseData is String &&
|
||||||
double.tryParse(responseData) != null) {
|
double.tryParse(responseData) != null) {
|
||||||
// Handle case where API returns raw numeric string
|
|
||||||
return SalaryResponseDto(
|
return SalaryResponseDto(
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
@@ -335,23 +393,49 @@ class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw ServerException(
|
throw ServerException(
|
||||||
message: 'استجابة غير صحيحة من الخادم: $responseData',
|
message: 'استجابة غير صحيحة من الخادم',
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw ServerException(
|
throw ServerException(
|
||||||
message: 'فشل في حساب الراتب (Status: ${response.statusCode})',
|
message: 'فشل في حساب الراتب',
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw ServerException(
|
_handleDioError(e, 'فشل في حساب الراتب');
|
||||||
message: e.message ?? 'Unknown error',
|
rethrow;
|
||||||
statusCode: e.response?.statusCode,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is ServerException || e is NetworkException) rethrow;
|
||||||
throw ServerException(message: 'خطأ غير متوقع');
|
throw ServerException(message: 'خطأ غير متوقع');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleDioError(DioException e, String defaultMessage) {
|
||||||
|
if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.receiveTimeout) {
|
||||||
|
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
|
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||||
|
} else if (e.response?.statusCode == 500) {
|
||||||
|
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||||
|
} else if (e.response != null) {
|
||||||
|
final data = e.response?.data;
|
||||||
|
String? message;
|
||||||
|
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
message = data['message']?.toString() ?? data['error']?.toString();
|
||||||
|
} else if (data is String) {
|
||||||
|
message = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ServerException(
|
||||||
|
message: message ?? defaultMessage,
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,34 @@ class AttendanceRepositoryImpl implements AttendanceRepository {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AttendanceModel?> getLastRecord({required String employeeId}) async {
|
||||||
|
final dto = await remoteDataSource.getLastRecord(employeeId: employeeId);
|
||||||
|
if (dto == null) return null;
|
||||||
|
|
||||||
|
int? hours;
|
||||||
|
if (dto.login != null && dto.logout != null) {
|
||||||
|
hours = dto.logout!.difference(dto.login!).inHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttendanceModel(
|
||||||
|
id: dto.id,
|
||||||
|
employeeId: dto.employeeId,
|
||||||
|
date: dto.createdAt ?? dto.login,
|
||||||
|
loginTime: dto.login,
|
||||||
|
logoutTime: dto.logout,
|
||||||
|
workHours: hours,
|
||||||
|
createdAt: dto.createdAt ?? dto.login,
|
||||||
|
reason: dto.reason,
|
||||||
|
isDeleted: dto.isDeleted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> hasActiveLogin({required String employeeId}) async {
|
||||||
|
return remoteDataSource.hasActiveLogin(employeeId: employeeId);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SalaryModel> calculateSalary({
|
Future<SalaryModel> calculateSalary({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ abstract class AttendanceRepository {
|
|||||||
Future<List<OvertimeModel>> getExtraHours({required String employeeId});
|
Future<List<OvertimeModel>> getExtraHours({required String employeeId});
|
||||||
Future<List<ExtraPaymentModel>> getRewards({required String employeeId});
|
Future<List<ExtraPaymentModel>> getRewards({required String employeeId});
|
||||||
Future<List<ExtraPaymentModel>> getPunishments({required String employeeId});
|
Future<List<ExtraPaymentModel>> getPunishments({required String employeeId});
|
||||||
|
Future<AttendanceModel?> getLastRecord({required String employeeId});
|
||||||
|
Future<bool> hasActiveLogin({required String employeeId});
|
||||||
Future<SalaryModel> calculateSalary({
|
Future<SalaryModel> calculateSalary({
|
||||||
required String employeeId,
|
required String employeeId,
|
||||||
required int month,
|
required int month,
|
||||||
|
|||||||
28
lib/presentation/face/face_feedback.dart
Normal file
28
lib/presentation/face/face_feedback.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum FaceHintType {
|
||||||
|
noFace,
|
||||||
|
tooDark,
|
||||||
|
tooClose,
|
||||||
|
tooFar,
|
||||||
|
notCentered,
|
||||||
|
lookStraight,
|
||||||
|
holdStill,
|
||||||
|
good,
|
||||||
|
}
|
||||||
|
|
||||||
|
class FaceFeedback {
|
||||||
|
final FaceHintType type;
|
||||||
|
final String message;
|
||||||
|
final double quality; // 0..1 (used for progress ring)
|
||||||
|
final Color borderColor;
|
||||||
|
|
||||||
|
FaceFeedback({
|
||||||
|
required this.type,
|
||||||
|
required this.message,
|
||||||
|
required this.quality,
|
||||||
|
required this.borderColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isGood => type == FaceHintType.good;
|
||||||
|
}
|
||||||
@@ -1,4 +1,393 @@
|
|||||||
import 'package:coda_project/presentation/screens/face_screen.dart';
|
// import 'package:coda_project/presentation/screens/face_screen2.dart';
|
||||||
|
// import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
|
// import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
// import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
// import '../widgets/settings_bar.dart';
|
||||||
|
// import '../../core/di/injection_container.dart';
|
||||||
|
// import '../../domain/models/attendance_login_request.dart';
|
||||||
|
// import '../../domain/models/attendance_logout_request.dart';
|
||||||
|
// import '../../domain/usecases/attendance_login_usecase.dart';
|
||||||
|
// import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||||
|
// import '../../data/datasources/user_local_data_source.dart';
|
||||||
|
// import '../../data/datasources/attendance_remote_data_source.dart';
|
||||||
|
|
||||||
|
// class AttendanceScreen extends StatelessWidget {
|
||||||
|
// const AttendanceScreen({super.key});
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// final screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
|
// final screenHeight = MediaQuery.sizeOf(context).height;
|
||||||
|
// return Directionality(
|
||||||
|
// textDirection: TextDirection.ltr,
|
||||||
|
// child: Stack(
|
||||||
|
// children: [
|
||||||
|
// SizedBox(height: MediaQuery.of(context).size.height),
|
||||||
|
|
||||||
|
// /// ------------------------------
|
||||||
|
// /// SETTINGS BAR (STATIC)
|
||||||
|
// /// ------------------------------
|
||||||
|
// SafeArea(
|
||||||
|
// child: SettingsBar(
|
||||||
|
// selectedIndex: 0,
|
||||||
|
// showBackButton: false,
|
||||||
|
// iconPaths: ['assets/images/user.svg', 'assets/images/ball.svg'],
|
||||||
|
// onTap: (index) {
|
||||||
|
// if (index == 0) {
|
||||||
|
// Navigator.push(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => UserSettingsScreen(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// } else if (index == 1) {
|
||||||
|
// Navigator.push(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => NotificationsScreen(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// /// ------------------------------
|
||||||
|
// /// GREETING TEXT
|
||||||
|
// /// ------------------------------
|
||||||
|
// Positioned(
|
||||||
|
// top:
|
||||||
|
// screenHeight *
|
||||||
|
// 0.14, // moved down because settings bar now exists
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// child: Center(
|
||||||
|
// child: Text(
|
||||||
|
// "صباح الخير, محمد",
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 24,
|
||||||
|
// fontWeight: FontWeight.w600,
|
||||||
|
// color: Colors.white,
|
||||||
|
// shadows: [Shadow(color: Color(0x42000000), blurRadius: 6)],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// /// ------------------------------
|
||||||
|
// /// MAIN CARD AREA
|
||||||
|
// /// ------------------------------
|
||||||
|
// Positioned(
|
||||||
|
// top:
|
||||||
|
// screenHeight *
|
||||||
|
// 0.2, // pushed down because of settings bar + greeting
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// child: Center(
|
||||||
|
// child: Padding(
|
||||||
|
// padding: EdgeInsets.symmetric(vertical: screenHeight * 0.05),
|
||||||
|
// child: Stack(
|
||||||
|
// children: [
|
||||||
|
// Container(
|
||||||
|
// height: screenHeight * 0.5,
|
||||||
|
// width: screenWidth * 0.7,
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// borderRadius: BorderRadius.circular(32),
|
||||||
|
// boxShadow: [
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0x1F2B2B2B),
|
||||||
|
// blurRadius: 5,
|
||||||
|
// offset: Offset(10, -10),
|
||||||
|
// ),
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0xABCECECE),
|
||||||
|
// blurRadius: 5,
|
||||||
|
// offset: Offset(-2, 5),
|
||||||
|
// ),
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color.fromARGB(148, 2, 70, 35),
|
||||||
|
// blurRadius: 80,
|
||||||
|
// offset: Offset(0, 10),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Container(
|
||||||
|
// height: screenHeight * 0.5,
|
||||||
|
// width: screenWidth * 0.7,
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: Color(0x92757575),
|
||||||
|
// borderRadius: BorderRadius.circular(32),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// /// ------------------------------
|
||||||
|
// /// LOGIN BUTTON
|
||||||
|
// /// ------------------------------
|
||||||
|
// Positioned(
|
||||||
|
// top: screenHeight * 0.21,
|
||||||
|
// left: screenWidth * 0.05,
|
||||||
|
// child: _ShadowedCard(
|
||||||
|
// shadow: [
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0x62000000),
|
||||||
|
// blurRadius: 10,
|
||||||
|
// spreadRadius: 5,
|
||||||
|
// offset: Offset(5, 5),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// child: _FingerButton(
|
||||||
|
// icon: "assets/images/faceLogin.svg",
|
||||||
|
// label: "تسجيل الدخول",
|
||||||
|
// onTap: () async {
|
||||||
|
// final employeeId =
|
||||||
|
// await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
|
// print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
|
||||||
|
// if (employeeId == null) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ------------------------------
|
||||||
|
// // ACTIVE SESSION CHECK (LOGIN)
|
||||||
|
// // ------------------------------
|
||||||
|
// try {
|
||||||
|
// // Optional: Show a loading dialog if it takes too long
|
||||||
|
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
// .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
// if (hasActive) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(content: Text('أنت مسجل دخول بالفعل')),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (context.mounted) {
|
||||||
|
// Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder:
|
||||||
|
// (_) => OvalCameraCapturePage(
|
||||||
|
// isLogin: true,
|
||||||
|
// onCapture: (imageFile) async {
|
||||||
|
// final loginUseCase =
|
||||||
|
// sl<AttendanceLoginUsecase>();
|
||||||
|
// await loginUseCase(
|
||||||
|
// AttendanceLoginRequest(
|
||||||
|
// employeeId: employeeId,
|
||||||
|
// faceImage: imageFile,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// checkIfLoggedIn: () {},
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// /// ------------------------------
|
||||||
|
// /// LOGOUT BUTTON
|
||||||
|
// /// ------------------------------
|
||||||
|
// Positioned(
|
||||||
|
// bottom: screenHeight * 0.2,
|
||||||
|
// right: screenWidth * 0.1,
|
||||||
|
// child: _ShadowedCard(
|
||||||
|
// shadow: [
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0xABCECECE),
|
||||||
|
// blurRadius: 5,
|
||||||
|
// spreadRadius: 3,
|
||||||
|
// offset: Offset(-6, -6),
|
||||||
|
// ),
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0x92014221),
|
||||||
|
// blurRadius: 10,
|
||||||
|
// offset: Offset(-5, -5),
|
||||||
|
// ),
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Color(0x7D1A1A1A),
|
||||||
|
// blurRadius: 10,
|
||||||
|
// spreadRadius: 3,
|
||||||
|
// offset: Offset(5, 5),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// child: _FingerButton(
|
||||||
|
// icon: "assets/images/faceLogout.svg",
|
||||||
|
// label: "تسجيل خروج",
|
||||||
|
// onTap: () async {
|
||||||
|
// final employeeId =
|
||||||
|
// await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||||
|
// if (employeeId == null) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ------------------------------
|
||||||
|
// // ACTIVE SESSION CHECK (LOGOUT)
|
||||||
|
// // ------------------------------
|
||||||
|
// try {
|
||||||
|
// final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
// .hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
// if (!hasActive) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// const SnackBar(
|
||||||
|
// content: Text(
|
||||||
|
// 'لا يوجد تسجيل دخول فعال لتسجيل الخروج',
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// if (context.mounted) {
|
||||||
|
// ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
// SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (context.mounted) {
|
||||||
|
// Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder:
|
||||||
|
// (_) => OvalCameraCapturePage(
|
||||||
|
// isLogin: false,
|
||||||
|
// onCapture: (imageFile) async {
|
||||||
|
// final logoutUseCase =
|
||||||
|
// sl<AttendanceLogoutUseCase>();
|
||||||
|
// await logoutUseCase(
|
||||||
|
// AttendanceLogoutRequest(
|
||||||
|
// employeeId: employeeId,
|
||||||
|
// faceImage: imageFile,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// checkIfLoggedIn: () {},
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// ---------------------------------------------
|
||||||
|
// /// SHADOW WRAPPER
|
||||||
|
// /// ---------------------------------------------
|
||||||
|
|
||||||
|
// class _ShadowedCard extends StatelessWidget {
|
||||||
|
// final Widget child;
|
||||||
|
// final List<BoxShadow> shadow;
|
||||||
|
|
||||||
|
// const _ShadowedCard({required this.child, required this.shadow});
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return Stack(
|
||||||
|
// children: [
|
||||||
|
// Container(
|
||||||
|
// height: 160,
|
||||||
|
// width: 160,
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// borderRadius: BorderRadius.circular(32),
|
||||||
|
// boxShadow: shadow,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// child,
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// ---------------------------------------------
|
||||||
|
// /// BUTTON WIDGET
|
||||||
|
// /// ---------------------------------------------
|
||||||
|
|
||||||
|
// class _FingerButton extends StatelessWidget {
|
||||||
|
// final String icon;
|
||||||
|
// final String label;
|
||||||
|
// final VoidCallback onTap;
|
||||||
|
|
||||||
|
// const _FingerButton({
|
||||||
|
// required this.icon,
|
||||||
|
// required this.label,
|
||||||
|
// required this.onTap,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return GestureDetector(
|
||||||
|
// onTap: onTap,
|
||||||
|
// child: Container(
|
||||||
|
// height: 160,
|
||||||
|
// width: 160,
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: Color(0xFFEAFBF3),
|
||||||
|
// borderRadius: BorderRadius.circular(32),
|
||||||
|
// ),
|
||||||
|
// child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// children: [
|
||||||
|
// SvgPicture.asset(icon, width: 75, height: 75),
|
||||||
|
// SizedBox(height: 10),
|
||||||
|
// Text(
|
||||||
|
// label,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18,
|
||||||
|
// fontWeight: FontWeight.w600,
|
||||||
|
// color: Colors.black,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
import 'package:coda_project/presentation/screens/face_screen2.dart';
|
||||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,6 +399,7 @@ import '../../domain/models/attendance_logout_request.dart';
|
|||||||
import '../../domain/usecases/attendance_login_usecase.dart';
|
import '../../domain/usecases/attendance_login_usecase.dart';
|
||||||
import '../../domain/usecases/attendance_logout_usecase.dart';
|
import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||||
import '../../data/datasources/user_local_data_source.dart';
|
import '../../data/datasources/user_local_data_source.dart';
|
||||||
|
import '../../data/datasources/attendance_remote_data_source.dart';
|
||||||
|
|
||||||
class AttendanceScreen extends StatelessWidget {
|
class AttendanceScreen extends StatelessWidget {
|
||||||
const AttendanceScreen({super.key});
|
const AttendanceScreen({super.key});
|
||||||
@@ -37,14 +427,14 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => UserSettingsScreen(),
|
builder: (context) => const UserSettingsScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (index == 1) {
|
} else if (index == 1) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NotificationsScreen(),
|
builder: (context) => const NotificationsScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,12 +446,10 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
/// GREETING TEXT
|
/// GREETING TEXT
|
||||||
/// ------------------------------
|
/// ------------------------------
|
||||||
Positioned(
|
Positioned(
|
||||||
top:
|
top: screenHeight * 0.14,
|
||||||
screenHeight *
|
|
||||||
0.14, // moved down because settings bar now exists
|
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"صباح الخير, محمد",
|
"صباح الخير, محمد",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -78,9 +466,7 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
/// MAIN CARD AREA
|
/// MAIN CARD AREA
|
||||||
/// ------------------------------
|
/// ------------------------------
|
||||||
Positioned(
|
Positioned(
|
||||||
top:
|
top: screenHeight * 0.2,
|
||||||
screenHeight *
|
|
||||||
0.2, // pushed down because of settings bar + greeting
|
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -94,20 +480,20 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
const BoxShadow(
|
||||||
color: Color(0x1F2B2B2B),
|
color: Color(0x1F2B2B2B),
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
offset: Offset(10, -10),
|
offset: Offset(10, -10),
|
||||||
),
|
),
|
||||||
BoxShadow(
|
const BoxShadow(
|
||||||
color: Color(0xABCECECE),
|
color: Color(0xABCECECE),
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
offset: Offset(-2, 5),
|
offset: Offset(-2, 5),
|
||||||
),
|
),
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color.fromARGB(148, 2, 70, 35),
|
color: const Color.fromARGB(148, 2, 70, 35),
|
||||||
blurRadius: 80,
|
blurRadius: 80,
|
||||||
offset: Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -116,7 +502,7 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
height: screenHeight * 0.5,
|
height: screenHeight * 0.5,
|
||||||
width: screenWidth * 0.7,
|
width: screenWidth * 0.7,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0x92757575),
|
color: const Color(0x92757575),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -134,7 +520,7 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
left: screenWidth * 0.05,
|
left: screenWidth * 0.05,
|
||||||
child: _ShadowedCard(
|
child: _ShadowedCard(
|
||||||
shadow: [
|
shadow: [
|
||||||
BoxShadow(
|
const BoxShadow(
|
||||||
color: Color(0x62000000),
|
color: Color(0x62000000),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
spreadRadius: 5,
|
spreadRadius: 5,
|
||||||
@@ -158,6 +544,47 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACTIVE SESSION CHECK (LOGIN)
|
||||||
|
try {
|
||||||
|
final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
.hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
if (hasActive) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(_) => AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'تنبيه',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
content: const Text(
|
||||||
|
'أنت مسجل دخول بالفعل، لا يمكنك تسجيل الدخول مرة أخرى.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => Navigator.of(context).pop(),
|
||||||
|
child: const Text('حسناً'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -190,7 +617,7 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
bottom: screenHeight * 0.2,
|
bottom: screenHeight * 0.2,
|
||||||
right: screenWidth * 0.1,
|
right: screenWidth * 0.1,
|
||||||
child: _ShadowedCard(
|
child: _ShadowedCard(
|
||||||
shadow: [
|
shadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color(0xABCECECE),
|
color: Color(0xABCECECE),
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
@@ -225,6 +652,47 @@ class AttendanceScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACTIVE SESSION CHECK (LOGOUT)
|
||||||
|
try {
|
||||||
|
final hasActive = await sl<AttendanceRemoteDataSource>()
|
||||||
|
.hasActiveLogin(employeeId: employeeId);
|
||||||
|
|
||||||
|
if (!hasActive) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(_) => AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'تنبيه',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
content: const Text(
|
||||||
|
'لا يوجد تسجيل دخول فعال لتسجيل الخروج.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => Navigator.of(context).pop(),
|
||||||
|
child: const Text('حسناً'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('فشل التحقق من الجلسة: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -306,17 +774,17 @@ class _FingerButton extends StatelessWidget {
|
|||||||
height: 160,
|
height: 160,
|
||||||
width: 160,
|
width: 160,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFFEAFBF3),
|
color: const Color(0xFFEAFBF3),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(icon, width: 75, height: 75),
|
SvgPicture.asset(icon, width: 75, height: 75),
|
||||||
SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
|
|||||||
824
lib/presentation/screens/face_screen2.dart
Normal file
824
lib/presentation/screens/face_screen2.dart
Normal file
@@ -0,0 +1,824 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||||
|
|
||||||
|
import '../../core/error/exceptions.dart';
|
||||||
|
import '../face/face_feedback.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 _isSubmitting = false;
|
||||||
|
bool _isStreaming = false;
|
||||||
|
|
||||||
|
// Smart feedback
|
||||||
|
FaceFeedback _feedback = FaceFeedback(
|
||||||
|
type: FaceHintType.noFace,
|
||||||
|
message: "ضع وجهك داخل الإطار",
|
||||||
|
quality: 0,
|
||||||
|
borderColor: Colors.white70,
|
||||||
|
);
|
||||||
|
|
||||||
|
double _progress = 0;
|
||||||
|
bool _isDetecting = false;
|
||||||
|
int _frameCount = 0;
|
||||||
|
|
||||||
|
// Stability tracking
|
||||||
|
Rect? _lastFaceRect;
|
||||||
|
int _stableFrames = 0;
|
||||||
|
bool _showManualCapture = false;
|
||||||
|
Timer? _manualCaptureTimer;
|
||||||
|
|
||||||
|
String _debugInfo = "Initializing...";
|
||||||
|
|
||||||
|
late final FaceDetector _faceDetector = FaceDetector(
|
||||||
|
options: FaceDetectorOptions(
|
||||||
|
performanceMode: FaceDetectorMode.fast,
|
||||||
|
enableTracking: true,
|
||||||
|
enableClassification: true,
|
||||||
|
enableLandmarks: false,
|
||||||
|
enableContours: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const int _throttleEveryNFrames = 5;
|
||||||
|
static const int _stableFramesNeeded = 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Go straight to camera — no network calls here
|
||||||
|
_initializeCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_manualCaptureTimer?.cancel();
|
||||||
|
_stopImageStream();
|
||||||
|
_cameraController?.dispose();
|
||||||
|
_faceDetector.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeCamera() async {
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = null;
|
||||||
|
_isCameraInitialized = false;
|
||||||
|
_isSuccess = false;
|
||||||
|
_isSubmitting = false;
|
||||||
|
_progress = 0;
|
||||||
|
_stableFrames = 0;
|
||||||
|
_lastFaceRect = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _cameraController?.dispose();
|
||||||
|
_cameraController = null;
|
||||||
|
|
||||||
|
final cameras = await availableCameras();
|
||||||
|
if (cameras.isEmpty) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||||
|
_isCameraInitialized = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final front = cameras.where(
|
||||||
|
(c) => c.lensDirection == CameraLensDirection.front,
|
||||||
|
);
|
||||||
|
final selectedCamera = front.isNotEmpty ? front.first : cameras.first;
|
||||||
|
|
||||||
|
_cameraController = CameraController(
|
||||||
|
selectedCamera,
|
||||||
|
ResolutionPreset.medium,
|
||||||
|
enableAudio: false,
|
||||||
|
imageFormatGroup:
|
||||||
|
Platform.isAndroid
|
||||||
|
? ImageFormatGroup.yuv420
|
||||||
|
: ImageFormatGroup.bgra8888,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _cameraController!.initialize();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isCameraInitialized = true;
|
||||||
|
_isStreaming = false;
|
||||||
|
_showManualCapture = false;
|
||||||
|
_debugInfo = "Ready. Cam: ${selectedCamera.lensDirection}";
|
||||||
|
});
|
||||||
|
|
||||||
|
_manualCaptureTimer?.cancel();
|
||||||
|
_manualCaptureTimer = Timer(const Duration(seconds: 10), () {
|
||||||
|
if (mounted && _isCameraInitialized && !_isSuccess && !_isSubmitting) {
|
||||||
|
setState(() {
|
||||||
|
_showManualCapture = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_startSmartStream();
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
|
||||||
|
_isCameraInitialized = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startSmartStream() {
|
||||||
|
if (_cameraController == null || !_cameraController!.value.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_isStreaming) return;
|
||||||
|
|
||||||
|
_isStreaming = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
_cameraController!.startImageStream((CameraImage image) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_isSubmitting || _isSuccess) return;
|
||||||
|
|
||||||
|
_frameCount++;
|
||||||
|
if (_frameCount % _throttleEveryNFrames != 0) return;
|
||||||
|
|
||||||
|
if (_isDetecting) return;
|
||||||
|
_isDetecting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final inputImage = _toInputImage(
|
||||||
|
image,
|
||||||
|
_cameraController!.description,
|
||||||
|
);
|
||||||
|
if (inputImage == null) {
|
||||||
|
_isDetecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final faces = await _faceDetector.processImage(inputImage);
|
||||||
|
|
||||||
|
if (faces.isEmpty) {
|
||||||
|
_stableFrames = 0;
|
||||||
|
_applyFeedback(
|
||||||
|
FaceFeedback(
|
||||||
|
type: FaceHintType.noFace,
|
||||||
|
message: "ضع وجهك داخل الإطار",
|
||||||
|
quality: 0,
|
||||||
|
borderColor: Colors.white70,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_isDetecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final face = faces.first;
|
||||||
|
final brightness = _estimateBrightness(image);
|
||||||
|
final rotation =
|
||||||
|
inputImage.metadata?.rotation ?? InputImageRotation.rotation0deg;
|
||||||
|
|
||||||
|
final feedback = _evaluate(
|
||||||
|
face: face,
|
||||||
|
brightness: brightness,
|
||||||
|
image: image,
|
||||||
|
rotation: rotation,
|
||||||
|
);
|
||||||
|
|
||||||
|
_applyFeedback(feedback);
|
||||||
|
|
||||||
|
if (feedback.isGood) {
|
||||||
|
_stableFrames++;
|
||||||
|
_progress = (_stableFrames / _stableFramesNeeded).clamp(0.0, 1.0);
|
||||||
|
if (_stableFrames >= _stableFramesNeeded) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_debugInfo = "جاري التحقق من الصورة...";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isDetecting = false;
|
||||||
|
await _captureAndSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_stableFrames > 0) _stableFrames--;
|
||||||
|
_progress = (_stableFrames / _stableFramesNeeded).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted && !_isSubmitting && !_isSuccess) {
|
||||||
|
setState(() {
|
||||||
|
_debugInfo =
|
||||||
|
"Faces: ${faces.length} | Bright: ${brightness.toStringAsFixed(1)}\n"
|
||||||
|
"Msg: ${feedback.message} | Stable: $_stableFrames";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Face detection error: $e");
|
||||||
|
} finally {
|
||||||
|
_isDetecting = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error starting image stream: $e");
|
||||||
|
_isStreaming = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopImageStream() async {
|
||||||
|
if (!_isStreaming || _cameraController == null) return;
|
||||||
|
try {
|
||||||
|
await _cameraController!.stopImageStream();
|
||||||
|
_isStreaming = false;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error stopping image stream: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopCameraCompletely() {
|
||||||
|
_manualCaptureTimer?.cancel();
|
||||||
|
try {
|
||||||
|
if (_isStreaming && _cameraController != null) {
|
||||||
|
_cameraController!.stopImageStream();
|
||||||
|
_isStreaming = false;
|
||||||
|
}
|
||||||
|
_cameraController?.dispose();
|
||||||
|
_cameraController = null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error stopping camera: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceFeedback _evaluate({
|
||||||
|
required Face face,
|
||||||
|
required double brightness,
|
||||||
|
required CameraImage image,
|
||||||
|
required InputImageRotation rotation,
|
||||||
|
}) {
|
||||||
|
// 1) lighting
|
||||||
|
if (brightness < 40) {
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.tooDark,
|
||||||
|
message: "المكان مظلم — انتقل لمكان أكثر إضاءة",
|
||||||
|
quality: 0.1,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) head pose
|
||||||
|
final yaw = (face.headEulerAngleY ?? 0).abs();
|
||||||
|
final pitch = (face.headEulerAngleX ?? 0).abs();
|
||||||
|
if (yaw > 20 || pitch > 20) {
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.lookStraight,
|
||||||
|
message: "انظر مباشرةً للكاميرا",
|
||||||
|
quality: 0.2,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) distance estimate
|
||||||
|
double frameWidth = image.width.toDouble();
|
||||||
|
double frameHeight = image.height.toDouble();
|
||||||
|
|
||||||
|
if (rotation == InputImageRotation.rotation90deg ||
|
||||||
|
rotation == InputImageRotation.rotation270deg) {
|
||||||
|
final temp = frameWidth;
|
||||||
|
frameWidth = frameHeight;
|
||||||
|
frameHeight = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
final box = face.boundingBox;
|
||||||
|
final frameArea = frameWidth * frameHeight;
|
||||||
|
final faceArea = box.width * box.height;
|
||||||
|
final ratio = faceArea / frameArea;
|
||||||
|
|
||||||
|
if (ratio < 0.05) {
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.tooFar,
|
||||||
|
message: "اقترب قليلاً",
|
||||||
|
quality: 0.3,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ratio > 0.8) {
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.tooClose,
|
||||||
|
message: "ابتعد قليلاً",
|
||||||
|
quality: 0.3,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) centered
|
||||||
|
final faceCenter = Offset(box.center.dx, box.center.dy);
|
||||||
|
final frameCenter = Offset(frameWidth / 2, frameHeight / 2);
|
||||||
|
final dist = (faceCenter - frameCenter).distance;
|
||||||
|
final maxAllowed = math.min(frameWidth, frameHeight) * 0.4;
|
||||||
|
|
||||||
|
if (dist > maxAllowed) {
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.notCentered,
|
||||||
|
message: "وسط وجهك داخل الإطار",
|
||||||
|
quality: 0.4,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) stability
|
||||||
|
if (_lastFaceRect != null) {
|
||||||
|
final moved = (box.center - _lastFaceRect!.center).distance;
|
||||||
|
if (moved > 40) {
|
||||||
|
_lastFaceRect = box;
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.holdStill,
|
||||||
|
message: "ثبت الهاتف وابقَ ثابتاً",
|
||||||
|
quality: 0.5,
|
||||||
|
borderColor: Colors.orangeAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastFaceRect = box;
|
||||||
|
|
||||||
|
return FaceFeedback(
|
||||||
|
type: FaceHintType.good,
|
||||||
|
message: "ممتاز — ثبت قليلاً",
|
||||||
|
quality: 1.0,
|
||||||
|
borderColor: Colors.greenAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFeedback(FaceFeedback f) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_feedback.type != f.type || _feedback.message != f.message) {
|
||||||
|
setState(() {
|
||||||
|
_feedback = f;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _captureAndSubmit() async {
|
||||||
|
if (_cameraController == null) return;
|
||||||
|
if (_isSubmitting || _isSuccess) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSubmitting = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _stopImageStream();
|
||||||
|
|
||||||
|
// Small delay to let camera settle after stopping stream
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
if (_cameraController == null ||
|
||||||
|
!_cameraController!.value.isInitialized) {
|
||||||
|
_handleScanError("الكاميرا غير جاهزة، حاول مرة أخرى");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final xFile = await _cameraController!.takePicture();
|
||||||
|
final file = File(xFile.path);
|
||||||
|
|
||||||
|
await widget.onCapture(file);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isSuccess = true;
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// Check if this is an "already logged in" error from the API
|
||||||
|
final msg = e.message.toLowerCase();
|
||||||
|
if (msg.contains('already logged in') ||
|
||||||
|
msg.contains('مسجل دخول بالفعل')) {
|
||||||
|
// Stop camera and go back with a dialog
|
||||||
|
_stopCameraCompletely();
|
||||||
|
if (mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder:
|
||||||
|
(_) => AlertDialog(
|
||||||
|
title: const Text('تنبيه', textAlign: TextAlign.center),
|
||||||
|
content: const Text(
|
||||||
|
'أنت مسجل دخول بالفعل، لا يمكنك تسجيل الدخول مرة أخرى.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(); // Close dialog
|
||||||
|
Navigator.of(context).pop(); // Go back from camera
|
||||||
|
},
|
||||||
|
child: const Text('حسناً'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_handleScanError(e.message);
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
_handleScanError(e.message);
|
||||||
|
} on CameraException catch (e) {
|
||||||
|
_handleScanError("فشل التقاط الصورة: ${e.description ?? e.code}");
|
||||||
|
} catch (e) {
|
||||||
|
_handleScanError("حدث خطأ غير متوقع: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleScanError(String msg) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isSubmitting = false;
|
||||||
|
_errorMessage = msg;
|
||||||
|
_progress = 0;
|
||||||
|
_stableFrames = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_cameraController != null &&
|
||||||
|
_cameraController!.value.isInitialized &&
|
||||||
|
!_isStreaming) {
|
||||||
|
_startSmartStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _estimateBrightness(CameraImage image) {
|
||||||
|
if (image.planes.isEmpty) return 0;
|
||||||
|
final bytes = image.planes[0].bytes;
|
||||||
|
if (bytes.isEmpty) return 0;
|
||||||
|
|
||||||
|
const step = 100;
|
||||||
|
int sum = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < bytes.length; i += step) {
|
||||||
|
sum += bytes[i];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count == 0 ? 0 : (sum / count);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputImage? _toInputImage(CameraImage image, CameraDescription camera) {
|
||||||
|
final sensorOrientation = camera.sensorOrientation;
|
||||||
|
InputImageRotation? rotation;
|
||||||
|
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
rotation = _rotationIntToImageRotation(sensorOrientation);
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
var rotationCompensation =
|
||||||
|
_orientations[_cameraController!.value.deviceOrientation];
|
||||||
|
if (rotationCompensation == null) return null;
|
||||||
|
if (camera.lensDirection == CameraLensDirection.front) {
|
||||||
|
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
|
||||||
|
} else {
|
||||||
|
rotationCompensation =
|
||||||
|
(sensorOrientation - rotationCompensation + 360) % 360;
|
||||||
|
}
|
||||||
|
rotation = _rotationIntToImageRotation(rotationCompensation);
|
||||||
|
}
|
||||||
|
if (rotation == null) return null;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final nv21 = _convertYUV420ToNV21(image);
|
||||||
|
|
||||||
|
return InputImage.fromBytes(
|
||||||
|
bytes: nv21,
|
||||||
|
metadata: InputImageMetadata(
|
||||||
|
size: Size(image.width.toDouble(), image.height.toDouble()),
|
||||||
|
rotation: rotation,
|
||||||
|
format: InputImageFormat.nv21,
|
||||||
|
bytesPerRow: image.width,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS BGRA8888
|
||||||
|
if (image.planes.length == 1) {
|
||||||
|
return InputImage.fromBytes(
|
||||||
|
bytes: image.planes.first.bytes,
|
||||||
|
metadata: InputImageMetadata(
|
||||||
|
size: Size(image.width.toDouble(), image.height.toDouble()),
|
||||||
|
rotation: rotation,
|
||||||
|
format: InputImageFormat.bgra8888,
|
||||||
|
bytesPerRow: image.planes.first.bytesPerRow,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _convertYUV420ToNV21(CameraImage image) {
|
||||||
|
final int width = image.width;
|
||||||
|
final int height = image.height;
|
||||||
|
|
||||||
|
final yPlane = image.planes[0];
|
||||||
|
final uPlane = image.planes[1];
|
||||||
|
final vPlane = image.planes[2];
|
||||||
|
|
||||||
|
final int ySize = width * height;
|
||||||
|
final int uvSize = ySize ~/ 2;
|
||||||
|
|
||||||
|
final Uint8List nv21 = Uint8List(ySize + uvSize);
|
||||||
|
|
||||||
|
// Y Channel
|
||||||
|
if (yPlane.bytesPerRow == width) {
|
||||||
|
nv21.setAll(0, yPlane.bytes);
|
||||||
|
} else {
|
||||||
|
int offset = 0;
|
||||||
|
for (int i = 0; i < height; i++) {
|
||||||
|
nv21.setRange(
|
||||||
|
offset,
|
||||||
|
offset + width,
|
||||||
|
yPlane.bytes,
|
||||||
|
i * yPlane.bytesPerRow,
|
||||||
|
);
|
||||||
|
offset += width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UV Channel (NV21 is VU interleaved)
|
||||||
|
final int uvWidth = width ~/ 2;
|
||||||
|
final int uvHeight = height ~/ 2;
|
||||||
|
final int uvPixelStride = uPlane.bytesPerPixel ?? 1;
|
||||||
|
|
||||||
|
int uvIndex = ySize;
|
||||||
|
|
||||||
|
for (int row = 0; row < uvHeight; row++) {
|
||||||
|
final int srcIndex = row * uPlane.bytesPerRow;
|
||||||
|
for (int col = 0; col < uvWidth; col++) {
|
||||||
|
final int pixelIndex = srcIndex + (col * uvPixelStride);
|
||||||
|
nv21[uvIndex++] = vPlane.bytes[pixelIndex];
|
||||||
|
nv21[uvIndex++] = uPlane.bytes[pixelIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nv21;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputImageRotation _rotationIntToImageRotation(int rotation) {
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return InputImageRotation.rotation90deg;
|
||||||
|
case 180:
|
||||||
|
return InputImageRotation.rotation180deg;
|
||||||
|
case 270:
|
||||||
|
return InputImageRotation.rotation270deg;
|
||||||
|
default:
|
||||||
|
return InputImageRotation.rotation0deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final _orientations = {
|
||||||
|
DeviceOrientation.portraitUp: 0,
|
||||||
|
DeviceOrientation.landscapeLeft: 90,
|
||||||
|
DeviceOrientation.portraitDown: 180,
|
||||||
|
DeviceOrientation.landscapeRight: 270,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_errorMessage != null && !_isCameraInitialized) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.red, size: 48),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(_errorMessage!, style: const TextStyle(color: Colors.white)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _initializeCamera,
|
||||||
|
child: const Text("إعادة المحاولة"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isCameraInitialized || _cameraController == null) {
|
||||||
|
return const Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Center(child: CircularProgressIndicator(color: Colors.red)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Center(child: CameraPreview(_cameraController!)),
|
||||||
|
CustomPaint(
|
||||||
|
painter: _OvalOverlayPainter(
|
||||||
|
borderColor: _feedback.borderColor,
|
||||||
|
progress: _progress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 60,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'Cairo',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_feedback.message,
|
||||||
|
style: TextStyle(
|
||||||
|
color: _feedback.borderColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isSubmitting)
|
||||||
|
const Center(child: CircularProgressIndicator(color: Colors.white)),
|
||||||
|
if (_isSuccess)
|
||||||
|
const Center(
|
||||||
|
child: Icon(Icons.check_circle, color: Colors.green, size: 80),
|
||||||
|
),
|
||||||
|
if (_errorMessage != null && _isCameraInitialized)
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black54,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 50,
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
child: Text(
|
||||||
|
_debugInfo,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.yellow,
|
||||||
|
fontSize: 12,
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showManualCapture && !_isSubmitting && !_isSuccess)
|
||||||
|
Positioned(
|
||||||
|
bottom: 110,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _captureAndSubmit,
|
||||||
|
icon: const Icon(Icons.camera_alt),
|
||||||
|
label: const Text("التقاط يدوياً"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OvalOverlayPainter extends CustomPainter {
|
||||||
|
final Color borderColor;
|
||||||
|
final double progress;
|
||||||
|
|
||||||
|
_OvalOverlayPainter({required this.borderColor, required this.progress});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final width = size.width * 0.75;
|
||||||
|
final height = size.height * 0.55;
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final ovalRect = Rect.fromCenter(
|
||||||
|
center: center,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
|
||||||
|
final screenPath =
|
||||||
|
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
final ovalPath = Path()..addOval(ovalRect);
|
||||||
|
final overlayPath = Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
screenPath,
|
||||||
|
ovalPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
final bgPaint =
|
||||||
|
Paint()
|
||||||
|
..color = Colors.black.withOpacity(0.6)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
canvas.drawPath(overlayPath, bgPaint);
|
||||||
|
|
||||||
|
final borderPaint =
|
||||||
|
Paint()
|
||||||
|
..color = borderColor
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4.0;
|
||||||
|
|
||||||
|
canvas.drawOval(ovalRect, borderPaint);
|
||||||
|
|
||||||
|
if (progress > 0) {
|
||||||
|
final progressPaint =
|
||||||
|
Paint()
|
||||||
|
..color = Colors.greenAccent
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 6.0
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
final startAngle = -math.pi / 2;
|
||||||
|
final sweepAngle = 2 * math.pi * progress;
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
ovalRect.inflate(10),
|
||||||
|
startAngle,
|
||||||
|
sweepAngle,
|
||||||
|
false,
|
||||||
|
progressPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _OvalOverlayPainter oldDelegate) {
|
||||||
|
return oldDelegate.borderColor != borderColor ||
|
||||||
|
oldDelegate.progress != progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@@ -280,6 +280,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.7.0"
|
version: "7.7.0"
|
||||||
|
google_mlkit_commons:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_mlkit_commons
|
||||||
|
sha256: "7e9a6d6e66b44aa8cfe944bda9bc3346c52486dd890ca49e5bc98845cda40d7f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.0"
|
||||||
|
google_mlkit_face_detection:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_mlkit_face_detection
|
||||||
|
sha256: "65988405c884fd84a4ccc8bded7b5e3e4c33362f6f4eaaa94818bdaaba7bab7d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ dependencies:
|
|||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
flutter_bloc: ^8.1.6
|
flutter_bloc: ^8.1.6
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
google_mlkit_face_detection: ^0.12.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user