feat: button longpress handler (#30200)

* feat: button longpress handler

* review suggestion

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-07-24 23:34:21 +05:30
committed by GitHub
co-authored by shenlong-tanwen
parent 4a5f13d0e5
commit 829b4e748e
3 changed files with 94 additions and 23 deletions
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:core';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_ui/src/constants.dart'; import 'package:immich_ui/src/constants.dart';
@@ -8,6 +9,7 @@ class ImmichColumnButton extends StatefulWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final FutureOr<void> Function() onPressed; final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final bool disabled; final bool disabled;
final bool? loading; final bool? loading;
@@ -16,6 +18,7 @@ class ImmichColumnButton extends StatefulWidget {
required this.icon, required this.icon,
required this.label, required this.label,
required this.onPressed, required this.onPressed,
this.onLongPress,
this.disabled = false, this.disabled = false,
this.loading, this.loading,
}); });
@@ -25,26 +28,44 @@ class ImmichColumnButton extends StatefulWidget {
} }
class _ImmichColumnButtonState extends State<ImmichColumnButton> { class _ImmichColumnButtonState extends State<ImmichColumnButton> {
bool _loading = false; bool _running = false;
bool get _isLoading => widget.loading ?? _loading; bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async { Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _loading = true); setState(() => _running = true);
try { try {
await widget.onPressed(); await action();
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _loading = false); setState(() => _running = false);
} }
} }
} }
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final foreground = context.colorOverride ?? Theme.of(context).colorScheme.onSurface; final foreground = context.colorOverride ?? Theme.of(context).colorScheme.onSurface;
return TextButton( return TextButton(
onPressed: widget.disabled || _isLoading ? null : _onPressed, onPressed: _onPressed,
onLongPress: _onLongPress,
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: foreground, foregroundColor: foreground,
padding: const .symmetric(horizontal: ImmichSpacing.sm, vertical: ImmichSpacing.md), padding: const .symmetric(horizontal: ImmichSpacing.sm, vertical: ImmichSpacing.md),
@@ -7,6 +7,7 @@ import 'package:immich_ui/src/internal.dart';
class ImmichIconButton extends StatefulWidget { class ImmichIconButton extends StatefulWidget {
final IconData icon; final IconData icon;
final FutureOr<void> Function() onPressed; final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final ImmichVariant variant; final ImmichVariant variant;
final ImmichColor color; final ImmichColor color;
final bool disabled; final bool disabled;
@@ -16,6 +17,7 @@ class ImmichIconButton extends StatefulWidget {
super.key, super.key,
required this.icon, required this.icon,
required this.onPressed, required this.onPressed,
this.onLongPress,
this.color = .primary, this.color = .primary,
this.variant = .filled, this.variant = .filled,
this.disabled = false, this.disabled = false,
@@ -27,20 +29,37 @@ class ImmichIconButton extends StatefulWidget {
} }
class _ImmichIconButtonState extends State<ImmichIconButton> { class _ImmichIconButtonState extends State<ImmichIconButton> {
bool _loading = false; bool _running = false;
bool get _isLoading => widget.loading ?? _loading; bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async { Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _loading = true); setState(() => _running = true);
try { try {
await widget.onPressed(); await action();
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _loading = false); setState(() => _running = false);
} }
} }
} }
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@@ -73,7 +92,8 @@ class _ImmichIconButtonState extends State<ImmichIconButton> {
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md), child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md),
) )
: Icon(widget.icon), : Icon(widget.icon),
onPressed: widget.disabled || _isLoading ? null : _onPressed, onPressed: _onPressed,
onLongPress: _onLongPress,
style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground), style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground),
); );
} }
@@ -7,6 +7,7 @@ class ImmichTextButton extends StatefulWidget {
final String labelText; final String labelText;
final IconData? icon; final IconData? icon;
final FutureOr<void> Function() onPressed; final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final ImmichVariant variant; final ImmichVariant variant;
final bool expanded; final bool expanded;
final bool disabled; final bool disabled;
@@ -17,6 +18,7 @@ class ImmichTextButton extends StatefulWidget {
required this.labelText, required this.labelText,
this.icon, this.icon,
required this.onPressed, required this.onPressed,
this.onLongPress,
this.variant = .filled, this.variant = .filled,
this.expanded = true, this.expanded = true,
@@ -29,20 +31,37 @@ class ImmichTextButton extends StatefulWidget {
} }
class _ImmichTextButtonState extends State<ImmichTextButton> { class _ImmichTextButtonState extends State<ImmichTextButton> {
bool _loading = false; bool _running = false;
bool get _isLoading => widget.loading ?? _loading; bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async { Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _loading = true); setState(() => _running = true);
try { try {
await widget.onPressed(); await action();
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _loading = false); setState(() => _running = false);
} }
} }
} }
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget? icon = _isLoading final Widget? icon = _isLoading
@@ -59,11 +78,22 @@ class _ImmichTextButtonState extends State<ImmichTextButton> {
style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold), style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold),
); );
final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md)); final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md));
final onPressed = widget.disabled || _isLoading ? null : _onPressed;
final button = switch (widget.variant) { final button = switch (widget.variant) {
ImmichVariant.filled => ElevatedButton.icon(style: style, onPressed: onPressed, icon: icon, label: label), ImmichVariant.filled => ElevatedButton.icon(
ImmichVariant.ghost => TextButton.icon(style: style, onPressed: onPressed, icon: icon, label: label), style: style,
onPressed: _onPressed,
onLongPress: _onLongPress,
icon: icon,
label: label,
),
ImmichVariant.ghost => TextButton.icon(
style: style,
onPressed: _onPressed,
onLongPress: _onLongPress,
icon: icon,
label: label,
),
}; };
if (widget.expanded) { if (widget.expanded) {