import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../theme/app_colors.dart'; /// エラー発生時に表示する再試行可能なウィジェット /// /// Riverpod の AsyncValue.error と組み合わせて使用: /// ```dart /// asyncValue.when( /// data: (data) => YourWidget(data), /// loading: () => const CircularProgressIndicator(), /// error: (err, stack) => ErrorRetryWidget( /// message: 'データの読み込みに失敗しました', /// details: err.toString(), /// onRetry: () => ref.refresh(yourProvider), /// ), /// ) /// ``` class ErrorRetryWidget extends StatelessWidget { /// エラーメッセージ(ユーザー向けの説明) final String message; /// 再試行ボタンが押されたときのコールバック final VoidCallback onRetry; /// エラーの詳細情報(デバッグ用、オプション) final String? details; /// ウィジェットを中央配置するか(デフォルト: true) /// false の場合は親ウィジェットのレイアウトに従う final bool centered; /// アイコンのサイズ final double iconSize; /// コンパクト表示(パディングを小さくする) final bool compact; const ErrorRetryWidget({ super.key, required this.message, required this.onRetry, this.details, this.centered = true, this.iconSize = 48, this.compact = false, }); @override Widget build(BuildContext context) { final appColors = Theme.of(context).extension()!; final padding = compact ? const EdgeInsets.all(16) : const EdgeInsets.symmetric(horizontal: 24, vertical: 32); final content = Padding( padding: padding, child: Column( mainAxisSize: MainAxisSize.min, children: [ // エラーアイコン Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: appColors.error.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( LucideIcons.alertCircle, size: iconSize, color: appColors.error, ), ), SizedBox(height: compact ? 12 : 16), // エラーメッセージ Text( message, style: TextStyle( fontSize: compact ? 14 : 16, fontWeight: FontWeight.w600, color: appColors.textPrimary, ), textAlign: TextAlign.center, ), // 詳細情報(存在する場合) if (details != null) ...[ SizedBox(height: compact ? 6 : 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: appColors.surfaceSubtle, borderRadius: BorderRadius.circular(8), border: Border.all(color: appColors.divider), ), child: Text( details!, style: TextStyle( fontSize: 12, color: appColors.textSecondary, fontFamily: 'monospace', ), textAlign: TextAlign.center, maxLines: 3, overflow: TextOverflow.ellipsis, ), ), ], SizedBox(height: compact ? 16 : 24), // 再試行ボタン ElevatedButton.icon( onPressed: onRetry, icon: const Icon(LucideIcons.refreshCw, size: 20), label: const Text('再試行'), style: ElevatedButton.styleFrom( backgroundColor: appColors.brandPrimary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ], ), ); return centered ? Center(child: content) : content; } } /// シンプルなエラー表示ウィジェット(再試行ボタンなし) /// /// 再試行が不要な場合や、別の方法で対処する場合に使用 class ErrorDisplayWidget extends StatelessWidget { final String message; final String? details; final bool centered; final double iconSize; final bool compact; const ErrorDisplayWidget({ super.key, required this.message, this.details, this.centered = true, this.iconSize = 48, this.compact = false, }); @override Widget build(BuildContext context) { final appColors = Theme.of(context).extension()!; final padding = compact ? const EdgeInsets.all(16) : const EdgeInsets.symmetric(horizontal: 24, vertical: 32); final content = Padding( padding: padding, child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.alertCircle, size: iconSize, color: appColors.error, ), SizedBox(height: compact ? 12 : 16), Text( message, style: TextStyle( fontSize: compact ? 14 : 16, color: appColors.textPrimary, ), textAlign: TextAlign.center, ), if (details != null) ...[ SizedBox(height: compact ? 6 : 8), Text( details!, style: TextStyle( fontSize: 12, color: appColors.textSecondary, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ); return centered ? Center(child: content) : content; } }