button_widget.dart 12 KB

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