button_widget.dart 18 KB

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