dialog_widget.dart 9.8 KB

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