backup_section_widget.dart 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // @dart=2.9
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. import 'package:photos/models/backup_status.dart';
  5. import 'package:photos/models/duplicate_files.dart';
  6. import 'package:photos/services/deduplication_service.dart';
  7. import 'package:photos/services/sync_service.dart';
  8. import 'package:photos/services/update_service.dart';
  9. import 'package:photos/theme/ente_theme.dart';
  10. import 'package:photos/ui/backup_folder_selection_page.dart';
  11. import 'package:photos/ui/backup_settings_screen.dart';
  12. import 'package:photos/ui/components/captioned_text_widget.dart';
  13. import 'package:photos/ui/components/dialog_widget.dart';
  14. import 'package:photos/ui/components/expandable_menu_item_widget.dart';
  15. import 'package:photos/ui/components/menu_item_widget.dart';
  16. import 'package:photos/ui/components/models/button_type.dart';
  17. import 'package:photos/ui/settings/common_settings.dart';
  18. import 'package:photos/ui/tools/deduplicate_page.dart';
  19. import 'package:photos/ui/tools/free_space_page.dart';
  20. import 'package:photos/utils/data_util.dart';
  21. import 'package:photos/utils/dialog_util.dart';
  22. import 'package:photos/utils/navigation_util.dart';
  23. import 'package:photos/utils/toast_util.dart';
  24. import 'package:url_launcher/url_launcher_string.dart';
  25. class BackupSectionWidget extends StatefulWidget {
  26. const BackupSectionWidget({Key key}) : super(key: key);
  27. @override
  28. BackupSectionWidgetState createState() => BackupSectionWidgetState();
  29. }
  30. class BackupSectionWidgetState extends State<BackupSectionWidget> {
  31. @override
  32. Widget build(BuildContext context) {
  33. return ExpandableMenuItemWidget(
  34. title: "Backup",
  35. selectionOptionsWidget: _getSectionOptions(context),
  36. leadingIcon: Icons.backup_outlined,
  37. );
  38. }
  39. Widget _getSectionOptions(BuildContext context) {
  40. final List<Widget> sectionOptions = [
  41. sectionOptionSpacing,
  42. MenuItemWidget(
  43. captionedTextWidget: const CaptionedTextWidget(
  44. title: "Backed up folders",
  45. ),
  46. pressedColor: getEnteColorScheme(context).fillFaint,
  47. trailingIcon: Icons.chevron_right_outlined,
  48. trailingIconIsMuted: true,
  49. onTap: () {
  50. routeToPage(
  51. context,
  52. const BackupFolderSelectionPage(
  53. buttonText: "Backup",
  54. ),
  55. );
  56. },
  57. ),
  58. sectionOptionSpacing,
  59. MenuItemWidget(
  60. captionedTextWidget: const CaptionedTextWidget(
  61. title: "Backup settings",
  62. ),
  63. pressedColor: getEnteColorScheme(context).fillFaint,
  64. trailingIcon: Icons.chevron_right_outlined,
  65. trailingIconIsMuted: true,
  66. onTap: () {
  67. routeToPage(
  68. context,
  69. const BackupSettingsScreen(),
  70. );
  71. },
  72. ),
  73. sectionOptionSpacing,
  74. ];
  75. sectionOptions.addAll(
  76. [
  77. MenuItemWidget(
  78. captionedTextWidget: const CaptionedTextWidget(
  79. title: "Free up device space",
  80. ),
  81. pressedColor: getEnteColorScheme(context).fillFaint,
  82. trailingIcon: Icons.chevron_right_outlined,
  83. trailingIconIsMuted: true,
  84. onTap: () async {
  85. final dialog = createProgressDialog(context, "Calculating...");
  86. await dialog.show();
  87. BackupStatus status;
  88. try {
  89. status = await SyncService.instance.getBackupStatus();
  90. } catch (e) {
  91. await dialog.hide();
  92. showGenericErrorDialog(context: context);
  93. return;
  94. }
  95. await dialog.hide();
  96. if (status.localIDs.isEmpty) {
  97. showErrorDialog(
  98. context,
  99. "✨ All clear",
  100. "You've no files on this device that can be deleted",
  101. );
  102. } else {
  103. final bool result =
  104. await routeToPage(context, FreeSpacePage(status));
  105. if (result == true) {
  106. _showSpaceFreedDialog(status);
  107. }
  108. }
  109. },
  110. ),
  111. sectionOptionSpacing,
  112. MenuItemWidget(
  113. captionedTextWidget: const CaptionedTextWidget(
  114. title: "Remove duplicates",
  115. ),
  116. pressedColor: getEnteColorScheme(context).fillFaint,
  117. trailingIcon: Icons.chevron_right_outlined,
  118. trailingIconIsMuted: true,
  119. onTap: () async {
  120. final dialog = createProgressDialog(context, "Calculating...");
  121. await dialog.show();
  122. List<DuplicateFiles> duplicates;
  123. try {
  124. duplicates =
  125. await DeduplicationService.instance.getDuplicateFiles();
  126. } catch (e) {
  127. await dialog.hide();
  128. showGenericErrorDialog(context: context);
  129. return;
  130. }
  131. await dialog.hide();
  132. if (duplicates.isEmpty) {
  133. showErrorDialog(
  134. context,
  135. "✨ No duplicates",
  136. "You've no duplicate files that can be cleared",
  137. );
  138. } else {
  139. final DeduplicationResult result =
  140. await routeToPage(context, DeduplicatePage(duplicates));
  141. if (result != null) {
  142. _showDuplicateFilesDeletedDialog(result);
  143. }
  144. }
  145. },
  146. ),
  147. sectionOptionSpacing,
  148. ],
  149. );
  150. return Column(
  151. children: sectionOptions,
  152. );
  153. }
  154. void _showSpaceFreedDialog(BackupStatus status) {
  155. final DialogWidget dialog = choiceDialog(
  156. title: "Success",
  157. body: "You have successfully freed up " + formatBytes(status.size) + "!",
  158. firstButtonLabel: "Rate us",
  159. firstButtonOnTap: () async {
  160. final url = UpdateService.instance.getRateDetails().item2;
  161. launchUrlString(url);
  162. },
  163. firstButtonType: ButtonType.primary,
  164. secondButtonLabel: "OK",
  165. secondButtonOnTap: () async {
  166. if (Platform.isIOS) {
  167. showToast(
  168. context,
  169. "Also empty \"Recently Deleted\" from \"Settings\" -> \"Storage\" to claim the freed space",
  170. );
  171. }
  172. },
  173. );
  174. showConfettiDialog(
  175. context: context,
  176. dialogBuilder: (BuildContext context) {
  177. return dialog;
  178. },
  179. barrierColor: Colors.black87,
  180. confettiAlignment: Alignment.topCenter,
  181. useRootNavigator: true,
  182. );
  183. }
  184. void _showDuplicateFilesDeletedDialog(DeduplicationResult result) {
  185. final String countText = result.count.toString() +
  186. " duplicate file" +
  187. (result.count == 1 ? "" : "s");
  188. final DialogWidget dialog = choiceDialog(
  189. title: "✨ Success",
  190. body: "You have cleaned up " +
  191. countText +
  192. ", saving " +
  193. formatBytes(result.size) +
  194. "!",
  195. firstButtonLabel: "Rate us",
  196. firstButtonOnTap: () async {
  197. // TODO: Replace with https://pub.dev/packages/in_app_review
  198. final url = UpdateService.instance.getRateDetails().item2;
  199. launchUrlString(url);
  200. },
  201. firstButtonType: ButtonType.primary,
  202. secondButtonLabel: "OK",
  203. secondButtonOnTap: () async {
  204. showShortToast(
  205. context,
  206. "Also empty your \"Trash\" to claim the freed up space",
  207. );
  208. },
  209. );
  210. showConfettiDialog(
  211. context: context,
  212. dialogBuilder: (BuildContext context) {
  213. return dialog;
  214. },
  215. barrierColor: Colors.black87,
  216. confettiAlignment: Alignment.topCenter,
  217. useRootNavigator: true,
  218. );
  219. }
  220. }