refactor : base page

This commit is contained in:
2025-09-24 16:24:45 +03:30
parent d2c495bfb1
commit 19802e913c
23 changed files with 562 additions and 568 deletions

View File

@@ -1,99 +1,37 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_chicken/presentation/widget/base_page/logic.dart';
import 'package:rasadyar_chicken/presentation/widget/search/logic.dart';
import 'package:rasadyar_core/core.dart';
/// Creates a customized AppBar for the Rasadyar Chicken app.
RAppBar chickenAppBar({
bool hasBack = true,
bool hasFilter = true,
bool hasSearch = true,
bool isBase = false,
VoidCallback? onBackPressed,
GestureTapCallback? onFilterTap,
GestureTapCallback? onSearchTap,
List<Widget>? additionalActions,
bool hasNotification = false,
bool hasNews = false,
int? backId,
VoidCallback? onBackTap,
VoidCallback? onFilterTap,
VoidCallback? onSearchTap,
VoidCallback? onNewsTap,
VoidCallback? onNotificationTap,
}) {
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.chickenSvg.svg(
width: 24,
height: 24,
),
],
),
additionalActions: [
if (!isBase && hasSearch) searchWidget(onSearchTap),
SizedBox(width: 8),
if (!isBase && hasFilter) filterWidget(onFilterTap),
SizedBox(width: 8),
if (additionalActions != null) ...additionalActions,
hasBack: hasBack,
hasSearch: hasSearch,
hasNotification: hasNotification,
hasNews: hasNews,
isBase: isBase,
backId: backId,
onSearchTap: onSearchTap,
onBackTap: onBackTap,
onNotificationTap: onNotificationTap,
onNewsTap: onNewsTap,
backgroundColor: AppColor.blueNormal,
children: [
Text('رصدطیور', style: AppFonts.yekan16Bold),
const SizedBox(width: 6),
Assets.vec.chickenSvg.svg(height: 24, width: 24),
],
);
}
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<BaseLogic>();
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<SearchLogic>();
return Visibility(
visible: controller.searchValue.value!=null,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
);
}),
],
),
);
}

View File

@@ -1,9 +0,0 @@
import 'package:rasadyar_core/core.dart';
class BaseLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
void toggleFilter() {
isFilterSelected.value = !isFilterSelected.value;
}
}

View File

@@ -1,112 +1,70 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_chicken/presentation/widget/app_bar.dart';
import 'package:rasadyar_chicken/presentation/widget/base_page/logic.dart';
import 'package:rasadyar_chicken/presentation/widget/page_route.dart';
import 'package:rasadyar_chicken/presentation/widget/search/logic.dart';
import 'package:rasadyar_chicken/presentation/widget/search/view.dart';
import 'package:rasadyar_core/core.dart';
class BasePage extends StatefulWidget {
const BasePage({
class ChickenBasePage extends GetView<BaseLogic> {
const ChickenBasePage({
super.key,
this.routes,
this.widgets,
this.routesWidget,
this.floatingActionButtonLocation,
this.floatingActionButton,
this.onSearchChanged,
this.child,
this.hasBack = true,
this.hasFilter = true,
this.hasSearch = true,
this.isBase = false,
this.onBackPressed,
this.hasNotification = false,
this.hasNews = false,
this.backId,
this.onBackTap,
this.onFilterTap,
this.onSearchTap,
this.onNewsTap,
this.onNotificationTap,
this.onSearchChanged,
this.routes,
this.routesWidget,
this.widgets,
this.child,
this.scrollable = false,
this.floatingActionButtonLocation,
this.floatingActionButton,
this.filteringWidget,
}) : assert(
(routes != null) || routesWidget != null,
'Either routes or routesWidget must be provided.',
);
//AppBar properties`
final bool hasBack;
final bool hasFilter;
final bool hasSearch;
final bool isBase;
final bool hasNotification;
final bool hasNews;
final int? backId;
final VoidCallback? onBackTap;
final VoidCallback? onFilterTap;
final VoidCallback? onSearchTap;
final VoidCallback? onNewsTap;
final VoidCallback? onNotificationTap;
final List<String>? routes;
final Widget? routesWidget;
final Breadcrumb? routesWidget;
final List<Widget>? widgets;
final Widget? child;
final bool scrollable;
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<BasePage> createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
BaseLogic get controller => Get.find<BaseLogic>();
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) {
// بررسی اینکه آیا این route در top است یا نه
final currentRoute = ModalRoute.of(context);
if (currentRoute?.isCurrent != true) {
return;
}
if (hasFilter && filteringWidget != null) {
final currentRoute = ModalRoute.of(Get.context!);
if (currentRoute?.isCurrent != true) return;
// مستقیماً bottomSheet را باز کنید
Get.bottomSheet(
widget.filteringWidget!,
filteringWidget!,
isScrollControlled: true,
isDismissible: true,
enableDrag: true,
@@ -116,140 +74,27 @@ class _BasePageState extends State<BasePage> {
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) => widget.onBackPressed,
child: Scaffold(
backgroundColor: AppColor.bgLight,
appBar: chickenAppBar(
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 ? () => Get.find<SearchLogic>().toggleSearch() : null,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.routesWidget != null ? widget.routesWidget! : buildPageRoute(widget.routes!),
if (!widget.isBase && widget.hasSearch) ...{
SearchWidget(onSearchChanged: widget.onSearchChanged),
},
if (widget.child != null) ...{Expanded(child: widget.child!)},
...?widget.widgets,
],
),
floatingActionButtonLocation: widget.floatingActionButtonLocation,
floatingActionButton: widget.floatingActionButton,
),
);
}
}
class BasePageWithScroll extends StatefulWidget {
const BasePageWithScroll({
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.onBackPressed,
this.onFilterTap,
this.onSearchTap,
this.filteringWidget,
}) : assert(
(routes != null) || routesWidget != null,
'Either routes or routesWidget must be provided.',
);
final List<String>? routes;
final Widget? routesWidget;
final List<Widget> 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<BasePageWithScroll> createState() => _BasePageWithScrollState();
}
class _BasePageWithScrollState extends State<BasePageWithScroll> {
BaseLogic get controller => Get.find<BaseLogic>();
Worker? filterWorker;
@override
void dispose() {
filterWorker?.dispose();
super.dispose();
}
void _onFilterTap() {
if (widget.hasFilter && widget.filteringWidget != null) {
// بررسی اینکه آیا این route در top است یا نه
final currentRoute = ModalRoute.of(context);
if (currentRoute?.isCurrent != true) {
return;
}
// مستقیماً bottomSheet را باز کنید
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: chickenAppBar(
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 ? () => Get.find<SearchLogic>().toggleSearch() : null,
),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.routesWidget != null ? widget.routesWidget! : buildPageRoute(widget.routes!),
if (!widget.isBase && widget.hasSearch) ...{
SearchWidget(onSearchChanged: widget.onSearchChanged),
},
...widget.widgets,
],
),
),
floatingActionButtonLocation: widget.floatingActionButtonLocation,
floatingActionButton: widget.floatingActionButton,
return BasePage(
routes: routes,
routesWidget: routesWidget,
widgets: widgets,
child: child,
scrollable: scrollable,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButton: floatingActionButton,
appBar: chickenAppBar(
isBase: isBase,
hasBack: isBase ? false : hasBack,
onBackTap: onBackTap,
onNewsTap: onNewsTap,
hasFilter: hasFilter,
hasSearch: hasSearch,
hasNews: hasNews,
hasNotification: hasNotification,
backId: backId,
onNotificationTap: onNotificationTap,
onFilterTap: hasFilter ? _onFilterTap : null,
onSearchTap: hasSearch ? controller.toggleSearch : null,
),
);
}

View File

@@ -2,10 +2,13 @@ import 'package:flutter/cupertino.dart';
import 'package:rasadyar_core/core.dart';
Widget buildPageRoute(List<String> route) {
if(route.isEmpty){
return SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.fromLTRB(0, 4, 7, 4),
child: Text(
route.isEmpty ? 'خانه' : route.join(" > "),
route.join(" > "),
style: AppFonts.yekan14.copyWith(color: AppColor.bgDark),
),
);

View File

@@ -1,21 +0,0 @@
import 'package:rasadyar_core/core.dart';
class SearchLogic extends GetxController {
final RxBool isSearchSelected = false.obs;
final RxnString searchValue = RxnString();
void setSearchCallback(void Function(String)? onSearchChanged) {
debounce<String?>(searchValue, (val) {
if (val != null && val.trim().isNotEmpty) {
onSearchChanged?.call(val);
}
}, time: const Duration(milliseconds: 600));
}
void toggleSearch() {
isSearchSelected.value = !isSearchSelected.value;
}
}

View File

@@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'logic.dart';
class SearchWidget extends StatefulWidget {
const SearchWidget({super.key, this.onSearchChanged});
final void Function(String?)? onSearchChanged;
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
late final SearchLogic controller;
final TextEditingController textEditingController = TextEditingController();
@override
void initState() {
super.initState();
controller = Get.find<SearchLogic>();
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);
}
}