123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- import 'package:expandable/expandable.dart';
- import 'package:flutter/material.dart';
- import 'package:photos/models/execution_states.dart';
- import 'package:photos/models/typedefs.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';
- 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;
- });
- }
- }
|