import 'package:flutter/material.dart'; import '../theme/m3_motion.dart'; /// A centered error state with an icon, title, human-readable message and an /// optional retry button. /// /// Usage: /// ```dart /// if (async.hasError && !async.hasValue) { /// return AppErrorView( /// error: async.error!, /// onRetry: () => ref.invalidate(myProvider), /// ); /// } /// ``` class AppErrorView extends StatelessWidget { const AppErrorView({ super.key, required this.error, this.title = 'Something went wrong', this.onRetry, }); final Object error; /// Short title shown above the error message. final String title; /// When provided, a "Try again" button is rendered below the message. final VoidCallback? onRetry; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final message = _humanise(error); return Center( child: M3FadeSlideIn( duration: M3Motion.standard, child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ M3BounceIcon( icon: Icons.error_outline_rounded, iconColor: cs.onErrorContainer, backgroundColor: cs.errorContainer, ), const SizedBox(height: 20), Text( title, style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w700), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( message, style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant), textAlign: TextAlign.center, maxLines: 4, overflow: TextOverflow.ellipsis, ), if (onRetry != null) ...[ const SizedBox(height: 24), OutlinedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh_rounded), label: const Text('Try again'), ), ], ], ), ), ), ); } /// Strips common Dart exception prefixes so the user sees a clean message. static String _humanise(Object error) { var text = error.toString(); const prefixes = ['Exception: ', 'Error: ', 'FormatException: ']; for (final p in prefixes) { if (text.startsWith(p)) { text = text.substring(p.length); break; } } return text.isEmpty ? 'An unexpected error occurred.' : text; } } /// A centered empty-state view with an icon, title and optional subtitle. /// /// Usage: /// ```dart /// if (items.isEmpty && !loading) { /// return const AppEmptyView( /// icon: Icons.task_outlined, /// title: 'No tasks yet', /// subtitle: 'Tasks assigned to you will appear here.', /// ); /// } /// ``` class AppEmptyView extends StatelessWidget { const AppEmptyView({ super.key, this.icon = Icons.inbox_outlined, required this.title, this.subtitle, this.action, }); final IconData icon; final String title; final String? subtitle; /// Optional call-to-action placed below the subtitle. final Widget? action; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; return Center( child: M3FadeSlideIn( duration: M3Motion.standard, child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ M3BounceIcon( icon: icon, iconColor: cs.onSurfaceVariant, backgroundColor: cs.surfaceContainerHighest, ), const SizedBox(height: 20), Text( title, style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), if (subtitle != null) ...[ const SizedBox(height: 8), Text( subtitle!, style: tt.bodyMedium?.copyWith(color: cs.onSurfaceVariant), textAlign: TextAlign.center, ), ], if (action != null) ...[ const SizedBox(height: 20), action!, ], ], ), ), ), ); } }