dialog_widget.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import 'package:photos/core/constants.dart';
  4. import 'package:photos/theme/colors.dart';
  5. import 'package:photos/theme/effects.dart';
  6. import 'package:photos/theme/ente_theme.dart';
  7. import 'package:photos/ui/components/button_widget.dart';
  8. import 'package:photos/ui/components/models/button_type.dart';
  9. import 'package:photos/utils/separators_util.dart';
  10. typedef FutureVoidCallbackParamStr = Future<void> Function(String);
  11. ///Will return null if dismissed by tapping outside
  12. Future<ButtonAction?> showDialogWidget({
  13. required BuildContext context,
  14. required String title,
  15. String? body,
  16. required List<ButtonWidget> buttons,
  17. IconData? icon,
  18. bool isDismissible = true,
  19. }) {
  20. return showDialog(
  21. barrierDismissible: isDismissible,
  22. barrierColor: backdropFaintDark,
  23. context: context,
  24. builder: (context) {
  25. final widthOfScreen = MediaQuery.of(context).size.width;
  26. final isMobileSmall = widthOfScreen <= mobileSmallThreshold;
  27. return Padding(
  28. padding: EdgeInsets.symmetric(horizontal: isMobileSmall ? 8 : 0),
  29. child: Dialog(
  30. insetPadding: EdgeInsets.zero,
  31. child: DialogWidget(
  32. title: title,
  33. body: body,
  34. buttons: buttons,
  35. icon: icon,
  36. ),
  37. ),
  38. );
  39. },
  40. );
  41. }
  42. class DialogWidget extends StatelessWidget {
  43. final String title;
  44. final String? body;
  45. final List<ButtonWidget> buttons;
  46. final IconData? icon;
  47. const DialogWidget({
  48. required this.title,
  49. this.body,
  50. required this.buttons,
  51. this.icon,
  52. super.key,
  53. });
  54. @override
  55. Widget build(BuildContext context) {
  56. final widthOfScreen = MediaQuery.of(context).size.width;
  57. final isMobileSmall = widthOfScreen <= mobileSmallThreshold;
  58. final colorScheme = getEnteColorScheme(context);
  59. return Container(
  60. width: min(widthOfScreen, 320),
  61. padding: isMobileSmall
  62. ? const EdgeInsets.all(0)
  63. : const EdgeInsets.fromLTRB(6, 8, 6, 6),
  64. decoration: BoxDecoration(
  65. color: colorScheme.backgroundElevated,
  66. boxShadow: shadowFloatLight,
  67. borderRadius: const BorderRadius.all(Radius.circular(8)),
  68. ),
  69. child: Padding(
  70. padding: const EdgeInsets.all(16),
  71. child: Column(
  72. mainAxisSize: MainAxisSize.min,
  73. children: [
  74. ContentContainer(
  75. title: title,
  76. body: body,
  77. icon: icon,
  78. ),
  79. const SizedBox(height: 36),
  80. Actions(buttons),
  81. ],
  82. ),
  83. ),
  84. );
  85. }
  86. }
  87. class ContentContainer extends StatelessWidget {
  88. final String title;
  89. final String? body;
  90. final IconData? icon;
  91. const ContentContainer({
  92. required this.title,
  93. this.body,
  94. this.icon,
  95. super.key,
  96. });
  97. @override
  98. Widget build(BuildContext context) {
  99. final textTheme = getEnteTextTheme(context);
  100. final colorScheme = getEnteColorScheme(context);
  101. return Column(
  102. mainAxisSize: MainAxisSize.min,
  103. crossAxisAlignment: CrossAxisAlignment.stretch,
  104. children: [
  105. icon == null
  106. ? const SizedBox.shrink()
  107. : Row(
  108. children: [
  109. Icon(
  110. icon,
  111. size: 32,
  112. ),
  113. ],
  114. ),
  115. icon == null ? const SizedBox.shrink() : const SizedBox(height: 19),
  116. Text(title, style: textTheme.largeBold),
  117. body != null ? const SizedBox(height: 19) : const SizedBox.shrink(),
  118. body != null
  119. ? Text(
  120. body!,
  121. style: textTheme.body.copyWith(color: colorScheme.textMuted),
  122. )
  123. : const SizedBox.shrink(),
  124. ],
  125. );
  126. }
  127. }
  128. class Actions extends StatelessWidget {
  129. final List<ButtonWidget> buttons;
  130. const Actions(this.buttons, {super.key});
  131. @override
  132. Widget build(BuildContext context) {
  133. return Column(
  134. children: addSeparators(
  135. buttons,
  136. const SizedBox(
  137. // In figma this white space is of height 8pts. But the Button
  138. // component has 1pts of invisible border by default in code. So two
  139. // 1pts borders will visually make the whitespace 8pts.
  140. // Height of button component in figma = 48, in code = 50 (2pts for
  141. // top + bottom border)
  142. height: 6,
  143. ),
  144. ),
  145. );
  146. }
  147. }
  148. class TextInputDialog extends StatefulWidget {
  149. final String title;
  150. final String? body;
  151. final String confirmationButtonLabel;
  152. final IconData? icon;
  153. final String? label;
  154. final String? message;
  155. final FutureVoidCallbackParamStr onConfirm;
  156. const TextInputDialog({
  157. required this.title,
  158. this.body,
  159. required this.confirmationButtonLabel,
  160. required this.onConfirm,
  161. this.icon,
  162. this.label,
  163. this.message,
  164. super.key,
  165. });
  166. @override
  167. State<TextInputDialog> createState() => _TextInputDialogState();
  168. }
  169. class _TextInputDialogState extends State<TextInputDialog> {
  170. final TextEditingController _textController = TextEditingController();
  171. @override
  172. Widget build(BuildContext context) {
  173. final widthOfScreen = MediaQuery.of(context).size.width;
  174. final isMobileSmall = widthOfScreen <= mobileSmallThreshold;
  175. final colorScheme = getEnteColorScheme(context);
  176. final textTheme = getEnteTextTheme(context);
  177. var textInputChildren = <Widget>[];
  178. if (widget.label != null) textInputChildren.add(Text(widget.label!));
  179. textInputChildren.add(
  180. ClipRRect(
  181. borderRadius: const BorderRadius.all(Radius.circular(8)),
  182. child: Material(
  183. child: TextFormField(
  184. controller: _textController,
  185. decoration: InputDecoration(
  186. hintText: "Placeholder",
  187. hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted),
  188. filled: true,
  189. contentPadding: const EdgeInsets.symmetric(
  190. vertical: 11,
  191. horizontal: 11,
  192. ),
  193. border: const UnderlineInputBorder(
  194. borderSide: BorderSide.none,
  195. ),
  196. focusedBorder: OutlineInputBorder(
  197. borderSide: BorderSide(color: colorScheme.strokeMuted),
  198. borderRadius: BorderRadius.circular(8),
  199. ),
  200. prefixIconConstraints: const BoxConstraints(
  201. maxHeight: 44,
  202. maxWidth: 44,
  203. minHeight: 44,
  204. minWidth: 44,
  205. ),
  206. suffixIconConstraints: const BoxConstraints(
  207. maxHeight: 44,
  208. maxWidth: 44,
  209. minHeight: 44,
  210. minWidth: 44,
  211. ),
  212. prefixIcon: Icon(
  213. Icons.search_outlined,
  214. color: colorScheme.strokeMuted,
  215. ),
  216. ),
  217. ),
  218. ),
  219. ),
  220. );
  221. if (widget.message != null) {
  222. textInputChildren.add(
  223. Padding(
  224. padding: const EdgeInsets.symmetric(horizontal: 8),
  225. child: Text(
  226. widget.message!,
  227. style: textTheme.small.copyWith(color: colorScheme.textMuted),
  228. ),
  229. ),
  230. );
  231. }
  232. textInputChildren =
  233. addSeparators(textInputChildren, const SizedBox(height: 4));
  234. return Container(
  235. width: min(widthOfScreen, 320),
  236. padding: isMobileSmall
  237. ? const EdgeInsets.all(0)
  238. : const EdgeInsets.fromLTRB(6, 8, 6, 6),
  239. decoration: BoxDecoration(
  240. color: colorScheme.backgroundElevated,
  241. boxShadow: shadowFloatLight,
  242. borderRadius: const BorderRadius.all(Radius.circular(8)),
  243. ),
  244. child: Padding(
  245. padding: const EdgeInsets.all(16),
  246. child: Column(
  247. mainAxisSize: MainAxisSize.min,
  248. children: [
  249. ContentContainer(
  250. title: widget.title,
  251. body: widget.body,
  252. icon: widget.icon,
  253. ),
  254. Padding(
  255. padding: const EdgeInsets.only(top: 19),
  256. child: Column(
  257. mainAxisSize: MainAxisSize.min,
  258. crossAxisAlignment: CrossAxisAlignment.start,
  259. children: textInputChildren,
  260. ),
  261. ),
  262. const SizedBox(height: 36),
  263. Row(
  264. mainAxisSize: MainAxisSize.min,
  265. children: [
  266. const Expanded(
  267. child: ButtonWidget(
  268. buttonType: ButtonType.secondary,
  269. buttonSize: ButtonSize.small,
  270. labelText: "Cancel",
  271. isInAlert: true,
  272. ),
  273. ),
  274. const SizedBox(width: 8),
  275. Expanded(
  276. child: ButtonWidget(
  277. buttonSize: ButtonSize.small,
  278. buttonType: ButtonType.neutral,
  279. labelText: widget.confirmationButtonLabel,
  280. onTap: _onTap,
  281. isInAlert: true,
  282. ),
  283. ),
  284. ],
  285. )
  286. ],
  287. ),
  288. ),
  289. );
  290. }
  291. Future<void> _onTap() async {
  292. await widget.onConfirm.call(_textController.text);
  293. }
  294. }