backup_section_widget.dart 9.9 KB

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