Implement core package refactoring: BaseLogic consolidation and new CoreButton/CoreLoadingIndicator

Co-authored-by: mes71 <53784874+mes71@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-29 06:39:37 +00:00
parent 2ad196bde6
commit bc0c069adf
8 changed files with 800 additions and 54 deletions

View File

@@ -1,31 +1,81 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
class BaseLogic extends GetxController {
/// Consolidated base logic controller that provides common functionality
/// for pages with search and filter capabilities.
///
/// This replaces the duplicate BaseLogic classes across different packages.
class BasePageLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
final RxBool isSearchSelected = false.obs;
final RxnString searchValue = RxnString();
final TextEditingController textEditingController = TextEditingController();
final TextEditingController searchTextController = TextEditingController();
void setSearchCallback(void Function(String?)? onSearchChanged) {
// Debounce time configuration
static const Duration _defaultDebounceTime = Duration(milliseconds: 600);
/// Sets up search callback with debouncing
/// [onSearchChanged] will be called when search value changes after debounce delay
/// [debounceTime] custom debounce duration, defaults to 600ms
void setSearchCallback(
void Function(String?)? onSearchChanged, {
Duration debounceTime = _defaultDebounceTime,
}) {
debounce<String?>(searchValue, (val) {
if (val != null && val.trim().isNotEmpty) {
onSearchChanged?.call(val);
} else {
// Call with null/empty to handle search clear
onSearchChanged?.call(null);
}
}, time: const Duration(milliseconds: 600));
}, time: debounceTime);
}
/// Toggles search visibility state
void toggleSearch() {
isSearchSelected.value = !isSearchSelected.value;
// Clear search when hiding
if (!isSearchSelected.value) {
clearSearch();
}
}
/// Clears search input and resets state
void clearSearch() {
textEditingController.clear();
searchTextController.clear();
searchValue.value = null;
isSearchSelected.value = false;
}
/// Toggles filter selection state
void toggleFilter() {
isFilterSelected.value = !isFilterSelected.value;
}
/// Resets all states to initial values
void resetStates() {
isFilterSelected.value = false;
isSearchSelected.value = false;
clearSearch();
}
@override
void onInit() {
super.onInit();
// Bind search controller to reactive value
searchTextController.addListener(() {
searchValue.value = searchTextController.text;
});
}
@override
void onClose() {
searchTextController.dispose();
super.onClose();
}
}
/// Backward compatibility alias - will be deprecated
@Deprecated('Use BasePageLogic instead. This will be removed in future versions.')
typedef BaseLogic = BasePageLogic;

View File

@@ -1,4 +1,5 @@
export 'animated_fab.dart';
export 'core_button.dart';
export 'elevated.dart';
export 'fab.dart';
export 'fab_outlined.dart';

View File

@@ -0,0 +1,407 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
/// Button variant types for consistent styling
enum CoreButtonVariant {
/// Filled button with primary background color
primary,
/// Filled button with secondary background color
secondary,
/// Button with transparent background and border
outlined,
/// Button with transparent background, no border
text,
/// Destructive action button (red theme)
destructive,
}
/// Button size presets
enum CoreButtonSize {
/// Small button - height 32
small,
/// Medium button - height 40 (default)
medium,
/// Large button - height 56
large,
}
/// A unified, configurable button widget that replaces RElevated, ROutlinedElevated, etc.
///
/// This widget provides a consistent API and theming system across the entire app.
/// It supports different variants, sizes, loading states, and full customization.
class CoreButton extends StatelessWidget {
/// Button text content
final String? text;
/// Custom child widget (overrides text if provided)
final Widget? child;
/// Button press callback
final VoidCallback? onPressed;
/// Button style variant
final CoreButtonVariant variant;
/// Button size preset
final CoreButtonSize size;
/// Custom width (overrides size preset)
final double? width;
/// Custom height (overrides size preset)
final double? height;
/// Whether button should take full width
final bool isFullWidth;
/// Loading state - shows progress indicator
final bool isLoading;
/// Progress value for loading indicator (0.0 to 1.0, null for indeterminate)
final double? progress;
/// Custom text style (overrides theme)
final TextStyle? textStyle;
/// Custom background color (overrides variant theme)
final Color? backgroundColor;
/// Custom foreground color (overrides variant theme)
final Color? foregroundColor;
/// Custom border color for outlined variant
final Color? borderColor;
/// Border radius (default: 8.0)
final double borderRadius;
/// Leading icon
final Widget? icon;
/// Icon placement
final bool iconAtEnd;
/// Spacing between icon and text
final double iconSpacing;
const CoreButton({
super.key,
this.text,
this.child,
required this.onPressed,
this.variant = CoreButtonVariant.primary,
this.size = CoreButtonSize.medium,
this.width,
this.height,
this.isFullWidth = false,
this.isLoading = false,
this.progress,
this.textStyle,
this.backgroundColor,
this.foregroundColor,
this.borderColor,
this.borderRadius = 8.0,
this.icon,
this.iconAtEnd = false,
this.iconSpacing = 8.0,
}) : assert(text != null || child != null, 'Either text or child must be provided');
/// Creates a primary filled button
const CoreButton.primary({
super.key,
this.text,
this.child,
required this.onPressed,
this.size = CoreButtonSize.medium,
this.width,
this.height,
this.isFullWidth = false,
this.isLoading = false,
this.progress,
this.textStyle,
this.backgroundColor,
this.foregroundColor,
this.borderRadius = 8.0,
this.icon,
this.iconAtEnd = false,
this.iconSpacing = 8.0,
}) : variant = CoreButtonVariant.primary,
borderColor = null,
assert(text != null || child != null, 'Either text or child must be provided');
/// Creates an outlined button
const CoreButton.outlined({
super.key,
this.text,
this.child,
required this.onPressed,
this.size = CoreButtonSize.medium,
this.width,
this.height,
this.isFullWidth = false,
this.isLoading = false,
this.progress,
this.textStyle,
this.foregroundColor,
this.borderColor,
this.borderRadius = 8.0,
this.icon,
this.iconAtEnd = false,
this.iconSpacing = 8.0,
}) : variant = CoreButtonVariant.outlined,
backgroundColor = null,
assert(text != null || child != null, 'Either text or child must be provided');
/// Creates a text button with no background
const CoreButton.text({
super.key,
this.text,
this.child,
required this.onPressed,
this.size = CoreButtonSize.medium,
this.width,
this.height,
this.isFullWidth = false,
this.isLoading = false,
this.progress,
this.textStyle,
this.foregroundColor,
this.borderRadius = 8.0,
this.icon,
this.iconAtEnd = false,
this.iconSpacing = 8.0,
}) : variant = CoreButtonVariant.text,
backgroundColor = null,
borderColor = null,
assert(text != null || child != null, 'Either text or child must be provided');
/// Creates a destructive action button
const CoreButton.destructive({
super.key,
this.text,
this.child,
required this.onPressed,
this.size = CoreButtonSize.medium,
this.width,
this.height,
this.isFullWidth = false,
this.isLoading = false,
this.progress,
this.textStyle,
this.backgroundColor,
this.foregroundColor,
this.borderRadius = 8.0,
this.icon,
this.iconAtEnd = false,
this.iconSpacing = 8.0,
}) : variant = CoreButtonVariant.destructive,
borderColor = null,
assert(text != null || child != null, 'Either text or child must be provided');
/// Gets button dimensions based on size preset
Size get _buttonSize {
final buttonWidth = isFullWidth ? double.infinity : (width ?? _defaultWidth);
final buttonHeight = height ?? _defaultHeight;
return Size(buttonWidth, buttonHeight);
}
double get _defaultWidth {
switch (size) {
case CoreButtonSize.small:
return 120.0;
case CoreButtonSize.medium:
return 150.0;
case CoreButtonSize.large:
return 200.0;
}
}
double get _defaultHeight {
switch (size) {
case CoreButtonSize.small:
return 32.0;
case CoreButtonSize.medium:
return 40.0;
case CoreButtonSize.large:
return 56.0;
}
}
/// Gets theme colors based on variant
_ButtonTheme get _theme {
switch (variant) {
case CoreButtonVariant.primary:
return _ButtonTheme(
backgroundColor: backgroundColor ?? AppColor.blueNormal,
foregroundColor: foregroundColor ?? Colors.white,
borderColor: null,
);
case CoreButtonVariant.secondary:
return _ButtonTheme(
backgroundColor: backgroundColor ?? AppColor.greyNormal,
foregroundColor: foregroundColor ?? AppColor.textColor,
borderColor: null,
);
case CoreButtonVariant.outlined:
return _ButtonTheme(
backgroundColor: backgroundColor ?? Colors.transparent,
foregroundColor: foregroundColor ?? (borderColor ?? AppColor.blueNormal),
borderColor: borderColor ?? AppColor.blueNormal,
);
case CoreButtonVariant.text:
return _ButtonTheme(
backgroundColor: backgroundColor ?? Colors.transparent,
foregroundColor: foregroundColor ?? AppColor.blueNormal,
borderColor: null,
);
case CoreButtonVariant.destructive:
return _ButtonTheme(
backgroundColor: backgroundColor ?? AppColor.error,
foregroundColor: foregroundColor ?? Colors.white,
borderColor: null,
);
}
}
TextStyle get _textTheme {
final baseStyle = textStyle ?? AppFonts.yekan16;
return baseStyle.copyWith(color: _theme.foregroundColor);
}
Widget _buildContent() {
if (isLoading) {
return SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(_theme.foregroundColor),
value: progress,
),
);
}
final content = child ?? Text(text!, style: _textTheme);
if (icon != null) {
final iconWidget = IconTheme(
data: IconThemeData(color: _theme.foregroundColor, size: 18),
child: icon!,
);
if (iconAtEnd) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
content,
SizedBox(width: iconSpacing),
iconWidget,
],
);
} else {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
iconWidget,
SizedBox(width: iconSpacing),
content,
],
);
}
}
return content;
}
@override
Widget build(BuildContext context) {
final isEnabled = onPressed != null && !isLoading;
final theme = _theme;
final buttonSize = _buttonSize;
if (variant == CoreButtonVariant.outlined) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: buttonSize.width,
height: buttonSize.height,
),
child: OutlinedButton(
onPressed: isEnabled ? onPressed : null,
style: OutlinedButton.styleFrom(
backgroundColor: theme.backgroundColor,
foregroundColor: theme.foregroundColor,
side: BorderSide(
color: isEnabled ? theme.borderColor! : theme.borderColor!.withOpacity(0.38),
width: 1.5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: EdgeInsets.zero,
textStyle: _textTheme,
),
child: _buildContent(),
),
);
}
if (variant == CoreButtonVariant.text) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: buttonSize.width,
height: buttonSize.height,
),
child: TextButton(
onPressed: isEnabled ? onPressed : null,
style: TextButton.styleFrom(
backgroundColor: theme.backgroundColor,
foregroundColor: theme.foregroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: EdgeInsets.zero,
textStyle: _textTheme,
),
child: _buildContent(),
),
);
}
// Default to ElevatedButton for primary, secondary, destructive
return ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: buttonSize.width,
height: buttonSize.height,
),
child: ElevatedButton(
onPressed: isEnabled ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.backgroundColor,
foregroundColor: theme.foregroundColor,
disabledBackgroundColor: theme.backgroundColor.withOpacity(0.38),
disabledForegroundColor: theme.foregroundColor.withOpacity(0.38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: EdgeInsets.zero,
textStyle: _textTheme,
),
child: _buildContent(),
),
);
}
}
/// Internal theme data for button variants
class _ButtonTheme {
final Color backgroundColor;
final Color foregroundColor;
final Color? borderColor;
const _ButtonTheme({
required this.backgroundColor,
required this.foregroundColor,
this.borderColor,
});
}

View File

@@ -0,0 +1,319 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
/// Loading indicator variant types
enum CoreLoadingVariant {
/// Material Design circular progress indicator
material,
/// iOS style activity indicator
cupertino,
/// Custom Lottie animation
lottie,
/// Linear progress indicator
linear,
}
/// Loading indicator size presets
enum CoreLoadingSize {
/// Small - 16x16
small,
/// Medium - 24x24 (default)
medium,
/// Large - 32x32
large,
/// Extra large - 48x48
extraLarge,
}
/// A unified loading indicator widget that replaces inconsistent loading patterns
///
/// This widget provides consistent loading states across the entire app with
/// support for different variants, sizes, colors, and text labels.
class CoreLoadingIndicator extends StatelessWidget {
/// Loading indicator variant
final CoreLoadingVariant variant;
/// Size preset
final CoreLoadingSize size;
/// Custom width (overrides size preset)
final double? width;
/// Custom height (overrides size preset)
final double? height;
/// Indicator color
final Color? color;
/// Progress value for determinate indicators (0.0 to 1.0)
final double? value;
/// Stroke width for circular indicators
final double? strokeWidth;
/// Loading text label
final String? label;
/// Text style for label
final TextStyle? labelStyle;
/// Spacing between indicator and label
final double labelSpacing;
/// Label position relative to indicator
final CrossAxisAlignment labelAlignment;
const CoreLoadingIndicator({
super.key,
this.variant = CoreLoadingVariant.material,
this.size = CoreLoadingSize.medium,
this.width,
this.height,
this.color,
this.value,
this.strokeWidth,
this.label,
this.labelStyle,
this.labelSpacing = 12.0,
this.labelAlignment = CrossAxisAlignment.center,
});
/// Creates a small Material loading indicator
const CoreLoadingIndicator.small({
super.key,
this.color,
this.value,
this.strokeWidth,
this.label,
this.labelStyle,
this.labelSpacing = 8.0,
this.labelAlignment = CrossAxisAlignment.center,
}) : variant = CoreLoadingVariant.material,
size = CoreLoadingSize.small,
width = null,
height = null;
/// Creates a Cupertino style activity indicator
const CoreLoadingIndicator.cupertino({
super.key,
this.size = CoreLoadingSize.medium,
this.width,
this.height,
this.color,
this.label,
this.labelStyle,
this.labelSpacing = 12.0,
this.labelAlignment = CrossAxisAlignment.center,
}) : variant = CoreLoadingVariant.cupertino,
value = null,
strokeWidth = null;
/// Creates a linear progress indicator
const CoreLoadingIndicator.linear({
super.key,
this.width,
this.height = 4.0,
this.color,
this.value,
this.label,
this.labelStyle,
this.labelSpacing = 8.0,
this.labelAlignment = CrossAxisAlignment.start,
}) : variant = CoreLoadingVariant.linear,
size = CoreLoadingSize.medium,
strokeWidth = null;
/// Creates a large Lottie animation loading indicator
const CoreLoadingIndicator.lottie({
super.key,
this.size = CoreLoadingSize.large,
this.width,
this.height,
this.label,
this.labelStyle,
this.labelSpacing = 16.0,
this.labelAlignment = CrossAxisAlignment.center,
}) : variant = CoreLoadingVariant.lottie,
color = null,
value = null,
strokeWidth = null;
/// Gets size dimensions based on size preset
Size get _indicatorSize {
final sizeValue = width ?? height ?? _defaultSize;
return Size(sizeValue, sizeValue);
}
double get _defaultSize {
switch (size) {
case CoreLoadingSize.small:
return 16.0;
case CoreLoadingSize.medium:
return 24.0;
case CoreLoadingSize.large:
return 32.0;
case CoreLoadingSize.extraLarge:
return 48.0;
}
}
Color get _indicatorColor {
return color ?? AppColor.blueNormal;
}
Widget _buildIndicator() {
final indicatorSize = _indicatorSize;
switch (variant) {
case CoreLoadingVariant.material:
return SizedBox(
width: indicatorSize.width,
height: indicatorSize.height,
child: CircularProgressIndicator(
value: value,
strokeWidth: strokeWidth ?? (indicatorSize.width * 0.08).clamp(1.5, 4.0),
valueColor: AlwaysStoppedAnimation<Color>(_indicatorColor),
),
);
case CoreLoadingVariant.cupertino:
return SizedBox(
width: indicatorSize.width,
height: indicatorSize.height,
child: CupertinoActivityIndicator(
color: _indicatorColor,
radius: indicatorSize.width * 0.4,
),
);
case CoreLoadingVariant.linear:
return SizedBox(
width: width ?? 200.0,
height: height ?? 4.0,
child: LinearProgressIndicator(
value: value,
backgroundColor: _indicatorColor.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(_indicatorColor),
),
);
case CoreLoadingVariant.lottie:
try {
return Assets.anim.loading.lottie(
width: indicatorSize.width,
height: indicatorSize.height,
);
} catch (e) {
// Fallback to Material indicator if Lottie fails
return SizedBox(
width: indicatorSize.width,
height: indicatorSize.height,
child: CircularProgressIndicator(
strokeWidth: strokeWidth ?? 3.0,
valueColor: AlwaysStoppedAnimation<Color>(_indicatorColor),
),
);
}
}
}
Widget _buildLabel() {
if (label == null) return const SizedBox.shrink();
return Text(
label!,
style: labelStyle ?? AppFonts.yekan14.copyWith(
color: AppColor.textColor,
),
textAlign: TextAlign.center,
);
}
@override
Widget build(BuildContext context) {
final indicator = _buildIndicator();
final labelWidget = _buildLabel();
if (label == null) {
return indicator;
}
if (variant == CoreLoadingVariant.linear) {
return Column(
crossAxisAlignment: labelAlignment,
mainAxisSize: MainAxisSize.min,
children: [
labelWidget,
SizedBox(height: labelSpacing),
indicator,
],
);
}
return Column(
crossAxisAlignment: labelAlignment,
mainAxisSize: MainAxisSize.min,
children: [
indicator,
SizedBox(height: labelSpacing),
labelWidget,
],
);
}
}
/// A full-screen loading overlay widget
class CoreLoadingOverlay extends StatelessWidget {
/// Loading indicator to display
final CoreLoadingIndicator indicator;
/// Background color of the overlay
final Color backgroundColor;
/// Whether the overlay should block user interaction
final bool barrierDismissible;
const CoreLoadingOverlay({
super.key,
this.indicator = const CoreLoadingIndicator.lottie(
label: 'در حال بارگذاری...',
),
this.backgroundColor = Colors.black54,
this.barrierDismissible = false,
});
@override
Widget build(BuildContext context) {
return Container(
color: backgroundColor,
child: Center(child: indicator),
);
}
/// Shows a loading overlay on top of current screen
static void show(
BuildContext context, {
CoreLoadingIndicator? indicator,
Color backgroundColor = Colors.black54,
bool barrierDismissible = false,
}) {
showDialog(
context: context,
barrierDismissible: barrierDismissible,
barrierColor: Colors.transparent,
builder: (context) => CoreLoadingOverlay(
indicator: indicator ?? const CoreLoadingIndicator.lottie(
label: 'در حال بارگذاری...',
),
backgroundColor: backgroundColor,
barrierDismissible: barrierDismissible,
),
);
}
/// Hides the currently displayed loading overlay
static void hide(BuildContext context) {
Navigator.of(context).pop();
}
}

View File

@@ -0,0 +1 @@
export 'core_loading_indicator.dart';

View File

@@ -11,8 +11,8 @@ export 'base_page/widgets/back_ground_widget.dart';
export 'base_page/widgets/breadcrumb.dart';
export 'base_page/widgets/search_widget.dart';
//buttons
//buttons - enhanced core widgets
export 'buttons/core_button.dart';
export 'buttons/buttons.dart';
export 'card/card_icon_widget.dart';
export 'chips/r_chips.dart';
@@ -24,6 +24,8 @@ export 'draggable_bottom_sheet/draggable_bottom_sheet.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet2.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet_controller.dart';
export 'empty_widget.dart';
//indicators - unified loading components
export 'indicators/core_loading_indicator.dart';
//inputs
export 'inputs/inputs.dart';
//list_item

View File

@@ -1,25 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:rasadyar_core/core.dart';
// This file now re-exports the consolidated BasePageLogic from rasadyar_core
// The BaseLogic class has been moved to the core package to eliminate duplication
class BaseLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
final RxBool isSearchSelected = false.obs;
final TextEditingController searchTextController = TextEditingController();
final RxnString searchValue = RxnString();
export 'package:rasadyar_core/presentation/widget/base_page/logic.dart';
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 toggleFilter() {
isFilterSelected.value = !isFilterSelected.value;
}
void toggleSearch() {
isSearchSelected.value = !isSearchSelected.value;
}
}
// Backward compatibility - can be removed in future versions
// import 'package:rasadyar_core/presentation/widget/base_page/logic.dart' as core;
// typedef BaseLogic = core.BasePageLogic;

View File

@@ -1,25 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:rasadyar_core/core.dart';
// This file now re-exports the consolidated BasePageLogic from rasadyar_core
// The BaseLogic class has been moved to the core package to eliminate duplication
class BaseLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
final RxBool isSearchSelected = false.obs;
final TextEditingController searchTextController = TextEditingController();
final RxnString searchValue = RxnString();
export 'package:rasadyar_core/presentation/widget/base_page/logic.dart';
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 toggleFilter() {
isFilterSelected.value = !isFilterSelected.value;
}
void toggleSearch() {
isSearchSelected.value = !isSearchSelected.value;
}
}
// Backward compatibility - can be removed in future versions
// import 'package:rasadyar_core/presentation/widget/base_page/logic.dart' as core;
// typedef BaseLogic = core.BasePageLogic;