button_widget.dart 20 KB

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