button_widget.dart 20 KB

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