dialog_util.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import "package:dio/dio.dart";
  2. import "package:flutter/foundation.dart";
  3. import 'package:flutter/material.dart';
  4. import "package:flutter/services.dart";
  5. import "package:photos/generated/l10n.dart";
  6. import 'package:photos/models/button_result.dart';
  7. import 'package:photos/models/typedefs.dart';
  8. import "package:photos/services/feature_flag_service.dart";
  9. import 'package:photos/theme/colors.dart';
  10. import 'package:photos/ui/common/loading_widget.dart';
  11. import 'package:photos/ui/common/progress_dialog.dart';
  12. import 'package:photos/ui/components/action_sheet_widget.dart';
  13. import 'package:photos/ui/components/buttons/button_widget.dart';
  14. import 'package:photos/ui/components/dialog_widget.dart';
  15. import 'package:photos/ui/components/models/button_type.dart';
  16. import "package:photos/utils/email_util.dart";
  17. typedef DialogBuilder = DialogWidget Function(BuildContext context);
  18. ///Will return null if dismissed by tapping outside
  19. Future<ButtonResult?> showErrorDialog(
  20. BuildContext context,
  21. String title,
  22. String? body, {
  23. bool isDismissable = true,
  24. }) async {
  25. return showDialogWidget(
  26. context: context,
  27. title: title,
  28. body: body,
  29. isDismissible: isDismissable,
  30. buttons: [
  31. ButtonWidget(
  32. buttonType: ButtonType.secondary,
  33. labelText: S.of(context).ok,
  34. isInAlert: true,
  35. buttonAction: ButtonAction.first,
  36. ),
  37. ],
  38. );
  39. }
  40. Future<ButtonResult?> showErrorDialogForException({
  41. required BuildContext context,
  42. required Exception exception,
  43. bool isDismissible = true,
  44. String apiErrorPrefix = "It looks like something went wrong.",
  45. String? message,
  46. }) async {
  47. String errorMessage =
  48. message ?? S.of(context).tempErrorContactSupportIfPersists;
  49. if (exception is DioError &&
  50. exception.response != null &&
  51. exception.response!.data["code"] != null) {
  52. errorMessage =
  53. "$apiErrorPrefix\n\nReason: " + exception.response!.data["code"];
  54. }
  55. return showDialogWidget(
  56. context: context,
  57. title: S.of(context).error,
  58. icon: Icons.error_outline_outlined,
  59. body: errorMessage,
  60. isDismissible: isDismissible,
  61. buttons: const [
  62. ButtonWidget(
  63. buttonType: ButtonType.secondary,
  64. labelText: "OK",
  65. isInAlert: true,
  66. ),
  67. ],
  68. );
  69. }
  70. String parseErrorForUI(
  71. BuildContext context,
  72. String genericError, {
  73. Object? error,
  74. bool surfaceError = kDebugMode,
  75. }) {
  76. if (error == null) {
  77. return genericError;
  78. }
  79. if (error is DioError) {
  80. final DioError dioError = error;
  81. if (dioError.type == DioErrorType.other) {
  82. if (dioError.error.toString().contains('Failed host lookup')) {
  83. return S.of(context).networkHostLookUpErr;
  84. } else if (dioError.error.toString().contains('SocketException')) {
  85. return S.of(context).networkConnectionRefusedErr;
  86. }
  87. }
  88. }
  89. // return generic error if the user is not internal and the error is not in debug mode
  90. if (!(FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
  91. kDebugMode)) {
  92. return genericError;
  93. }
  94. String errorInfo = "";
  95. if (error is DioError) {
  96. final DioError dioError = error;
  97. if (dioError.type == DioErrorType.response) {
  98. if (dioError.response?.data["code"] != null) {
  99. errorInfo = "Reason: " + dioError.response!.data["code"];
  100. } else {
  101. errorInfo = "Reason: " + dioError.response!.data.toString();
  102. }
  103. } else if (dioError.type == DioErrorType.other) {
  104. errorInfo = "Reason: " + dioError.error.toString();
  105. } else {
  106. errorInfo = "Reason: " + dioError.type.toString();
  107. }
  108. } else {
  109. errorInfo = error.toString().split('Source stack')[0];
  110. }
  111. if (errorInfo.isNotEmpty) {
  112. return "$genericError\n\n$errorInfo";
  113. }
  114. return genericError;
  115. }
  116. ///Will return null if dismissed by tapping outside
  117. Future<ButtonResult?> showGenericErrorDialog({
  118. required BuildContext context,
  119. bool isDismissible = true,
  120. required Object? error,
  121. }) async {
  122. final errorBody = parseErrorForUI(
  123. context,
  124. S.of(context).itLooksLikeSomethingWentWrongPleaseRetryAfterSome,
  125. error: error,
  126. );
  127. final ButtonResult? result = await showDialogWidget(
  128. context: context,
  129. title: S.of(context).error,
  130. icon: Icons.error_outline_outlined,
  131. body: errorBody,
  132. isDismissible: isDismissible,
  133. buttons: [
  134. ButtonWidget(
  135. buttonType: ButtonType.primary,
  136. labelText: S.of(context).ok,
  137. buttonAction: ButtonAction.first,
  138. isInAlert: true,
  139. ),
  140. ButtonWidget(
  141. buttonType: ButtonType.secondary,
  142. labelText: S.of(context).contactSupport,
  143. buttonAction: ButtonAction.second,
  144. onTap: () async {
  145. await sendLogs(
  146. context,
  147. S.of(context).contactSupport,
  148. "support@ente.io",
  149. postShare: () {},
  150. );
  151. },
  152. ),
  153. ],
  154. );
  155. return result;
  156. }
  157. DialogWidget choiceDialog({
  158. required String title,
  159. String? body,
  160. required String firstButtonLabel,
  161. String secondButtonLabel = "Cancel",
  162. ButtonType firstButtonType = ButtonType.neutral,
  163. ButtonType secondButtonType = ButtonType.secondary,
  164. ButtonAction firstButtonAction = ButtonAction.first,
  165. ButtonAction secondButtonAction = ButtonAction.cancel,
  166. FutureVoidCallback? firstButtonOnTap,
  167. FutureVoidCallback? secondButtonOnTap,
  168. bool isCritical = false,
  169. IconData? icon,
  170. }) {
  171. final buttons = [
  172. ButtonWidget(
  173. buttonType: isCritical ? ButtonType.critical : firstButtonType,
  174. labelText: firstButtonLabel,
  175. isInAlert: true,
  176. onTap: firstButtonOnTap,
  177. buttonAction: firstButtonAction,
  178. ),
  179. ButtonWidget(
  180. buttonType: secondButtonType,
  181. labelText: secondButtonLabel,
  182. isInAlert: true,
  183. onTap: secondButtonOnTap,
  184. buttonAction: secondButtonAction,
  185. ),
  186. ];
  187. return DialogWidget(title: title, body: body, buttons: buttons, icon: icon);
  188. }
  189. ///Will return null if dismissed by tapping outside
  190. Future<ButtonResult?> showChoiceDialog(
  191. BuildContext context, {
  192. required String title,
  193. String? body,
  194. required String firstButtonLabel,
  195. String secondButtonLabel = "Cancel",
  196. ButtonType firstButtonType = ButtonType.neutral,
  197. ButtonType secondButtonType = ButtonType.secondary,
  198. ButtonAction firstButtonAction = ButtonAction.first,
  199. ButtonAction secondButtonAction = ButtonAction.cancel,
  200. FutureVoidCallback? firstButtonOnTap,
  201. FutureVoidCallback? secondButtonOnTap,
  202. bool isCritical = false,
  203. IconData? icon,
  204. bool isDismissible = true,
  205. }) async {
  206. final buttons = [
  207. ButtonWidget(
  208. buttonType: isCritical ? ButtonType.critical : firstButtonType,
  209. labelText: firstButtonLabel,
  210. isInAlert: true,
  211. onTap: firstButtonOnTap,
  212. buttonAction: firstButtonAction,
  213. ),
  214. ButtonWidget(
  215. buttonType: secondButtonType,
  216. labelText: secondButtonLabel,
  217. isInAlert: true,
  218. onTap: secondButtonOnTap,
  219. buttonAction: secondButtonAction,
  220. ),
  221. ];
  222. return showDialogWidget(
  223. context: context,
  224. title: title,
  225. body: body,
  226. buttons: buttons,
  227. icon: icon,
  228. isDismissible: isDismissible,
  229. );
  230. }
  231. ///Will return null if dismissed by tapping outside
  232. Future<ButtonResult?> showChoiceActionSheet(
  233. BuildContext context, {
  234. required String title,
  235. String? body,
  236. required String firstButtonLabel,
  237. String secondButtonLabel = "Cancel",
  238. ButtonType firstButtonType = ButtonType.neutral,
  239. ButtonType secondButtonType = ButtonType.secondary,
  240. ButtonAction firstButtonAction = ButtonAction.first,
  241. ButtonAction secondButtonAction = ButtonAction.cancel,
  242. FutureVoidCallback? firstButtonOnTap,
  243. FutureVoidCallback? secondButtonOnTap,
  244. bool isCritical = false,
  245. IconData? icon,
  246. bool isDismissible = true,
  247. }) async {
  248. final buttons = [
  249. ButtonWidget(
  250. buttonType: isCritical ? ButtonType.critical : firstButtonType,
  251. labelText: firstButtonLabel,
  252. isInAlert: true,
  253. onTap: firstButtonOnTap,
  254. buttonAction: firstButtonAction,
  255. shouldStickToDarkTheme: true,
  256. ),
  257. ButtonWidget(
  258. buttonType: secondButtonType,
  259. labelText: secondButtonLabel,
  260. isInAlert: true,
  261. onTap: secondButtonOnTap,
  262. buttonAction: secondButtonAction,
  263. shouldStickToDarkTheme: true,
  264. ),
  265. ];
  266. return showActionSheet(
  267. context: context,
  268. title: title,
  269. body: body,
  270. buttons: buttons,
  271. isDismissible: isDismissible,
  272. );
  273. }
  274. ProgressDialog createProgressDialog(
  275. BuildContext context,
  276. String message, {
  277. isDismissible = false,
  278. }) {
  279. final dialog = ProgressDialog(
  280. context,
  281. type: ProgressDialogType.normal,
  282. isDismissible: isDismissible,
  283. );
  284. dialog.style(
  285. message: message,
  286. messageTextStyle: Theme.of(context).textTheme.bodySmall,
  287. backgroundColor: Theme.of(context).dialogTheme.backgroundColor,
  288. progressWidget: const EnteLoadingWidget(),
  289. borderRadius: 10,
  290. elevation: 10.0,
  291. insetAnimCurve: Curves.easeInOut,
  292. );
  293. return dialog;
  294. }
  295. ///Can return ButtonResult? from ButtonWidget or Exception? from TextInputDialog
  296. Future<dynamic> showTextInputDialog(
  297. BuildContext context, {
  298. required String title,
  299. String? body,
  300. required String submitButtonLabel,
  301. IconData? icon,
  302. String? label,
  303. String? message,
  304. String? hintText,
  305. required FutureVoidCallbackParamStr onSubmit,
  306. IconData? prefixIcon,
  307. String? initialValue,
  308. Alignment? alignMessage,
  309. int? maxLength,
  310. bool showOnlyLoadingState = false,
  311. TextCapitalization textCapitalization = TextCapitalization.none,
  312. bool alwaysShowSuccessState = false,
  313. bool isPasswordInput = false,
  314. TextEditingController? textEditingController,
  315. List<TextInputFormatter>? textInputFormatter,
  316. TextInputType? textInputType,
  317. }) {
  318. return showDialog(
  319. barrierColor: backdropFaintDark,
  320. context: context,
  321. builder: (context) {
  322. final bottomInset = MediaQuery.of(context).viewInsets.bottom;
  323. final isKeyboardUp = bottomInset > 100;
  324. return Center(
  325. child: Padding(
  326. padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0),
  327. child: TextInputDialog(
  328. title: title,
  329. message: message,
  330. label: label,
  331. body: body,
  332. icon: icon,
  333. submitButtonLabel: submitButtonLabel,
  334. onSubmit: onSubmit,
  335. hintText: hintText,
  336. prefixIcon: prefixIcon,
  337. initialValue: initialValue,
  338. alignMessage: alignMessage,
  339. maxLength: maxLength,
  340. showOnlyLoadingState: showOnlyLoadingState,
  341. textCapitalization: textCapitalization,
  342. alwaysShowSuccessState: alwaysShowSuccessState,
  343. isPasswordInput: isPasswordInput,
  344. textEditingController: textEditingController,
  345. textInputFormatter: textInputFormatter,
  346. textInputType: textInputType,
  347. ),
  348. ),
  349. );
  350. },
  351. );
  352. }