Merge pull request #6
Refactor Flutter architecture: Consolidate core widgets and eliminate code duplication
This commit is contained in:
@@ -1,31 +1,81 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rasadyar_core/core.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 isFilterSelected = false.obs;
|
||||||
final RxBool isSearchSelected = false.obs;
|
final RxBool isSearchSelected = false.obs;
|
||||||
final RxnString searchValue = RxnString();
|
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) {
|
debounce<String?>(searchValue, (val) {
|
||||||
if (val != null && val.trim().isNotEmpty) {
|
if (val != null && val.trim().isNotEmpty) {
|
||||||
onSearchChanged?.call(val);
|
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() {
|
void toggleSearch() {
|
||||||
isSearchSelected.value = !isSearchSelected.value;
|
isSearchSelected.value = !isSearchSelected.value;
|
||||||
|
|
||||||
|
// Clear search when hiding
|
||||||
|
if (!isSearchSelected.value) {
|
||||||
|
clearSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears search input and resets state
|
||||||
void clearSearch() {
|
void clearSearch() {
|
||||||
textEditingController.clear();
|
searchTextController.clear();
|
||||||
searchValue.value = null;
|
searchValue.value = null;
|
||||||
isSearchSelected.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggles filter selection state
|
||||||
void toggleFilter() {
|
void toggleFilter() {
|
||||||
isFilterSelected.value = !isFilterSelected.value;
|
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;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export 'animated_fab.dart';
|
export 'animated_fab.dart';
|
||||||
|
export 'core_button.dart';
|
||||||
export 'elevated.dart';
|
export 'elevated.dart';
|
||||||
export 'fab.dart';
|
export 'fab.dart';
|
||||||
export 'fab_outlined.dart';
|
export 'fab_outlined.dart';
|
||||||
|
|||||||
407
packages/core/lib/presentation/widget/buttons/core_button.dart
Normal file
407
packages/core/lib/presentation/widget/buttons/core_button.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export 'core_loading_indicator.dart';
|
||||||
@@ -11,8 +11,8 @@ export 'base_page/widgets/back_ground_widget.dart';
|
|||||||
export 'base_page/widgets/breadcrumb.dart';
|
export 'base_page/widgets/breadcrumb.dart';
|
||||||
export 'base_page/widgets/search_widget.dart';
|
export 'base_page/widgets/search_widget.dart';
|
||||||
|
|
||||||
|
//buttons - enhanced core widgets
|
||||||
//buttons
|
export 'buttons/core_button.dart';
|
||||||
export 'buttons/buttons.dart';
|
export 'buttons/buttons.dart';
|
||||||
export 'card/card_icon_widget.dart';
|
export 'card/card_icon_widget.dart';
|
||||||
export 'chips/r_chips.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_sheet2.dart';
|
||||||
export 'draggable_bottom_sheet/draggable_bottom_sheet_controller.dart';
|
export 'draggable_bottom_sheet/draggable_bottom_sheet_controller.dart';
|
||||||
export 'empty_widget.dart';
|
export 'empty_widget.dart';
|
||||||
|
//indicators - unified loading components
|
||||||
|
export 'indicators/core_loading_indicator.dart';
|
||||||
//inputs
|
//inputs
|
||||||
export 'inputs/inputs.dart';
|
export 'inputs/inputs.dart';
|
||||||
//list_item
|
//list_item
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
// This file now re-exports the consolidated BasePageLogic from rasadyar_core
|
||||||
import 'package:rasadyar_core/core.dart';
|
// The BaseLogic class has been moved to the core package to eliminate duplication
|
||||||
|
|
||||||
class BaseLogic extends GetxController {
|
export 'package:rasadyar_core/presentation/widget/base_page/logic.dart';
|
||||||
final RxBool isFilterSelected = false.obs;
|
|
||||||
final RxBool isSearchSelected = false.obs;
|
|
||||||
final TextEditingController searchTextController = TextEditingController();
|
|
||||||
final RxnString searchValue = RxnString();
|
|
||||||
|
|
||||||
void setSearchCallback(void Function(String)? onSearchChanged) {
|
// Backward compatibility - can be removed in future versions
|
||||||
debounce<String?>(searchValue, (val) {
|
// import 'package:rasadyar_core/presentation/widget/base_page/logic.dart' as core;
|
||||||
if (val != null && val.trim().isNotEmpty) {
|
// typedef BaseLogic = core.BasePageLogic;
|
||||||
onSearchChanged?.call(val);
|
|
||||||
}
|
|
||||||
}, time: const Duration(milliseconds: 600));
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleFilter() {
|
|
||||||
isFilterSelected.value = !isFilterSelected.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleSearch() {
|
|
||||||
isSearchSelected.value = !isSearchSelected.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
// This file now re-exports the consolidated BasePageLogic from rasadyar_core
|
||||||
import 'package:rasadyar_core/core.dart';
|
// The BaseLogic class has been moved to the core package to eliminate duplication
|
||||||
|
|
||||||
class BaseLogic extends GetxController {
|
export 'package:rasadyar_core/presentation/widget/base_page/logic.dart';
|
||||||
final RxBool isFilterSelected = false.obs;
|
|
||||||
final RxBool isSearchSelected = false.obs;
|
|
||||||
final TextEditingController searchTextController = TextEditingController();
|
|
||||||
final RxnString searchValue = RxnString();
|
|
||||||
|
|
||||||
void setSearchCallback(void Function(String)? onSearchChanged) {
|
// Backward compatibility - can be removed in future versions
|
||||||
debounce<String?>(searchValue, (val) {
|
// import 'package:rasadyar_core/presentation/widget/base_page/logic.dart' as core;
|
||||||
if (val != null && val.trim().isNotEmpty) {
|
// typedef BaseLogic = core.BasePageLogic;
|
||||||
onSearchChanged?.call(val);
|
|
||||||
}
|
|
||||||
}, time: const Duration(milliseconds: 600));
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleFilter() {
|
|
||||||
isFilterSelected.value = !isFilterSelected.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleSearch() {
|
|
||||||
isSearchSelected.value = !isSearchSelected.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user