284 lines
9.3 KiB
Dart
284 lines
9.3 KiB
Dart
import 'package:ente_auth/models/execution_states.dart';
|
|
import 'package:ente_auth/models/typedefs.dart';
|
|
import 'package:ente_auth/theme/ente_theme.dart';
|
|
import 'package:ente_auth/ui/components/menu_item_child_widgets.dart';
|
|
import 'package:ente_auth/utils/debouncer.dart';
|
|
import 'package:expandable/expandable.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class MenuItemWidget extends StatefulWidget {
|
|
final Widget captionedTextWidget;
|
|
final bool isExpandable;
|
|
|
|
/// leading icon can be passed without specifing size of icon,
|
|
/// this component sets size to 20x20 irrespective of passed icon's size
|
|
final IconData? leadingIcon;
|
|
final Color? leadingIconColor;
|
|
|
|
final Widget? leadingIconWidget;
|
|
|
|
// leadIconSize deafult value is 20.
|
|
final double leadingIconSize;
|
|
|
|
/// trailing icon can be passed without size as default size set by
|
|
/// flutter is what this component expects
|
|
final IconData? trailingIcon;
|
|
final Color? trailingIconColor;
|
|
final Widget? trailingWidget;
|
|
final bool trailingIconIsMuted;
|
|
|
|
/// If provided, add this much extra spacing to the right of the trailing icon.
|
|
final double trailingExtraMargin;
|
|
final FutureVoidCallback? onTap;
|
|
final VoidCallback? onDoubleTap;
|
|
final Color? menuItemColor;
|
|
final bool alignCaptionedTextToLeft;
|
|
|
|
// singleBorderRadius is applied to the border when it's a standalone menu item.
|
|
// Widget will apply singleBorderRadius if value of both isTopBorderRadiusRemoved
|
|
// and isBottomBorderRadiusRemoved is false. Otherwise, multipleBorderRadius will
|
|
// be applied
|
|
final double singleBorderRadius;
|
|
final double multipleBorderRadius;
|
|
final Color? pressedColor;
|
|
final ExpandableController? expandableController;
|
|
final bool isBottomBorderRadiusRemoved;
|
|
final bool isTopBorderRadiusRemoved;
|
|
|
|
/// disable gesture detector if not used
|
|
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({
|
|
required this.captionedTextWidget,
|
|
this.isExpandable = false,
|
|
this.leadingIcon,
|
|
this.leadingIconColor,
|
|
this.leadingIconSize = 20.0,
|
|
this.leadingIconWidget,
|
|
this.trailingIcon,
|
|
this.trailingIconColor,
|
|
this.trailingWidget,
|
|
this.trailingIconIsMuted = false,
|
|
this.trailingExtraMargin = 0.0,
|
|
this.onTap,
|
|
this.onDoubleTap,
|
|
this.menuItemColor,
|
|
this.alignCaptionedTextToLeft = false,
|
|
this.singleBorderRadius = 4.0,
|
|
this.multipleBorderRadius = 8.0,
|
|
this.pressedColor,
|
|
this.expandableController,
|
|
this.isBottomBorderRadiusRemoved = false,
|
|
this.isTopBorderRadiusRemoved = false,
|
|
this.isGestureDetectorDisabled = false,
|
|
this.showOnlyLoadingState = false,
|
|
this.surfaceExecutionStates = true,
|
|
this.alwaysShowSuccessState = false,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<MenuItemWidget> createState() => _MenuItemWidgetState();
|
|
}
|
|
|
|
class _MenuItemWidgetState extends State<MenuItemWidget> {
|
|
final _debouncer = Debouncer(const Duration(milliseconds: 300));
|
|
ValueNotifier<ExecutionState> executionStateNotifier =
|
|
ValueNotifier(ExecutionState.idle);
|
|
|
|
Color? menuItemColor;
|
|
late double borderRadius;
|
|
|
|
@override
|
|
void initState() {
|
|
menuItemColor = widget.menuItemColor;
|
|
borderRadius =
|
|
(widget.isBottomBorderRadiusRemoved || widget.isTopBorderRadiusRemoved)
|
|
? widget.multipleBorderRadius
|
|
: widget.singleBorderRadius;
|
|
if (widget.expandableController != null) {
|
|
widget.expandableController!.addListener(() {
|
|
setState(() {});
|
|
});
|
|
}
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
menuItemColor = widget.menuItemColor;
|
|
super.didChangeDependencies();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant MenuItemWidget oldWidget) {
|
|
menuItemColor = widget.menuItemColor;
|
|
super.didUpdateWidget(oldWidget);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (widget.expandableController != null) {
|
|
widget.expandableController!.dispose();
|
|
}
|
|
executionStateNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return widget.isExpandable || widget.isGestureDetectorDisabled
|
|
? menuItemWidget(context)
|
|
: GestureDetector(
|
|
onTap: _onTap,
|
|
onDoubleTap: widget.onDoubleTap,
|
|
onTapDown: _onTapDown,
|
|
onTapUp: _onTapUp,
|
|
onTapCancel: _onCancel,
|
|
child: menuItemWidget(context),
|
|
);
|
|
}
|
|
|
|
Widget menuItemWidget(BuildContext context) {
|
|
final circularRadius = Radius.circular(borderRadius);
|
|
final isExpanded = widget.expandableController?.value;
|
|
final bottomBorderRadius =
|
|
(isExpanded != null && isExpanded) || widget.isBottomBorderRadiusRemoved
|
|
? const Radius.circular(0)
|
|
: circularRadius;
|
|
final topBorderRadius = widget.isTopBorderRadiusRemoved
|
|
? const Radius.circular(0)
|
|
: circularRadius;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 20),
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.only(left: 16, right: 12),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: topBorderRadius,
|
|
topRight: topBorderRadius,
|
|
bottomLeft: bottomBorderRadius,
|
|
bottomRight: bottomBorderRadius,
|
|
),
|
|
color: menuItemColor,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
widget.alignCaptionedTextToLeft && widget.leadingIcon == null
|
|
? const SizedBox.shrink()
|
|
: LeadingWidget(
|
|
leadingIconSize: widget.leadingIconSize,
|
|
leadingIcon: widget.leadingIcon,
|
|
leadingIconColor: widget.leadingIconColor,
|
|
leadingIconWidget: widget.leadingIconWidget,
|
|
),
|
|
widget.captionedTextWidget,
|
|
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 {
|
|
if (executionStateNotifier.value == ExecutionState.inProgress ||
|
|
executionStateNotifier.value == ExecutionState.successful) return;
|
|
_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) {
|
|
if (executionStateNotifier.value == ExecutionState.inProgress ||
|
|
executionStateNotifier.value == ExecutionState.successful) return;
|
|
setState(() {
|
|
if (widget.pressedColor == null) {
|
|
hasPassedGestureCallbacks()
|
|
? menuItemColor = getEnteColorScheme(context).fillFaintPressed
|
|
: menuItemColor = widget.menuItemColor;
|
|
} else {
|
|
menuItemColor = widget.pressedColor;
|
|
}
|
|
});
|
|
}
|
|
|
|
bool hasPassedGestureCallbacks() {
|
|
return widget.onDoubleTap != null || widget.onTap != null;
|
|
}
|
|
|
|
void _onTapUp(details) {
|
|
if (executionStateNotifier.value == ExecutionState.inProgress ||
|
|
executionStateNotifier.value == ExecutionState.successful) return;
|
|
Future.delayed(
|
|
const Duration(milliseconds: 100),
|
|
() => setState(() {
|
|
menuItemColor = widget.menuItemColor;
|
|
}),
|
|
);
|
|
}
|
|
|
|
void _onCancel() {
|
|
if (executionStateNotifier.value == ExecutionState.inProgress ||
|
|
executionStateNotifier.value == ExecutionState.successful) return;
|
|
setState(() {
|
|
menuItemColor = widget.menuItemColor;
|
|
});
|
|
}
|
|
}
|