backup_section_widget.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // @dart=2.9
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. import 'package:photos/core/configuration.dart';
  5. import 'package:photos/ente_theme_data.dart';
  6. import 'package:photos/models/backup_status.dart';
  7. import 'package:photos/models/duplicate_files.dart';
  8. import 'package:photos/services/deduplication_service.dart';
  9. import 'package:photos/services/sync_service.dart';
  10. import 'package:photos/ui/backup_folder_selection_page.dart';
  11. import 'package:photos/ui/common/dialogs.dart';
  12. import 'package:photos/ui/components/captioned_text_widget.dart';
  13. import 'package:photos/ui/components/expandable_menu_item_widget.dart';
  14. import 'package:photos/ui/components/menu_item_widget.dart';
  15. import 'package:photos/ui/settings/common_settings.dart';
  16. import 'package:photos/ui/tools/deduplicate_page.dart';
  17. import 'package:photos/ui/tools/free_space_page.dart';
  18. import 'package:photos/utils/data_util.dart';
  19. import 'package:photos/utils/dialog_util.dart';
  20. import 'package:photos/utils/navigation_util.dart';
  21. import 'package:photos/utils/toast_util.dart';
  22. import 'package:url_launcher/url_launcher_string.dart';
  23. class BackupSectionWidget extends StatefulWidget {
  24. const BackupSectionWidget({Key key}) : super(key: key);
  25. @override
  26. BackupSectionWidgetState createState() => BackupSectionWidgetState();
  27. }
  28. class BackupSectionWidgetState extends State<BackupSectionWidget> {
  29. @override
  30. Widget build(BuildContext context) {
  31. return ExpandableMenuItemWidget(
  32. title: "Backup",
  33. selectionOptionsWidget: _getSectionOptions(context),
  34. leadingIcon: Icons.backup_outlined,
  35. );
  36. }
  37. Widget _getSectionOptions(BuildContext context) {
  38. final List<Widget> sectionOptions = [
  39. sectionOptionSpacing,
  40. MenuItemWidget(
  41. captionedTextWidget: const CaptionedTextWidget(
  42. text: "Backed up folders",
  43. ),
  44. trailingIcon: Icons.chevron_right_outlined,
  45. trailingIconIsMuted: true,
  46. onTap: () {
  47. routeToPage(
  48. context,
  49. const BackupFolderSelectionPage(
  50. buttonText: "Backup",
  51. ),
  52. );
  53. },
  54. ),
  55. sectionOptionSpacing,
  56. MenuItemWidget(
  57. captionedTextWidget: const CaptionedTextWidget(
  58. text: "Backup over mobile data",
  59. ),
  60. trailingSwitch: Switch.adaptive(
  61. value: Configuration.instance.shouldBackupOverMobileData(),
  62. onChanged: (value) async {
  63. Configuration.instance.setBackupOverMobileData(value);
  64. setState(() {});
  65. },
  66. ),
  67. ),
  68. sectionOptionSpacing,
  69. MenuItemWidget(
  70. captionedTextWidget: const CaptionedTextWidget(
  71. text: "Backup videos",
  72. ),
  73. trailingSwitch: Switch.adaptive(
  74. value: Configuration.instance.shouldBackupVideos(),
  75. onChanged: (value) async {
  76. Configuration.instance.setShouldBackupVideos(value);
  77. setState(() {});
  78. },
  79. ),
  80. ),
  81. sectionOptionSpacing,
  82. ];
  83. if (Platform.isIOS) {
  84. sectionOptions.addAll([
  85. MenuItemWidget(
  86. captionedTextWidget: const CaptionedTextWidget(
  87. text: "Disable auto lock",
  88. ),
  89. trailingSwitch: Switch.adaptive(
  90. value: Configuration.instance.shouldKeepDeviceAwake(),
  91. onChanged: (value) async {
  92. if (value) {
  93. final choice = await showChoiceDialog(
  94. context,
  95. "Disable automatic screen lock when ente is running?",
  96. "This will ensure faster uploads by ensuring your device does not sleep when uploads are in progress.",
  97. firstAction: "No",
  98. secondAction: "Yes",
  99. );
  100. if (choice != DialogUserChoice.secondChoice) {
  101. return;
  102. }
  103. }
  104. await Configuration.instance.setShouldKeepDeviceAwake(value);
  105. setState(() {});
  106. },
  107. ),
  108. ),
  109. sectionOptionSpacing,
  110. ]);
  111. }
  112. sectionOptions.addAll(
  113. [
  114. MenuItemWidget(
  115. captionedTextWidget: const CaptionedTextWidget(
  116. text: "Free up space",
  117. ),
  118. trailingIcon: Icons.chevron_right_outlined,
  119. trailingIconIsMuted: true,
  120. onTap: () async {
  121. final dialog = createProgressDialog(context, "Calculating...");
  122. await dialog.show();
  123. BackupStatus status;
  124. try {
  125. status = await SyncService.instance.getBackupStatus();
  126. } catch (e) {
  127. await dialog.hide();
  128. showGenericErrorDialog(context);
  129. return;
  130. }
  131. await dialog.hide();
  132. if (status.localIDs.isEmpty) {
  133. showErrorDialog(
  134. context,
  135. "✨ All clear",
  136. "You've no files on this device that can be deleted",
  137. );
  138. } else {
  139. final bool result =
  140. await routeToPage(context, FreeSpacePage(status));
  141. if (result == true) {
  142. _showSpaceFreedDialog(status);
  143. }
  144. }
  145. },
  146. ),
  147. sectionOptionSpacing,
  148. MenuItemWidget(
  149. captionedTextWidget: const CaptionedTextWidget(
  150. text: "Deduplicate files",
  151. ),
  152. trailingIcon: Icons.chevron_right_outlined,
  153. trailingIconIsMuted: true,
  154. onTap: () async {
  155. final dialog = createProgressDialog(context, "Calculating...");
  156. await dialog.show();
  157. List<DuplicateFiles> duplicates;
  158. try {
  159. duplicates =
  160. await DeduplicationService.instance.getDuplicateFiles();
  161. } catch (e) {
  162. await dialog.hide();
  163. showGenericErrorDialog(context);
  164. return;
  165. }
  166. await dialog.hide();
  167. if (duplicates.isEmpty) {
  168. showErrorDialog(
  169. context,
  170. "✨ No duplicates",
  171. "You've no duplicate files that can be cleared",
  172. );
  173. } else {
  174. final DeduplicationResult result =
  175. await routeToPage(context, DeduplicatePage(duplicates));
  176. if (result != null) {
  177. _showDuplicateFilesDeletedDialog(result);
  178. }
  179. }
  180. },
  181. ),
  182. sectionOptionSpacing,
  183. ],
  184. );
  185. return Column(
  186. children: sectionOptions,
  187. );
  188. }
  189. void _showSpaceFreedDialog(BackupStatus status) {
  190. final AlertDialog alert = AlertDialog(
  191. title: const Text("Success"),
  192. content: Text(
  193. "You have successfully freed up " + formatBytes(status.size) + "!",
  194. ),
  195. actions: [
  196. TextButton(
  197. child: Text(
  198. "Rate us",
  199. style: TextStyle(
  200. color: Theme.of(context).colorScheme.greenAlternative,
  201. ),
  202. ),
  203. onPressed: () {
  204. Navigator.of(context, rootNavigator: true).pop('dialog');
  205. // TODO: Replace with https://pub.dev/packages/in_app_review
  206. if (Platform.isAndroid) {
  207. launchUrlString(
  208. "https://play.google.com/store/apps/details?id=io.ente.photos",
  209. );
  210. } else {
  211. launchUrlString(
  212. "https://apps.apple.com/in/app/ente-photos/id1542026904",
  213. );
  214. }
  215. },
  216. ),
  217. TextButton(
  218. child: const Text(
  219. "Ok",
  220. ),
  221. onPressed: () {
  222. if (Platform.isIOS) {
  223. showToast(
  224. context,
  225. "Also empty \"Recently Deleted\" from \"Settings\" -> \"Storage\" to claim the freed space",
  226. );
  227. }
  228. Navigator.of(context, rootNavigator: true).pop('dialog');
  229. },
  230. ),
  231. ],
  232. );
  233. showConfettiDialog(
  234. context: context,
  235. builder: (BuildContext context) {
  236. return alert;
  237. },
  238. barrierColor: Colors.black87,
  239. confettiAlignment: Alignment.topCenter,
  240. useRootNavigator: true,
  241. );
  242. }
  243. void _showDuplicateFilesDeletedDialog(DeduplicationResult result) {
  244. final String countText = result.count.toString() +
  245. " duplicate file" +
  246. (result.count == 1 ? "" : "s");
  247. final AlertDialog alert = AlertDialog(
  248. title: const Text("✨ Success"),
  249. content: Text(
  250. "You have cleaned up " +
  251. countText +
  252. ", saving " +
  253. formatBytes(result.size) +
  254. "!",
  255. ),
  256. actions: [
  257. TextButton(
  258. child: Text(
  259. "Rate us",
  260. style: TextStyle(
  261. color: Theme.of(context).colorScheme.greenAlternative,
  262. ),
  263. ),
  264. onPressed: () {
  265. Navigator.of(context, rootNavigator: true).pop('dialog');
  266. // TODO: Replace with https://pub.dev/packages/in_app_review
  267. if (Platform.isAndroid) {
  268. launchUrlString(
  269. "https://play.google.com/store/apps/details?id=io.ente.photos",
  270. );
  271. } else {
  272. launchUrlString(
  273. "https://apps.apple.com/in/app/ente-photos/id1542026904",
  274. );
  275. }
  276. },
  277. ),
  278. TextButton(
  279. child: const Text(
  280. "Ok",
  281. ),
  282. onPressed: () {
  283. showToast(
  284. context,
  285. "Also empty your \"Trash\" to claim the freed up space",
  286. );
  287. Navigator.of(context, rootNavigator: true).pop('dialog');
  288. },
  289. ),
  290. ],
  291. );
  292. showConfettiDialog(
  293. context: context,
  294. builder: (BuildContext context) {
  295. return alert;
  296. },
  297. barrierColor: Colors.black87,
  298. confettiAlignment: Alignment.topCenter,
  299. useRootNavigator: true,
  300. );
  301. }
  302. }