mirror of
https://github.com/immich-app/immich.git
synced 2026-06-22 14:52:17 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b49a65af4 | |||
| d32cff8f2c | |||
| a66852e2b5 | |||
| 4cf71ed752 | |||
| 05aff251d0 |
@@ -1,7 +1,9 @@
|
||||
export 'src/components/close_button.dart';
|
||||
export 'src/components/column_button.dart';
|
||||
export 'src/components/form.dart';
|
||||
export 'src/components/formatted_text.dart';
|
||||
export 'src/components/icon_button.dart';
|
||||
export 'src/components/menu_item.dart';
|
||||
export 'src/components/password_input.dart';
|
||||
export 'src/components/text_button.dart';
|
||||
export 'src/components/text_input.dart';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ImmichColorOverride extends InheritedWidget {
|
||||
const ImmichColorOverride({super.key, required this.color, required super.child});
|
||||
|
||||
final Color color;
|
||||
|
||||
static Color? maybeOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<ImmichColorOverride>()?.color;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ImmichColorOverride oldWidget) => color != oldWidget.color;
|
||||
}
|
||||
@@ -16,10 +16,9 @@ class ImmichCloseButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImmichIconButton(
|
||||
key: key,
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichColumnButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
const ImmichColumnButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichColumnButton> createState() => _ImmichColumnButtonState();
|
||||
}
|
||||
|
||||
class _ImmichColumnButtonState extends State<ImmichColumnButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final foreground = context.colorOverride ?? Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
return TextButton(
|
||||
onPressed: widget.disabled || _isLoading ? null : _onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
padding: const .symmetric(horizontal: ImmichSpacing.sm, vertical: ImmichSpacing.md),
|
||||
tapTargetSize: .shrinkWrap,
|
||||
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.xl))),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const .new(maxWidth: 90),
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
_isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: Icon(widget.icon, size: ImmichIconSize.md),
|
||||
const SizedBox(height: ImmichSpacing.sm),
|
||||
Text(
|
||||
widget.label,
|
||||
maxLines: 2,
|
||||
textAlign: .center,
|
||||
overflow: .ellipsis,
|
||||
style: const .new(fontSize: ImmichTextSize.label, fontWeight: .w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class _ImmichFormState extends State<ImmichForm> {
|
||||
builder: (context, _) => ImmichTextButton(
|
||||
labelText: submitText,
|
||||
icon: widget.submitIcon,
|
||||
variant: ImmichVariant.filled,
|
||||
variant: .filled,
|
||||
loading: _controller.isLoading,
|
||||
onPressed: _controller.submit,
|
||||
disabled: _controller.onSubmit == null,
|
||||
|
||||
@@ -94,12 +94,12 @@ class _ImmichFormattedTextState extends State<ImmichFormattedText> {
|
||||
|
||||
final tag = match.group(1)!.toLowerCase();
|
||||
final content = match.group(2)!;
|
||||
final formattedSpan = (widget.spanBuilder ?? _defaultSpanBuilder)(tag);
|
||||
final style = formattedSpan.style ?? _defaultTextStyle(tag);
|
||||
final span = widget.spanBuilder?.call(tag);
|
||||
final style = span?.style ?? _defaultTextStyle(tag);
|
||||
|
||||
GestureRecognizer? recognizer;
|
||||
if (formattedSpan.onTap != null) {
|
||||
recognizer = TapGestureRecognizer()..onTap = formattedSpan.onTap;
|
||||
if (span?.onTap != null) {
|
||||
recognizer = TapGestureRecognizer()..onTap = span!.onTap;
|
||||
_recognizers.add(recognizer);
|
||||
}
|
||||
spans.add(TextSpan(text: content, style: style, recognizer: recognizer));
|
||||
@@ -114,19 +114,12 @@ class _ImmichFormattedTextState extends State<ImmichFormattedText> {
|
||||
return spans;
|
||||
}
|
||||
|
||||
FormattedSpan _defaultSpanBuilder(String tag) => switch (tag) {
|
||||
'b' => const FormattedSpan(style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
'link' => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)),
|
||||
_ when tag.endsWith('-link') => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)),
|
||||
_ => const FormattedSpan(),
|
||||
};
|
||||
|
||||
TextStyle? _defaultTextStyle(String tag) => switch (tag) {
|
||||
'b' => const TextStyle(fontWeight: FontWeight.bold),
|
||||
'link' => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ => null,
|
||||
};
|
||||
'b' => const TextStyle(fontWeight: FontWeight.bold),
|
||||
'link' => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,54 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichIconButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.color = .primary,
|
||||
this.variant = .filled,
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichIconButton> createState() => _ImmichIconButtonState();
|
||||
}
|
||||
|
||||
class _ImmichIconButtonState extends State<ImmichIconButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
final background = switch (widget.variant) {
|
||||
.filled => switch (widget.color) {
|
||||
.primary => colorScheme.primary,
|
||||
.secondary => colorScheme.secondary,
|
||||
},
|
||||
.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => colorScheme.onPrimary,
|
||||
ImmichColor.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
final effectiveOnPressed = disabled ? null : onPressed;
|
||||
final foreground =
|
||||
context.colorOverride ??
|
||||
switch (widget.variant) {
|
||||
.filled => switch (widget.color) {
|
||||
.primary => colorScheme.onPrimary,
|
||||
.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
.ghost => switch (widget.color) {
|
||||
.primary => colorScheme.primary,
|
||||
.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: effectiveOnPressed,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
),
|
||||
icon: _isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.sm,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md),
|
||||
)
|
||||
: Icon(widget.icon),
|
||||
onPressed: widget.disabled || _isLoading ? null : _onPressed,
|
||||
style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichMenu extends StatefulWidget {
|
||||
final List<Widget> children;
|
||||
final MenuAnchorChildBuilder builder;
|
||||
final MenuStyle? style;
|
||||
final bool consumeOutsideTap;
|
||||
final Widget? child;
|
||||
|
||||
const ImmichMenu({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.builder,
|
||||
this.style,
|
||||
this.consumeOutsideTap = false,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichMenu> createState() => _ImmichMenuState();
|
||||
}
|
||||
|
||||
class _ImmichMenuState extends State<ImmichMenu> {
|
||||
final _controller = MenuController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ImmichMenuScope(
|
||||
controller: _controller,
|
||||
child: MenuAnchor(
|
||||
controller: _controller,
|
||||
style: widget.style,
|
||||
consumeOutsideTap: widget.consumeOutsideTap,
|
||||
menuChildren: widget.children,
|
||||
builder: widget.builder,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImmichMenuScope extends InheritedWidget {
|
||||
final MenuController controller;
|
||||
|
||||
const _ImmichMenuScope({required this.controller, required super.child});
|
||||
|
||||
static MenuController? maybeOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<_ImmichMenuScope>()?.controller;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ImmichMenuScope oldWidget) => controller != oldWidget.controller;
|
||||
}
|
||||
|
||||
class ImmichMenuItem extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichMenuItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichMenuItem> createState() => _ImmichMenuItemState();
|
||||
}
|
||||
|
||||
class _ImmichMenuItemState extends State<ImmichMenuItem> {
|
||||
Future<void> _onPressed(MenuController? controller) async {
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
controller?.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = _ImmichMenuScope.maybeOf(context);
|
||||
return MenuItemButton(
|
||||
onPressed: widget.disabled ? null : () => _onPressed(controller),
|
||||
closeOnActivate: controller == null,
|
||||
style: MenuItemButton.styleFrom(
|
||||
foregroundColor: context.colorOverride,
|
||||
alignment: .centerLeft,
|
||||
padding: const .symmetric(horizontal: ImmichSpacing.lg, vertical: ImmichSpacing.md),
|
||||
),
|
||||
leadingIcon: Icon(widget.icon, size: ImmichIconSize.sm),
|
||||
child: Text(widget.label, style: const .new(fontSize: ImmichTextSize.body)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,6 @@ class _ImmichPasswordInputState extends State<ImmichPasswordInput> {
|
||||
icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded),
|
||||
),
|
||||
autofillHints: [AutofillHints.password],
|
||||
keyboardType: TextInputType.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,72 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class ImmichTextButton extends StatelessWidget {
|
||||
class ImmichTextButton extends StatefulWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool expanded;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
const ImmichTextButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = .filled,
|
||||
this.expanded = true,
|
||||
this.loading = false,
|
||||
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
|
||||
Widget _buildButton(ImmichVariant variant) {
|
||||
final Widget? effectiveIcon = loading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: icon != null
|
||||
? Icon(icon, fontWeight: FontWeight.w600)
|
||||
: null;
|
||||
final hasIcon = effectiveIcon != null;
|
||||
@override
|
||||
State<ImmichTextButton> createState() => _ImmichTextButtonState();
|
||||
}
|
||||
|
||||
final label = Text(labelText, style: const TextStyle(fontSize: ImmichTextSize.body, fontWeight: FontWeight.bold));
|
||||
final style = ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: ImmichSpacing.md));
|
||||
class _ImmichTextButtonState extends State<ImmichTextButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
|
||||
final effectiveOnPressed = disabled || loading ? null : onPressed;
|
||||
|
||||
switch (variant) {
|
||||
case ImmichVariant.filled:
|
||||
if (hasIcon) {
|
||||
return ElevatedButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
case ImmichVariant.ghost:
|
||||
if (hasIcon) {
|
||||
return TextButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final button = _buildButton(variant);
|
||||
if (expanded) {
|
||||
final Widget? icon = _isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: widget.icon != null
|
||||
? Icon(widget.icon, fontWeight: .w600)
|
||||
: null;
|
||||
|
||||
final label = Text(
|
||||
widget.labelText,
|
||||
style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold),
|
||||
);
|
||||
final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md));
|
||||
final onPressed = widget.disabled || _isLoading ? null : _onPressed;
|
||||
|
||||
final button = switch (widget.variant) {
|
||||
ImmichVariant.filled => ElevatedButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
ImmichVariant.ghost => TextButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
};
|
||||
|
||||
if (widget.expanded) {
|
||||
return SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
return button;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/color_override.dart';
|
||||
import 'package:immich_ui/src/translation.dart';
|
||||
|
||||
extension TranslationHelper on BuildContext {
|
||||
ImmichTranslations get translations => ImmichTranslationProvider.of(this);
|
||||
}
|
||||
|
||||
extension ColorHelper on BuildContext {
|
||||
Color? get colorOverride => ImmichColorOverride.maybeOf(this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/components/column_button.dart';
|
||||
import 'package:immich_ui/src/previews.dart';
|
||||
|
||||
void _previewNoop() {}
|
||||
|
||||
@ImmichPreview(group: 'ColumnButton', name: 'Default')
|
||||
Widget previewColumnButtonDefault() => const Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichColumnButton(onPressed: _previewNoop, icon: Icons.favorite_border_rounded, label: 'Favorite'),
|
||||
ImmichColumnButton(onPressed: _previewNoop, icon: Icons.archive_outlined, label: 'Archive'),
|
||||
ImmichColumnButton(onPressed: _previewNoop, icon: Icons.delete_outline_rounded, label: 'Delete'),
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'ColumnButton', name: 'Loading')
|
||||
Widget previewColumnButtonLoading() => ImmichColumnButton(
|
||||
onPressed: () => Future<void>.delayed(const .new(seconds: 2)),
|
||||
icon: Icons.download,
|
||||
label: 'Download',
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'ColumnButton', name: 'Disabled')
|
||||
Widget previewColumnButtonDisabled() =>
|
||||
const ImmichColumnButton(onPressed: _previewNoop, icon: Icons.ios_share_rounded, label: 'Share', disabled: true);
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/components/menu_item.dart';
|
||||
import 'package:immich_ui/src/previews.dart';
|
||||
|
||||
void _previewNoop() {}
|
||||
|
||||
@ImmichPreview(group: 'MenuItem', name: 'Default')
|
||||
Widget previewMenuItemDefault() => const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ImmichMenuItem(onPressed: _previewNoop, icon: Icons.info_outline, label: 'Info'),
|
||||
ImmichMenuItem(onPressed: _previewNoop, icon: Icons.help_outline_rounded, label: 'Troubleshoot'),
|
||||
ImmichMenuItem(onPressed: _previewNoop, icon: Icons.cast_rounded, label: 'Cast'),
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'MenuItem', name: 'Disabled')
|
||||
Widget previewMenuItemDisabled() =>
|
||||
const ImmichMenuItem(onPressed: _previewNoop, icon: Icons.delete_outline_rounded, label: 'Delete', disabled: true);
|
||||
@@ -15,16 +15,6 @@ Widget previewTextButtonVariants() => const Wrap(
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Colors')
|
||||
Widget previewTextButtonColors() => const Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(onPressed: _previewNoop, labelText: 'Primary', expanded: false),
|
||||
ImmichTextButton(onPressed: _previewNoop, labelText: 'Secondary', color: ImmichColor.secondary, expanded: false),
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'With Icons')
|
||||
Widget previewTextButtonWithIcons() => const Wrap(
|
||||
spacing: 12,
|
||||
@@ -42,7 +32,11 @@ Widget previewTextButtonWithIcons() => const Wrap(
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Loading')
|
||||
Widget previewTextButtonLoading() => const _PreviewLoadingDemo();
|
||||
Widget previewTextButtonLoading() => ImmichTextButton(
|
||||
onPressed: () => Future<void>.delayed(const Duration(seconds: 2)),
|
||||
labelText: 'Click me',
|
||||
expanded: false,
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Disabled')
|
||||
Widget previewTextButtonDisabled() => const Wrap(
|
||||
@@ -59,30 +53,3 @@ Widget previewTextButtonDisabled() => const Wrap(
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class _PreviewLoadingDemo extends StatefulWidget {
|
||||
const _PreviewLoadingDemo();
|
||||
|
||||
@override
|
||||
State<_PreviewLoadingDemo> createState() => _PreviewLoadingDemoState();
|
||||
}
|
||||
|
||||
class _PreviewLoadingDemoState extends State<_PreviewLoadingDemo> {
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichTextButton(
|
||||
onPressed: () async {
|
||||
setState(() => _isLoading = true);
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
},
|
||||
labelText: _isLoading ? 'Loading...' : 'Click Me',
|
||||
loading: _isLoading,
|
||||
expanded: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_ui/src/color_override.dart';
|
||||
import 'package:immich_ui/src/components/icon_button.dart';
|
||||
|
||||
import 'test_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('ImmichColorOverride', () {
|
||||
testWidgets('exposes the override color to descendants', (tester) async {
|
||||
Color? captured;
|
||||
await tester.pumpTestWidget(
|
||||
ImmichColorOverride(
|
||||
color: Colors.green,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
captured = ImmichColorOverride.maybeOf(context);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(captured, Colors.green);
|
||||
});
|
||||
|
||||
testWidgets('maybeOf returns null when there is no override', (tester) async {
|
||||
Color? captured = Colors.black;
|
||||
await tester.pumpTestWidget(
|
||||
Builder(
|
||||
builder: (context) {
|
||||
captured = ImmichColorOverride.maybeOf(context);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(captured, isNull);
|
||||
});
|
||||
|
||||
testWidgets('a descendant component adopts the override as its foreground', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
ImmichColorOverride(
|
||||
color: Colors.green,
|
||||
child: ImmichIconButton(icon: Icons.add, onPressed: () {}),
|
||||
),
|
||||
);
|
||||
|
||||
final button = tester.widget<IconButton>(find.byType(IconButton));
|
||||
expect(button.style?.foregroundColor?.resolve(<WidgetState>{}), Colors.green);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user