100 lines
3.2 KiB
Dart
100 lines
3.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:shuffle/core/theme/app_spacing.dart';
|
|
|
|
enum AppBannerType { info, error }
|
|
|
|
/// Inline message banner (info / error) above forms.
|
|
///
|
|
/// Type is conveyed by icon + [typeLabel] (spoken prefix, e.g. "Error"),
|
|
/// never by color alone. The message is a live region, so screen readers
|
|
/// announce it when it appears. The optional action is a separate 48 dp text
|
|
/// button. All texts come from the caller (l10n).
|
|
class AppBanner extends StatelessWidget {
|
|
const AppBanner({
|
|
required this.type,
|
|
required this.message,
|
|
required this.typeLabel,
|
|
super.key,
|
|
this.actionLabel,
|
|
this.onAction,
|
|
this.actionEnabled = true,
|
|
}) : assert(
|
|
(actionLabel == null) == (onAction == null),
|
|
'actionLabel and onAction belong together',
|
|
);
|
|
|
|
final AppBannerType type;
|
|
final String message;
|
|
final String typeLabel;
|
|
final String? actionLabel;
|
|
final VoidCallback? onAction;
|
|
|
|
/// `false` keeps the action visible and in the focus order but disabled
|
|
/// (e.g. while a request runs). Screen readers still find it and hear that
|
|
/// it is unavailable, instead of the control vanishing under their focus.
|
|
final bool actionEnabled;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final scheme = theme.colorScheme;
|
|
final isError = type == AppBannerType.error;
|
|
final background = isError
|
|
? scheme.errorContainer
|
|
: scheme.secondaryContainer;
|
|
final foreground = isError
|
|
? scheme.onErrorContainer
|
|
: scheme.onSecondaryContainer;
|
|
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: background,
|
|
borderRadius: AppRadius.mdAll,
|
|
border: Border.all(color: foreground.withValues(alpha: 0.5)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Semantics(
|
|
container: true,
|
|
liveRegion: true,
|
|
label: '$typeLabel: $message',
|
|
child: ExcludeSemantics(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(
|
|
isError ? Icons.error_outline : Icons.info_outline,
|
|
color: foreground,
|
|
),
|
|
const SizedBox(width: AppSpacing.md),
|
|
Expanded(
|
|
child: Text(
|
|
message,
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: foreground,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (actionLabel != null)
|
|
Align(
|
|
alignment: AlignmentDirectional.centerEnd,
|
|
child: TextButton(
|
|
onPressed: actionEnabled ? onAction : null,
|
|
style: TextButton.styleFrom(foregroundColor: foreground),
|
|
child: Text(actionLabel!),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|