Files
iiEsaywebUIapp/lib/shared/widgets/improved_loading_states.dart

667 lines
18 KiB
Dart
Raw Normal View History

2025-08-10 01:20:45 +05:30
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'skeleton_loader.dart';
import '../theme/theme_extensions.dart';
import 'conduit_components.dart';
/// Improved loading state widget with accessibility and better hierarchy
class ImprovedLoadingState extends StatefulWidget {
final String? message;
final bool showProgress;
final double? progress;
final Widget? customWidget;
final bool useSkeletonLoader;
final int skeletonCount;
final double skeletonHeight;
final bool isCompact;
const ImprovedLoadingState({
super.key,
this.message,
this.showProgress = false,
this.progress,
this.customWidget,
this.useSkeletonLoader = false,
this.skeletonCount = 3,
this.skeletonHeight = 100,
this.isCompact = false,
});
@override
State<ImprovedLoadingState> createState() => _ImprovedLoadingStateState();
}
class _ImprovedLoadingStateState extends State<ImprovedLoadingState>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: AnimationDuration.standard,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: AnimationCurves.standard,
);
_animationController.forward();
// Announce loading state for screen readers
if (widget.message != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
SemanticsService.announce(
'Loading: ${widget.message}',
TextDirection.ltr,
);
});
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.customWidget != null) {
return widget.customWidget!;
}
if (widget.useSkeletonLoader) {
return _buildSkeletonLoader();
}
return FadeTransition(
opacity: _fadeAnimation,
child: Center(
child: Semantics(
label: widget.message ?? 'Loading content',
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.showProgress && widget.progress != null)
_buildProgressIndicator()
else
_buildCircularIndicator(),
if (widget.message != null) ...[
SizedBox(height: widget.isCompact ? Spacing.sm : Spacing.md),
Text(
widget.message!,
style: AppTypography.standard.copyWith(
color: context.conduitTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
],
),
),
),
);
}
Widget _buildCircularIndicator() {
return SizedBox(
width: widget.isCompact ? IconSize.large : IconSize.xxl,
height: widget.isCompact ? IconSize.large : IconSize.xxl,
child: CircularProgressIndicator(
strokeWidth: widget.isCompact ? 2 : 3,
valueColor: AlwaysStoppedAnimation<Color>(
context.conduitTheme.buttonPrimary,
),
),
);
}
Widget _buildProgressIndicator() {
return Column(
children: [
SizedBox(
width: widget.isCompact ? 150 : 200,
child: LinearProgressIndicator(
value: widget.progress,
minHeight: widget.isCompact ? 3 : 4,
backgroundColor: context.conduitTheme.dividerColor,
valueColor: AlwaysStoppedAnimation<Color>(
context.conduitTheme.buttonPrimary,
),
),
),
SizedBox(height: widget.isCompact ? Spacing.xs : Spacing.sm),
Text(
'${(widget.progress! * 100).toInt()}%',
style: AppTypography.small.copyWith(
color: context.conduitTheme.textSecondary,
),
),
],
);
}
Widget _buildSkeletonLoader() {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.skeletonCount,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.symmetric(
horizontal: widget.isCompact ? Spacing.sm : Spacing.md,
vertical: widget.isCompact ? Spacing.xs : Spacing.sm,
),
child: SkeletonLoader(
height: widget.skeletonHeight,
isCompact: widget.isCompact,
),
),
);
}
}
/// Improved empty state with better UX and hierarchy
class ImprovedEmptyState extends StatelessWidget {
final String title;
final String? subtitle;
final IconData? icon;
final Widget? customIcon;
final VoidCallback? onAction;
final String? actionLabel;
final bool showAnimation;
final bool isCompact;
const ImprovedEmptyState({
super.key,
required this.title,
this.subtitle,
this.icon,
this.customIcon,
this.onAction,
this.actionLabel,
this.showAnimation = true,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
Widget content = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon or custom widget
if (customIcon != null)
customIcon!
else if (icon != null)
showAnimation
? TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: AnimationDuration.standard,
curve: AnimationCurves.elastic,
builder: (context, value, child) => Transform.scale(
scale: value,
child: Icon(
icon,
size: isCompact ? IconSize.large : IconSize.xxl,
color: theme.iconSecondary,
),
),
)
: Icon(
icon,
size: isCompact ? IconSize.large : IconSize.xxl,
color: theme.iconSecondary,
),
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
// Title
Text(
title,
style: AppTypography.headlineSmallStyle.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
// Subtitle
if (subtitle != null) ...[
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
Text(
subtitle!,
style: AppTypography.standard.copyWith(color: theme.textSecondary),
textAlign: TextAlign.center,
),
],
// Action button
if (actionLabel != null && onAction != null) ...[
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
ConduitButton(
text: actionLabel!,
onPressed: onAction,
isCompact: isCompact,
),
],
],
);
return Center(
child: Padding(
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg),
child: showAnimation
? content.animate().fadeIn(
duration: AnimationDuration.standard,
curve: AnimationCurves.standard,
)
: content,
),
);
}
}
/// Enhanced loading overlay with better hierarchy
class LoadingOverlay extends StatelessWidget {
final Widget child;
final bool isLoading;
final String? message;
final bool isCompact;
const LoadingOverlay({
super.key,
required this.child,
required this.isLoading,
this.message,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Container(
color: context.conduitTheme.surfaceBackground.withValues(
alpha: Alpha.overlay,
),
child: Center(
child: Container(
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card),
boxShadow: ConduitShadows.card,
),
child: ImprovedLoadingState(
message: message,
isCompact: isCompact,
),
),
),
),
],
);
}
}
/// Enhanced loading button with better hierarchy
class LoadingButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isDestructive;
final bool isSecondary;
final IconData? icon;
final double? width;
final bool isFullWidth;
final bool isCompact;
const LoadingButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isDestructive = false,
this.isSecondary = false,
this.icon,
this.width,
this.isFullWidth = false,
this.isCompact = false,
});
@override
State<LoadingButton> createState() => _LoadingButtonState();
}
class _LoadingButtonState extends State<LoadingButton> {
@override
Widget build(BuildContext context) {
return ConduitButton(
text: widget.text,
onPressed: widget.isLoading ? null : widget.onPressed,
isLoading: widget.isLoading,
isDestructive: widget.isDestructive,
isSecondary: widget.isSecondary,
icon: widget.icon,
width: widget.width,
isFullWidth: widget.isFullWidth,
isCompact: widget.isCompact,
);
}
}
/// Enhanced loading list with better hierarchy
class LoadingList extends StatelessWidget {
final bool isLoading;
final Widget child;
final int skeletonCount;
final double skeletonHeight;
final bool isCompact;
const LoadingList({
super.key,
required this.isLoading,
required this.child,
this.skeletonCount = 5,
this.skeletonHeight = 80,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
if (isLoading) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: skeletonCount,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.symmetric(
horizontal: isCompact ? Spacing.sm : Spacing.md,
vertical: isCompact ? Spacing.xs : Spacing.sm,
),
child: SkeletonLoader(height: skeletonHeight, isCompact: isCompact),
),
);
}
return child;
}
}
/// Enhanced loading card with better hierarchy
class LoadingCard extends StatelessWidget {
final bool isLoading;
final Widget child;
final bool isCompact;
const LoadingCard({
super.key,
required this.isLoading,
required this.child,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
if (isLoading) {
return ConduitCard(
isCompact: isCompact,
child: ImprovedLoadingState(
message: 'Loading...',
isCompact: isCompact,
),
);
}
return child;
}
}
/// Shimmer loading effect
class ShimmerLoader extends StatefulWidget {
final double width;
final double height;
final BorderRadius? borderRadius;
final EdgeInsetsGeometry? margin;
const ShimmerLoader({
super.key,
this.width = double.infinity,
this.height = 20,
this.borderRadius,
this.margin,
});
@override
State<ShimmerLoader> createState() => _ShimmerLoaderState();
}
class _ShimmerLoaderState extends State<ShimmerLoader>
with SingleTickerProviderStateMixin {
late AnimationController _shimmerController;
late Animation<double> _shimmerAnimation;
@override
void initState() {
super.initState();
_shimmerController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_shimmerAnimation = Tween<double>(begin: -1.0, end: 2.0).animate(
CurvedAnimation(parent: _shimmerController, curve: Curves.linear),
);
}
@override
void dispose() {
_shimmerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
return Container(
width: widget.width,
height: widget.height,
margin: widget.margin,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? BorderRadius.circular(4),
color: theme.surfaceContainer,
),
child: AnimatedBuilder(
animation: _shimmerAnimation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? BorderRadius.circular(4),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
theme.shimmerBase,
theme.shimmerHighlight,
theme.shimmerBase,
],
stops: [
_shimmerAnimation.value - 0.3,
_shimmerAnimation.value,
_shimmerAnimation.value + 0.3,
],
),
),
);
},
),
);
}
}
/// Content placeholder for loading states
class ContentPlaceholder extends StatelessWidget {
final int lineCount;
final double lineHeight;
final double spacing;
final EdgeInsetsGeometry? padding;
final bool showAvatar;
final bool showActions;
const ContentPlaceholder({
super.key,
this.lineCount = 3,
this.lineHeight = 16,
this.spacing = 8,
this.padding,
this.showAvatar = false,
this.showActions = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showAvatar)
Row(
children: [
const ShimmerLoader(
width: 48,
height: 48,
borderRadius: BorderRadius.all(Radius.circular(24)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerLoader(width: 120, height: lineHeight),
SizedBox(height: spacing / 2),
ShimmerLoader(width: 80, height: lineHeight * 0.8),
],
),
),
],
),
if (showAvatar) SizedBox(height: spacing * 2),
...List.generate(lineCount, (index) {
final isLast = index == lineCount - 1;
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : spacing),
child: ShimmerLoader(
width: isLast ? 200 : double.infinity,
height: lineHeight,
),
);
}),
if (showActions) ...[
SizedBox(height: spacing * 2),
Row(
children: [
ShimmerLoader(
width: 80,
height: 32,
borderRadius: BorderRadius.circular(16),
),
const SizedBox(width: 8),
ShimmerLoader(
width: 80,
height: 32,
borderRadius: BorderRadius.circular(16),
),
],
),
],
],
),
);
}
}
/// Error state widget with retry
class ErrorStateWidget extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
final Object? error;
final bool showDetails;
const ErrorStateWidget({
super.key,
required this.message,
this.onRetry,
this.error,
this.showDetails = false,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: theme.colorScheme.error),
const SizedBox(height: 16),
Text(
'Oops! Something went wrong',
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
if (showDetails && error != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
error.toString(),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onErrorContainer,
),
),
),
],
if (onRetry != null) ...[
const SizedBox(height: 24),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Try Again'),
),
],
],
),
),
);
}
}