menu_item_widget.dart 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import 'package:expandable/expandable.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:photos/models/execution_states.dart';
  4. import 'package:photos/models/typedefs.dart';
  5. import 'package:photos/theme/ente_theme.dart';
  6. import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart';
  7. import 'package:photos/utils/debouncer.dart';
  8. class MenuItemWidget extends StatefulWidget {
  9. final Widget captionedTextWidget;
  10. final bool isExpandable;
  11. /// leading icon can be passed without specifing size of icon,
  12. /// this component sets size to 20x20 irrespective of passed icon's size
  13. final IconData? leadingIcon;
  14. final Color? leadingIconColor;
  15. final Widget? leadingIconWidget;
  16. // leadIconSize deafult value is 20.
  17. final double leadingIconSize;
  18. /// trailing icon can be passed without size as default size set by
  19. /// flutter is what this component expects
  20. final IconData? trailingIcon;
  21. final Color? trailingIconColor;
  22. final Widget? trailingWidget;
  23. final bool trailingIconIsMuted;
  24. /// If provided, add this much extra spacing to the right of the trailing icon.
  25. final double trailingExtraMargin;
  26. final FutureVoidCallback? onTap;
  27. final VoidCallback? onDoubleTap;
  28. final Color? menuItemColor;
  29. final bool alignCaptionedTextToLeft;
  30. // singleBorderRadius is applied to the border when it's a standalone menu item.
  31. // Widget will apply singleBorderRadius if value of both isTopBorderRadiusRemoved
  32. // and isBottomBorderRadiusRemoved is false. Otherwise, multipleBorderRadius will
  33. // be applied
  34. final double singleBorderRadius;
  35. final double multipleBorderRadius;
  36. final Color? pressedColor;
  37. final ExpandableController? expandableController;
  38. final bool isBottomBorderRadiusRemoved;
  39. final bool isTopBorderRadiusRemoved;
  40. /// disable gesture detector if not used
  41. final bool isGestureDetectorDisabled;
  42. ///Success state will not be shown if this flag is set to true, only idle and
  43. ///loading state
  44. final bool showOnlyLoadingState;
  45. final bool surfaceExecutionStates;
  46. ///To show success state even when execution time < debouce time, set this
  47. ///flag to true. If the loading state needs to be shown and success state not,
  48. ///set the showOnlyLoadingState flag to true, setting this flag to false won't
  49. ///help.
  50. final bool alwaysShowSuccessState;
  51. const MenuItemWidget({
  52. required this.captionedTextWidget,
  53. this.isExpandable = false,
  54. this.leadingIcon,
  55. this.leadingIconColor,
  56. this.leadingIconSize = 20.0,
  57. this.leadingIconWidget,
  58. this.trailingIcon,
  59. this.trailingIconColor,
  60. this.trailingWidget,
  61. this.trailingIconIsMuted = false,
  62. this.trailingExtraMargin = 0.0,
  63. this.onTap,
  64. this.onDoubleTap,
  65. this.menuItemColor,
  66. this.alignCaptionedTextToLeft = false,
  67. this.singleBorderRadius = 4.0,
  68. this.multipleBorderRadius = 8.0,
  69. this.pressedColor,
  70. this.expandableController,
  71. this.isBottomBorderRadiusRemoved = false,
  72. this.isTopBorderRadiusRemoved = false,
  73. this.isGestureDetectorDisabled = false,
  74. this.showOnlyLoadingState = false,
  75. this.surfaceExecutionStates = true,
  76. this.alwaysShowSuccessState = false,
  77. Key? key,
  78. }) : super(key: key);
  79. @override
  80. State<MenuItemWidget> createState() => _MenuItemWidgetState();
  81. }
  82. class _MenuItemWidgetState extends State<MenuItemWidget> {
  83. final _debouncer = Debouncer(const Duration(milliseconds: 300));
  84. ValueNotifier<ExecutionState> executionStateNotifier =
  85. ValueNotifier(ExecutionState.idle);
  86. Color? menuItemColor;
  87. late double borderRadius;
  88. @override
  89. void initState() {
  90. menuItemColor = widget.menuItemColor;
  91. borderRadius =
  92. (widget.isBottomBorderRadiusRemoved || widget.isTopBorderRadiusRemoved)
  93. ? widget.multipleBorderRadius
  94. : widget.singleBorderRadius;
  95. if (widget.expandableController != null) {
  96. widget.expandableController!.addListener(() {
  97. setState(() {});
  98. });
  99. }
  100. super.initState();
  101. }
  102. @override
  103. void didChangeDependencies() {
  104. menuItemColor = widget.menuItemColor;
  105. super.didChangeDependencies();
  106. }
  107. @override
  108. void didUpdateWidget(covariant MenuItemWidget oldWidget) {
  109. menuItemColor = widget.menuItemColor;
  110. super.didUpdateWidget(oldWidget);
  111. }
  112. @override
  113. void dispose() {
  114. if (widget.expandableController != null) {
  115. widget.expandableController!.dispose();
  116. }
  117. executionStateNotifier.dispose();
  118. super.dispose();
  119. }
  120. @override
  121. Widget build(BuildContext context) {
  122. return widget.isExpandable || widget.isGestureDetectorDisabled
  123. ? menuItemWidget(context)
  124. : GestureDetector(
  125. onTap: _onTap,
  126. onDoubleTap: widget.onDoubleTap,
  127. onTapDown: _onTapDown,
  128. onTapUp: _onTapUp,
  129. onTapCancel: _onCancel,
  130. child: menuItemWidget(context),
  131. );
  132. }
  133. Widget menuItemWidget(BuildContext context) {
  134. final circularRadius = Radius.circular(borderRadius);
  135. final isExpanded = widget.expandableController?.value;
  136. final bottomBorderRadius =
  137. (isExpanded != null && isExpanded) || widget.isBottomBorderRadiusRemoved
  138. ? const Radius.circular(0)
  139. : circularRadius;
  140. final topBorderRadius = widget.isTopBorderRadiusRemoved
  141. ? const Radius.circular(0)
  142. : circularRadius;
  143. return AnimatedContainer(
  144. duration: const Duration(milliseconds: 20),
  145. width: double.infinity,
  146. padding: const EdgeInsets.only(left: 16, right: 12),
  147. decoration: BoxDecoration(
  148. borderRadius: BorderRadius.only(
  149. topLeft: topBorderRadius,
  150. topRight: topBorderRadius,
  151. bottomLeft: bottomBorderRadius,
  152. bottomRight: bottomBorderRadius,
  153. ),
  154. color: menuItemColor,
  155. ),
  156. child: Row(
  157. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  158. children: [
  159. widget.alignCaptionedTextToLeft && widget.leadingIcon == null
  160. ? const SizedBox.shrink()
  161. : LeadingWidget(
  162. leadingIconSize: widget.leadingIconSize,
  163. leadingIcon: widget.leadingIcon,
  164. leadingIconColor: widget.leadingIconColor,
  165. leadingIconWidget: widget.leadingIconWidget,
  166. ),
  167. widget.captionedTextWidget,
  168. if (widget.expandableController != null)
  169. ExpansionTrailingIcon(
  170. isExpanded: isExpanded!,
  171. trailingIcon: widget.trailingIcon,
  172. trailingIconColor: widget.trailingIconColor,
  173. )
  174. else
  175. TrailingWidget(
  176. executionStateNotifier: executionStateNotifier,
  177. trailingIcon: widget.trailingIcon,
  178. trailingIconColor: widget.trailingIconColor,
  179. trailingWidget: widget.trailingWidget,
  180. trailingIconIsMuted: widget.trailingIconIsMuted,
  181. trailingExtraMargin: widget.trailingExtraMargin,
  182. showExecutionStates: widget.surfaceExecutionStates,
  183. key: ValueKey(widget.trailingIcon.hashCode),
  184. ),
  185. ],
  186. ),
  187. );
  188. }
  189. Future<void> _onTap() async {
  190. if (executionStateNotifier.value == ExecutionState.inProgress ||
  191. executionStateNotifier.value == ExecutionState.successful) return;
  192. _debouncer.run(
  193. () => Future(
  194. () {
  195. executionStateNotifier.value = ExecutionState.inProgress;
  196. },
  197. ),
  198. );
  199. await widget.onTap?.call().then(
  200. (value) {
  201. widget.alwaysShowSuccessState
  202. ? executionStateNotifier.value = ExecutionState.successful
  203. : null;
  204. },
  205. onError: (error, stackTrace) => _debouncer.cancelDebounce(),
  206. );
  207. _debouncer.cancelDebounce();
  208. if (widget.alwaysShowSuccessState) {
  209. Future.delayed(const Duration(seconds: 2), () {
  210. executionStateNotifier.value = ExecutionState.idle;
  211. });
  212. return;
  213. }
  214. if (executionStateNotifier.value == ExecutionState.inProgress) {
  215. if (widget.showOnlyLoadingState) {
  216. executionStateNotifier.value = ExecutionState.idle;
  217. } else {
  218. executionStateNotifier.value = ExecutionState.successful;
  219. Future.delayed(const Duration(seconds: 2), () {
  220. executionStateNotifier.value = ExecutionState.idle;
  221. });
  222. }
  223. }
  224. }
  225. void _onTapDown(details) {
  226. if (executionStateNotifier.value == ExecutionState.inProgress ||
  227. executionStateNotifier.value == ExecutionState.successful) return;
  228. setState(() {
  229. if (widget.pressedColor == null) {
  230. hasPassedGestureCallbacks()
  231. ? menuItemColor = getEnteColorScheme(context).fillFaintPressed
  232. : menuItemColor = widget.menuItemColor;
  233. } else {
  234. menuItemColor = widget.pressedColor;
  235. }
  236. });
  237. }
  238. bool hasPassedGestureCallbacks() {
  239. return widget.onDoubleTap != null || widget.onTap != null;
  240. }
  241. void _onTapUp(details) {
  242. if (executionStateNotifier.value == ExecutionState.inProgress ||
  243. executionStateNotifier.value == ExecutionState.successful) return;
  244. Future.delayed(
  245. const Duration(milliseconds: 100),
  246. () => setState(() {
  247. menuItemColor = widget.menuItemColor;
  248. }),
  249. );
  250. }
  251. void _onCancel() {
  252. if (executionStateNotifier.value == ExecutionState.inProgress ||
  253. executionStateNotifier.value == ExecutionState.successful) return;
  254. setState(() {
  255. menuItemColor = widget.menuItemColor;
  256. });
  257. }
  258. }