feat : login api call
This commit is contained in:
@@ -67,11 +67,13 @@ class _SystemDesignPageState extends State<SystemDesignPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
spacing: 14,
|
spacing: 14,
|
||||||
children: [
|
children: [
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
hintText: 'حجم کشتار را در روز به قطعه وارد کنید',
|
hintText: 'حجم کشتار را در روز به قطعه وارد کنید',
|
||||||
hintStyle: AppFonts.yekan13,
|
hintStyle: AppFonts.yekan13,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'تلفن مرغداری',
|
label: 'تلفن مرغداری',
|
||||||
labelStyle: AppFonts.yekan10,
|
labelStyle: AppFonts.yekan10,
|
||||||
),
|
),
|
||||||
|
|||||||
53
packages/auth/lib/data/common/dio_error_handler.dart
Normal file
53
packages/auth/lib/data/common/dio_error_handler.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
|
class DioErrorHandler {
|
||||||
|
void handle(DioException error) {
|
||||||
|
switch (error.response?.statusCode) {
|
||||||
|
case 401:
|
||||||
|
_handle401();
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
_handle403();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_handleGeneric(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//wrong password/user name => "detail": "No active account found with the given credentials" - 401
|
||||||
|
void _handle401() {
|
||||||
|
Get.showSnackbar(
|
||||||
|
_errorSnackBar('نام کاربری یا رمز عبور اشتباه است'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//wrong captcha => "detail": "Captcha code is incorrect" - 403
|
||||||
|
void _handle403() {
|
||||||
|
Get.showSnackbar(
|
||||||
|
_errorSnackBar('کد امنیتی اشتباه است'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleGeneric(DioException error) {
|
||||||
|
// General error handling
|
||||||
|
}
|
||||||
|
|
||||||
|
GetSnackBar _errorSnackBar(String message) {
|
||||||
|
return GetSnackBar(
|
||||||
|
titleText: Text(
|
||||||
|
'خطا',
|
||||||
|
style: AppFonts.yekan14.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
messageText: Text(
|
||||||
|
message,
|
||||||
|
style: AppFonts.yekan12.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColor.error,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
borderRadius: 12,
|
||||||
|
duration: Duration(milliseconds: 3500),
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:rasadyar_auth/data/common/constant.dart';
|
import 'package:rasadyar_auth/data/common/constant.dart';
|
||||||
|
import 'package:rasadyar_auth/data/common/dio_error_handler.dart';
|
||||||
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
|
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
|
||||||
import 'package:rasadyar_auth/data/services/auth_service.dart';
|
import 'package:rasadyar_auth/data/services/auth_service.dart';
|
||||||
import 'package:rasadyar_auth/data/services/token_storage_service.dart';
|
import 'package:rasadyar_auth/data/services/token_storage_service.dart';
|
||||||
@@ -16,6 +17,7 @@ Future<void> setupAuthDI() async {
|
|||||||
diAuth.registerCachedFactory<AuthRepositoryImpl>(
|
diAuth.registerCachedFactory<AuthRepositoryImpl>(
|
||||||
() => AuthRepositoryImpl(dioRemote),
|
() => AuthRepositoryImpl(dioRemote),
|
||||||
);
|
);
|
||||||
diAuth.registerLazySingleton(() => AuthService());
|
diAuth.registerLazySingleton<AuthService>(() => AuthService());
|
||||||
diAuth.registerLazySingleton(() => TokenStorageService());
|
diAuth.registerLazySingleton<TokenStorageService>(() => TokenStorageService());
|
||||||
|
diAuth.registerLazySingleton<DioErrorHandler>(() => DioErrorHandler());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,23 @@ abstract class LoginRequestModel with _$LoginRequestModel {
|
|||||||
String? captchaKey,
|
String? captchaKey,
|
||||||
}) = _LoginRequestModel;
|
}) = _LoginRequestModel;
|
||||||
|
|
||||||
|
factory LoginRequestModel.createWithCaptcha({
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String captchaCode,
|
||||||
|
required String captchaKey,
|
||||||
|
}) {
|
||||||
|
return LoginRequestModel(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
captchaCode: captchaCode,
|
||||||
|
captchaKey: 'rest_captcha_$captchaKey.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory LoginRequestModel.fromJson(Map<String, dynamic> json) =>
|
factory LoginRequestModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$LoginRequestModelFromJson(json);
|
_$LoginRequestModelFromJson(json);
|
||||||
|
|
||||||
const LoginRequestModel._();
|
const LoginRequestModel._();
|
||||||
|
|
||||||
String get formattedCaptchaKey => 'rest_captcha_$captchaKey.0';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,64 +14,34 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
Future<AuthResponseModel?> login({
|
Future<AuthResponseModel?> login({
|
||||||
required Map<String, dynamic> authRequest,
|
required Map<String, dynamic> authRequest,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await safeCall<DioResponse<AuthResponseModel>>(
|
var res = await _httpClient.post<AuthResponseModel>(
|
||||||
call:
|
'$_BASE_URL/login/',
|
||||||
() async => await _httpClient.post<AuthResponseModel>(
|
data: authRequest,
|
||||||
'$_BASE_URL/login/',
|
fromJson: AuthResponseModel.fromJson,
|
||||||
data: authRequest,
|
headers: {'Content-Type': 'application/json'},
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
),
|
|
||||||
onSuccess: (response) {
|
|
||||||
iLog(response);
|
|
||||||
},
|
|
||||||
onError: (error, trace) {
|
|
||||||
throw Exception('Error during sign in: $error');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
return res.data;
|
||||||
return response?.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CaptchaResponseModel?> captcha() async {
|
Future<CaptchaResponseModel?> captcha() async {
|
||||||
final response = await safeCall<CaptchaResponseModel?>(
|
var res = await _httpClient.post<CaptchaResponseModel?>(
|
||||||
call: () async {
|
'captcha/',
|
||||||
var res = await _httpClient.post<CaptchaResponseModel?>(
|
fromJson: CaptchaResponseModel.fromJson,
|
||||||
'captcha/',
|
|
||||||
fromJson: CaptchaResponseModel.fromJson,
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
onSuccess: (response) {
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
onError: (error, trace) {
|
|
||||||
throw Exception('Error during captcha : $error');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return response;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<AuthResponseModel?> loginWithRefreshToken({
|
Future<AuthResponseModel?> loginWithRefreshToken({
|
||||||
required Map<String, dynamic> authRequest,
|
required Map<String, dynamic> authRequest,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await safeCall<DioResponse<AuthResponseModel>>(
|
var res = await _httpClient.post<AuthResponseModel>(
|
||||||
call:
|
'$_BASE_URL/login/',
|
||||||
() async => await _httpClient.post<AuthResponseModel>(
|
data: authRequest,
|
||||||
'$_BASE_URL/login/',
|
headers: {'Content-Type': 'application/json'},
|
||||||
data: authRequest,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
),
|
|
||||||
onSuccess: (response) {
|
|
||||||
iLog(response);
|
|
||||||
},
|
|
||||||
onError: (error, trace) {
|
|
||||||
throw Exception('Error during sign in: $error');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
return res.data;
|
||||||
return response?.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -82,20 +52,11 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> hasAuthenticated() async {
|
Future<bool> hasAuthenticated() async {
|
||||||
final response = await safeCall<DioResponse<bool>>(
|
final response = await _httpClient.get<bool>(
|
||||||
call:
|
'$_BASE_URL/login/',
|
||||||
() async => await _httpClient.get<bool>(
|
headers: {'Content-Type': 'application/json'},
|
||||||
'$_BASE_URL/login/',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
),
|
|
||||||
onSuccess: (response) {
|
|
||||||
iLog(response);
|
|
||||||
},
|
|
||||||
onError: (error, trace) {
|
|
||||||
throw Exception('Error during sign in: $error');
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return response?.data ?? false;
|
return response.data ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
import 'package:rasadyar_core/injection/di.dart';
|
|
||||||
|
|
||||||
class TokenStorageService extends GetxService {
|
class TokenStorageService extends GetxService {
|
||||||
static const String _boxName = 'secureBox';
|
static const String _boxName = 'secureBox';
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rasadyar_auth/auth.dart';
|
import 'package:rasadyar_auth/auth.dart';
|
||||||
|
import 'package:rasadyar_auth/data/common/dio_error_handler.dart';
|
||||||
|
import 'package:rasadyar_auth/data/models/request/login_request/login_request_model.dart';
|
||||||
|
import 'package:rasadyar_auth/data/models/response/auth/auth_response_model.dart';
|
||||||
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
|
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
|
||||||
|
import 'package:rasadyar_auth/data/services/token_storage_service.dart';
|
||||||
|
import 'package:rasadyar_auth/presentation/widget/captcha/logic.dart';
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
enum AuthType { useAndPass, otp }
|
enum AuthType { useAndPass, otp }
|
||||||
@@ -12,7 +17,8 @@ enum AuthStatus { init }
|
|||||||
enum OtpStatus { init, sent, verified, reSend }
|
enum OtpStatus { init, sent, verified, reSend }
|
||||||
|
|
||||||
class AuthLogic extends GetxController {
|
class AuthLogic extends GetxController {
|
||||||
Rx<GlobalKey<FormState>> formKey = GlobalKey<FormState>().obs;
|
GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
Rx<GlobalKey<FormState>> formKeyOtp = GlobalKey<FormState>().obs;
|
Rx<GlobalKey<FormState>> formKeyOtp = GlobalKey<FormState>().obs;
|
||||||
Rx<GlobalKey<FormState>> formKeySentOtp = GlobalKey<FormState>().obs;
|
Rx<GlobalKey<FormState>> formKeySentOtp = GlobalKey<FormState>().obs;
|
||||||
Rx<TextEditingController> phoneNumberController = TextEditingController().obs;
|
Rx<TextEditingController> phoneNumberController = TextEditingController().obs;
|
||||||
@@ -21,11 +27,12 @@ class AuthLogic extends GetxController {
|
|||||||
TextEditingController().obs;
|
TextEditingController().obs;
|
||||||
Rx<TextEditingController> otpCodeController = TextEditingController().obs;
|
Rx<TextEditingController> otpCodeController = TextEditingController().obs;
|
||||||
|
|
||||||
|
var captchaController = Get.find<CaptchaWidgetLogic>();
|
||||||
|
|
||||||
RxnString phoneNumber = RxnString(null);
|
RxnString phoneNumber = RxnString(null);
|
||||||
RxnString password = RxnString(null);
|
RxBool isLoading = false.obs;
|
||||||
RxBool isOnError = false.obs;
|
TokenStorageService tokenStorageService = diAuth.get<TokenStorageService>();
|
||||||
RxBool hidePassword = true.obs;
|
|
||||||
Rx<AuthType> authType = AuthType.useAndPass.obs;
|
Rx<AuthType> authType = AuthType.useAndPass.obs;
|
||||||
Rx<AuthStatus> authStatus = AuthStatus.init.obs;
|
Rx<AuthStatus> authStatus = AuthStatus.init.obs;
|
||||||
Rx<OtpStatus> otpStatus = OtpStatus.init.obs;
|
Rx<OtpStatus> otpStatus = OtpStatus.init.obs;
|
||||||
@@ -61,7 +68,7 @@ class AuthLogic extends GetxController {
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
tokenStorageService.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -76,5 +83,46 @@ class AuthLogic extends GetxController {
|
|||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isFormValid() {
|
||||||
|
final isCaptchaValid =
|
||||||
|
captchaController.formKey.currentState?.validate() ?? false;
|
||||||
|
final isFormValid = formKey.currentState?.validate() ?? false;
|
||||||
|
return isCaptchaValid && isFormValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginRequestModel _buildLoginRequest() {
|
||||||
|
final phone = phoneNumberController.value.text;
|
||||||
|
final pass = passwordController.value.text;
|
||||||
|
final code = captchaController.textController.value.text;
|
||||||
|
final key = captchaController.captchaKey.value;
|
||||||
|
|
||||||
|
return LoginRequestModel.createWithCaptcha(
|
||||||
|
username: phone,
|
||||||
|
password: pass,
|
||||||
|
captchaCode: code,
|
||||||
|
captchaKey: key!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> submitLoginForm() async {
|
||||||
|
if (!_isFormValid()) return;
|
||||||
|
|
||||||
|
final loginRequestModel = _buildLoginRequest();
|
||||||
|
isLoading.value = true;
|
||||||
|
await safeCall<AuthResponseModel?>(
|
||||||
|
call: () => authRepository.login(authRequest: loginRequestModel.toJson()),
|
||||||
|
onSuccess: (result) async{
|
||||||
|
await tokenStorageService.saveRefreshToken(result!.refresh!);
|
||||||
|
await tokenStorageService.saveAccessToken(result!.access!);
|
||||||
|
//Get.offAndToNamed(Routes.home);
|
||||||
|
},
|
||||||
|
onError: (error, stackTrace) {
|
||||||
|
if (error is DioException) {
|
||||||
|
diAuth.get<DioErrorHandler>().handle(error);
|
||||||
|
}
|
||||||
|
captchaController.getCaptcha();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rasadyar_auth/presentation/widget/captcha/view.dart';
|
import 'package:rasadyar_auth/presentation/widget/captcha/view.dart';
|
||||||
import 'package:rasadyar_auth/presentation/widget/clear_button.dart';
|
import 'package:rasadyar_auth/presentation/widget/clear_button.dart';
|
||||||
|
import 'package:rasadyar_auth/presentation/widget/logo_widget.dart';
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
import 'logic.dart';
|
import 'logic.dart';
|
||||||
@@ -17,11 +17,11 @@ class AuthPage extends GetView<AuthLogic> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 80),
|
SizedBox(height: 80),
|
||||||
logoWidget(),
|
LogoWidget(),
|
||||||
ObxValue((types) {
|
ObxValue((types) {
|
||||||
switch (types.value) {
|
switch (types.value) {
|
||||||
case AuthType.otp:
|
case AuthType.otp:
|
||||||
//return otpForm();
|
//return otpForm();
|
||||||
case AuthType.useAndPass:
|
case AuthType.useAndPass:
|
||||||
return useAndPassFrom();
|
return useAndPassFrom();
|
||||||
}
|
}
|
||||||
@@ -87,180 +87,114 @@ class AuthPage extends GetView<AuthLogic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget useAndPassFrom() {
|
Widget useAndPassFrom() {
|
||||||
return ObxValue((data) {
|
return Padding(
|
||||||
return Padding(
|
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50),
|
child: Form(
|
||||||
child: Form(
|
key: controller.formKey,
|
||||||
key: data.value,
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
ObxValue(
|
||||||
ObxValue((phoneController) {
|
(phoneController) => RTextField(
|
||||||
return TextFormField(
|
label: 'نام کاربری',
|
||||||
controller: controller.phoneNumberController.value,
|
maxLength: 11,
|
||||||
decoration: InputDecoration(
|
maxLines: 1,
|
||||||
border: OutlineInputBorder(
|
controller: phoneController.value,
|
||||||
borderRadius: BorderRadius.circular(8),
|
keyboardType: TextInputType.number,
|
||||||
gapPadding: 11,
|
initText: phoneController.value.text,
|
||||||
),
|
onChanged: (value) {
|
||||||
labelText: 'نام کاربری',
|
phoneController.value.text = value;
|
||||||
labelStyle: AppFonts.yekan13,
|
phoneController.refresh();
|
||||||
errorStyle: AppFonts.yekan13.copyWith(
|
},
|
||||||
color: AppColor.redNormal,
|
prefixIcon: Padding(
|
||||||
),
|
padding: const EdgeInsets.fromLTRB(0, 8, 6, 8),
|
||||||
|
child: vecWidget(Assets.vecCallSvg),
|
||||||
prefixIconConstraints: BoxConstraints(
|
),
|
||||||
maxHeight: 40,
|
suffixIcon:
|
||||||
minHeight: 40,
|
phoneController.value.text.trim().isNotEmpty
|
||||||
maxWidth: 40,
|
? clearButton(() {
|
||||||
minWidth: 40,
|
phoneController.value.clear();
|
||||||
),
|
phoneController.refresh();
|
||||||
prefixIcon: Padding(
|
})
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 6, 8),
|
: null,
|
||||||
child: vecWidget(Assets.vecCallSvg),
|
validator: (value) {
|
||||||
),
|
if (value == null || value.isEmpty) {
|
||||||
suffix:
|
return '⚠️ شماره موبایل را وارد کنید';
|
||||||
phoneController.value.text.trim().isNotEmpty
|
}
|
||||||
? clearButton(() {
|
/*else if (value.length < 11) {
|
||||||
phoneController.value.clear();
|
return '⚠️ شماره موبایل باید 11 رقم باشد';
|
||||||
phoneController.refresh();
|
}*/
|
||||||
})
|
return null;
|
||||||
: null,
|
},
|
||||||
counterText: '',
|
style: AppFonts.yekan13,
|
||||||
),
|
errorStyle: AppFonts.yekan13.copyWith(
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
color: AppColor.redNormal,
|
||||||
decimal: false,
|
),
|
||||||
signed: false,
|
labelStyle: AppFonts.yekan13,
|
||||||
),
|
boxConstraints: const BoxConstraints(
|
||||||
|
maxHeight: 40,
|
||||||
maxLines: 1,
|
minHeight: 40,
|
||||||
maxLength: 11,
|
maxWidth: 40,
|
||||||
onChanged: (value) {
|
minWidth: 40,
|
||||||
if (controller.isOnError.value) {
|
),
|
||||||
controller.isOnError.value = !controller.isOnError.value;
|
),
|
||||||
data.value.currentState?.reset();
|
controller.phoneNumberController,
|
||||||
|
),
|
||||||
data.refresh();
|
const SizedBox(height: 26),
|
||||||
phoneController.value.text = value;
|
ObxValue(
|
||||||
}
|
(passwordController) => RTextField(
|
||||||
phoneController.refresh();
|
label: 'رمز عبور',
|
||||||
},
|
filled: false,
|
||||||
textInputAction: TextInputAction.next,
|
controller: passwordController.value,
|
||||||
validator: (value) {
|
variant: RTextFieldVariant.password,
|
||||||
if (value == null) {
|
initText: passwordController.value.text,
|
||||||
return '⚠️ شماره موبایل را وارد کنید';
|
onChanged: (value) {
|
||||||
} else if (value.length < 11) {
|
passwordController.refresh();
|
||||||
return '⚠️ شماره موبایل باید 11 رقم باشد';
|
},
|
||||||
}
|
validator: (value) {
|
||||||
return null;
|
if (value == null || value.isEmpty) {
|
||||||
},
|
return '⚠️ رمز عبور را وارد کنید';
|
||||||
style: AppFonts.yekan13,
|
}
|
||||||
);
|
return null;
|
||||||
}, controller.phoneNumberController),
|
},
|
||||||
|
style: AppFonts.yekan13,
|
||||||
SizedBox(height: 26),
|
errorStyle: AppFonts.yekan13.copyWith(
|
||||||
|
color: AppColor.redNormal,
|
||||||
ObxValue((passwordController) {
|
),
|
||||||
return TextFormField(
|
labelStyle: AppFonts.yekan13,
|
||||||
controller: passwordController.value,
|
prefixIcon: Padding(
|
||||||
obscureText: controller.hidePassword.value,
|
padding: const EdgeInsets.fromLTRB(0, 8, 8, 8),
|
||||||
decoration: InputDecoration(
|
child: vecWidget(Assets.vecKeySvg),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
boxConstraints: const BoxConstraints(
|
||||||
gapPadding: 11,
|
maxHeight: 34,
|
||||||
),
|
minHeight: 34,
|
||||||
labelText: 'رمز عبور',
|
maxWidth: 34,
|
||||||
labelStyle: AppFonts.yekan13,
|
minWidth: 34,
|
||||||
errorStyle: AppFonts.yekan13.copyWith(
|
),
|
||||||
color: AppColor.redNormal,
|
),
|
||||||
),
|
controller.passwordController,
|
||||||
|
),
|
||||||
prefixIconConstraints: BoxConstraints(
|
SizedBox(height: 26),
|
||||||
maxHeight: 34,
|
CaptchaWidget(),
|
||||||
minHeight: 34,
|
SizedBox(height: 23),
|
||||||
maxWidth: 34,
|
ObxValue((data) {
|
||||||
minWidth: 34,
|
return RElevated(
|
||||||
),
|
|
||||||
prefixIcon: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 8, 8),
|
|
||||||
child: vecWidget(Assets.vecKeySvg),
|
|
||||||
),
|
|
||||||
suffix:
|
|
||||||
passwordController.value.text.trim().isNotEmpty
|
|
||||||
? GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
controller.hidePassword.value =
|
|
||||||
!controller.hidePassword.value;
|
|
||||||
},
|
|
||||||
child: Icon(
|
|
||||||
controller.hidePassword.value
|
|
||||||
? CupertinoIcons.eye
|
|
||||||
: CupertinoIcons.eye_slash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
counterText: '',
|
|
||||||
),
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
keyboardType: TextInputType.visiblePassword,
|
|
||||||
maxLines: 1,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (controller.isOnError.value) {
|
|
||||||
controller.isOnError.value = !controller.isOnError.value;
|
|
||||||
data.value.currentState?.reset();
|
|
||||||
passwordController.value.text = value;
|
|
||||||
}
|
|
||||||
passwordController.refresh();
|
|
||||||
},
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return '⚠️ رمز عبور را وارد کنید'; // "Please enter the password"
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
style: AppFonts.yekan13,
|
|
||||||
);
|
|
||||||
}, controller.passwordController),
|
|
||||||
SizedBox(height: 26),
|
|
||||||
|
|
||||||
CaptchaWidget(),
|
|
||||||
|
|
||||||
SizedBox(height: 23),
|
|
||||||
RElevated(
|
|
||||||
text: 'ورود',
|
text: 'ورود',
|
||||||
|
isLoading: data.value,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Jalali? picked = await showPersianDatePicker(
|
await controller.submitLoginForm();
|
||||||
context: Get.context!,
|
|
||||||
|
|
||||||
initialDate: Jalali.now(),
|
|
||||||
firstDate: Jalali(1385, 8),
|
|
||||||
lastDate: Jalali(1450, 9),
|
|
||||||
initialEntryMode: PersianDatePickerEntryMode.calendarOnly,
|
|
||||||
initialDatePickerMode: PersianDatePickerMode.year,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
width: Get.width,
|
width: Get.width,
|
||||||
height: 48,
|
height: 48,
|
||||||
),
|
);
|
||||||
],
|
}, controller.isLoading),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}, controller.formKey);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Widget otpForm() {
|
/*
|
||||||
return ObxValue((status) {
|
|
||||||
switch (status.value) {
|
|
||||||
case OtpStatus.init:
|
|
||||||
return sendCodeForm();
|
|
||||||
case OtpStatus.sent:
|
|
||||||
case OtpStatus.verified:
|
|
||||||
case OtpStatus.reSend:
|
|
||||||
return confirmCodeForm();
|
|
||||||
}
|
|
||||||
}, controller.otpStatus);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
Widget sendCodeForm() {
|
Widget sendCodeForm() {
|
||||||
return ObxValue((data) {
|
return ObxValue((data) {
|
||||||
return Form(
|
return Form(
|
||||||
@@ -494,18 +428,5 @@ class AuthPage extends GetView<AuthLogic> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, controller.formKeySentOtp);
|
}, controller.formKeySentOtp);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
Widget logoWidget() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Row(),
|
|
||||||
Image.asset(Assets.imagesInnerSplash, width: 120, height: 120),
|
|
||||||
Text(
|
|
||||||
'سامانه رصدیار',
|
|
||||||
style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyNormal),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:rasadyar_auth/presentation/widget/captcha/logic.dart';
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
import '../pages/auth/logic.dart';
|
import '../pages/auth/logic.dart';
|
||||||
@@ -14,15 +15,16 @@ sealed class AuthPages {
|
|||||||
page: () => AuthPage(),
|
page: () => AuthPage(),
|
||||||
binding: BindingsBuilder(() {
|
binding: BindingsBuilder(() {
|
||||||
Get.lazyPut(() => AuthLogic());
|
Get.lazyPut(() => AuthLogic());
|
||||||
|
Get.lazyPut(() => CaptchaWidgetLogic());
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: AuthPaths.auth,
|
name: AuthPaths.auth,
|
||||||
page: () => AuthPage(),
|
page: () => AuthPage(),
|
||||||
binding: BindingsBuilder(() {
|
binding: BindingsBuilder(() {
|
||||||
Get.lazyPut(() => AuthLogic());
|
Get.lazyPut(() => AuthLogic());
|
||||||
|
Get.lazyPut(() => CaptchaWidgetLogic());
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import 'package:rasadyar_core/core.dart';
|
|||||||
|
|
||||||
class CaptchaWidgetLogic extends GetxController
|
class CaptchaWidgetLogic extends GetxController
|
||||||
with StateMixin<CaptchaResponseModel> {
|
with StateMixin<CaptchaResponseModel> {
|
||||||
TextEditingController textController = TextEditingController();
|
Rx<TextEditingController> textController = TextEditingController().obs;
|
||||||
|
RxnString captchaKey = RxnString();
|
||||||
GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>();
|
AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>();
|
||||||
|
|
||||||
@@ -17,17 +18,19 @@ class CaptchaWidgetLogic extends GetxController
|
|||||||
getCaptcha();
|
getCaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
textController.value.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getCaptcha() async {
|
Future<void> getCaptcha() async {
|
||||||
change(null, status: RxStatus.loading());
|
change(null, status: RxStatus.loading());
|
||||||
|
textController.value.clear();
|
||||||
safeCall(
|
safeCall(
|
||||||
call: () async => await authRepository.captcha(),
|
call: () async => await authRepository.captcha(),
|
||||||
onSuccess: (value) {
|
onSuccess: (value) {
|
||||||
|
captchaKey.value = value?.captchaKey;
|
||||||
change(value, status: RxStatus.success());
|
change(value, status: RxStatus.success());
|
||||||
},
|
},
|
||||||
onError: (error, stackTrace) {
|
onError: (error, stackTrace) {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import 'package:rasadyar_core/core.dart';
|
|||||||
import 'logic.dart';
|
import 'logic.dart';
|
||||||
|
|
||||||
class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
|
class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
|
||||||
CaptchaWidget({super.key});
|
const CaptchaWidget({super.key});
|
||||||
|
|
||||||
final CaptchaWidgetLogic logic = Get.put(CaptchaWidgetLogic());
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -19,7 +17,7 @@ class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 135,
|
width: 135,
|
||||||
height: 48,
|
height: 50,
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.whiteNormalHover,
|
color: AppColor.whiteNormalHover,
|
||||||
@@ -27,75 +25,62 @@ class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: controller.obx(
|
child: controller.obx(
|
||||||
(state) => Image.memory(base64Decode(state?.captchaImage??''),fit: BoxFit.cover,),
|
(state) => Image.memory(
|
||||||
|
base64Decode(state?.captchaImage ?? ''),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
onLoading: const Center(
|
onLoading: const Center(
|
||||||
child: CupertinoActivityIndicator(
|
child: CupertinoActivityIndicator(color: AppColor.blueNormal),
|
||||||
color: AppColor.blueNormal,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'خطا در بارگذاری کد امنیتی',
|
'خطا در بارگذاری کد امنیتی',
|
||||||
style: AppFonts.yekan13,));
|
style: AppFonts.yekan13,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
GestureDetector(
|
||||||
IconButton(
|
onTap: controller.getCaptcha,
|
||||||
padding: EdgeInsets.zero,
|
child: Padding(
|
||||||
onPressed: controller.getCaptcha,
|
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||||
icon: Icon(CupertinoIcons.refresh, size: 16),
|
child: Icon(CupertinoIcons.refresh, size: 20),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Form(
|
child: Form(
|
||||||
key: controller.formKey,
|
key: controller.formKey,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.disabled,
|
||||||
child: TextFormField(
|
child: ObxValue((data) {
|
||||||
controller: controller.textController,
|
return RTextField(
|
||||||
decoration: InputDecoration(
|
label: 'کد امنیتی',
|
||||||
border: OutlineInputBorder(
|
controller: data.value,
|
||||||
borderRadius: BorderRadius.circular(8),
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
gapPadding: 11,
|
decimal: false,
|
||||||
|
signed: false,
|
||||||
),
|
),
|
||||||
labelText: 'کد امنیتی',
|
maxLines: 1,
|
||||||
labelStyle: AppFonts.yekan13,
|
maxLength: 6,
|
||||||
errorStyle: AppFonts.yekan10.copyWith(
|
suffixIcon:
|
||||||
color: AppColor.redNormal,
|
(data.value.text.trim().isNotEmpty ?? false)
|
||||||
fontSize: 8,
|
? clearButton(
|
||||||
),
|
() => controller.textController.value.clear(),
|
||||||
suffixIconConstraints: BoxConstraints(
|
)
|
||||||
maxHeight: 24,
|
: null,
|
||||||
minHeight: 24,
|
|
||||||
maxWidth: 24,
|
onSubmitted: (data) {},
|
||||||
minWidth: 24,
|
validator: (value) {
|
||||||
),
|
if (value == null || value.isEmpty) {
|
||||||
suffix:
|
return 'کد امنیتی را وارد کنید';
|
||||||
controller.textController.text
|
}
|
||||||
.trim()
|
return null;
|
||||||
.isNotEmpty
|
},
|
||||||
? clearButton(() => controller.textController.clear())
|
style: AppFonts.yekan13,
|
||||||
: null,
|
);
|
||||||
counterText: '',
|
}, controller.textController),
|
||||||
),
|
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
|
||||||
decimal: false,
|
|
||||||
signed: false,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
maxLength: 6,
|
|
||||||
onChanged: (value) {},
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'کد امنیتی را وارد کنید';
|
|
||||||
}
|
|
||||||
/*if (value != controller.captchaCode.toString()) {
|
|
||||||
return '⚠️کد امنیتی وارد شده اشتباه است';
|
|
||||||
}*/
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
style: AppFonts.yekan13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import 'package:flutter/cupertino.dart';
|
|||||||
Widget clearButton(VoidCallback onTap) {
|
Widget clearButton(VoidCallback onTap) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Icon(CupertinoIcons.multiply_circle, size: 24),
|
child: Icon(CupertinoIcons.multiply_circle, size: 20),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/auth/lib/presentation/widget/logo_widget.dart
Normal file
20
packages/auth/lib/presentation/widget/logo_widget.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
|
class LogoWidget extends StatelessWidget {
|
||||||
|
const LogoWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(),
|
||||||
|
Image.asset(Assets.imagesInnerSplash, width: 120, height: 120),
|
||||||
|
Text(
|
||||||
|
'سامانه رصدیار',
|
||||||
|
style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyNormal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,10 +30,13 @@ export 'package:rasadyar_core/presentation/common/common.dart';
|
|||||||
export 'package:rasadyar_core/presentation/utils/utils.dart';
|
export 'package:rasadyar_core/presentation/utils/utils.dart';
|
||||||
export 'package:rasadyar_core/presentation/widget/widget.dart';
|
export 'package:rasadyar_core/presentation/widget/widget.dart';
|
||||||
|
|
||||||
export 'infrastructure/remote/dio_form_data.dart';
|
|
||||||
//network
|
//network
|
||||||
|
export 'infrastructure/remote/dio_form_data.dart';
|
||||||
export 'infrastructure/remote/dio_remote.dart';
|
export 'infrastructure/remote/dio_remote.dart';
|
||||||
export 'infrastructure/remote/dio_response.dart';
|
export 'infrastructure/remote/dio_response.dart';
|
||||||
|
export 'package:dio/dio.dart' show DioException;
|
||||||
|
|
||||||
//utils
|
//utils
|
||||||
export 'utils/logger_utils.dart';
|
export 'utils/logger_utils.dart';
|
||||||
export 'utils/safe_call_utils.dart';
|
export 'utils/safe_call_utils.dart';
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ class DioRemote implements IHttpClient {
|
|||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
final dio = Dio(BaseOptions(baseUrl: baseUrl));
|
final dio = Dio(BaseOptions(baseUrl: baseUrl));
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
dio.interceptors.add(PrettyDioLogger());
|
dio.interceptors.add(PrettyDioLogger(
|
||||||
|
requestHeader: true,
|
||||||
|
responseHeader: true,
|
||||||
|
requestBody: true
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_dio = dio;
|
_dio = dio;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,51 +3,65 @@ import 'package:rasadyar_core/presentation/common/app_color.dart';
|
|||||||
import 'package:rasadyar_core/presentation/common/app_fonts.dart';
|
import 'package:rasadyar_core/presentation/common/app_fonts.dart';
|
||||||
|
|
||||||
class RElevated extends StatelessWidget {
|
class RElevated extends StatelessWidget {
|
||||||
RElevated({
|
const RElevated({
|
||||||
super.key,
|
super.key,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.foregroundColor,
|
this.foregroundColor = Colors.white,
|
||||||
this.backgroundColor,
|
this.backgroundColor = AppColor.blueNormal,
|
||||||
this.disabledBackgroundColor,
|
this.disabledBackgroundColor,
|
||||||
this.disabledForegroundColor,
|
this.disabledForegroundColor = Colors.white,
|
||||||
this.radius,
|
this.radius = 8.0,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
this.width = 150.0,
|
this.width = 150.0,
|
||||||
this.height = 56.0,
|
this.height = 56.0,
|
||||||
this.isFullWidth,
|
this.isFullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final bool? isFullWidth;
|
final bool isFullWidth;
|
||||||
Color? foregroundColor;
|
final Color foregroundColor;
|
||||||
Color? backgroundColor;
|
final Color backgroundColor;
|
||||||
Color? disabledForegroundColor;
|
final Color? disabledForegroundColor;
|
||||||
Color? disabledBackgroundColor;
|
final Color? disabledBackgroundColor;
|
||||||
double? radius;
|
final double radius;
|
||||||
TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bool isEnabled = onPressed != null && !isLoading;
|
||||||
|
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: onPressed,
|
onPressed: isEnabled ? onPressed : null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? AppColor.blueNormal,
|
backgroundColor: backgroundColor,
|
||||||
foregroundColor: foregroundColor ?? Colors.white,
|
foregroundColor: foregroundColor,
|
||||||
disabledBackgroundColor:
|
disabledBackgroundColor:
|
||||||
disabledBackgroundColor ?? AppColor.blueNormal.withAlpha(38),
|
disabledBackgroundColor ?? backgroundColor.withAlpha(38),
|
||||||
disabledForegroundColor: disabledForegroundColor ?? Colors.white,
|
disabledForegroundColor: disabledForegroundColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(radius ?? 8),
|
borderRadius: BorderRadius.circular(radius),
|
||||||
),
|
),
|
||||||
minimumSize: Size((isFullWidth ??false) ? double.infinity : width, height),
|
minimumSize: Size(isFullWidth ? double.infinity : width, height),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
textStyle: textStyle ?? AppFonts.yekan24,
|
textStyle: textStyle ?? AppFonts.yekan24,
|
||||||
),
|
),
|
||||||
child: Text(text),
|
child:
|
||||||
|
isLoading
|
||||||
|
? SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(text),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,164 +2,106 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
@immutable
|
enum RTextFieldVariant {
|
||||||
|
normal,
|
||||||
|
noBorder,
|
||||||
|
password,
|
||||||
|
passwordNoBorder,
|
||||||
|
}
|
||||||
|
|
||||||
class RTextField extends StatefulWidget {
|
class RTextField extends StatefulWidget {
|
||||||
RTextField({
|
final TextEditingController controller;
|
||||||
super.key,
|
|
||||||
this.maxLines,
|
|
||||||
this.maxLength,
|
|
||||||
this.hintText,
|
|
||||||
this.padding,
|
|
||||||
this.onChanged,
|
|
||||||
this.onSubmitted,
|
|
||||||
this.keyboardType,
|
|
||||||
this.showCounter = false,
|
|
||||||
this.isDense,
|
|
||||||
this.initText,
|
|
||||||
this.isForNumber = false,
|
|
||||||
this.style,
|
|
||||||
this.hintStyle,
|
|
||||||
this.suffixIcon,
|
|
||||||
this.prefixIcon,
|
|
||||||
this.validator,
|
|
||||||
this.readonly = false,
|
|
||||||
this.boxConstraints,
|
|
||||||
this.minLines,
|
|
||||||
this.radius,
|
|
||||||
this.filled,
|
|
||||||
this.filledColor,
|
|
||||||
this.enabled,
|
|
||||||
this.errorStyle,
|
|
||||||
this.labelStyle,
|
|
||||||
this.label,
|
|
||||||
}) {
|
|
||||||
filled = filled ?? false;
|
|
||||||
obscure = false;
|
|
||||||
_inputBorder = OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(radius ?? 8),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RTextField.noBorder({
|
|
||||||
super.key,
|
|
||||||
this.maxLines,
|
|
||||||
this.maxLength,
|
|
||||||
this.hintText,
|
|
||||||
this.padding,
|
|
||||||
this.onChanged,
|
|
||||||
this.onSubmitted,
|
|
||||||
this.keyboardType,
|
|
||||||
this.showCounter = false,
|
|
||||||
this.isDense,
|
|
||||||
this.initText,
|
|
||||||
this.style,
|
|
||||||
this.hintStyle,
|
|
||||||
this.suffixIcon,
|
|
||||||
this.radius,
|
|
||||||
this.validator,
|
|
||||||
this.boxConstraints,
|
|
||||||
this.minLines,
|
|
||||||
this.isForNumber = false,
|
|
||||||
this.readonly = false,
|
|
||||||
this.label,
|
|
||||||
this.filled,
|
|
||||||
this.filledColor,
|
|
||||||
this.errorStyle,
|
|
||||||
this.labelStyle,
|
|
||||||
this.enabled,
|
|
||||||
}) {
|
|
||||||
_inputBorder = OutlineInputBorder(
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
borderRadius: BorderRadius.circular(radius ?? 16),
|
|
||||||
);
|
|
||||||
obscure = false;
|
|
||||||
filled = filled ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
RTextField.password({
|
|
||||||
super.key,
|
|
||||||
this.maxLines = 1,
|
|
||||||
this.maxLength,
|
|
||||||
this.hintText,
|
|
||||||
this.padding,
|
|
||||||
this.onChanged,
|
|
||||||
this.onSubmitted,
|
|
||||||
this.keyboardType,
|
|
||||||
this.showCounter = false,
|
|
||||||
this.isDense,
|
|
||||||
this.initText,
|
|
||||||
this.style,
|
|
||||||
this.hintStyle,
|
|
||||||
this.suffixIcon,
|
|
||||||
this.prefixIcon,
|
|
||||||
this.radius,
|
|
||||||
this.validator,
|
|
||||||
this.boxConstraints,
|
|
||||||
this.minLines,
|
|
||||||
this.isForNumber = false,
|
|
||||||
this.readonly = false,
|
|
||||||
this.label,
|
|
||||||
this.filled,
|
|
||||||
this.filledColor,
|
|
||||||
this.errorStyle,
|
|
||||||
this.labelStyle,
|
|
||||||
this.enabled,
|
|
||||||
}) {
|
|
||||||
_inputBorder = OutlineInputBorder(
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
borderRadius: BorderRadius.circular(radius ?? 16),
|
|
||||||
);
|
|
||||||
filled = filled ?? true;
|
|
||||||
obscure = true;
|
|
||||||
_isPassword = true;
|
|
||||||
prefixIcon = prefixIcon ?? const Icon(CupertinoIcons.person);
|
|
||||||
}
|
|
||||||
|
|
||||||
final int? maxLines;
|
|
||||||
final int? minLines;
|
|
||||||
final int? maxLength;
|
|
||||||
final String? hintText;
|
|
||||||
final String? label;
|
final String? label;
|
||||||
final EdgeInsets? padding;
|
final String? hintText;
|
||||||
|
final String? initText;
|
||||||
|
final bool obscure;
|
||||||
|
final bool readonly;
|
||||||
|
final bool enabled;
|
||||||
|
final int? maxLength;
|
||||||
|
final int? minLines;
|
||||||
|
final int? maxLines;
|
||||||
|
final Widget? suffixIcon;
|
||||||
|
final Widget? prefixIcon;
|
||||||
|
final BoxConstraints? boxConstraints;
|
||||||
|
final RTextFieldVariant variant;
|
||||||
|
final bool filled;
|
||||||
|
final Color? filledColor;
|
||||||
|
final bool showCounter;
|
||||||
|
final bool isDense;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final TextStyle? errorStyle;
|
|
||||||
final TextStyle? hintStyle;
|
final TextStyle? hintStyle;
|
||||||
final TextStyle? labelStyle;
|
final TextStyle? labelStyle;
|
||||||
final bool showCounter;
|
final TextStyle? errorStyle;
|
||||||
final bool? isDense;
|
final EdgeInsets? padding;
|
||||||
final bool? isForNumber;
|
final FormFieldValidator<String>? validator;
|
||||||
final bool readonly;
|
final void Function(String)? onChanged;
|
||||||
bool? obscure;
|
final void Function(String)? onSubmitted;
|
||||||
final bool? enabled;
|
|
||||||
final double? radius;
|
const RTextField({
|
||||||
final TextInputType? keyboardType;
|
super.key,
|
||||||
final Function(String)? onChanged;
|
required this.controller,
|
||||||
final Function(String)? onSubmitted;
|
this.label,
|
||||||
final FormFieldValidator? validator;
|
this.hintText,
|
||||||
final String? initText;
|
this.initText,
|
||||||
Widget? suffixIcon;
|
this.obscure = false,
|
||||||
Widget? prefixIcon;
|
this.readonly = false,
|
||||||
bool? filled;
|
this.enabled = true,
|
||||||
Color? filledColor;
|
this.maxLength,
|
||||||
bool _isPassword = false;
|
this.minLines,
|
||||||
|
this.maxLines = 1,
|
||||||
|
this.suffixIcon,
|
||||||
|
this.prefixIcon,
|
||||||
|
this.boxConstraints,
|
||||||
|
this.variant = RTextFieldVariant.normal,
|
||||||
|
this.filled = false,
|
||||||
|
this.filledColor,
|
||||||
|
this.showCounter = false,
|
||||||
|
this.isDense = false,
|
||||||
|
this.keyboardType,
|
||||||
|
this.style,
|
||||||
|
this.hintStyle,
|
||||||
|
this.labelStyle,
|
||||||
|
this.errorStyle,
|
||||||
|
this.padding,
|
||||||
|
this.validator,
|
||||||
|
this.onChanged,
|
||||||
|
this.onSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
final BoxConstraints? boxConstraints;
|
|
||||||
late final InputBorder? _inputBorder;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RTextField> createState() => _RTextFieldState();
|
State<RTextField> createState() => _RTextFieldState();
|
||||||
|
|
||||||
|
bool get _isPassword => variant == RTextFieldVariant.password;
|
||||||
|
bool get _noBorder => variant == RTextFieldVariant.noBorder;
|
||||||
|
bool get _passwordNoBorder => variant == RTextFieldVariant.passwordNoBorder;
|
||||||
|
|
||||||
|
|
||||||
|
InputBorder get _inputBorder =>
|
||||||
|
_noBorder || _passwordNoBorder ? InputBorder.none : OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: AppColor.lightGreyDarkActive,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RTextFieldState extends State<RTextField> {
|
class _RTextFieldState extends State<RTextField> {
|
||||||
final TextEditingController _controller = TextEditingController();
|
late bool obscure;
|
||||||
bool? obscure;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.initText != null) {
|
if (widget.initText != null) {
|
||||||
_controller.text = widget.initText!;
|
widget.controller.text = widget.initText!;
|
||||||
}
|
}
|
||||||
obscure = widget.obscure;
|
obscure = widget.obscure;
|
||||||
}
|
}
|
||||||
@@ -167,51 +109,56 @@ class _RTextFieldState extends State<RTextField> {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant RTextField oldWidget) {
|
void didUpdateWidget(covariant RTextField oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.initText != oldWidget.initText) {
|
if (widget.initText != null && widget.initText != oldWidget.initText) {
|
||||||
_controller.text = widget.initText ?? '';
|
widget.controller.text = widget.initText!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSuffixIcon() {
|
||||||
|
if (widget.suffixIcon != null) return widget.suffixIcon!;
|
||||||
|
if (!widget._isPassword) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
obscure = !obscure;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
obscure ? CupertinoIcons.eye : CupertinoIcons.eye_slash,
|
||||||
|
color: AppColor.darkGreyDarkActive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding ?? EdgeInsets.zero,
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller,
|
controller: widget.controller,
|
||||||
readOnly: widget.readonly,
|
readOnly: widget.readonly,
|
||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
validator: widget.validator,
|
validator: widget.validator,
|
||||||
enabled: widget.enabled,
|
enabled: widget.enabled,
|
||||||
obscureText: obscure ?? false,
|
obscureText: obscure,
|
||||||
onTapOutside: (event) {
|
onTapOutside: (_) => FocusScope.of(context).unfocus(),
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
onFieldSubmitted: widget.onSubmitted,
|
onFieldSubmitted: widget.onSubmitted,
|
||||||
maxLength: widget.maxLength,
|
maxLength: widget.maxLength,
|
||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
style: widget.style,
|
style: widget.style,
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
errorStyle: widget.errorStyle,
|
errorStyle: widget.errorStyle,
|
||||||
errorMaxLines: 1,
|
errorMaxLines: 1,
|
||||||
isDense: widget.isDense,
|
isDense: widget.isDense,
|
||||||
suffixIcon:
|
suffixIcon: _buildSuffixIcon(),
|
||||||
widget.suffixIcon ??
|
|
||||||
(widget._isPassword
|
|
||||||
? IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
obscure = !obscure!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
!obscure! ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null),
|
|
||||||
suffixIconConstraints: widget.boxConstraints,
|
suffixIconConstraints: widget.boxConstraints,
|
||||||
prefixIcon: widget.prefixIcon,
|
prefixIcon: widget.prefixIcon,
|
||||||
prefixIconConstraints: widget.boxConstraints,
|
prefixIconConstraints: widget.boxConstraints,
|
||||||
@@ -221,7 +168,7 @@ class _RTextFieldState extends State<RTextField> {
|
|||||||
labelStyle: AppFonts.yekan14
|
labelStyle: AppFonts.yekan14
|
||||||
.copyWith(color: AppColor.lightGreyDarkActive)
|
.copyWith(color: AppColor.lightGreyDarkActive)
|
||||||
.merge(widget.labelStyle),
|
.merge(widget.labelStyle),
|
||||||
filled: widget.filled,
|
filled: widget.filled || widget._noBorder || widget._passwordNoBorder,
|
||||||
fillColor: widget.filledColor,
|
fillColor: widget.filledColor,
|
||||||
counter: widget.showCounter ? null : const SizedBox(),
|
counter: widget.showCounter ? null : const SizedBox(),
|
||||||
hintStyle: widget.hintStyle,
|
hintStyle: widget.hintStyle,
|
||||||
@@ -232,4 +179,4 @@ class _RTextFieldState extends State<RTextField> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:rasadyar_core/core.dart';
|
import 'package:rasadyar_core/core.dart';
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace);
|
|||||||
typedef VoidCallback = void Function();
|
typedef VoidCallback = void Function();
|
||||||
|
|
||||||
// تعریف دقیق تابع safeCall
|
// تعریف دقیق تابع safeCall
|
||||||
Future<T?> safeCall<T>({
|
Future<void> safeCall<T>({
|
||||||
required AsyncCallback<T> call,
|
required AsyncCallback<T> call,
|
||||||
Function(T result)? onSuccess,
|
Function(T result)? onSuccess,
|
||||||
ErrorCallback? onError,
|
ErrorCallback? onError,
|
||||||
@@ -33,7 +34,7 @@ Future<T?> safeCall<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSuccess?.call(result);
|
onSuccess?.call(result);
|
||||||
return result;
|
|
||||||
|
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
if (showError) {
|
if (showError) {
|
||||||
@@ -45,7 +46,6 @@ Future<T?> safeCall<T>({
|
|||||||
print('safeCall error: $error\n$stackTrace');
|
print('safeCall error: $error\n$stackTrace');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
(onHideLoading ?? _defaultHideLoading)();
|
(onHideLoading ?? _defaultHideLoading)();
|
||||||
|
|||||||
@@ -68,24 +68,28 @@ Container mobileInspectorWidget() {
|
|||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'نام و نام خانوادگی',
|
label: 'نام و نام خانوادگی',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'شماره مجوز',
|
label: 'شماره مجوز',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'شماره ثبت',
|
label: 'شماره ثبت',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'کد اقتصادی',
|
label: 'کد اقتصادی',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
|
|||||||
@@ -86,13 +86,17 @@ class AddSupervisionPage extends GetView<AddSupervisionLogic> {
|
|||||||
);
|
);
|
||||||
}, controller.violationSegmentsSelected),
|
}, controller.violationSegmentsSelected),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
RTextField(label: 'صادر کننده پروانه'),
|
RTextField(
|
||||||
|
controller: TextEditingController(),label: 'صادر کننده پروانه'),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
RTextField(label: 'شماره مجوز'),
|
RTextField(
|
||||||
|
controller: TextEditingController(),label: 'شماره مجوز'),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
RTextField(label: 'شماره ثبت'),
|
RTextField(
|
||||||
|
controller: TextEditingController(),label: 'شماره ثبت'),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
RTextField(label: 'کد اقتصادی'),
|
RTextField(
|
||||||
|
controller: TextEditingController(),label: 'کد اقتصادی'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -108,29 +108,34 @@ Widget violationWidget() {
|
|||||||
child: Column(
|
child: Column(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'توضیحات تخلف',
|
label: 'توضیحات تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'توضیحات تخلف',
|
label: 'توضیحات تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
|
|||||||
@@ -69,29 +69,34 @@ Container violationWidget() {
|
|||||||
child: Column(
|
child: Column(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'توضیحات تخلف',
|
label: 'توضیحات تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'عنوان تخلف',
|
label: 'عنوان تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
),
|
),
|
||||||
RTextField(
|
RTextField(
|
||||||
|
controller: TextEditingController(),
|
||||||
label: 'توضیحات تخلف',
|
label: 'توضیحات تخلف',
|
||||||
filled: true,
|
filled: true,
|
||||||
filledColor: AppColor.whiteLight,
|
filledColor: AppColor.whiteLight,
|
||||||
|
|||||||
Reference in New Issue
Block a user