button_widget.dart 16 KB

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