205 lines
5.8 KiB
Dart
205 lines
5.8 KiB
Dart
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<AppColors>()!;
|
||
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<AppColors>()!;
|
||
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;
|
||
}
|
||
}
|