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;
|
|||
|
|
}
|
|||
|
|
}
|