backup_section_widget.dart 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // @dart=2.9
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. import 'package:photos/ente_theme_data.dart';
  5. import 'package:photos/models/backup_status.dart';
  6. import 'package:photos/models/duplicate_files.dart';
  7. import 'package:photos/services/deduplication_service.dart';
  8. import 'package:photos/services/sync_service.dart';
  9. import 'package:photos/services/update_service.dart';
  10. import 'package:photos/theme/ente_theme.dart';
  11. import 'package:photos/ui/backup_folder_selection_page.dart';
  12. import 'package:photos/ui/backup_settings_screen.dart';
  13. import 'package:photos/ui/components/captioned_text_widget.dart';
  14. import 'package:photos/ui/components/dialog_widget.dart';
  15. import 'package:photos/ui/components/expandable_menu_item_widget.dart';
  16. import 'package:photos/ui/components/menu_item_widget.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 AlertDialog alert = AlertDialog(
  156. title: const Text("Success"),
  157. content: Text(
  158. "You have successfully freed up " + formatBytes(status.size) + "!",
  159. ),
  160. actions: [
  161. TextButton(
  162. child: Text(
  163. "Rate us",
  164. style: TextStyle(
  165. color: Theme.of(context).colorScheme.greenAlternative,
  166. ),
  167. ),
  168. onPressed: () {
  169. Navigator.of(context, rootNavigator: true).pop('dialog');
  170. final url = UpdateService.instance.getRateDetails().item2;
  171. launchUrlString(url);
  172. },
  173. ),
  174. TextButton(
  175. child: const Text(
  176. "Ok",
  177. ),
  178. onPressed: () {
  179. if (Platform.isIOS) {
  180. showToast(
  181. context,
  182. "Also empty \"Recently Deleted\" from \"Settings\" -> \"Storage\" to claim the freed space",
  183. );
  184. }
  185. Navigator.of(context, rootNavigator: true).pop('dialog');
  186. },
  187. ),
  188. ],
  189. );
  190. showConfettiDialog(
  191. context: context,
  192. builder: (BuildContext context) {
  193. return alert;
  194. },
  195. barrierColor: Colors.black87,
  196. confettiAlignment: Alignment.topCenter,
  197. useRootNavigator: true,
  198. );
  199. }
  200. void _showDuplicateFilesDeletedDialog(DeduplicationResult result) {
  201. final String countText = result.count.toString() +
  202. " duplicate file" +
  203. (result.count == 1 ? "" : "s");
  204. final AlertDialog alert = AlertDialog(
  205. title: const Text("✨ Success"),
  206. content: Text(
  207. "You have cleaned up " +
  208. countText +
  209. ", saving " +
  210. formatBytes(result.size) +
  211. "!",
  212. ),
  213. actions: [
  214. TextButton(
  215. child: Text(
  216. "Rate us",
  217. style: TextStyle(
  218. color: Theme.of(context).colorScheme.greenAlternative,
  219. ),
  220. ),
  221. onPressed: () {
  222. Navigator.of(context, rootNavigator: true).pop('dialog');
  223. // TODO: Replace with https://pub.dev/packages/in_app_review
  224. final url = UpdateService.instance.getRateDetails().item2;
  225. launchUrlString(url);
  226. },
  227. ),
  228. TextButton(
  229. child: const Text(
  230. "Ok",
  231. ),
  232. onPressed: () {
  233. showShortToast(
  234. context,
  235. "Also empty your \"Trash\" to claim the freed up space",
  236. );
  237. Navigator.of(context, rootNavigator: true).pop('dialog');
  238. },
  239. ),
  240. ],
  241. );
  242. showConfettiDialog(
  243. context: context,
  244. builder: (BuildContext context) {
  245. return alert;
  246. },
  247. barrierColor: Colors.black87,
  248. confettiAlignment: Alignment.topCenter,
  249. useRootNavigator: true,
  250. );
  251. }
  252. }