import 'package:flutter/material.dart'; import '../theme/theme_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import '../services/brand_service.dart'; import '../theme/app_theme.dart'; import 'package:conduit/l10n/app_localizations.dart'; /// Standard loading indicators following Conduit design patterns class ConduitLoading { // Private constructor to prevent instantiation ConduitLoading._(); /// Primary loading indicator static Widget primary({ double size = IconSize.lg, Color? color, String? message, }) { return _LoadingIndicator( size: size, color: color ?? BrandService.primaryBrandColor, message: message, type: _LoadingType.primary, ); } /// Inline loading for content areas static Widget inline({ double size = IconSize.md, Color? color, String? message, BuildContext? context, }) { return _LoadingIndicator( size: size, color: color ?? (context?.conduitTheme.loadingIndicator ?? context?.conduitTheme.buttonPrimary ?? AppTheme.brandPrimary), message: message, type: _LoadingType.inline, ); } /// Button loading state static Widget button({ double size = IconSize.sm, Color? color, BuildContext? context, }) { return _LoadingIndicator( size: size, color: color ?? (context?.conduitTheme.buttonPrimaryText ?? context?.conduitTheme.textPrimary ?? AppTheme.neutral50), type: _LoadingType.button, ); } /// Overlay loading for full screen static Widget overlay({String? message, bool darkBackground = true}) { return _LoadingOverlay(message: message, darkBackground: darkBackground); } /// Skeleton loading for content placeholders static Widget skeleton({ double width = double.infinity, double height = 20, BorderRadius? borderRadius, }) { return _SkeletonLoader( width: width, height: height, borderRadius: borderRadius ?? BorderRadius.circular(AppBorderRadius.xs), ); } /// List item skeleton static Widget listItemSkeleton({bool showAvatar = true, int lines = 2}) { return _ListItemSkeleton(showAvatar: showAvatar, lines: lines); } } enum _LoadingType { primary, inline, button } class _LoadingIndicator extends StatelessWidget { final double size; final Color color; final String? message; final _LoadingType type; const _LoadingIndicator({ required this.size, required this.color, this.message, required this.type, }); @override Widget build(BuildContext context) { Widget indicator; if (Platform.isIOS) { indicator = CupertinoActivityIndicator(color: color, radius: size / 2); } else { indicator = SizedBox( width: size, height: size, child: CircularProgressIndicator( strokeWidth: size / 8, valueColor: AlwaysStoppedAnimation(color), ), ); } if (message == null) { return indicator; } final spacing = type == _LoadingType.button ? Spacing.sm : Spacing.xs; return Column( mainAxisSize: MainAxisSize.min, children: [ indicator, SizedBox(height: spacing), Text( message!, style: TextStyle( color: color, fontSize: type == _LoadingType.button ? AppTypography.bodySmall : AppTypography.bodyLarge, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ], ); } } class _LoadingOverlay extends StatelessWidget { final String? message; final bool darkBackground; const _LoadingOverlay({this.message, required this.darkBackground}); @override Widget build(BuildContext context) { return Container( color: darkBackground ? context.conduitTheme.surfaceBackground.withValues( alpha: Alpha.strong, ) : context.conduitTheme.surfaceBackground.withValues( alpha: Alpha.intense, ), child: Center( child: Container( padding: const EdgeInsets.all(Spacing.lg), decoration: BoxDecoration( color: darkBackground ? context.conduitTheme.surfaceBackground : context.conduitTheme.surfaceBackground, borderRadius: BorderRadius.circular(AppBorderRadius.lg), boxShadow: ConduitShadows.high, ), child: ConduitLoading.primary( size: IconSize.xl, color: context.conduitTheme.buttonPrimary, message: message, ), ), ), ).animate().fadeIn(duration: AnimationDuration.fast); } } class _SkeletonLoader extends StatefulWidget { final double width; final double height; final BorderRadius borderRadius; const _SkeletonLoader({ required this.width, required this.height, required this.borderRadius, }); @override State<_SkeletonLoader> createState() => _SkeletonLoaderState(); } class _SkeletonLoaderState extends State<_SkeletonLoader> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: AnimationDuration.ultra, vsync: this, ); _animation = Tween( begin: AnimationValues.shimmerBegin, end: AnimationValues.shimmerEnd, ).animate( CurvedAnimation( parent: _controller, curve: AnimationCurves.easeInOut, ), ); _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 Widget build(BuildContext context) { return Container( width: widget.width, height: widget.height, decoration: BoxDecoration( borderRadius: widget.borderRadius, color: context.conduitTheme.shimmerBase, ), child: AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( decoration: BoxDecoration( borderRadius: widget.borderRadius, gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ Colors.transparent, context.conduitTheme.shimmerHighlight, Colors.transparent, ], stops: [ (_animation.value - 0.3).clamp(0.0, 1.0), _animation.value.clamp(0.0, 1.0), (_animation.value + 0.3).clamp(0.0, 1.0), ], ), ), ); }, ), ); } } class _ListItemSkeleton extends StatelessWidget { final bool showAvatar; final int lines; const _ListItemSkeleton({required this.showAvatar, required this.lines}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.xs, ), child: Row( children: [ if (showAvatar) ...[ ConduitLoading.skeleton( width: TouchTarget.minimum, height: TouchTarget.minimum, borderRadius: BorderRadius.circular(AppBorderRadius.xl), ), const SizedBox(width: Spacing.xs), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(lines, (index) { return Padding( padding: EdgeInsets.only( bottom: index < lines - 1 ? Spacing.sm : 0, ), child: ConduitLoading.skeleton( width: index == lines - 1 ? 150 : double.infinity, height: index == 0 ? 16 : 14, ), ); }), ), ), ], ), ); } } /// Loading state wrapper for async operations class LoadingStateWrapper extends StatelessWidget { final AsyncValue asyncValue; final Widget Function(T data) builder; final Widget? loadingWidget; final Widget Function(Object error, StackTrace stackTrace)? errorBuilder; final bool showLoadingOverlay; const LoadingStateWrapper({ super.key, required this.asyncValue, required this.builder, this.loadingWidget, this.errorBuilder, this.showLoadingOverlay = false, }); @override Widget build(BuildContext context) { return asyncValue.when( data: builder, loading: () => showLoadingOverlay ? ConduitLoading.overlay(message: AppLocalizations.of(context)!.loadingContent) : loadingWidget ?? ConduitLoading.primary(message: AppLocalizations.of(context)!.loadingContent), error: (error, stackTrace) { if (errorBuilder != null) { return errorBuilder!(error, stackTrace); } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Platform.isIOS ? CupertinoIcons.exclamationmark_triangle : Icons.error_outline, size: IconSize.xxl, color: context.conduitTheme.error, ), const SizedBox(height: Spacing.md), Text( AppLocalizations.of(context)!.errorMessage, style: TextStyle( color: context.conduitTheme.textSecondary, fontSize: AppTypography.headlineSmall, fontWeight: FontWeight.w500, ), ), const SizedBox(height: Spacing.sm), Text( error.toString(), style: TextStyle( color: context.conduitTheme.textSecondary, fontSize: AppTypography.bodySmall, ), textAlign: TextAlign.center, ), ], ), ); }, ); } } /// Button with loading state class LoadingButton extends StatelessWidget { final VoidCallback? onPressed; final Widget child; final bool isLoading; final bool isPrimary; const LoadingButton({ super.key, required this.onPressed, required this.child, this.isLoading = false, this.isPrimary = true, }); @override Widget build(BuildContext context) { return FilledButton( onPressed: isLoading ? null : onPressed, style: isPrimary ? FilledButton.styleFrom( backgroundColor: context.conduitTheme.buttonPrimary, foregroundColor: context.conduitTheme.buttonPrimaryText, ) : null, child: isLoading ? ConduitLoading.button(context: context) : child, ); } } /// Refresh indicator with Conduit styling class ConduitRefreshIndicator extends StatelessWidget { final Widget child; final Future Function() onRefresh; const ConduitRefreshIndicator({ super.key, required this.child, required this.onRefresh, }); @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: onRefresh, color: context.conduitTheme.buttonPrimary, backgroundColor: context.conduitTheme.surfaceBackground, child: child, ); } }