button_widget.dart 13 KB

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