backup_controller_page.dart 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. import 'dart:io';
  2. import 'package:auto_route/auto_route.dart';
  3. import 'package:connectivity_plus/connectivity_plus.dart';
  4. import 'package:easy_localization/easy_localization.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter_hooks/flutter_hooks.dart';
  7. import 'package:hooks_riverpod/hooks_riverpod.dart';
  8. import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
  9. import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
  10. import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
  11. import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
  12. import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
  13. import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
  14. import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
  15. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  16. import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
  17. import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
  18. import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
  19. import 'package:immich_mobile/routing/router.dart';
  20. import 'package:immich_mobile/shared/models/asset.dart';
  21. import 'package:immich_mobile/shared/providers/asset.provider.dart';
  22. import 'package:immich_mobile/shared/providers/websocket.provider.dart';
  23. import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
  24. import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
  25. import 'package:immich_mobile/shared/ui/immich_toast.dart';
  26. import 'package:permission_handler/permission_handler.dart';
  27. import 'package:url_launcher/url_launcher.dart';
  28. import 'package:wakelock_plus/wakelock_plus.dart';
  29. class BackupControllerPage extends HookConsumerWidget {
  30. const BackupControllerPage({Key? key}) : super(key: key);
  31. @override
  32. Widget build(BuildContext context, WidgetRef ref) {
  33. BackUpState backupState = ref.watch(backupProvider);
  34. final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
  35. final settingsService = ref.watch(appSettingsServiceProvider);
  36. final showBackupFix = Platform.isAndroid &&
  37. settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting);
  38. final appRefreshDisabled =
  39. Platform.isIOS && settings?.appRefreshEnabled != true;
  40. bool hasExclusiveAccess =
  41. backupState.backupProgress != BackUpProgressEnum.inBackground;
  42. bool shouldBackup = backupState.allUniqueAssets.length -
  43. backupState.selectedAlbumsBackupAssetsIds.length ==
  44. 0 ||
  45. !hasExclusiveAccess
  46. ? false
  47. : true;
  48. var isDarkMode = Theme.of(context).brightness == Brightness.dark;
  49. final checkInProgress = useState(false);
  50. useEffect(
  51. () {
  52. if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
  53. backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
  54. ref.watch(backupProvider.notifier).getBackupInfo();
  55. }
  56. // Update the background settings information just to make sure we
  57. // have the latest, since the platform channel will not update
  58. // automatically
  59. if (Platform.isIOS) {
  60. ref.watch(iOSBackgroundSettingsProvider.notifier).refresh();
  61. }
  62. ref
  63. .watch(websocketProvider.notifier)
  64. .stopListenToEvent('on_upload_success');
  65. return null;
  66. },
  67. [],
  68. );
  69. Future<void> performDeletion(List<Asset> assets) async {
  70. try {
  71. checkInProgress.value = true;
  72. ImmichToast.show(
  73. context: context,
  74. msg: "Deleting ${assets.length} assets on the server...",
  75. );
  76. await ref
  77. .read(assetProvider.notifier)
  78. .deleteAssets(assets, force: true);
  79. ImmichToast.show(
  80. context: context,
  81. msg: "Deleted ${assets.length} assets on the server. "
  82. "You can now start a manual backup",
  83. toastType: ToastType.success,
  84. );
  85. } finally {
  86. checkInProgress.value = false;
  87. }
  88. }
  89. void performBackupCheck() async {
  90. try {
  91. checkInProgress.value = true;
  92. if (backupState.allUniqueAssets.length >
  93. backupState.selectedAlbumsBackupAssetsIds.length) {
  94. ImmichToast.show(
  95. context: context,
  96. msg: "Backup all assets before starting this check!",
  97. toastType: ToastType.error,
  98. );
  99. return;
  100. }
  101. final connection = await Connectivity().checkConnectivity();
  102. if (connection != ConnectivityResult.wifi) {
  103. ImmichToast.show(
  104. context: context,
  105. msg: "Make sure to be connected to unmetered Wi-Fi",
  106. toastType: ToastType.error,
  107. );
  108. return;
  109. }
  110. WakelockPlus.enable();
  111. const limit = 100;
  112. final toDelete = await ref
  113. .read(backupVerificationServiceProvider)
  114. .findWronglyBackedUpAssets(limit: limit);
  115. if (toDelete.isEmpty) {
  116. ImmichToast.show(
  117. context: context,
  118. msg: "Did not find any corrupt asset backups!",
  119. toastType: ToastType.success,
  120. );
  121. } else {
  122. await showDialog(
  123. context: context,
  124. builder: (context) => ConfirmDialog(
  125. onOk: () => performDeletion(toDelete),
  126. title: "Corrupt backups!",
  127. ok: "Delete",
  128. content:
  129. "Found ${toDelete.length} (max $limit at once) corrupt asset backups. "
  130. "Run the check again to find more.\n"
  131. "Do you want to delete the corrupt asset backups now?",
  132. ),
  133. );
  134. }
  135. } finally {
  136. WakelockPlus.disable();
  137. checkInProgress.value = false;
  138. }
  139. }
  140. Widget buildCheckCorruptBackups() {
  141. return ListTile(
  142. leading: Icon(
  143. Icons.warning_rounded,
  144. color: Theme.of(context).primaryColor,
  145. ),
  146. title: const Text(
  147. "Check for corrupt asset backups",
  148. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  149. ),
  150. isThreeLine: true,
  151. subtitle: Column(
  152. crossAxisAlignment: CrossAxisAlignment.start,
  153. children: [
  154. const Text("Run this check only over Wi-Fi and once all assets "
  155. "have been backed-up. The procedure might take a few minutes."),
  156. ElevatedButton(
  157. onPressed: checkInProgress.value ? null : performBackupCheck,
  158. child: checkInProgress.value
  159. ? const CircularProgressIndicator()
  160. : const Text("Perform check"),
  161. ),
  162. ],
  163. ),
  164. );
  165. }
  166. Widget buildStorageInformation() {
  167. return ListTile(
  168. leading: Icon(
  169. Icons.storage_rounded,
  170. color: Theme.of(context).primaryColor,
  171. ),
  172. title: const Text(
  173. "backup_controller_page_server_storage",
  174. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  175. ).tr(),
  176. isThreeLine: true,
  177. subtitle: Padding(
  178. padding: const EdgeInsets.only(top: 8.0),
  179. child: Column(
  180. crossAxisAlignment: CrossAxisAlignment.start,
  181. children: [
  182. Padding(
  183. padding: const EdgeInsets.only(top: 8.0),
  184. child: LinearProgressIndicator(
  185. minHeight: 10.0,
  186. value: backupState.serverInfo.diskUsagePercentage / 100.0,
  187. backgroundColor: Colors.grey,
  188. color: Theme.of(context).primaryColor,
  189. ),
  190. ),
  191. Padding(
  192. padding: const EdgeInsets.only(top: 12.0),
  193. child: const Text('backup_controller_page_storage_format').tr(
  194. args: [
  195. backupState.serverInfo.diskUse,
  196. backupState.serverInfo.diskSize,
  197. ],
  198. ),
  199. ),
  200. ],
  201. ),
  202. ),
  203. );
  204. }
  205. ListTile buildAutoBackupController() {
  206. final isAutoBackup = backupState.autoBackup;
  207. final backUpOption = isAutoBackup
  208. ? "backup_controller_page_status_on".tr()
  209. : "backup_controller_page_status_off".tr();
  210. final backupBtnText = isAutoBackup
  211. ? "backup_controller_page_turn_off".tr()
  212. : "backup_controller_page_turn_on".tr();
  213. return ListTile(
  214. isThreeLine: true,
  215. leading: isAutoBackup
  216. ? Icon(
  217. Icons.cloud_done_rounded,
  218. color: Theme.of(context).primaryColor,
  219. )
  220. : const Icon(Icons.cloud_off_rounded),
  221. title: Text(
  222. backUpOption,
  223. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  224. ),
  225. subtitle: Padding(
  226. padding: const EdgeInsets.symmetric(vertical: 8.0),
  227. child: Column(
  228. crossAxisAlignment: CrossAxisAlignment.start,
  229. children: [
  230. if (!isAutoBackup)
  231. const Text(
  232. "backup_controller_page_desc_backup",
  233. style: TextStyle(fontSize: 14),
  234. ).tr(),
  235. Padding(
  236. padding: const EdgeInsets.only(top: 8.0),
  237. child: ElevatedButton(
  238. onPressed: () => ref
  239. .read(backupProvider.notifier)
  240. .setAutoBackup(!isAutoBackup),
  241. child: Text(
  242. backupBtnText,
  243. style: const TextStyle(
  244. fontWeight: FontWeight.bold,
  245. fontSize: 12,
  246. ),
  247. ),
  248. ),
  249. ),
  250. ],
  251. ),
  252. ),
  253. );
  254. }
  255. void showErrorToUser(String msg) {
  256. final snackBar = SnackBar(
  257. content: Text(
  258. msg.tr(),
  259. ),
  260. backgroundColor: Colors.red,
  261. );
  262. ScaffoldMessenger.of(context).showSnackBar(snackBar);
  263. }
  264. void showBatteryOptimizationInfoToUser() {
  265. showDialog<void>(
  266. context: context,
  267. barrierDismissible: false,
  268. builder: (BuildContext context) {
  269. return AlertDialog(
  270. title: const Text(
  271. 'backup_controller_page_background_battery_info_title',
  272. ).tr(),
  273. content: SingleChildScrollView(
  274. child: const Text(
  275. 'backup_controller_page_background_battery_info_message',
  276. ).tr(),
  277. ),
  278. actions: [
  279. ElevatedButton(
  280. onPressed: () => launchUrl(
  281. Uri.parse('https://dontkillmyapp.com'),
  282. mode: LaunchMode.externalApplication,
  283. ),
  284. child: const Text(
  285. "backup_controller_page_background_battery_info_link",
  286. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  287. ).tr(),
  288. ),
  289. ElevatedButton(
  290. child: const Text(
  291. 'backup_controller_page_background_battery_info_ok',
  292. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  293. ).tr(),
  294. onPressed: () {
  295. Navigator.of(context).pop();
  296. },
  297. ),
  298. ],
  299. );
  300. },
  301. );
  302. }
  303. Widget buildBackgroundBackupController() {
  304. final bool isBackgroundEnabled = backupState.backgroundBackup;
  305. final bool isWifiRequired = backupState.backupRequireWifi;
  306. final bool isChargingRequired = backupState.backupRequireCharging;
  307. final Color activeColor = Theme.of(context).primaryColor;
  308. String formatBackupDelaySliderValue(double v) {
  309. if (v == 0.0) {
  310. return 'setting_notifications_notify_seconds'.tr(args: const ['5']);
  311. } else if (v == 1.0) {
  312. return 'setting_notifications_notify_seconds'.tr(args: const ['30']);
  313. } else if (v == 2.0) {
  314. return 'setting_notifications_notify_minutes'.tr(args: const ['2']);
  315. } else {
  316. return 'setting_notifications_notify_minutes'.tr(args: const ['10']);
  317. }
  318. }
  319. int backupDelayToMilliseconds(double v) {
  320. if (v == 0.0) {
  321. return 5000;
  322. } else if (v == 1.0) {
  323. return 30000;
  324. } else if (v == 2.0) {
  325. return 120000;
  326. } else {
  327. return 600000;
  328. }
  329. }
  330. double backupDelayToSliderValue(int ms) {
  331. if (ms == 5000) {
  332. return 0.0;
  333. } else if (ms == 30000) {
  334. return 1.0;
  335. } else if (ms == 120000) {
  336. return 2.0;
  337. } else {
  338. return 3.0;
  339. }
  340. }
  341. final triggerDelay =
  342. useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
  343. return Column(
  344. children: [
  345. ListTile(
  346. isThreeLine: true,
  347. leading: isBackgroundEnabled
  348. ? Icon(
  349. Icons.cloud_sync_rounded,
  350. color: activeColor,
  351. )
  352. : const Icon(Icons.cloud_sync_rounded),
  353. title: Text(
  354. isBackgroundEnabled
  355. ? "backup_controller_page_background_is_on"
  356. : "backup_controller_page_background_is_off",
  357. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  358. ).tr(),
  359. subtitle: Column(
  360. crossAxisAlignment: CrossAxisAlignment.start,
  361. children: [
  362. if (!isBackgroundEnabled)
  363. Padding(
  364. padding: const EdgeInsets.symmetric(vertical: 8.0),
  365. child: const Text(
  366. "backup_controller_page_background_description",
  367. ).tr(),
  368. ),
  369. if (isBackgroundEnabled && Platform.isAndroid)
  370. SwitchListTile.adaptive(
  371. title: const Text("backup_controller_page_background_wifi")
  372. .tr(),
  373. secondary: Icon(
  374. Icons.wifi,
  375. color: isWifiRequired ? activeColor : null,
  376. ),
  377. dense: true,
  378. activeColor: activeColor,
  379. value: isWifiRequired,
  380. onChanged: (isChecked) => ref
  381. .read(backupProvider.notifier)
  382. .configureBackgroundBackup(
  383. requireWifi: isChecked,
  384. onError: showErrorToUser,
  385. onBatteryInfo: showBatteryOptimizationInfoToUser,
  386. ),
  387. ),
  388. if (isBackgroundEnabled)
  389. SwitchListTile.adaptive(
  390. title:
  391. const Text("backup_controller_page_background_charging")
  392. .tr(),
  393. secondary: Icon(
  394. Icons.charging_station,
  395. color: isChargingRequired ? activeColor : null,
  396. ),
  397. dense: true,
  398. activeColor: activeColor,
  399. value: isChargingRequired,
  400. onChanged: (isChecked) => ref
  401. .read(backupProvider.notifier)
  402. .configureBackgroundBackup(
  403. requireCharging: isChecked,
  404. onError: showErrorToUser,
  405. onBatteryInfo: showBatteryOptimizationInfoToUser,
  406. ),
  407. ),
  408. if (isBackgroundEnabled && Platform.isAndroid)
  409. ListTile(
  410. isThreeLine: false,
  411. dense: true,
  412. title: const Text(
  413. 'backup_controller_page_background_delay',
  414. style: TextStyle(
  415. fontWeight: FontWeight.bold,
  416. ),
  417. ).tr(
  418. args: [formatBackupDelaySliderValue(triggerDelay.value)],
  419. ),
  420. subtitle: Slider(
  421. value: triggerDelay.value,
  422. onChanged: (double v) => triggerDelay.value = v,
  423. onChangeEnd: (double v) => ref
  424. .read(backupProvider.notifier)
  425. .configureBackgroundBackup(
  426. triggerDelay: backupDelayToMilliseconds(v),
  427. onError: showErrorToUser,
  428. onBatteryInfo: showBatteryOptimizationInfoToUser,
  429. ),
  430. max: 3.0,
  431. divisions: 3,
  432. label: formatBackupDelaySliderValue(triggerDelay.value),
  433. activeColor: Theme.of(context).primaryColor,
  434. ),
  435. ),
  436. ElevatedButton(
  437. onPressed: () => ref
  438. .read(backupProvider.notifier)
  439. .configureBackgroundBackup(
  440. enabled: !isBackgroundEnabled,
  441. onError: showErrorToUser,
  442. onBatteryInfo: showBatteryOptimizationInfoToUser,
  443. ),
  444. child: Text(
  445. isBackgroundEnabled
  446. ? "backup_controller_page_background_turn_off"
  447. : "backup_controller_page_background_turn_on",
  448. style: const TextStyle(
  449. fontWeight: FontWeight.bold,
  450. fontSize: 12,
  451. ),
  452. ).tr(),
  453. ),
  454. ],
  455. ),
  456. ),
  457. if (isBackgroundEnabled && Platform.isIOS)
  458. FutureBuilder(
  459. future: ref
  460. .read(backgroundServiceProvider)
  461. .getIOSBackgroundAppRefreshEnabled(),
  462. builder: (context, snapshot) {
  463. final enabled = snapshot.data;
  464. // If it's not enabled, show them some kind of alert that says
  465. // background refresh is not enabled
  466. if (enabled != null && !enabled) {}
  467. // If it's enabled, no need to bother them
  468. return Container();
  469. },
  470. ),
  471. if (Platform.isIOS && isBackgroundEnabled && settings != null)
  472. IosDebugInfoTile(
  473. settings: settings,
  474. ),
  475. ],
  476. );
  477. }
  478. Widget buildBackgroundAppRefreshWarning() {
  479. return ListTile(
  480. isThreeLine: true,
  481. leading: const Icon(
  482. Icons.task_outlined,
  483. ),
  484. title: const Text(
  485. 'backup_controller_page_background_app_refresh_disabled_title',
  486. style: TextStyle(
  487. fontWeight: FontWeight.bold,
  488. fontSize: 14,
  489. ),
  490. ).tr(),
  491. subtitle: Column(
  492. crossAxisAlignment: CrossAxisAlignment.start,
  493. children: [
  494. Padding(
  495. padding: const EdgeInsets.symmetric(vertical: 8.0),
  496. child: const Text(
  497. 'backup_controller_page_background_app_refresh_disabled_content',
  498. ).tr(),
  499. ),
  500. ElevatedButton(
  501. onPressed: () => openAppSettings(),
  502. child: const Text(
  503. 'backup_controller_page_background_app_refresh_enable_button_text',
  504. style: TextStyle(
  505. fontWeight: FontWeight.bold,
  506. fontSize: 12,
  507. ),
  508. ).tr(),
  509. ),
  510. ],
  511. ),
  512. );
  513. }
  514. Widget buildSelectedAlbumName() {
  515. var text = "backup_controller_page_backup_selected".tr();
  516. var albums = ref.watch(backupProvider).selectedBackupAlbums;
  517. if (albums.isNotEmpty) {
  518. for (var album in albums) {
  519. if (album.name == "Recent" || album.name == "Recents") {
  520. text += "${album.name} (${'backup_all'.tr()}), ";
  521. } else {
  522. text += "${album.name}, ";
  523. }
  524. }
  525. return Padding(
  526. padding: const EdgeInsets.only(top: 8.0),
  527. child: Text(
  528. text.trim().substring(0, text.length - 2),
  529. style: TextStyle(
  530. color: Theme.of(context).primaryColor,
  531. fontSize: 12,
  532. fontWeight: FontWeight.bold,
  533. ),
  534. ),
  535. );
  536. } else {
  537. return Padding(
  538. padding: const EdgeInsets.only(top: 8.0),
  539. child: Text(
  540. "backup_controller_page_none_selected".tr(),
  541. style: TextStyle(
  542. color: Theme.of(context).primaryColor,
  543. fontSize: 12,
  544. fontWeight: FontWeight.bold,
  545. ),
  546. ),
  547. );
  548. }
  549. }
  550. Widget buildExcludedAlbumName() {
  551. var text = "backup_controller_page_excluded".tr();
  552. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  553. if (albums.isNotEmpty) {
  554. for (var album in albums) {
  555. text += "${album.name}, ";
  556. }
  557. return Padding(
  558. padding: const EdgeInsets.only(top: 8.0),
  559. child: Text(
  560. text.trim().substring(0, text.length - 2),
  561. style: TextStyle(
  562. color: Colors.red[300],
  563. fontSize: 12,
  564. fontWeight: FontWeight.bold,
  565. ),
  566. ),
  567. );
  568. } else {
  569. return const SizedBox();
  570. }
  571. }
  572. buildFolderSelectionTile() {
  573. return Card(
  574. shape: RoundedRectangleBorder(
  575. borderRadius: BorderRadius.circular(20),
  576. side: BorderSide(
  577. color: isDarkMode
  578. ? const Color.fromARGB(255, 56, 56, 56)
  579. : Colors.black12,
  580. width: 1,
  581. ),
  582. ),
  583. elevation: 0,
  584. borderOnForeground: false,
  585. child: ListTile(
  586. minVerticalPadding: 15,
  587. title: const Text(
  588. "backup_controller_page_albums",
  589. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
  590. ).tr(),
  591. subtitle: Padding(
  592. padding: const EdgeInsets.only(top: 8.0),
  593. child: Column(
  594. crossAxisAlignment: CrossAxisAlignment.start,
  595. children: [
  596. const Text(
  597. "backup_controller_page_to_backup",
  598. style: TextStyle(fontSize: 12),
  599. ).tr(),
  600. buildSelectedAlbumName(),
  601. buildExcludedAlbumName(),
  602. ],
  603. ),
  604. ),
  605. trailing: ElevatedButton(
  606. onPressed: () {
  607. AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
  608. },
  609. child: const Text(
  610. "backup_controller_page_select",
  611. style: TextStyle(
  612. fontWeight: FontWeight.bold,
  613. fontSize: 12,
  614. ),
  615. ).tr(),
  616. ),
  617. ),
  618. );
  619. }
  620. void startBackup() {
  621. ref.watch(errorBackupListProvider.notifier).empty();
  622. if (ref.watch(backupProvider).backupProgress !=
  623. BackUpProgressEnum.inBackground) {
  624. ref.watch(backupProvider.notifier).startBackupProcess();
  625. }
  626. }
  627. Widget buildBackupButton() {
  628. return Padding(
  629. padding: const EdgeInsets.only(
  630. top: 24,
  631. ),
  632. child: Container(
  633. child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
  634. backupState.backupProgress ==
  635. BackUpProgressEnum.manualInProgress
  636. ? ElevatedButton(
  637. style: ElevatedButton.styleFrom(
  638. foregroundColor: Colors.grey[50],
  639. backgroundColor: Colors.red[300],
  640. // padding: const EdgeInsets.all(14),
  641. ),
  642. onPressed: () {
  643. if (backupState.backupProgress ==
  644. BackUpProgressEnum.manualInProgress) {
  645. ref.read(manualUploadProvider.notifier).cancelBackup();
  646. } else {
  647. ref.read(backupProvider.notifier).cancelBackup();
  648. }
  649. },
  650. child: const Text(
  651. "backup_controller_page_cancel",
  652. style: TextStyle(
  653. fontSize: 14,
  654. fontWeight: FontWeight.bold,
  655. ),
  656. ).tr(),
  657. )
  658. : ElevatedButton(
  659. onPressed: shouldBackup ? startBackup : null,
  660. child: const Text(
  661. "backup_controller_page_start_backup",
  662. style: TextStyle(
  663. fontSize: 14,
  664. fontWeight: FontWeight.bold,
  665. ),
  666. ).tr(),
  667. ),
  668. ),
  669. );
  670. }
  671. buildBackgroundBackupInfo() {
  672. return const ListTile(
  673. leading: Icon(Icons.info_outline_rounded),
  674. title: Text(
  675. "Background backup is currently running, cannot start manual backup",
  676. ),
  677. );
  678. }
  679. return Scaffold(
  680. appBar: AppBar(
  681. elevation: 0,
  682. title: const Text(
  683. "backup_controller_page_backup",
  684. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  685. ).tr(),
  686. leading: IconButton(
  687. onPressed: () {
  688. ref.watch(websocketProvider.notifier).listenUploadEvent();
  689. AutoRouter.of(context).pop(true);
  690. },
  691. splashRadius: 24,
  692. icon: const Icon(
  693. Icons.arrow_back_ios_rounded,
  694. ),
  695. ),
  696. ),
  697. body: Padding(
  698. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
  699. child: ListView(
  700. // crossAxisAlignment: CrossAxisAlignment.start,
  701. children: [
  702. Padding(
  703. padding: const EdgeInsets.all(8.0),
  704. child: const Text(
  705. "backup_controller_page_info",
  706. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  707. ).tr(),
  708. ),
  709. buildFolderSelectionTile(),
  710. BackupInfoCard(
  711. title: "backup_controller_page_total".tr(),
  712. subtitle: "backup_controller_page_total_sub".tr(),
  713. info: ref.watch(backupProvider).availableAlbums.isEmpty
  714. ? "..."
  715. : "${backupState.allUniqueAssets.length}",
  716. ),
  717. BackupInfoCard(
  718. title: "backup_controller_page_backup".tr(),
  719. subtitle: "backup_controller_page_backup_sub".tr(),
  720. info: ref.watch(backupProvider).availableAlbums.isEmpty
  721. ? "..."
  722. : "${backupState.selectedAlbumsBackupAssetsIds.length}",
  723. ),
  724. BackupInfoCard(
  725. title: "backup_controller_page_remainder".tr(),
  726. subtitle: "backup_controller_page_remainder_sub".tr(),
  727. info: ref.watch(backupProvider).availableAlbums.isEmpty
  728. ? "..."
  729. : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  730. ),
  731. const Divider(),
  732. buildAutoBackupController(),
  733. const Divider(),
  734. AnimatedSwitcher(
  735. duration: const Duration(milliseconds: 500),
  736. child: Platform.isIOS
  737. ? (appRefreshDisabled
  738. ? buildBackgroundAppRefreshWarning()
  739. : buildBackgroundBackupController())
  740. : buildBackgroundBackupController(),
  741. ),
  742. if (showBackupFix) const Divider(),
  743. if (showBackupFix) buildCheckCorruptBackups(),
  744. const Divider(),
  745. buildStorageInformation(),
  746. const Divider(),
  747. const CurrentUploadingAssetInfoBox(),
  748. if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
  749. buildBackupButton(),
  750. ],
  751. ),
  752. ),
  753. );
  754. }
  755. }