button_widget.dart 14 KB

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