|
@@ -1,7 +1,17 @@
|
|
import 'package:expandable/expandable.dart';
|
|
import 'package:expandable/expandable.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
-import 'package:photos/ente_theme_data.dart';
|
|
|
|
import 'package:photos/theme/ente_theme.dart';
|
|
import 'package:photos/theme/ente_theme.dart';
|
|
|
|
+import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart';
|
|
|
|
+import 'package:photos/utils/debouncer.dart';
|
|
|
|
+
|
|
|
|
+enum ExecutionState {
|
|
|
|
+ idle,
|
|
|
|
+ inProgress,
|
|
|
|
+ error,
|
|
|
|
+ successful;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+typedef FutureVoidCallback = Future<void> Function();
|
|
|
|
|
|
class MenuItemWidget extends StatefulWidget {
|
|
class MenuItemWidget extends StatefulWidget {
|
|
final Widget captionedTextWidget;
|
|
final Widget captionedTextWidget;
|
|
@@ -26,7 +36,7 @@ class MenuItemWidget extends StatefulWidget {
|
|
|
|
|
|
/// If provided, add this much extra spacing to the right of the trailing icon.
|
|
/// If provided, add this much extra spacing to the right of the trailing icon.
|
|
final double trailingExtraMargin;
|
|
final double trailingExtraMargin;
|
|
- final VoidCallback? onTap;
|
|
|
|
|
|
+ final FutureVoidCallback? onTap;
|
|
final VoidCallback? onDoubleTap;
|
|
final VoidCallback? onDoubleTap;
|
|
final Color? menuItemColor;
|
|
final Color? menuItemColor;
|
|
final bool alignCaptionedTextToLeft;
|
|
final bool alignCaptionedTextToLeft;
|
|
@@ -45,6 +55,18 @@ class MenuItemWidget extends StatefulWidget {
|
|
/// disable gesture detector if not used
|
|
/// disable gesture detector if not used
|
|
final bool isGestureDetectorDisabled;
|
|
final bool isGestureDetectorDisabled;
|
|
|
|
|
|
|
|
+ ///Success state will not be shown if this flag is set to true, only idle and
|
|
|
|
+ ///loading state
|
|
|
|
+ final bool showOnlyLoadingState;
|
|
|
|
+
|
|
|
|
+ final bool surfaceExecutionStates;
|
|
|
|
+
|
|
|
|
+ ///To show success state even when execution time < debouce time, set this
|
|
|
|
+ ///flag to true. If the loading state needs to be shown and success state not,
|
|
|
|
+ ///set the showOnlyLoadingState flag to true, setting this flag to false won't
|
|
|
|
+ ///help.
|
|
|
|
+ final bool alwaysShowSuccessState;
|
|
|
|
+
|
|
const MenuItemWidget({
|
|
const MenuItemWidget({
|
|
required this.captionedTextWidget,
|
|
required this.captionedTextWidget,
|
|
this.isExpandable = false,
|
|
this.isExpandable = false,
|
|
@@ -68,6 +90,9 @@ class MenuItemWidget extends StatefulWidget {
|
|
this.isBottomBorderRadiusRemoved = false,
|
|
this.isBottomBorderRadiusRemoved = false,
|
|
this.isTopBorderRadiusRemoved = false,
|
|
this.isTopBorderRadiusRemoved = false,
|
|
this.isGestureDetectorDisabled = false,
|
|
this.isGestureDetectorDisabled = false,
|
|
|
|
+ this.showOnlyLoadingState = false,
|
|
|
|
+ this.surfaceExecutionStates = true,
|
|
|
|
+ this.alwaysShowSuccessState = false,
|
|
Key? key,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
}) : super(key: key);
|
|
|
|
|
|
@@ -76,6 +101,10 @@ class MenuItemWidget extends StatefulWidget {
|
|
}
|
|
}
|
|
|
|
|
|
class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
|
|
+ final _debouncer = Debouncer(const Duration(milliseconds: 300));
|
|
|
|
+ ValueNotifier<ExecutionState> executionStateNotifier =
|
|
|
|
+ ValueNotifier(ExecutionState.idle);
|
|
|
|
+
|
|
Color? menuItemColor;
|
|
Color? menuItemColor;
|
|
late double borderRadius;
|
|
late double borderRadius;
|
|
|
|
|
|
@@ -105,6 +134,7 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
if (widget.expandableController != null) {
|
|
if (widget.expandableController != null) {
|
|
widget.expandableController!.dispose();
|
|
widget.expandableController!.dispose();
|
|
}
|
|
}
|
|
|
|
+ executionStateNotifier.dispose();
|
|
super.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
|
|
@@ -113,7 +143,7 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
return widget.isExpandable || widget.isGestureDetectorDisabled
|
|
return widget.isExpandable || widget.isGestureDetectorDisabled
|
|
? menuItemWidget(context)
|
|
? menuItemWidget(context)
|
|
: GestureDetector(
|
|
: GestureDetector(
|
|
- onTap: widget.onTap,
|
|
|
|
|
|
+ onTap: _onTap,
|
|
onDoubleTap: widget.onDoubleTap,
|
|
onDoubleTap: widget.onDoubleTap,
|
|
onTapDown: _onTapDown,
|
|
onTapDown: _onTapDown,
|
|
onTapUp: _onTapUp,
|
|
onTapUp: _onTapUp,
|
|
@@ -123,7 +153,6 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
}
|
|
}
|
|
|
|
|
|
Widget menuItemWidget(BuildContext context) {
|
|
Widget menuItemWidget(BuildContext context) {
|
|
- final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
|
|
|
|
final circularRadius = Radius.circular(borderRadius);
|
|
final circularRadius = Radius.circular(borderRadius);
|
|
final isExpanded = widget.expandableController?.value;
|
|
final isExpanded = widget.expandableController?.value;
|
|
final bottomBorderRadius =
|
|
final bottomBorderRadius =
|
|
@@ -151,66 +180,70 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
children: [
|
|
children: [
|
|
widget.alignCaptionedTextToLeft && widget.leadingIcon == null
|
|
widget.alignCaptionedTextToLeft && widget.leadingIcon == null
|
|
? const SizedBox.shrink()
|
|
? const SizedBox.shrink()
|
|
- : Padding(
|
|
|
|
- padding: const EdgeInsets.only(right: 10),
|
|
|
|
- child: SizedBox(
|
|
|
|
- height: widget.leadingIconSize,
|
|
|
|
- width: widget.leadingIconSize,
|
|
|
|
- child: widget.leadingIcon == null
|
|
|
|
- ? (widget.leadingIconWidget != null
|
|
|
|
- ? FittedBox(
|
|
|
|
- fit: BoxFit.contain,
|
|
|
|
- child: widget.leadingIconWidget,
|
|
|
|
- )
|
|
|
|
- : const SizedBox.shrink())
|
|
|
|
- : FittedBox(
|
|
|
|
- fit: BoxFit.contain,
|
|
|
|
- child: Icon(
|
|
|
|
- widget.leadingIcon,
|
|
|
|
- color: widget.leadingIconColor ??
|
|
|
|
- enteColorScheme.strokeBase,
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
|
|
+ : LeadingWidget(
|
|
|
|
+ leadingIconSize: widget.leadingIconSize,
|
|
|
|
+ leadingIcon: widget.leadingIcon,
|
|
|
|
+ leadingIconColor: widget.leadingIconColor,
|
|
|
|
+ leadingIconWidget: widget.leadingIconWidget,
|
|
),
|
|
),
|
|
widget.captionedTextWidget,
|
|
widget.captionedTextWidget,
|
|
- widget.expandableController != null
|
|
|
|
- ? AnimatedOpacity(
|
|
|
|
- duration: const Duration(milliseconds: 100),
|
|
|
|
- curve: Curves.easeInOut,
|
|
|
|
- opacity: isExpanded! ? 0 : 1,
|
|
|
|
- child: AnimatedSwitcher(
|
|
|
|
- transitionBuilder: (child, animation) {
|
|
|
|
- return ScaleTransition(scale: animation, child: child);
|
|
|
|
- },
|
|
|
|
- duration: const Duration(milliseconds: 200),
|
|
|
|
- switchInCurve: Curves.easeOut,
|
|
|
|
- child: isExpanded
|
|
|
|
- ? const SizedBox.shrink()
|
|
|
|
- : Icon(
|
|
|
|
- widget.trailingIcon,
|
|
|
|
- color: widget.trailingIconColor,
|
|
|
|
- ),
|
|
|
|
- ),
|
|
|
|
- )
|
|
|
|
- : widget.trailingIcon != null
|
|
|
|
- ? Padding(
|
|
|
|
- padding: EdgeInsets.only(
|
|
|
|
- right: widget.trailingExtraMargin,
|
|
|
|
- ),
|
|
|
|
- child: Icon(
|
|
|
|
- widget.trailingIcon,
|
|
|
|
- color: widget.trailingIconIsMuted
|
|
|
|
- ? enteColorScheme.strokeMuted
|
|
|
|
- : widget.trailingIconColor,
|
|
|
|
- ),
|
|
|
|
- )
|
|
|
|
- : widget.trailingWidget ?? const SizedBox.shrink(),
|
|
|
|
|
|
+ if (widget.expandableController != null)
|
|
|
|
+ ExpansionTrailingIcon(
|
|
|
|
+ isExpanded: isExpanded!,
|
|
|
|
+ trailingIcon: widget.trailingIcon,
|
|
|
|
+ trailingIconColor: widget.trailingIconColor,
|
|
|
|
+ )
|
|
|
|
+ else
|
|
|
|
+ TrailingWidget(
|
|
|
|
+ executionStateNotifier: executionStateNotifier,
|
|
|
|
+ trailingIcon: widget.trailingIcon,
|
|
|
|
+ trailingIconColor: widget.trailingIconColor,
|
|
|
|
+ trailingWidget: widget.trailingWidget,
|
|
|
|
+ trailingIconIsMuted: widget.trailingIconIsMuted,
|
|
|
|
+ trailingExtraMargin: widget.trailingExtraMargin,
|
|
|
|
+ showExecutionStates: widget.surfaceExecutionStates,
|
|
|
|
+ key: ValueKey(widget.trailingIcon.hashCode),
|
|
|
|
+ ),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ Future<void> _onTap() async {
|
|
|
|
+ _debouncer.run(
|
|
|
|
+ () => Future(
|
|
|
|
+ () {
|
|
|
|
+ executionStateNotifier.value = ExecutionState.inProgress;
|
|
|
|
+ },
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ await widget.onTap?.call().then(
|
|
|
|
+ (value) {
|
|
|
|
+ widget.alwaysShowSuccessState
|
|
|
|
+ ? executionStateNotifier.value = ExecutionState.successful
|
|
|
|
+ : null;
|
|
|
|
+ },
|
|
|
|
+ onError: (error, stackTrace) => _debouncer.cancelDebounce(),
|
|
|
|
+ );
|
|
|
|
+ _debouncer.cancelDebounce();
|
|
|
|
+ if (widget.alwaysShowSuccessState) {
|
|
|
|
+ Future.delayed(const Duration(seconds: 2), () {
|
|
|
|
+ executionStateNotifier.value = ExecutionState.idle;
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (executionStateNotifier.value == ExecutionState.inProgress) {
|
|
|
|
+ if (widget.showOnlyLoadingState) {
|
|
|
|
+ executionStateNotifier.value = ExecutionState.idle;
|
|
|
|
+ } else {
|
|
|
|
+ executionStateNotifier.value = ExecutionState.successful;
|
|
|
|
+ Future.delayed(const Duration(seconds: 2), () {
|
|
|
|
+ executionStateNotifier.value = ExecutionState.idle;
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
void _onTapDown(details) {
|
|
void _onTapDown(details) {
|
|
setState(() {
|
|
setState(() {
|
|
if (widget.pressedColor == null) {
|
|
if (widget.pressedColor == null) {
|