bottleneckcrm/mobile/lib/widgets/common/loading.dart
2026-10-20 21:48:14 +05:30

288 lines
7.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/theme/theme.dart';
/// Loading Dots Animation - Used on splash screen
class LoadingDots extends StatefulWidget {
final Color? color;
final double size;
const LoadingDots({super.key, this.color, this.size = 8});
@override
State<LoadingDots> createState() => _LoadingDotsState();
}
class _LoadingDotsState extends State<LoadingDots>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final dotColor = widget.color ?? Colors.white;
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final delay = index * 0.2;
final value = (_controller.value - delay) % 1.0;
final scale = value < 0.5
? 1.0 + (value * 0.5)
: 1.5 - ((value - 0.5) * 0.5);
final opacity = value < 0.5
? 0.3 + (value * 1.4)
: 1.0 - ((value - 0.5) * 1.4);
return Container(
margin: EdgeInsets.symmetric(horizontal: widget.size * 0.4),
child: Transform.scale(
scale: scale.clamp(0.8, 1.2),
child: Opacity(
opacity: opacity.clamp(0.3, 1.0),
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
),
),
);
},
);
}),
);
}
}
/// Loading Spinner - Circular progress indicator
class LoadingSpinner extends StatelessWidget {
final Color? color;
final double size;
final double strokeWidth;
const LoadingSpinner({
super.key,
this.color,
this.size = 24,
this.strokeWidth = 2.5,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: strokeWidth,
valueColor: AlwaysStoppedAnimation<Color>(
color ?? AppColors.primary600,
),
),
);
}
}
/// Full Screen Loading Overlay
class LoadingOverlay extends StatelessWidget {
final String? message;
const LoadingOverlay({super.key, this.message});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withValues(alpha: 0.3),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppLayout.borderRadiusLg,
boxShadow: AppLayout.shadowLg,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const LoadingSpinner(size: 32),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: AppTypography.body.copyWith(
color: AppColors.textSecondary,
),
),
],
],
),
),
),
);
}
}
/// Shimmer Loading Effect
class ShimmerLoading extends StatefulWidget {
final double width;
final double height;
final BorderRadius? borderRadius;
const ShimmerLoading({
super.key,
required this.width,
required this.height,
this.borderRadius,
});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
_animation = Tween<double>(
begin: -1,
end: 2,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? AppLayout.borderRadiusSm,
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: const [
AppColors.gray100,
AppColors.gray50,
AppColors.gray100,
],
stops: [
_animation.value - 0.3,
_animation.value,
_animation.value + 0.3,
].map((e) => e.clamp(0.0, 1.0)).toList(),
),
),
);
},
);
}
}
/// Empty State Widget
class EmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String? description;
final String? actionLabel;
final VoidCallback? onAction;
/// A second, quieter action under the first, such as Import beside Add.
final String? secondaryActionLabel;
final VoidCallback? onSecondaryAction;
const EmptyState({
super.key,
required this.icon,
required this.title,
this.description,
this.actionLabel,
this.onAction,
this.secondaryActionLabel,
this.onSecondaryAction,
});
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
padding: AppSpacing.page,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.gray100,
shape: BoxShape.circle,
),
child: Icon(icon, size: 36, color: AppColors.gray400),
),
const SizedBox(height: 16),
Text(
title,
style: AppTypography.h3.copyWith(color: AppColors.gray700),
textAlign: TextAlign.center,
),
if (description != null) ...[
const SizedBox(height: 8),
Text(
description!,
style: AppTypography.body.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 24),
ElevatedButton(onPressed: onAction, child: Text(actionLabel!)),
],
if (secondaryActionLabel != null && onSecondaryAction != null) ...[
const SizedBox(height: 8),
OutlinedButton(
onPressed: onSecondaryAction,
child: Text(secondaryActionLabel!),
),
],
],
),
),
);
}
}