button_widget.dart 14 KB

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