diff --git a/mobile/packages/ui/lib/immich_ui.dart b/mobile/packages/ui/lib/immich_ui.dart index cd6cec3ff3..eaf7051637 100644 --- a/mobile/packages/ui/lib/immich_ui.dart +++ b/mobile/packages/ui/lib/immich_ui.dart @@ -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'; diff --git a/mobile/packages/ui/lib/src/components/column_button.dart b/mobile/packages/ui/lib/src/components/column_button.dart new file mode 100644 index 0000000000..bf0023c1bc --- /dev/null +++ b/mobile/packages/ui/lib/src/components/column_button.dart @@ -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 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 createState() => _ImmichColumnButtonState(); +} + +class _ImmichColumnButtonState extends State { + bool _loading = false; + bool get _isLoading => widget.loading ?? _loading; + + Future _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), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/packages/ui/lib/src/components/menu_item.dart b/mobile/packages/ui/lib/src/components/menu_item.dart new file mode 100644 index 0000000000..84a0936e5d --- /dev/null +++ b/mobile/packages/ui/lib/src/components/menu_item.dart @@ -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 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 createState() => _ImmichMenuState(); +} + +class _ImmichMenuState extends State { + 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 Function() onPressed; + final bool disabled; + + const ImmichMenuItem({ + super.key, + required this.icon, + required this.label, + required this.onPressed, + this.disabled = false, + }); + + @override + State createState() => _ImmichMenuItemState(); +} + +class _ImmichMenuItemState extends State { + Future _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)), + ); + } +} diff --git a/mobile/packages/ui/lib/src/previews/column_button.dart b/mobile/packages/ui/lib/src/previews/column_button.dart new file mode 100644 index 0000000000..95f5bfee53 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/column_button.dart @@ -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.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); diff --git a/mobile/packages/ui/lib/src/previews/menu_item.dart b/mobile/packages/ui/lib/src/previews/menu_item.dart new file mode 100644 index 0000000000..759c55cf9c --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/menu_item.dart @@ -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);