button_widget.dart 16 KB

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