button_widget.dart 15 KB

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