button_widget.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/scheduler.dart';
  3. import 'package:photos/models/execution_states.dart';
  4. import "package:photos/models/search/button_result.dart";
  5. import 'package:photos/models/typedefs.dart';
  6. import 'package:photos/theme/colors.dart';
  7. import 'package:photos/theme/ente_theme.dart';
  8. import 'package:photos/theme/text_style.dart';
  9. import 'package:photos/ui/common/loading_widget.dart';
  10. import 'package:photos/ui/components/models/button_type.dart';
  11. import 'package:photos/ui/components/models/custom_button_style.dart';
  12. import 'package:photos/utils/debouncer.dart';
  13. enum ButtonSize {
  14. small,
  15. large;
  16. }
  17. enum ButtonAction {
  18. first,
  19. second,
  20. third,
  21. fourth,
  22. cancel,
  23. error;
  24. }
  25. class ButtonWidget extends StatelessWidget {
  26. final IconData? icon;
  27. final String? labelText;
  28. final ButtonType buttonType;
  29. final FutureVoidCallback? onTap;
  30. final bool isDisabled;
  31. final ButtonSize buttonSize;
  32. ///Setting this flag to true will show a success confirmation as a 'check'
  33. ///icon once the onTap(). This is expected to be used only if time taken to
  34. ///execute onTap() takes less than debouce time.
  35. final bool shouldShowSuccessConfirmation;
  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. this.shouldShowSuccessConfirmation = false,
  71. super.key,
  72. });
  73. @override
  74. Widget build(BuildContext context) {
  75. final colorScheme =
  76. shouldStickToDarkTheme ? darkScheme : getEnteColorScheme(context);
  77. final inverseColorScheme = shouldStickToDarkTheme
  78. ? lightScheme
  79. : getEnteColorScheme(context, inverse: true);
  80. final textTheme =
  81. shouldStickToDarkTheme ? darkTextTheme : getEnteTextTheme(context);
  82. final inverseTextTheme = shouldStickToDarkTheme
  83. ? lightTextTheme
  84. : getEnteTextTheme(context, inverse: true);
  85. final buttonStyle = CustomButtonStyle(
  86. //Dummy default values since we need to keep these properties non-nullable
  87. defaultButtonColor: Colors.transparent,
  88. defaultBorderColor: Colors.transparent,
  89. defaultIconColor: Colors.transparent,
  90. defaultLabelStyle: textTheme.body,
  91. );
  92. buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
  93. buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
  94. buttonStyle.disabledButtonColor =
  95. buttonType.disabledButtonColor(colorScheme, buttonSize);
  96. buttonStyle.defaultBorderColor =
  97. buttonType.defaultBorderColor(colorScheme, buttonSize);
  98. buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(
  99. colorScheme: colorScheme,
  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. shouldShowSuccessConfirmation: shouldShowSuccessConfirmation,
  135. );
  136. }
  137. }
  138. class ButtonChildWidget extends StatefulWidget {
  139. final CustomButtonStyle buttonStyle;
  140. final FutureVoidCallback? onTap;
  141. final ButtonType buttonType;
  142. final String? labelText;
  143. final IconData? icon;
  144. final bool isDisabled;
  145. final ButtonSize buttonSize;
  146. final ButtonAction? buttonAction;
  147. final bool isInAlert;
  148. final bool shouldSurfaceExecutionStates;
  149. final ValueNotifier<String>? progressStatus;
  150. final bool shouldShowSuccessConfirmation;
  151. const ButtonChildWidget({
  152. required this.buttonStyle,
  153. required this.buttonType,
  154. required this.isDisabled,
  155. required this.buttonSize,
  156. required this.isInAlert,
  157. required this.shouldSurfaceExecutionStates,
  158. required this.shouldShowSuccessConfirmation,
  159. this.progressStatus,
  160. this.onTap,
  161. this.labelText,
  162. this.icon,
  163. this.buttonAction,
  164. super.key,
  165. });
  166. @override
  167. State<ButtonChildWidget> createState() => _ButtonChildWidgetState();
  168. }
  169. class _ButtonChildWidgetState extends State<ButtonChildWidget> {
  170. late Color buttonColor;
  171. late Color borderColor;
  172. late Color iconColor;
  173. late TextStyle labelStyle;
  174. late Color checkIconColor;
  175. late Color loadingIconColor;
  176. ValueNotifier<String>? progressStatus;
  177. ///This is used to store the width of the button in idle state (small button)
  178. ///to be used as width for the button when the loading/succes states comes.
  179. double? widthOfButton;
  180. final _debouncer = Debouncer(const Duration(milliseconds: 300));
  181. ExecutionState executionState = ExecutionState.idle;
  182. Exception? _exception;
  183. @override
  184. void initState() {
  185. _setButtonTheme();
  186. super.initState();
  187. }
  188. @override
  189. void didUpdateWidget(covariant ButtonChildWidget oldWidget) {
  190. _setButtonTheme();
  191. super.didUpdateWidget(oldWidget);
  192. }
  193. @override
  194. Widget build(BuildContext context) {
  195. if (executionState == ExecutionState.successful) {
  196. Future.delayed(Duration(seconds: widget.isInAlert ? 1 : 2), () {
  197. setState(() {
  198. executionState = ExecutionState.idle;
  199. });
  200. });
  201. }
  202. return GestureDetector(
  203. onTap: _shouldRegisterGestures ? _onTap : null,
  204. onTapDown: _shouldRegisterGestures ? _onTapDown : null,
  205. onTapUp: _shouldRegisterGestures ? _onTapUp : null,
  206. onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
  207. child: Container(
  208. decoration: BoxDecoration(
  209. borderRadius: const BorderRadius.all(Radius.circular(4)),
  210. border: widget.buttonType == ButtonType.tertiaryCritical
  211. ? Border.all(color: borderColor)
  212. : null,
  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. void _setButtonTheme() {
  354. progressStatus = widget.progressStatus;
  355. checkIconColor = widget.buttonStyle.checkIconColor ??
  356. widget.buttonStyle.defaultIconColor;
  357. loadingIconColor = widget.buttonStyle.defaultIconColor;
  358. if (widget.isDisabled) {
  359. buttonColor = widget.buttonStyle.disabledButtonColor ??
  360. widget.buttonStyle.defaultButtonColor;
  361. borderColor = widget.buttonStyle.disabledBorderColor ??
  362. widget.buttonStyle.defaultBorderColor;
  363. iconColor = widget.buttonStyle.disabledIconColor ??
  364. widget.buttonStyle.defaultIconColor;
  365. labelStyle = widget.buttonStyle.disabledLabelStyle ??
  366. widget.buttonStyle.defaultLabelStyle;
  367. } else {
  368. buttonColor = widget.buttonStyle.defaultButtonColor;
  369. borderColor = widget.buttonStyle.defaultBorderColor;
  370. iconColor = widget.buttonStyle.defaultIconColor;
  371. labelStyle = widget.buttonStyle.defaultLabelStyle;
  372. }
  373. }
  374. bool get _shouldRegisterGestures =>
  375. !widget.isDisabled && executionState == ExecutionState.idle;
  376. void _onTap() async {
  377. if (widget.onTap != null) {
  378. _debouncer.run(
  379. () => Future(() {
  380. setState(() {
  381. executionState = ExecutionState.inProgress;
  382. });
  383. }),
  384. );
  385. await widget.onTap!.call().then(
  386. (value) {
  387. _exception = null;
  388. },
  389. onError: (error, stackTrace) {
  390. executionState = ExecutionState.error;
  391. _exception = error as Exception;
  392. _debouncer.cancelDebounce();
  393. },
  394. );
  395. widget.shouldShowSuccessConfirmation && _debouncer.isActive()
  396. ? executionState = ExecutionState.successful
  397. : null;
  398. _debouncer.cancelDebounce();
  399. if (executionState == ExecutionState.successful) {
  400. setState(() {});
  401. }
  402. // when the time taken by widget.onTap is approximately equal to the debounce
  403. // time, the callback is getting executed when/after the if condition
  404. // below is executing/executed which results in execution state stuck at
  405. // idle state. This Future is for delaying the execution of the if
  406. // condition so that the calback in the debouncer finishes execution before.
  407. await Future.delayed(const Duration(milliseconds: 5));
  408. }
  409. if (executionState == ExecutionState.inProgress ||
  410. executionState == ExecutionState.error) {
  411. if (executionState == ExecutionState.inProgress) {
  412. if (mounted) {
  413. setState(() {
  414. executionState = ExecutionState.successful;
  415. Future.delayed(
  416. Duration(
  417. seconds: widget.shouldSurfaceExecutionStates
  418. ? (widget.isInAlert ? 1 : 2)
  419. : 0,
  420. ), () {
  421. widget.isInAlert
  422. ? _popWithButtonAction(
  423. context,
  424. buttonAction: widget.buttonAction,
  425. )
  426. : null;
  427. if (mounted) {
  428. setState(() {
  429. executionState = ExecutionState.idle;
  430. });
  431. }
  432. });
  433. });
  434. }
  435. }
  436. if (executionState == ExecutionState.error) {
  437. setState(() {
  438. executionState = ExecutionState.idle;
  439. widget.isInAlert
  440. ? Future.delayed(
  441. const Duration(seconds: 0),
  442. () => _popWithButtonAction(
  443. context,
  444. buttonAction: ButtonAction.error,
  445. exception: _exception,
  446. ),
  447. )
  448. : null;
  449. });
  450. }
  451. } else {
  452. if (widget.isInAlert) {
  453. Future.delayed(
  454. Duration(seconds: widget.shouldShowSuccessConfirmation ? 1 : 0),
  455. () =>
  456. _popWithButtonAction(context, buttonAction: widget.buttonAction),
  457. );
  458. }
  459. }
  460. }
  461. void _popWithButtonAction(
  462. BuildContext context, {
  463. required ButtonAction? buttonAction,
  464. Exception? exception,
  465. }) {
  466. Navigator.of(context).canPop()
  467. ? Navigator.of(context).pop(
  468. ButtonResult(action: buttonAction, exception: exception),
  469. )
  470. : null;
  471. }
  472. void _onTapDown(details) {
  473. setState(() {
  474. buttonColor = widget.buttonStyle.pressedButtonColor ??
  475. widget.buttonStyle.defaultButtonColor;
  476. borderColor = widget.buttonStyle.pressedBorderColor ??
  477. widget.buttonStyle.defaultBorderColor;
  478. iconColor = widget.buttonStyle.pressedIconColor ??
  479. widget.buttonStyle.defaultIconColor;
  480. labelStyle = widget.buttonStyle.pressedLabelStyle ??
  481. widget.buttonStyle.defaultLabelStyle;
  482. });
  483. }
  484. void _onTapUp(details) {
  485. Future.delayed(
  486. const Duration(milliseconds: 84),
  487. () => setState(() {
  488. setAllStylesToDefault();
  489. }),
  490. );
  491. }
  492. void _onTapCancel() {
  493. setState(() {
  494. setAllStylesToDefault();
  495. });
  496. }
  497. void setAllStylesToDefault() {
  498. buttonColor = widget.buttonStyle.defaultButtonColor;
  499. borderColor = widget.buttonStyle.defaultBorderColor;
  500. iconColor = widget.buttonStyle.defaultIconColor;
  501. labelStyle = widget.buttonStyle.defaultLabelStyle;
  502. }
  503. }