Files
iiEsaywebUIapp/lib/shared/widgets/skeleton_loader.dart
2025-08-28 19:17:05 +05:30

378 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/theme_extensions.dart';
/// Enhanced skeleton loader with production-grade animations and better hierarchy
class SkeletonLoader extends StatefulWidget {
final double? width;
final double? height;
final BorderRadius? borderRadius;
final Duration? duration;
final Color? baseColor;
final Color? highlightColor;
final bool isCompact;
const SkeletonLoader({
super.key,
this.width,
this.height,
this.borderRadius,
this.duration,
this.baseColor,
this.highlightColor,
this.isCompact = false,
});
@override
State<SkeletonLoader> createState() => _SkeletonLoaderState();
}
class _SkeletonLoaderState extends State<SkeletonLoader>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration ?? AnimationDuration.typingIndicator,
vsync: this,
);
_animation =
Tween<double>(
begin: AnimationValues.shimmerBegin,
end: AnimationValues.shimmerEnd,
).animate(
CurvedAnimation(parent: _controller, curve: AnimationCurves.linear),
);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void deactivate() {
// Pause shimmer during deactivation to avoid rebuilds in wrong build scope
_controller.stop();
super.deactivate();
}
@override
void activate() {
super.activate();
if (!_controller.isAnimating) {
// Resume shimmer after re-activation
_controller.repeat();
}
}
@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 ??
BorderRadius.circular(
widget.isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
widget.baseColor ?? context.conduitTheme.shimmerBase,
widget.highlightColor ?? context.conduitTheme.shimmerHighlight,
widget.baseColor ?? context.conduitTheme.shimmerBase,
],
stops: [
_animation.value - 0.3,
_animation.value,
_animation.value + 0.3,
],
),
),
);
},
);
}
}
/// Enhanced skeleton for chat messages with better hierarchy
class SkeletonChatMessage extends StatelessWidget {
final bool isUser;
final int lines;
final bool isCompact;
const SkeletonChatMessage({
super.key,
this.isUser = false,
this.lines = 2,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: isCompact ? Spacing.sm : Spacing.messagePadding,
vertical: isCompact ? Spacing.xs : Spacing.sm,
),
child: Row(
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
SkeletonLoader(
width: isCompact ? 32 : 40,
height: isCompact ? 32 : 40,
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
),
SizedBox(width: isCompact ? Spacing.xs : Spacing.sm),
],
Expanded(
child: Column(
crossAxisAlignment: isUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
for (int i = 0; i < lines; i++)
Padding(
padding: EdgeInsets.only(
bottom: i < lines - 1
? (isCompact ? Spacing.xs : Spacing.sm)
: 0,
),
child: SkeletonLoader(
width: isUser
? null
: (MediaQuery.of(context).size.width * 0.6),
height: isCompact ? 12 : 16,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
),
],
),
),
if (isUser) ...[
SizedBox(width: isCompact ? Spacing.xs : Spacing.sm),
SkeletonLoader(
width: isCompact ? 32 : 40,
height: isCompact ? 32 : 40,
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
),
],
],
),
);
}
}
/// Enhanced skeleton for list items with better hierarchy
class SkeletonListItem extends StatelessWidget {
final bool showAvatar;
final bool showSubtitle;
final bool isCompact;
const SkeletonListItem({
super.key,
this.showAvatar = true,
this.showSubtitle = true,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(isCompact ? Spacing.sm : Spacing.listItemPadding),
child: Row(
children: [
if (showAvatar) ...[
SkeletonLoader(
width: isCompact ? 32 : 40,
height: isCompact ? 32 : 40,
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
),
SizedBox(width: isCompact ? Spacing.sm : Spacing.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SkeletonLoader(
width: double.infinity,
height: isCompact ? 14 : 16,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
if (showSubtitle) ...[
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
SkeletonLoader(
width: MediaQuery.of(context).size.width * 0.7,
height: isCompact ? 12 : 14,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
],
],
),
),
],
),
);
}
}
/// Enhanced skeleton for cards with better hierarchy
class SkeletonCard extends StatelessWidget {
final bool showTitle;
final bool showContent;
final bool showActions;
final bool isCompact;
const SkeletonCard({
super.key,
this.showTitle = true,
this.showContent = true,
this.showActions = false,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(isCompact ? Spacing.sm : Spacing.cardPadding),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all(
color: context.conduitTheme.cardBorder,
width: BorderWidth.standard,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showTitle) ...[
SkeletonLoader(
width: MediaQuery.of(context).size.width * 0.8,
height: isCompact ? 16 : 20,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
SizedBox(height: isCompact ? Spacing.sm : Spacing.md),
],
if (showContent) ...[
SkeletonLoader(
width: double.infinity,
height: isCompact ? 12 : 14,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
SkeletonLoader(
width: MediaQuery.of(context).size.width * 0.6,
height: isCompact ? 12 : 14,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
if (showActions) ...[
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SkeletonLoader(
width: isCompact ? 60 : 80,
height: isCompact ? 32 : 40,
borderRadius: BorderRadius.circular(AppBorderRadius.button),
),
SizedBox(width: isCompact ? Spacing.sm : Spacing.md),
SkeletonLoader(
width: isCompact ? 60 : 80,
height: isCompact ? 32 : 40,
borderRadius: BorderRadius.circular(AppBorderRadius.button),
),
],
),
],
],
],
),
);
}
}
/// Enhanced skeleton for input fields with better hierarchy
class SkeletonInput extends StatelessWidget {
final bool showLabel;
final bool isCompact;
const SkeletonInput({
super.key,
this.showLabel = true,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel) ...[
SkeletonLoader(
width: 80,
height: isCompact ? 14 : 16,
borderRadius: BorderRadius.circular(
isCompact ? AppBorderRadius.xs : AppBorderRadius.sm,
),
),
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
],
SkeletonLoader(
width: double.infinity,
height: isCompact ? 40 : 48,
borderRadius: BorderRadius.circular(AppBorderRadius.input),
),
],
);
}
}
/// Enhanced skeleton for buttons with better hierarchy
class SkeletonButton extends StatelessWidget {
final bool isFullWidth;
final bool isCompact;
const SkeletonButton({
super.key,
this.isFullWidth = false,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
return SkeletonLoader(
width: isFullWidth ? double.infinity : (isCompact ? 80 : 120),
height: isCompact ? TouchTarget.medium : TouchTarget.comfortable,
borderRadius: BorderRadius.circular(AppBorderRadius.button),
);
}
}