button_widget.dart 19 KB

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