button_widget.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import 'package:flutter/material.dart';
  2. import 'package:photos/theme/colors.dart';
  3. import 'package:photos/theme/ente_theme.dart';
  4. import 'package:photos/theme/text_style.dart';
  5. import 'package:photos/ui/common/loading_widget.dart';
  6. import 'package:photos/ui/components/models/button_type.dart';
  7. import 'package:photos/ui/components/models/custom_button_style.dart';
  8. import 'package:photos/utils/debouncer.dart';
  9. enum ExecutionState {
  10. idle,
  11. inProgress,
  12. successful,
  13. }
  14. enum ButtonSize {
  15. small,
  16. large;
  17. }
  18. typedef FutureVoidCallback = Future<void> Function();
  19. class ButtonWidget extends StatelessWidget {
  20. final IconData? icon;
  21. final String? labelText;
  22. final ButtonType buttonType;
  23. final FutureVoidCallback? onTap;
  24. final bool isDisabled;
  25. final ButtonSize buttonSize;
  26. ///setting this flag to true will make the button appear like how it would
  27. ///on dark theme irrespective of the app's theme.
  28. final bool isInActionSheet;
  29. const ButtonWidget({
  30. required this.buttonType,
  31. required this.buttonSize,
  32. this.icon,
  33. this.labelText,
  34. this.onTap,
  35. this.isInActionSheet = false,
  36. this.isDisabled = false,
  37. super.key,
  38. });
  39. @override
  40. Widget build(BuildContext context) {
  41. final colorScheme =
  42. isInActionSheet ? darkScheme : getEnteColorScheme(context);
  43. final inverseColorScheme = isInActionSheet
  44. ? lightScheme
  45. : getEnteColorScheme(context, inverse: true);
  46. final textTheme =
  47. isInActionSheet ? darkTextTheme : getEnteTextTheme(context);
  48. final inverseTextTheme = isInActionSheet
  49. ? lightTextTheme
  50. : getEnteTextTheme(context, inverse: true);
  51. final buttonStyle = CustomButtonStyle(
  52. //Dummy default values since we need to keep these properties non-nullable
  53. defaultButtonColor: Colors.transparent,
  54. defaultBorderColor: Colors.transparent,
  55. defaultIconColor: Colors.transparent,
  56. defaultLabelStyle: textTheme.body,
  57. );
  58. buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
  59. buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
  60. buttonStyle.disabledButtonColor =
  61. buttonType.disabledButtonColor(colorScheme);
  62. buttonStyle.defaultBorderColor = buttonType.defaultBorderColor(colorScheme);
  63. buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(colorScheme);
  64. buttonStyle.disabledBorderColor =
  65. buttonType.disabledBorderColor(colorScheme);
  66. buttonStyle.defaultIconColor = buttonType.defaultIconColor(
  67. colorScheme: colorScheme,
  68. inverseColorScheme: inverseColorScheme,
  69. );
  70. buttonStyle.pressedIconColor = buttonType.pressedIconColor(colorScheme);
  71. buttonStyle.disabledIconColor = buttonType.disabledIconColor(colorScheme);
  72. buttonStyle.defaultLabelStyle = buttonType.defaultLabelStyle(
  73. textTheme: textTheme,
  74. inverseTextTheme: inverseTextTheme,
  75. );
  76. buttonStyle.pressedLabelStyle =
  77. buttonType.pressedLabelStyle(textTheme, colorScheme);
  78. buttonStyle.disabledLabelStyle =
  79. buttonType.disabledLabelStyle(textTheme, colorScheme);
  80. buttonStyle.checkIconColor = buttonType.checkIconColor(colorScheme);
  81. return LargeButtonChildWidget(
  82. buttonStyle: buttonStyle,
  83. buttonType: buttonType,
  84. isDisabled: isDisabled,
  85. buttonSize: buttonSize,
  86. onTap: onTap,
  87. labelText: labelText,
  88. icon: icon,
  89. );
  90. }
  91. }
  92. class LargeButtonChildWidget extends StatefulWidget {
  93. final CustomButtonStyle buttonStyle;
  94. final FutureVoidCallback? onTap;
  95. final ButtonType buttonType;
  96. final String? labelText;
  97. final IconData? icon;
  98. final bool isDisabled;
  99. final ButtonSize buttonSize;
  100. const LargeButtonChildWidget({
  101. required this.buttonStyle,
  102. required this.buttonType,
  103. required this.isDisabled,
  104. required this.buttonSize,
  105. this.onTap,
  106. this.labelText,
  107. this.icon,
  108. super.key,
  109. });
  110. @override
  111. State<LargeButtonChildWidget> createState() => _LargeButtonChildWidgetState();
  112. }
  113. class _LargeButtonChildWidgetState extends State<LargeButtonChildWidget> {
  114. late Color buttonColor;
  115. late Color borderColor;
  116. late Color iconColor;
  117. late TextStyle labelStyle;
  118. late Color checkIconColor;
  119. late Color loadingIconColor;
  120. late bool hasExecutionStates;
  121. final _debouncer = Debouncer(const Duration(milliseconds: 300));
  122. ExecutionState executionState = ExecutionState.idle;
  123. @override
  124. void initState() {
  125. checkIconColor = widget.buttonStyle.checkIconColor ??
  126. widget.buttonStyle.defaultIconColor;
  127. loadingIconColor = widget.buttonStyle.defaultIconColor;
  128. hasExecutionStates = widget.buttonType.hasExecutionStates;
  129. if (widget.isDisabled) {
  130. buttonColor = widget.buttonStyle.disabledButtonColor ??
  131. widget.buttonStyle.defaultButtonColor;
  132. borderColor = widget.buttonStyle.disabledBorderColor ??
  133. widget.buttonStyle.defaultBorderColor;
  134. iconColor = widget.buttonStyle.disabledIconColor ??
  135. widget.buttonStyle.defaultIconColor;
  136. labelStyle = widget.buttonStyle.disabledLabelStyle ??
  137. widget.buttonStyle.defaultLabelStyle;
  138. } else {
  139. buttonColor = widget.buttonStyle.defaultButtonColor;
  140. borderColor = widget.buttonStyle.defaultBorderColor;
  141. iconColor = widget.buttonStyle.defaultIconColor;
  142. labelStyle = widget.buttonStyle.defaultLabelStyle;
  143. }
  144. super.initState();
  145. }
  146. @override
  147. Widget build(BuildContext context) {
  148. return GestureDetector(
  149. onTap: _shouldRegisterGestures ? _onTap : null,
  150. onTapDown: _shouldRegisterGestures ? _onTapDown : null,
  151. onTapUp: _shouldRegisterGestures ? _onTapUp : null,
  152. onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
  153. child: AnimatedContainer(
  154. duration: const Duration(milliseconds: 16),
  155. width: widget.buttonSize == ButtonSize.large ? double.infinity : null,
  156. decoration: BoxDecoration(
  157. borderRadius: const BorderRadius.all(Radius.circular(4)),
  158. color: buttonColor,
  159. border: Border.all(color: borderColor),
  160. ),
  161. child: Padding(
  162. padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
  163. child: AnimatedSwitcher(
  164. duration: const Duration(milliseconds: 175),
  165. switchInCurve: Curves.easeInOutExpo,
  166. switchOutCurve: Curves.easeInOutExpo,
  167. child: executionState == ExecutionState.idle
  168. ? widget.buttonType.hasTrailingIcon
  169. ? Row(
  170. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  171. children: [
  172. widget.labelText == null
  173. ? const SizedBox.shrink()
  174. : Flexible(
  175. child: Padding(
  176. padding: widget.icon == null
  177. ? const EdgeInsets.symmetric(
  178. horizontal: 8,
  179. )
  180. : const EdgeInsets.only(right: 16),
  181. child: Text(
  182. widget.labelText!,
  183. overflow: TextOverflow.ellipsis,
  184. maxLines: 2,
  185. style: labelStyle,
  186. ),
  187. ),
  188. ),
  189. widget.icon == null
  190. ? const SizedBox.shrink()
  191. : Icon(
  192. widget.icon,
  193. size: 20,
  194. color: iconColor,
  195. ),
  196. ],
  197. )
  198. : Row(
  199. mainAxisSize: widget.buttonSize == ButtonSize.large
  200. ? MainAxisSize.max
  201. : MainAxisSize.min,
  202. mainAxisAlignment: MainAxisAlignment.center,
  203. children: [
  204. widget.icon == null
  205. ? const SizedBox.shrink()
  206. : Icon(
  207. widget.icon,
  208. size: 20,
  209. color: iconColor,
  210. ),
  211. widget.icon == null || widget.labelText == null
  212. ? const SizedBox.shrink()
  213. : const SizedBox(width: 8),
  214. widget.labelText == null
  215. ? const SizedBox.shrink()
  216. : Flexible(
  217. child: Padding(
  218. padding: const EdgeInsets.symmetric(
  219. horizontal: 8,
  220. ),
  221. child: Text(
  222. widget.labelText!,
  223. style: labelStyle,
  224. maxLines: 2,
  225. overflow: TextOverflow.ellipsis,
  226. ),
  227. ),
  228. )
  229. ],
  230. )
  231. : executionState == ExecutionState.inProgress
  232. ? Row(
  233. mainAxisSize: MainAxisSize.min,
  234. children: [
  235. EnteLoadingWidget(
  236. is20pts: true,
  237. color: loadingIconColor,
  238. ),
  239. ],
  240. )
  241. : executionState == ExecutionState.successful
  242. ? Icon(
  243. Icons.check_outlined,
  244. size: 20,
  245. color: checkIconColor,
  246. )
  247. : const SizedBox.shrink(), //fallback
  248. ),
  249. ),
  250. ),
  251. );
  252. }
  253. bool get _shouldRegisterGestures =>
  254. !widget.isDisabled &&
  255. (widget.onTap != null) &&
  256. executionState == ExecutionState.idle;
  257. void _onTap() async {
  258. if (hasExecutionStates) {
  259. _debouncer.run(
  260. () => Future(() {
  261. setState(() {
  262. executionState = ExecutionState.inProgress;
  263. });
  264. }),
  265. );
  266. await widget.onTap!
  267. .call()
  268. .onError((error, stackTrace) => _debouncer.cancelDebounce());
  269. _debouncer.cancelDebounce();
  270. // when the time taken by widget.onTap is approximately equal to the debounce
  271. // time, the callback is getting executed when/after the if condition
  272. // below is executing/executed which results in execution state stuck at
  273. // idle state. This Future is for delaying the execution of the if
  274. // condition so that the calback in the debouncer finishes execution before.
  275. await Future.delayed(const Duration(milliseconds: 5));
  276. if (executionState == ExecutionState.inProgress) {
  277. setState(() {
  278. executionState = ExecutionState.successful;
  279. Future.delayed(const Duration(seconds: 2), () {
  280. setState(() {
  281. executionState = ExecutionState.idle;
  282. });
  283. });
  284. });
  285. }
  286. } else {
  287. widget.onTap!.call();
  288. }
  289. }
  290. void _onTapDown(details) {
  291. setState(() {
  292. buttonColor = widget.buttonStyle.pressedButtonColor ??
  293. widget.buttonStyle.defaultButtonColor;
  294. borderColor = widget.buttonStyle.pressedBorderColor ??
  295. widget.buttonStyle.defaultBorderColor;
  296. iconColor = widget.buttonStyle.pressedIconColor ??
  297. widget.buttonStyle.defaultIconColor;
  298. labelStyle = widget.buttonStyle.pressedLabelStyle ??
  299. widget.buttonStyle.defaultLabelStyle;
  300. });
  301. }
  302. void _onTapUp(details) {
  303. Future.delayed(
  304. const Duration(milliseconds: 84),
  305. () => setState(() {
  306. setAllStylesToDefault();
  307. }),
  308. );
  309. }
  310. void _onTapCancel() {
  311. setState(() {
  312. setAllStylesToDefault();
  313. });
  314. }
  315. void setAllStylesToDefault() {
  316. buttonColor = widget.buttonStyle.defaultButtonColor;
  317. borderColor = widget.buttonStyle.defaultBorderColor;
  318. iconColor = widget.buttonStyle.defaultIconColor;
  319. labelStyle = widget.buttonStyle.defaultLabelStyle;
  320. }
  321. }