449 lines
12 KiB
Dart
449 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
|
|
|
enum MwButtonVariant { primary, secondary, ghost, destructive }
|
|
|
|
class MwButton extends StatelessWidget {
|
|
const MwButton({
|
|
super.key,
|
|
required this.label,
|
|
required this.onPressed,
|
|
this.icon,
|
|
this.variant = MwButtonVariant.primary,
|
|
this.loading = false,
|
|
this.expand = false,
|
|
});
|
|
|
|
final String label;
|
|
final VoidCallback? onPressed;
|
|
final IconData? icon;
|
|
final MwButtonVariant variant;
|
|
final bool loading;
|
|
final bool expand;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = MoonWellDesignTokens.of(context);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final child = loading
|
|
? const SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, size: 18),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Flexible(child: Text(label, overflow: TextOverflow.ellipsis)),
|
|
],
|
|
);
|
|
final callback = loading ? null : onPressed;
|
|
final button = switch (variant) {
|
|
MwButtonVariant.primary => ElevatedButton(
|
|
onPressed: callback,
|
|
child: child,
|
|
),
|
|
MwButtonVariant.secondary => OutlinedButton(
|
|
onPressed: callback,
|
|
child: child,
|
|
),
|
|
MwButtonVariant.ghost => TextButton(onPressed: callback, child: child),
|
|
MwButtonVariant.destructive => ElevatedButton(
|
|
onPressed: callback,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: scheme.error,
|
|
foregroundColor: scheme.onError,
|
|
disabledBackgroundColor: scheme.error.withAlpha(50),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(tokens.radiusMedium),
|
|
),
|
|
),
|
|
child: child,
|
|
),
|
|
};
|
|
return Semantics(
|
|
button: true,
|
|
enabled: callback != null,
|
|
child: SizedBox(width: expand ? double.infinity : null, child: button),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MwIconButton extends StatelessWidget {
|
|
const MwIconButton({
|
|
super.key,
|
|
required this.icon,
|
|
required this.tooltip,
|
|
required this.onPressed,
|
|
this.selected = false,
|
|
this.destructive = false,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String tooltip;
|
|
final VoidCallback? onPressed;
|
|
final bool selected;
|
|
final bool destructive;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
return Tooltip(
|
|
message: tooltip,
|
|
child: Semantics(
|
|
label: tooltip,
|
|
button: true,
|
|
enabled: onPressed != null,
|
|
selected: selected,
|
|
child: IconButton(
|
|
onPressed: onPressed,
|
|
isSelected: selected,
|
|
icon: Icon(icon),
|
|
color: destructive ? scheme.error : scheme.onSurfaceVariant,
|
|
style: IconButton.styleFrom(minimumSize: const Size(44, 44)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MwTextField extends StatelessWidget {
|
|
const MwTextField({
|
|
super.key,
|
|
required this.label,
|
|
this.controller,
|
|
this.hint,
|
|
this.errorText,
|
|
this.prefixIcon,
|
|
this.enabled = true,
|
|
this.obscureText = false,
|
|
this.textInputAction,
|
|
this.onSubmitted,
|
|
});
|
|
|
|
final String label;
|
|
final TextEditingController? controller;
|
|
final String? hint;
|
|
final String? errorText;
|
|
final IconData? prefixIcon;
|
|
final bool enabled;
|
|
final bool obscureText;
|
|
final TextInputAction? textInputAction;
|
|
final ValueChanged<String>? onSubmitted;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextField(
|
|
controller: controller,
|
|
enabled: enabled,
|
|
obscureText: obscureText,
|
|
textInputAction: textInputAction,
|
|
onSubmitted: onSubmitted,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
hintText: hint,
|
|
errorText: errorText,
|
|
prefixIcon: prefixIcon == null ? null : Icon(prefixIcon),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MwCheckbox extends StatelessWidget {
|
|
const MwCheckbox({
|
|
super.key,
|
|
required this.label,
|
|
required this.value,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final String label;
|
|
final bool value;
|
|
final ValueChanged<bool?>? onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CheckboxListTile(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
title: Text(label),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
contentPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
);
|
|
}
|
|
}
|
|
|
|
class MwTabs extends StatelessWidget {
|
|
const MwTabs({
|
|
super.key,
|
|
required this.labels,
|
|
required this.selectedIndex,
|
|
required this.onSelected,
|
|
this.disabledIndices = const {},
|
|
});
|
|
|
|
final List<String> labels;
|
|
final int selectedIndex;
|
|
final ValueChanged<int> onSelected;
|
|
final Set<int> disabledIndices;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Wrap(
|
|
spacing: 4,
|
|
children: [
|
|
for (var index = 0; index < labels.length; index++)
|
|
ChoiceChip(
|
|
label: Text(labels[index]),
|
|
selected: selectedIndex == index,
|
|
onSelected: disabledIndices.contains(index)
|
|
? null
|
|
: (_) => onSelected(index),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
enum MwBadgeTone { neutral, success, warning, error, comingSoon }
|
|
|
|
class MwBadge extends StatelessWidget {
|
|
const MwBadge({
|
|
super.key,
|
|
required this.label,
|
|
this.tone = MwBadgeTone.neutral,
|
|
});
|
|
|
|
final String label;
|
|
final MwBadgeTone tone;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = MoonWellDesignTokens.of(context);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final color = switch (tone) {
|
|
MwBadgeTone.neutral => scheme.onSurfaceVariant,
|
|
MwBadgeTone.success => tokens.success,
|
|
MwBadgeTone.warning => tokens.warning,
|
|
MwBadgeTone.error => scheme.error,
|
|
MwBadgeTone.comingSoon => tokens.accentMuted,
|
|
};
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withAlpha(24),
|
|
border: Border.all(color: color.withAlpha(130)),
|
|
borderRadius: BorderRadius.circular(99),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
color: color,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum MwPanelLevel { low, regular, high }
|
|
|
|
class MwPanel extends StatelessWidget {
|
|
const MwPanel({
|
|
super.key,
|
|
required this.child,
|
|
this.level = MwPanelLevel.regular,
|
|
this.padding = const EdgeInsets.all(24),
|
|
});
|
|
|
|
final Widget child;
|
|
final MwPanelLevel level;
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = MoonWellDesignTokens.of(context);
|
|
final color = switch (level) {
|
|
MwPanelLevel.low => tokens.surfaceLow,
|
|
MwPanelLevel.regular => tokens.surface,
|
|
MwPanelLevel.high => tokens.surfaceHigh,
|
|
};
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: color.withAlpha(238),
|
|
border: Border.fromBorderSide(tokens.border),
|
|
borderRadius: BorderRadius.circular(tokens.radiusLarge),
|
|
boxShadow: tokens.panelShadow,
|
|
),
|
|
child: Padding(padding: padding, child: child),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum MwProgressState { active, paused, completed, failure }
|
|
|
|
class MwProgressBar extends StatelessWidget {
|
|
const MwProgressBar({
|
|
super.key,
|
|
this.value,
|
|
this.label,
|
|
this.state = MwProgressState.active,
|
|
});
|
|
|
|
final double? value;
|
|
final String? label;
|
|
final MwProgressState state;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = MoonWellDesignTokens.of(context);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final color = switch (state) {
|
|
MwProgressState.active => scheme.primary,
|
|
MwProgressState.paused => tokens.warning,
|
|
MwProgressState.completed => tokens.success,
|
|
MwProgressState.failure => scheme.error,
|
|
};
|
|
return Semantics(
|
|
label: label ?? 'Прогресс',
|
|
value: value == null ? null : '${(value! * 100).round()}%',
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (label != null) ...[
|
|
Text(label!, style: Theme.of(context).textTheme.bodySmall),
|
|
const SizedBox(height: 8),
|
|
],
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(99),
|
|
child: LinearProgressIndicator(
|
|
value: value,
|
|
color: color,
|
|
backgroundColor: color.withAlpha(35),
|
|
minHeight: 7,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum MwStatusTone { operational, success, warning, error }
|
|
|
|
class MwStatusText extends StatelessWidget {
|
|
const MwStatusText({
|
|
super.key,
|
|
required this.text,
|
|
this.tone = MwStatusTone.operational,
|
|
this.monospace = false,
|
|
});
|
|
|
|
final String text;
|
|
final MwStatusTone tone;
|
|
final bool monospace;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = MoonWellDesignTokens.of(context);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final color = switch (tone) {
|
|
MwStatusTone.operational => scheme.onSurfaceVariant,
|
|
MwStatusTone.success => tokens.success,
|
|
MwStatusTone.warning => tokens.warning,
|
|
MwStatusTone.error => scheme.error,
|
|
};
|
|
return Text(
|
|
text,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: color,
|
|
fontFamily: monospace ? tokens.monospaceFontFamily : null,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum MwStateKind { loading, empty, error }
|
|
|
|
class MwStateView extends StatelessWidget {
|
|
const MwStateView({
|
|
super.key,
|
|
required this.kind,
|
|
required this.title,
|
|
this.description,
|
|
this.onRetry,
|
|
});
|
|
|
|
final MwStateKind kind;
|
|
final String title;
|
|
final String? description;
|
|
final VoidCallback? onRetry;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final icon = switch (kind) {
|
|
MwStateKind.loading => null,
|
|
MwStateKind.empty => Icons.inbox_outlined,
|
|
MwStateKind.error => Icons.cloud_off_outlined,
|
|
};
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon == null)
|
|
const SizedBox.square(
|
|
dimension: 28,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
else
|
|
Icon(icon, size: 34),
|
|
const SizedBox(height: 12),
|
|
Text(title, textAlign: TextAlign.center),
|
|
if (description != null) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
description!,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
if (onRetry != null) ...[
|
|
const SizedBox(height: 16),
|
|
MwButton(
|
|
label: 'Повторить',
|
|
onPressed: onRetry,
|
|
variant: MwButtonVariant.secondary,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MwDisabledFeature extends StatelessWidget {
|
|
const MwDisabledFeature({
|
|
super.key,
|
|
required this.message,
|
|
required this.child,
|
|
});
|
|
|
|
final String message;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Tooltip(message: message, child: child);
|
|
}
|
|
}
|