button_widget.dart 18 KB

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