diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 96504e7..d9e2fba 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -44,6 +44,12 @@
+
+
+
+
+
+
+
diff --git a/lib/infrastructure/service/app_navigation_observer.dart b/lib/infrastructure/service/app_navigation_observer.dart
index 44357cd..3fe440f 100644
--- a/lib/infrastructure/service/app_navigation_observer.dart
+++ b/lib/infrastructure/service/app_navigation_observer.dart
@@ -16,7 +16,6 @@ class CustomNavigationObserver extends NavigatorObserver {
@override
void didPush(Route route, Route? previousRoute) async {
- super.didPush(route, previousRoute);
final routeName = route.settings.name;
if (!_isWorkDone && (routeName == ChickenRoutes.init || routeName == ChickenRoutes.auth)) {
_isWorkDone = true;
@@ -31,6 +30,7 @@ class CustomNavigationObserver extends NavigatorObserver {
_isWorkDone = true;
await setupLiveStockDI();
}
+ super.didPush(route, previousRoute);
tLog('CustomNavigationObserver: didPush - $routeName');
}
diff --git a/lib/presentation/pages/modules/logic.dart b/lib/presentation/pages/modules/logic.dart
index d35b096..8985caf 100644
--- a/lib/presentation/pages/modules/logic.dart
+++ b/lib/presentation/pages/modules/logic.dart
@@ -1,7 +1,12 @@
+import 'package:rasadyar_chicken/presentation/routes/routes.dart';
import 'package:rasadyar_core/core.dart';
+import 'package:rasadyar_inspection/inspection.dart';
+import 'package:rasadyar_livestock/injection/live_stock_di.dart';
+import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
class ModulesLogic extends GetxController {
TokenStorageService tokenService = Get.find();
+ RxBool isLoading = false.obs;
List moduleList = [
ModuleModel(title: 'بازرسی', icon: Assets.icons.inspection.path, module: Module.inspection),
@@ -25,4 +30,23 @@ class ModulesLogic extends GetxController {
tokenService.saveModule(module);
tokenService.appModule.value = module;
}
+
+ Future navigateToModule(Module module) async {
+ if (module == Module.inspection) {
+ Get.offAllNamed(InspectionRoutes.init);
+ } else if (module == Module.liveStocks) {
+ await setupLiveStockDI();
+ Get.offAllNamed(LiveStockRoutes.init);
+ } else if (module == Module.chicken) {
+ Get.offAllNamed(ChickenRoutes.init);
+ }
+ }
+
+ void onTapCard(Module module, int index) async {
+ isLoading.value = true;
+ selectedIndex.value = index;
+ saveModule(module);
+ await Future.delayed(Duration(milliseconds: 800)); // Simulate loading delay
+ navigateToModule(module);
+ }
}
diff --git a/lib/presentation/pages/modules/view.dart b/lib/presentation/pages/modules/view.dart
index 880050a..6ff320a 100644
--- a/lib/presentation/pages/modules/view.dart
+++ b/lib/presentation/pages/modules/view.dart
@@ -1,7 +1,6 @@
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:rasadyar_chicken/chicken.dart';
import 'package:rasadyar_core/core.dart';
-import 'package:rasadyar_inspection/inspection.dart';
import 'logic.dart';
@@ -16,39 +15,38 @@ class ModulesPage extends GetView {
centerTitle: true,
backgroundColor: AppColor.blueNormal,
),
- body: GridView.builder(
- padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
-
- itemBuilder: (context, index) {
- final module = controller.moduleList[index];
- return CardIcon(
- title: module.title,
- icon: module.icon,
- onTap: () {
- controller.selectedIndex.value = index;
- controller.saveModule(module.module);
-
- // Navigate to the appropriate route based on the selected module
- switch (module.module) {
- case Module.inspection:
- Get.toNamed(InspectionRoutes.init);
- break;
- case Module.liveStocks:
- //TODO: Implement liveStocks module navigation
- case Module.chicken:
- Get.toNamed(ChickenRoutes.init);
- break;
- }
+ body: Stack(
+ fit: StackFit.expand,
+ alignment: Alignment.center,
+ children: [
+ GridView.builder(
+ padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
+ itemBuilder: (context, index) {
+ final module = controller.moduleList[index];
+ return CardIcon(
+ title: module.title,
+ icon: module.icon,
+ onTap: () => controller.onTapCard(module.module, index),
+ );
},
- );
- },
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- mainAxisSpacing: 10,
- crossAxisSpacing: 10,
- ),
- physics: BouncingScrollPhysics(),
- itemCount: controller.moduleList.length,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3,
+ mainAxisSpacing: 10,
+ crossAxisSpacing: 10,
+ ),
+ physics: BouncingScrollPhysics(),
+ itemCount: controller.moduleList.length,
+ ),
+ ObxValue((loading) {
+ if (!controller.isLoading.value) return SizedBox.shrink();
+ return Container(
+ color: Colors.grey.withValues(alpha: 0.5),
+ child: Center(
+ child: CupertinoActivityIndicator(color: AppColor.greenNormal, radius: 30),
+ ),
+ );
+ }, controller.isLoading),
+ ],
),
);
}
diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart
index c881e08..1539941 100644
--- a/packages/core/lib/core.dart
+++ b/packages/core/lib/core.dart
@@ -8,6 +8,7 @@ export 'package:dio/dio.dart';
export 'package:flutter_localizations/flutter_localizations.dart';
export 'package:flutter_map/flutter_map.dart';
export 'package:flutter_map_animations/flutter_map_animations.dart';
+export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
export 'package:flutter_rating_bar/flutter_rating_bar.dart';
export 'package:flutter_screenutil/flutter_screenutil.dart';
export 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -21,6 +22,7 @@ export 'package:get/get.dart' hide FormData, MultipartFile, Response;
export 'package:get_it/get_it.dart';
//local storage
export 'package:hive_ce_flutter/hive_flutter.dart';
+export 'package:image_cropper/image_cropper.dart';
///image picker
export 'package:image_picker/image_picker.dart';
//encryption
@@ -36,7 +38,6 @@ export 'package:pretty_dio_logger/pretty_dio_logger.dart';
export 'package:rasadyar_core/presentation/common/common.dart';
export 'package:rasadyar_core/presentation/utils/utils.dart';
export 'package:rasadyar_core/presentation/widget/widget.dart';
-export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
//models
export 'data/model/model.dart';
@@ -57,5 +58,4 @@ export 'utils/map_utils.dart';
export 'utils/network/network.dart';
export 'utils/route_utils.dart';
export 'utils/separator_input_formatter.dart';
-
export 'utils/utils.dart';
diff --git a/packages/core/lib/infrastructure/remote/app_interceptor.dart b/packages/core/lib/infrastructure/remote/app_interceptor.dart
index 8af53d5..5685f57 100644
--- a/packages/core/lib/infrastructure/remote/app_interceptor.dart
+++ b/packages/core/lib/infrastructure/remote/app_interceptor.dart
@@ -10,7 +10,7 @@ class AppInterceptor extends Interceptor {
final RefreshTokenCallback? refreshTokenCallback;
final SaveTokenCallback saveTokenCallback;
final ClearTokenCallback clearTokenCallback;
- late final Dio dio;
+ late Dio dio;
dynamic authArguments;
static Completer? _refreshCompleter;
static bool _isRefreshing = false;
@@ -44,7 +44,7 @@ class AppInterceptor extends Interceptor {
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
- if (err.response?.statusCode == 401) {
+ if (err.response?.statusCode == 401 && err.response?.data['detail'] != "No active account found with the given credentials") {
final retryResult = await _handleUnauthorizedError(err);
if (retryResult != null) {
handler.resolve(retryResult);
@@ -104,6 +104,7 @@ class AppInterceptor extends Interceptor {
return dio.fetch(newOptions);
}
+ //TODO
void _handleRefreshFailure() {
ApiHandler.cancelAllRequests("Token refresh failed");
diff --git a/packages/core/lib/infrastructure/remote/dio_remote.dart b/packages/core/lib/infrastructure/remote/dio_remote.dart
index 473ca3e..184595a 100644
--- a/packages/core/lib/infrastructure/remote/dio_remote.dart
+++ b/packages/core/lib/infrastructure/remote/dio_remote.dart
@@ -12,6 +12,7 @@ class DioRemote implements IHttpClient {
Future init() async {
dio = Dio(BaseOptions(baseUrl: baseUrl ?? ''));
if (interceptors != null) {
+ interceptors!.dio = dio;
dio.interceptors.add(interceptors!);
}
diff --git a/packages/core/pubspec.lock b/packages/core/pubspec.lock
index 9eea597..936d0e5 100644
--- a/packages/core/pubspec.lock
+++ b/packages/core/pubspec.lock
@@ -725,6 +725,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
+ image_cropper:
+ dependency: "direct main"
+ description:
+ name: image_cropper
+ sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.1.0"
+ image_cropper_for_web:
+ dependency: transitive
+ description:
+ name: image_cropper_for_web
+ sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.0"
+ image_cropper_platform_interface:
+ dependency: transitive
+ description:
+ name: image_cropper_platform_interface
+ sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.1.0"
image_picker:
dependency: "direct main"
description:
diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml
index f87b92b..90dd599 100644
--- a/packages/core/pubspec.yaml
+++ b/packages/core/pubspec.yaml
@@ -18,7 +18,7 @@ dependencies:
##image_picker
image_picker: ^1.1.2
-
+ image_cropper: ^9.1.0
#UI
cupertino_icons: ^1.0.8
diff --git a/packages/livestock/lib/data/common/dio_exception_handeler.dart b/packages/livestock/lib/data/common/dio_exception_handeler.dart
index 6f5e233..e3f34d2 100644
--- a/packages/livestock/lib/data/common/dio_exception_handeler.dart
+++ b/packages/livestock/lib/data/common/dio_exception_handeler.dart
@@ -32,22 +32,16 @@ class DioErrorHandler {
_errorSnackBar(
error.response?.data.keys.first == 'is_user'
? 'کاربر با این شماره تلفن وجود ندارد'
- : error.response?.data[error.response?.data.keys.first] ??
- 'خطا در برقراری ارتباط با سرور',
+ : '${error.response?.statusCode} - ${error.response?.data[error.response?.data.keys.first]}' ??
+ 'خطا در برقراری ارتباط با سرور',
),
);
}
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),
- ),
+ 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,
diff --git a/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart b/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart
index f67a63d..e4f227b 100644
--- a/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart
+++ b/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart
@@ -16,7 +16,6 @@ class AuthRemoteDataSourceImp extends AuthRemoteDataSource {
'${_BASE_URL}login/',
data: authRequest,
fromJson: AuthResponseModel.fromJson,
- headers: {'Content-Type': 'application/json'},
);
return res.data;
}
diff --git a/packages/livestock/lib/injection/live_stock_di.dart b/packages/livestock/lib/injection/live_stock_di.dart
index 10dd71e..954be4c 100644
--- a/packages/livestock/lib/injection/live_stock_di.dart
+++ b/packages/livestock/lib/injection/live_stock_di.dart
@@ -11,27 +11,17 @@ GetIt get diLiveStock => GetIt.instance;
Future setupLiveStockDI() async {
diLiveStock.registerSingleton(DioErrorHandler());
-
final tokenService = Get.find();
-
if (tokenService.baseurl.value == null) {
await tokenService.saveBaseUrl('https://api.dam.rasadyar.net/');
}
-
- diLiveStock.registerLazySingleton(
- () => AuthRemoteDataSourceImp(diLiveStock.get()),
- );
-
- diLiveStock.registerLazySingleton(
- () => AuthRepositoryImp(diLiveStock.get()),
- );
-
-
+ // First register AppInterceptor with lazy callbacks
diLiveStock.registerLazySingleton(
- () => AppInterceptor(
+ () => AppInterceptor(
refreshTokenCallback: () async {
+ // Use lazy access to avoid circular dependency
final authRepository = diLiveStock.get();
final hasAuthenticated = await authRepository.hasAuthenticated();
if (hasAuthenticated) {
@@ -53,13 +43,26 @@ Future setupLiveStockDI() async {
),
);
-
+ // Register DioRemote with the interceptor
diLiveStock.registerLazySingleton(
- () => DioRemote(
+ () => DioRemote(
baseUrl: tokenService.baseurl.value,
- interceptors: diLiveStock.get(),
+ // interceptors: diLiveStock.get(),
),
);
+ // Initialize DioRemote
await diLiveStock.get().init();
+
+ // Now register the data source and repository
+ diLiveStock.registerLazySingleton(
+ () => AuthRemoteDataSourceImp(diLiveStock.get()),
+ );
+
+ diLiveStock.registerLazySingleton(
+ () => AuthRepositoryImp(diLiveStock.get()),
+ );
+
+ diLiveStock.registerLazySingleton(() => ImagePicker());
+ await diLiveStock.allReady();
}
diff --git a/packages/livestock/lib/presentation/page/auth/logic.dart b/packages/livestock/lib/presentation/page/auth/logic.dart
index 2431423..755fe07 100644
--- a/packages/livestock/lib/presentation/page/auth/logic.dart
+++ b/packages/livestock/lib/presentation/page/auth/logic.dart
@@ -122,7 +122,7 @@ class AuthLogic extends GetxController with GetTickerProviderStateMixin {
final loginRequestModel = _buildLoginRequest();
isLoading.value = true;
await safeCall(
- call: () async => authRepository.login(authRequest: loginRequestModel.toJson()),
+ call: () async => await authRepository.login(authRequest: loginRequestModel.toJson()),
onSuccess: (result) async {
await tokenStorageService.saveModule(_module);
await tokenStorageService.saveRefreshToken(result?.refresh ?? '');
diff --git a/packages/livestock/lib/presentation/page/auth/view.dart b/packages/livestock/lib/presentation/page/auth/view.dart
index 2c5d35c..f0a71b4 100644
--- a/packages/livestock/lib/presentation/page/auth/view.dart
+++ b/packages/livestock/lib/presentation/page/auth/view.dart
@@ -17,27 +17,29 @@ class AuthPage extends GetView {
children: [
Assets.vec.bgAuthSvg.svg(fit: BoxFit.fill),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 10.r),
- child: FadeTransition(
- opacity: controller.textAnimation,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.center,
- spacing: 12,
- children: [
- Text(
- 'به سامانه رصدیار خوش آمدید!',
- textAlign: TextAlign.right,
- style: AppFonts.yekan25Bold.copyWith(color: Colors.white),
- ),
- Text(
- 'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی',
- textAlign: TextAlign.center,
- style: AppFonts.yekan16.copyWith(color: Colors.white),
- ),
- ],
+ Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.r),
+ child: FadeTransition(
+ opacity: controller.textAnimation,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ spacing: 12,
+ children: [
+ Text(
+ 'به سامانه رصدیار خوش آمدید!',
+ textAlign: TextAlign.right,
+ style: AppFonts.yekan25Bold.copyWith(color: Colors.white),
+ ),
+ Text(
+ 'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی',
+ textAlign: TextAlign.center,
+ style: AppFonts.yekan16.copyWith(color: Colors.white),
+ ),
+ ],
+ ),
),
),
),
diff --git a/packages/livestock/lib/presentation/page/map/logic.dart b/packages/livestock/lib/presentation/page/map/logic.dart
index 04370f3..f68b238 100644
--- a/packages/livestock/lib/presentation/page/map/logic.dart
+++ b/packages/livestock/lib/presentation/page/map/logic.dart
@@ -1,8 +1,7 @@
import 'package:rasadyar_core/core.dart';
+import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
class MapLogic extends GetxController {
-
var ss = Get.find();
-
-
+ BaseLogic baseLogic = Get.find();
}
diff --git a/packages/livestock/lib/presentation/page/map/view.dart b/packages/livestock/lib/presentation/page/map/view.dart
index 451a95e..670308d 100644
--- a/packages/livestock/lib/presentation/page/map/view.dart
+++ b/packages/livestock/lib/presentation/page/map/view.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/view.dart';
+import 'package:rasadyar_livestock/presentation/widgets/base_page/view.dart';
import 'logic.dart';
@@ -9,6 +10,158 @@ class MapPage extends GetView {
@override
Widget build(BuildContext context) {
- return Scaffold(body: Stack(children: [MapWidget()]));
+ return BasePage(
+ hasSearch: true,
+ hasFilter: true,
+ hasBack: false,
+ defaultSearch: false,
+ filteringWidget: filterWidget(showIndex: 3.obs, filterIndex: 5.obs),
+ widgets: [MapWidget()],
+ );
+ }
+
+ Widget filterWidget({required RxInt filterIndex, required RxInt showIndex}) {
+ return BaseBottomSheet(
+ height: Get.height * 0.5,
+ child: Container(color: Colors.red),
+ );
+ }
+
+ BaseBottomSheet searchWidget() {
+ return BaseBottomSheet(
+ height: Get.height * 0.85,
+ rootChild: Column(
+ spacing: 8,
+ children: [
+ Row(
+ spacing: 12,
+ children: [
+ Expanded(
+ child: RTextField(
+ height: 40,
+ borderColor: AppColor.blackLight,
+ suffixIcon: ObxValue(
+ (data) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: (data.value == null)
+ ? Assets.vec.searchSvg.svg(
+ width: 10,
+ height: 10,
+ colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
+ )
+ : IconButton(
+ onPressed: () {
+ controller.baseLogic.searchTextController.clear();
+ controller.baseLogic.searchValue.value = null;
+ controller.baseLogic.isSearchSelected.value = false;
+ //controller.mapLogic.hasFilterOrSearch.value = false;
+ //controller.searchedPoultryLocation.value = Resource.initial();
+ },
+ enableFeedback: true,
+ padding: EdgeInsets.zero,
+ iconSize: 24,
+ splashRadius: 50,
+ icon: Assets.vec.closeCircleSvg.svg(
+ width: 20,
+ height: 20,
+ colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
+ ),
+ ),
+ ),
+ controller.baseLogic.searchValue,
+ ),
+ hintText: 'جستجو کنید ...',
+ hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal),
+ filledColor: Colors.white,
+ filled: true,
+ controller: controller.baseLogic.searchTextController,
+ onChanged: (val) => controller.baseLogic.searchValue.value = val,
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ Get.back();
+ },
+ child: Assets.vec.mapSvg.svg(
+ width: 24.w,
+ height: 24.h,
+ colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
+ ),
+ ),
+ ],
+ ),
+ /* Expanded(
+ child: ObxValue((rxData) {
+ final resource = rxData.value;
+ final status = resource.status;
+ final items = resource.data;
+ final message = resource.message ?? 'خطا در بارگذاری';
+
+ if (status == ResourceStatus.initial) {
+ return Center(child: Text('ابتدا جستجو کنید'));
+ }
+
+ if (status == ResourceStatus.loading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ if (status == ResourceStatus.error) {
+ return Center(child: Text(message));
+ }
+
+ if (items == null || items.isEmpty) {
+ return Center(child: EmptyWidget());
+ }
+
+ return ListView.separated(
+ itemCount: items.length,
+ separatorBuilder: (context, index) => SizedBox(height: 8),
+ itemBuilder: (context, index) {
+ final item = items[index]; // اگر item استفاده نمیشه، میتونه حذف بشه
+ return ListItem2(
+ index: index,
+ labelColor: AppColor.blueLight,
+ labelIcon: Assets.vec.cowSvg.path,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ item.unitName ?? 'N/A',
+ style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
+ ),
+ Text(
+ item.user?.fullname ?? '',
+ style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
+ ),
+ ],
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ 'جوجه ریزی فعال',
+ style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
+ ),
+ Text(
+ (item.hatching != null && item.hatching!.isNotEmpty)
+ ? 'دارد'
+ : 'ندراد',
+ style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }, controller.searchedPoultryLocation),
+ ),*/
+ ],
+ ),
+ );
}
}
diff --git a/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart b/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart
index bd2226b..30c7a42 100644
--- a/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart
+++ b/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
+import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
import 'logic.dart';
@@ -172,8 +173,40 @@ class MapWidget extends GetView {
.map(
(element) => Marker(
point: element,
- child: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error),
+ child: IconButton(
+ onPressed: () {
+ Get.bottomSheet(
+ detailsBottomSheet(),
+ isScrollControlled: true,
+ isDismissible: true,
+ ignoreSafeArea: false,
+ );
+ },
+ icon: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error),
+ ),
),
)
.toList();
+
+ Widget detailsBottomSheet() {
+ return BaseBottomSheet(
+ height: 250.h,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 20,
+ children: [
+ Text('مشخصات محل', style: AppFonts.yekan16Bold),
+ // Add more details here
+ RElevated(
+ text: 'ایجاد بازرسی',
+ width: Get.width,
+ height: 40.h,
+ onPressed: () {
+ Get.toNamed(LiveStockRoutes.requestTagging);
+ },
+ ),
+ ],
+ ),
+ );
+ }
}
diff --git a/packages/livestock/lib/presentation/page/request_tagging/logic.dart b/packages/livestock/lib/presentation/page/request_tagging/logic.dart
index de94cdb..f8875fc 100644
--- a/packages/livestock/lib/presentation/page/request_tagging/logic.dart
+++ b/packages/livestock/lib/presentation/page/request_tagging/logic.dart
@@ -1,10 +1,32 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
-import 'package:rasadyar_livestock/presentation/page/root/logic.dart';
+import 'package:rasadyar_livestock/injection/live_stock_di.dart';
class RequestTaggingLogic extends GetxController {
+ RxInt currentIndex = 0.obs;
+ final int maxStep = 3;
+
+ RxBool nextButtonEnabled = true.obs;
+
+ final TextEditingController phoneController = TextEditingController();
+ final TextEditingController fullNameController = TextEditingController();
+ final TextEditingController addressController = TextEditingController();
+
+ ImagePicker imagePicker = diLiveStock.get();
+ Rxn rancherImage = Rxn(null);
+
+ @override
+ void onInit() {
+ super.onInit();
+ setUpTextControllerListeners();
+ setUpNextButtonListeners();
+ ever(rancherImage, (callback) {
+ setUpNextButtonListeners();
+ });
+ }
-final TextEditingController phoneController = TextEditingController();
@override
void onReady() {
super.onReady();
@@ -14,4 +36,68 @@ final TextEditingController phoneController = TextEditingController();
void onClose() {
super.onClose();
}
+
+ void onNext() {
+ if (currentIndex.value < maxStep) {
+ if (currentIndex.value == 0) {}
+ currentIndex.value++;
+ }
+ }
+
+ void onPrevious() {
+ if (currentIndex.value > 0) {
+ currentIndex.value--;
+ }
+ }
+
+ void setUpNextButtonListeners() {
+ if (currentIndex.value == 0) {
+ nextButtonEnabled.value =
+ phoneController.text.isNotEmpty &&
+ fullNameController.text.isNotEmpty &&
+ addressController.text.isNotEmpty &&
+ rancherImage.value != null;
+
+ return;
+ }
+ }
+
+ void setUpTextControllerListeners() {
+ phoneController.addListener(setUpNextButtonListeners);
+ fullNameController.addListener(setUpNextButtonListeners);
+ addressController.addListener(setUpNextButtonListeners);
+ }
+
+ Future pickImage() async {
+ rancherImage.value = await imagePicker.pickImage(
+ source: ImageSource.camera,
+ imageQuality: 60,
+ maxWidth: 1080,
+ maxHeight: 720,
+ );
+
+ getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Picked');
+ }
+
+ Future cropImage() async {
+ if (rancherImage.value == null) return;
+
+ final CroppedFile? cropped = await ImageCropper().cropImage(
+ sourcePath: rancherImage.value!.path,
+ maxWidth: 1080,
+ maxHeight: 720,
+ compressQuality: 60,
+ );
+ if (cropped == null) return;
+ rancherImage.value = XFile(cropped.path);
+
+ getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Cropped');
+ }
+
+ void getFileSizeInKB(String filePath, {String? tag}) {
+ final file = File(filePath);
+ final bytes = file.lengthSync();
+ var size = (bytes / 1024).ceil();
+ iLog('${tag ?? 'Picked'} image Size: $size');
+ }
}
diff --git a/packages/livestock/lib/presentation/page/request_tagging/view.dart b/packages/livestock/lib/presentation/page/request_tagging/view.dart
index 0ff57ed..73ed45c 100644
--- a/packages/livestock/lib/presentation/page/request_tagging/view.dart
+++ b/packages/livestock/lib/presentation/page/request_tagging/view.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
@@ -19,91 +21,272 @@ class RequestTaggingPage extends GetView {
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
- child: Column(
- children: [
- RTextField(
- controller: controller.phoneController,
- label: 'تلفن دامدار',
+ child: ObxValue((index) {
+ return Column(
+ children: [
+ Expanded(child: _buildStep(index.value)),
+ nextOrPreviousWidget(),
+ ],
+ );
+ }, controller.currentIndex),
+ ),
+ );
+ }
+
+ Row nextOrPreviousWidget() {
+ return Row(
+ spacing: 10,
+ children: [
+ ObxValue(
+ (data) => Expanded(
+ flex: 2,
+ child: RElevated(
+ height: 40.h,
+ enabled: data.value,
+ onPressed: controller.onNext,
+ child: Text('بعدی'),
+ backgroundColor: AppColor.blueNormal,
),
+ ),
+ controller.nextButtonEnabled,
+ ),
+ Expanded(
+ child: ROutlinedElevated(
+ enabled: controller.currentIndex.value > 0,
+ onPressed: controller.onPrevious,
+ child: Text('قبلی'),
+ borderColor: AppColor.error,
+ ),
+ ),
+ ],
+ );
+ }
- SizedBox(
- width: Get.width,
- height: 356,
- child: Card(
- color: Colors.white,
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- children: [
- Expanded(
- child: Container(
- width: Get.width,
+ Widget _buildStep(int index) {
+ switch (index) {
+ case 0:
+ return firstStepWidget();
+ default:
+ return Center(
+ child: Text(
+ 'مرحله $index در دست توسعه است',
+ style: AppFonts.yekan16.copyWith(color: AppColor.redNormal),
+ textDirection: TextDirection.rtl,
+ ),
+ );
+ }
+ }
- decoration: BoxDecoration(
- color: AppColor.lightGreyNormal,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Center(
- child: Assets.images.placeHolder.image(
- height: 150,
- width: 200,
- ),
- ),
- ),
+ Widget firstStepWidget() {
+ return Form(
+ child: Column(
+ spacing: 16,
+ children: [
+ Container(
+ padding: EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColor.lightGreyNormal, width: 1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ spacing: 10,
+ children: [
+ Row(children: [Text('اطلاعات دامدار', style: AppFonts.yekan16Bold)]),
+ RTextField(controller: controller.fullNameController, label: 'نام ونام خانوادگی'),
+ RTextField(controller: controller.phoneController, label: 'تلفن'),
+ RTextField(controller: controller.addressController, label: 'ادرس'),
+ ],
+ ),
+ ),
+
+ SizedBox(
+ width: Get.width,
+ height: 356.h,
+ child: Container(
+ padding: EdgeInsets.all(8.r),
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColor.lightGreyNormal, width: 1.w),
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ child: Column(
+ spacing: 8,
+ children: [
+ Row(children: [Text('تصویر دامدار', style: AppFonts.yekan16Bold)]),
+ Expanded(
+ child: Container(
+ width: Get.width,
+ decoration: BoxDecoration(
+ color: AppColor.lightGreyNormal,
+ borderRadius: BorderRadius.circular(8.r),
),
- SizedBox(height: 15),
- Container(
- width: Get.width,
- height: 40,
- clipBehavior: Clip.antiAlias,
- decoration: ShapeDecoration(
- color: AppColor.blueNormal,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- child: Padding(
- padding: const EdgeInsets.all(10.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- ' تصویر گله',
- style: AppFonts.yekan14.copyWith(
- color: Colors.white,
- ),
- ),
- Icon(
- CupertinoIcons.arrow_up_doc,
- color: Colors.white,
- ),
- ],
- ),
- ),
+ child: Center(
+ child: ObxValue((tmpImage) {
+ if (tmpImage.value == null) {
+ return Assets.vec.placeHolderSvg.svg(height: 150.h, width: 200.w);
+ } else {
+ return Image.file(File(tmpImage.value!.path), fit: BoxFit.cover);
+ }
+ }, controller.rancherImage),
),
- ],
+ ),
),
- ),
+ GestureDetector(
+ onTap: () async {
+ await controller.pickImage();
+ await showCropDialog();
+ },
+ child: Container(
+ width: Get.width,
+ height: 40.h,
+ clipBehavior: Clip.antiAlias,
+ decoration: ShapeDecoration(
+ color: AppColor.blueNormal,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(10.r),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(' دوربین', style: AppFonts.yekan14.copyWith(color: Colors.white)),
+ Icon(CupertinoIcons.arrow_up_doc, color: Colors.white),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
),
),
+ ),
- Spacer(),
+ Spacer(),
+ /* RElevated(
+ text: 'ارسال تصویر گله',
+ onPressed: () {
+ Get.toNamed(LiveStockRoutes.tagging);
+ },
+ height: 40,
+ isFullWidth: true,
+ backgroundColor: AppColor.greenNormal,
+ textStyle: AppFonts.yekan16.copyWith(color: Colors.white),
+ ),*/
+ ],
+ ),
+ );
+ }
+ Future showCropDialog() async {
+ await Get.dialog(
+ Dialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ 'آیا نیازی به برش تصویر دارید؟',
+ style: AppFonts.yekan16Bold,
+ textAlign: TextAlign.center,
+ ),
- RElevated(
- text: 'ارسال تصویر گله',
- onPressed: () {
- Get.toNamed(LiveStockRoutes.tagging);
- },
- height: 40,
- isFullWidth: true,
- backgroundColor: AppColor.greenNormal,
- textStyle: AppFonts.yekan16.copyWith(color: Colors.white),
- ),
- ],
+ const SizedBox(height: 24),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ spacing: 12.w,
+ children: [
+ RElevated(
+ height: 40.h,
+ onPressed: () async {
+ Get.back();
+ await controller.cropImage();
+ },
+ child: const Text('بله'),
+ ),
+ ROutlinedElevated(
+ onPressed: () => Get.back(),
+ child: const Text('خیر'),
+ borderColor: AppColor.error,
+ ),
+ ],
+ ),
+ ],
+ ),
),
),
);
}
+
+ Column secondStepWidget() {
+ return Column(
+ children: [
+ RTextField(controller: controller.phoneController, label: 'تلفن دامدار'),
+
+ SizedBox(
+ width: Get.width,
+ height: 356,
+ child: Card(
+ color: Colors.white,
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: [
+ Expanded(
+ child: Container(
+ width: Get.width,
+
+ decoration: BoxDecoration(
+ color: AppColor.lightGreyNormal,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Center(
+ child: Assets.images.placeHolder.image(height: 150, width: 200),
+ ),
+ ),
+ ),
+ SizedBox(height: 15),
+ Container(
+ width: Get.width,
+ height: 40,
+ clipBehavior: Clip.antiAlias,
+ decoration: ShapeDecoration(
+ color: AppColor.blueNormal,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(' تصویر گله', style: AppFonts.yekan14.copyWith(color: Colors.white)),
+ Icon(CupertinoIcons.arrow_up_doc, color: Colors.white),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ Spacer(),
+
+ RElevated(
+ text: 'ارسال تصویر گله',
+ onPressed: () {
+ Get.toNamed(LiveStockRoutes.tagging);
+ },
+ height: 40,
+ isFullWidth: true,
+ backgroundColor: AppColor.greenNormal,
+ textStyle: AppFonts.yekan16.copyWith(color: Colors.white),
+ ),
+ ],
+ );
+ }
}
diff --git a/packages/livestock/lib/presentation/page/root/logic.dart b/packages/livestock/lib/presentation/page/root/logic.dart
index 4bb8c4c..7496d7a 100644
--- a/packages/livestock/lib/presentation/page/root/logic.dart
+++ b/packages/livestock/lib/presentation/page/root/logic.dart
@@ -26,7 +26,7 @@ class RootLogic extends GetxController {
ProfilePage(),
];
- RxInt currentIndex = 1.obs;
+ RxInt currentIndex = 0.obs;
@override
void onReady() {
diff --git a/packages/livestock/lib/presentation/routes/app_pages.dart b/packages/livestock/lib/presentation/routes/app_pages.dart
index fa24e46..411f5c0 100644
--- a/packages/livestock/lib/presentation/routes/app_pages.dart
+++ b/packages/livestock/lib/presentation/routes/app_pages.dart
@@ -11,6 +11,7 @@ import 'package:rasadyar_livestock/presentation/page/root/logic.dart';
import 'package:rasadyar_livestock/presentation/page/root/view.dart';
import 'package:rasadyar_livestock/presentation/page/tagging/logic.dart';
import 'package:rasadyar_livestock/presentation/page/tagging/view.dart';
+import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
import 'package:rasadyar_livestock/presentation/widgets/captcha/logic.dart';
part 'app_routes.dart';
@@ -38,6 +39,7 @@ sealed class LiveStockPages {
Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => MapWidgetLogic());
+ Get.lazyPut(() => BaseLogic());
}),
children: [
/*GetPage(
diff --git a/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart b/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart
new file mode 100644
index 0000000..2d776c5
--- /dev/null
+++ b/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:rasadyar_core/core.dart';
+import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
+
+RAppBar liveStockAppBar({
+ bool hasBack = true,
+ bool hasFilter = true,
+ bool hasSearch = true,
+ bool isBase = false,
+ VoidCallback? onBackPressed,
+ GestureTapCallback? onFilterTap,
+ GestureTapCallback? onSearchTap,
+}) {
+ return RAppBar(
+ hasBack: isBase == true ? false : hasBack,
+ onBackPressed: onBackPressed,
+ leadingWidth: 155,
+ leading: Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 6,
+ children: [
+ Text('رصددام', style: AppFonts.yekan16Bold.copyWith(color: Colors.white)),
+ Assets.vec.appBarInspectionSvg.svg(width: 24, height: 24),
+ ],
+ ),
+ additionalActions: [
+ if (!isBase && hasSearch) searchWidget(onSearchTap),
+ SizedBox(width: 8),
+ if (!isBase && hasFilter) filterWidget(onFilterTap),
+ SizedBox(width: 8),
+ ],
+ );
+}
+
+GestureDetector filterWidget(GestureTapCallback? onFilterTap) {
+ return GestureDetector(
+ onTap: onFilterTap,
+ child: Stack(
+ alignment: Alignment.topRight,
+ children: [
+ Assets.vec.filterOutlineSvg.svg(
+ width: 20,
+ height: 20,
+ colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
+ ),
+ Obx(() {
+ final controller = Get.find();
+ return Visibility(
+ visible: controller.isFilterSelected.value,
+ child: Container(
+ width: 8,
+ height: 8,
+ decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
+ ),
+ );
+ }),
+ ],
+ ),
+ );
+}
+
+GestureDetector searchWidget(GestureTapCallback? onSearchTap) {
+ return GestureDetector(
+ onTap: onSearchTap,
+ child: Stack(
+ alignment: Alignment.topRight,
+ children: [
+ Assets.vec.searchSvg.svg(
+ width: 24,
+ height: 24,
+ colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
+ ),
+ Obx(() {
+ final controller = Get.find();
+ return Visibility(
+ visible: controller.searchValue.value != null,
+ child: Container(
+ width: 8,
+ height: 8,
+ decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
+ ),
+ );
+ }),
+ ],
+ ),
+ );
+}
diff --git a/packages/livestock/lib/presentation/widgets/base_page/logic.dart b/packages/livestock/lib/presentation/widgets/base_page/logic.dart
new file mode 100644
index 0000000..79ac095
--- /dev/null
+++ b/packages/livestock/lib/presentation/widgets/base_page/logic.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/cupertino.dart';
+import 'package:rasadyar_core/core.dart';
+
+class BaseLogic extends GetxController {
+ final RxBool isFilterSelected = false.obs;
+ final RxBool isSearchSelected = false.obs;
+ final TextEditingController searchTextController = TextEditingController();
+ final RxnString searchValue = RxnString();
+
+ void setSearchCallback(void Function(String)? onSearchChanged) {
+ debounce(searchValue, (val) {
+ if (val != null && val.trim().isNotEmpty) {
+ onSearchChanged?.call(val);
+ }
+ }, time: const Duration(milliseconds: 600));
+ }
+
+ void toggleFilter() {
+ isFilterSelected.value = !isFilterSelected.value;
+ }
+
+ void toggleSearch() {
+ isSearchSelected.value = !isSearchSelected.value;
+ }
+}
diff --git a/packages/livestock/lib/presentation/widgets/base_page/view.dart b/packages/livestock/lib/presentation/widgets/base_page/view.dart
new file mode 100644
index 0000000..adeea02
--- /dev/null
+++ b/packages/livestock/lib/presentation/widgets/base_page/view.dart
@@ -0,0 +1,143 @@
+import 'package:flutter/material.dart';
+import 'package:rasadyar_core/core.dart';
+import 'package:rasadyar_livestock/presentation/widgets/app_bar/i_app_bar.dart';
+import 'package:rasadyar_livestock/presentation/widgets/search.dart';
+
+import 'logic.dart';
+
+class BasePage extends StatefulWidget {
+ const BasePage({
+ super.key,
+ this.routes,
+ required this.widgets,
+ this.routesWidget,
+ this.floatingActionButtonLocation,
+ this.floatingActionButton,
+ this.onSearchChanged,
+ this.hasBack = true,
+ this.hasFilter = true,
+ this.hasSearch = true,
+ this.isBase = false,
+ this.defaultSearch = true,
+ this.onBackPressed,
+ this.onFilterTap,
+ this.onSearchTap,
+ this.filteringWidget,
+ });
+
+ final List? routes;
+ final Widget? routesWidget;
+ final bool defaultSearch;
+ final List widgets;
+ final FloatingActionButtonLocation? floatingActionButtonLocation;
+ final Widget? floatingActionButton;
+ final Widget? filteringWidget;
+ final void Function(String?)? onSearchChanged;
+ final bool hasBack;
+ final bool hasFilter;
+ final bool hasSearch;
+ final bool isBase;
+ final VoidCallback? onBackPressed;
+ final GestureTapCallback? onFilterTap;
+ final GestureTapCallback? onSearchTap;
+
+ @override
+ State createState() => _BasePageState();
+}
+
+class _BasePageState extends State {
+ BaseLogic get controller => Get.find();
+ Worker? filterWorker;
+ bool _isBottomSheetOpen = false;
+
+ @override
+ void initState() {
+ super.initState();
+ /* filterWorker = ever(controller.isFilterSelected, (bool isSelected) {
+ if (!mounted) return;
+
+ if (isSelected && widget.filteringWidget != null) {
+ // بررسی اینکه آیا bottomSheet از قبل باز است یا نه
+ if (_isBottomSheetOpen) {
+ controller.isFilterSelected.value = false;
+ return;
+ }
+
+ // بررسی اینکه آیا route فعلی current است یا نه
+ if (ModalRoute.of(context)?.isCurrent != true) {
+ controller.isFilterSelected.value = false;
+ return;
+ }
+
+ _isBottomSheetOpen = true;
+ Get.bottomSheet(
+ widget.filteringWidget!,
+ isScrollControlled: true,
+ isDismissible: true,
+ enableDrag: true,
+ ).then((_) {
+ // تنظیم مقدار به false بعد از بسته شدن bottomSheet
+ if (mounted) {
+ _isBottomSheetOpen = false;
+ controller.isFilterSelected.value = false;
+ }
+ });
+ }
+ });*/
+ }
+
+ @override
+ void dispose() {
+ filterWorker?.dispose();
+ super.dispose();
+ }
+
+ void _onFilterTap() {
+ if (widget.hasFilter && widget.filteringWidget != null) {
+ final currentRoute = ModalRoute.of(context);
+ if (currentRoute?.isCurrent != true) {
+ return;
+ }
+
+ Get.bottomSheet(
+ widget.filteringWidget!,
+ isScrollControlled: true,
+ isDismissible: true,
+ enableDrag: true,
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (didPop, result) => widget.onBackPressed,
+ child: Scaffold(
+ backgroundColor: AppColor.bgLight,
+ appBar: liveStockAppBar(
+ hasBack: widget.isBase ? false : widget.hasBack,
+ onBackPressed: widget.onBackPressed,
+ hasFilter: widget.hasFilter,
+ hasSearch: widget.hasSearch,
+ isBase: widget.isBase,
+ onFilterTap: widget.hasFilter ? _onFilterTap : null,
+ onSearchTap: widget.hasSearch ? () => controller.toggleSearch() : null,
+ ),
+ body: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ //widget.routesWidget != null ? widget.routesWidget! : buildPageRoute(widget.routes!),
+ if (!widget.isBase && widget.hasSearch && widget.defaultSearch) ...{
+ SearchWidget(onSearchChanged: widget.onSearchChanged),
+ },
+ ...widget.widgets,
+ ],
+ ),
+ floatingActionButtonLocation:
+ widget.floatingActionButtonLocation ?? FloatingActionButtonLocation.startFloat,
+ floatingActionButton: widget.floatingActionButton,
+ ),
+ );
+ }
+}
diff --git a/packages/livestock/lib/presentation/widgets/search.dart b/packages/livestock/lib/presentation/widgets/search.dart
new file mode 100644
index 0000000..753e624
--- /dev/null
+++ b/packages/livestock/lib/presentation/widgets/search.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'package:rasadyar_core/core.dart';
+
+import 'base_page/logic.dart';
+
+
+class SearchWidget extends StatefulWidget {
+ const SearchWidget({super.key, this.onSearchChanged});
+
+ final void Function(String?)? onSearchChanged;
+
+ @override
+ State createState() => _SearchWidgetState();
+}
+
+class _SearchWidgetState extends State {
+ late final BaseLogic controller;
+ final TextEditingController textEditingController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ controller = Get.find();
+ controller.setSearchCallback(widget.onSearchChanged);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ObxValue((data) {
+ return AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ height: data.value ? 40 : 0,
+ child: Visibility(
+ visible: data.value,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: RTextField(
+ height: 40,
+ borderColor: AppColor.blackLight,
+ suffixIcon: ObxValue(
+ (data) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: (data.value == null)
+ ? Assets.vec.searchSvg.svg(
+ width: 10,
+ height: 10,
+ colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
+ )
+ : IconButton(
+ onPressed: () {
+ textEditingController.clear();
+ controller.searchValue.value = null;
+ controller.isSearchSelected.value = false;
+ widget.onSearchChanged?.call(null);
+ },
+ enableFeedback: true,
+ padding: EdgeInsets.zero,
+ iconSize: 24,
+ splashRadius: 50,
+ icon: Assets.vec.closeCircleSvg.svg(
+ width: 20,
+ height: 20,
+ colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
+ ),
+ ),
+ ),
+ controller.searchValue,
+ ),
+ hintText: 'جستجو کنید ...',
+ hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal),
+ filledColor: Colors.white,
+ filled: true,
+ controller: textEditingController,
+ onChanged: (val) => controller.searchValue.value = val,
+ ),
+ ),
+ ),
+ );
+ }, controller.isSearchSelected);
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 8351850..c98f20e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -749,6 +749,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
+ image_cropper:
+ dependency: transitive
+ description:
+ name: image_cropper
+ sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.1.0"
+ image_cropper_for_web:
+ dependency: transitive
+ description:
+ name: image_cropper_for_web
+ sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.0"
+ image_cropper_platform_interface:
+ dependency: transitive
+ description:
+ name: image_cropper_platform_interface
+ sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.1.0"
image_picker:
dependency: transitive
description: