backup_controller_page.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  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. ListTile buildAutoBackupController() {
  167. final isAutoBackup = backupState.autoBackup;
  168. final backUpOption = isAutoBackup
  169. ? "backup_controller_page_status_on".tr()
  170. : "backup_controller_page_status_off".tr();
  171. final backupBtnText = isAutoBackup
  172. ? "backup_controller_page_turn_off".tr()
  173. : "backup_controller_page_turn_on".tr();
  174. return ListTile(
  175. isThreeLine: true,
  176. leading: isAutoBackup
  177. ? Icon(
  178. Icons.cloud_done_rounded,
  179. color: Theme.of(context).primaryColor,
  180. )
  181. : const Icon(Icons.cloud_off_rounded),
  182. title: Text(
  183. backUpOption,
  184. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  185. ),
  186. subtitle: Padding(
  187. padding: const EdgeInsets.symmetric(vertical: 8.0),
  188. child: Column(
  189. crossAxisAlignment: CrossAxisAlignment.start,
  190. children: [
  191. if (!isAutoBackup)
  192. const Text(
  193. "backup_controller_page_desc_backup",
  194. style: TextStyle(fontSize: 14),
  195. ).tr(),
  196. Padding(
  197. padding: const EdgeInsets.only(top: 8.0),
  198. child: ElevatedButton(
  199. onPressed: () => ref
  200. .read(backupProvider.notifier)
  201. .setAutoBackup(!isAutoBackup),
  202. child: Text(
  203. backupBtnText,
  204. style: const TextStyle(
  205. fontWeight: FontWeight.bold,
  206. fontSize: 12,
  207. ),
  208. ),
  209. ),
  210. ),
  211. ],
  212. ),
  213. ),
  214. );
  215. }
  216. void showErrorToUser(String msg) {
  217. final snackBar = SnackBar(
  218. content: Text(
  219. msg.tr(),
  220. ),
  221. backgroundColor: Colors.red,
  222. );
  223. ScaffoldMessenger.of(context).showSnackBar(snackBar);
  224. }
  225. void showBatteryOptimizationInfoToUser() {
  226. showDialog<void>(
  227. context: context,
  228. barrierDismissible: false,
  229. builder: (BuildContext context) {
  230. return AlertDialog(
  231. title: const Text(
  232. 'backup_controller_page_background_battery_info_title',
  233. ).tr(),
  234. content: SingleChildScrollView(
  235. child: const Text(
  236. 'backup_controller_page_background_battery_info_message',
  237. ).tr(),
  238. ),
  239. actions: [
  240. ElevatedButton(
  241. onPressed: () => launchUrl(
  242. Uri.parse('https://dontkillmyapp.com'),
  243. mode: LaunchMode.externalApplication,
  244. ),
  245. child: const Text(
  246. "backup_controller_page_background_battery_info_link",
  247. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  248. ).tr(),
  249. ),
  250. ElevatedButton(
  251. child: const Text(
  252. 'backup_controller_page_background_battery_info_ok',
  253. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  254. ).tr(),
  255. onPressed: () {
  256. Navigator.of(context).pop();
  257. },
  258. ),
  259. ],
  260. );
  261. },
  262. );
  263. }
  264. Widget buildBackgroundBackupController() {
  265. final bool isBackgroundEnabled = backupState.backgroundBackup;
  266. final bool isWifiRequired = backupState.backupRequireWifi;
  267. final bool isChargingRequired = backupState.backupRequireCharging;
  268. final Color activeColor = Theme.of(context).primaryColor;
  269. String formatBackupDelaySliderValue(double v) {
  270. if (v == 0.0) {
  271. return 'setting_notifications_notify_seconds'.tr(args: const ['5']);
  272. } else if (v == 1.0) {
  273. return 'setting_notifications_notify_seconds'.tr(args: const ['30']);
  274. } else if (v == 2.0) {
  275. return 'setting_notifications_notify_minutes'.tr(args: const ['2']);
  276. } else {
  277. return 'setting_notifications_notify_minutes'.tr(args: const ['10']);
  278. }
  279. }
  280. int backupDelayToMilliseconds(double v) {
  281. if (v == 0.0) {
  282. return 5000;
  283. } else if (v == 1.0) {
  284. return 30000;
  285. } else if (v == 2.0) {
  286. return 120000;
  287. } else {
  288. return 600000;
  289. }
  290. }
  291. double backupDelayToSliderValue(int ms) {
  292. if (ms == 5000) {
  293. return 0.0;
  294. } else if (ms == 30000) {
  295. return 1.0;
  296. } else if (ms == 120000) {
  297. return 2.0;
  298. } else {
  299. return 3.0;
  300. }
  301. }
  302. final triggerDelay =
  303. useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
  304. return Column(
  305. children: [
  306. ListTile(
  307. isThreeLine: true,
  308. leading: isBackgroundEnabled
  309. ? Icon(
  310. Icons.cloud_sync_rounded,
  311. color: activeColor,
  312. )
  313. : const Icon(Icons.cloud_sync_rounded),
  314. title: Text(
  315. isBackgroundEnabled
  316. ? "backup_controller_page_background_is_on"
  317. : "backup_controller_page_background_is_off",
  318. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  319. ).tr(),
  320. subtitle: Column(
  321. crossAxisAlignment: CrossAxisAlignment.start,
  322. children: [
  323. if (!isBackgroundEnabled)
  324. Padding(
  325. padding: const EdgeInsets.symmetric(vertical: 8.0),
  326. child: const Text(
  327. "backup_controller_page_background_description",
  328. ).tr(),
  329. ),
  330. if (isBackgroundEnabled && Platform.isAndroid)
  331. SwitchListTile.adaptive(
  332. title: const Text("backup_controller_page_background_wifi")
  333. .tr(),
  334. secondary: Icon(
  335. Icons.wifi,
  336. color: isWifiRequired ? activeColor : null,
  337. ),
  338. dense: true,
  339. activeColor: activeColor,
  340. value: isWifiRequired,
  341. onChanged: (isChecked) => ref
  342. .read(backupProvider.notifier)
  343. .configureBackgroundBackup(
  344. requireWifi: isChecked,
  345. onError: showErrorToUser,
  346. onBatteryInfo: showBatteryOptimizationInfoToUser,
  347. ),
  348. ),
  349. if (isBackgroundEnabled)
  350. SwitchListTile.adaptive(
  351. title:
  352. const Text("backup_controller_page_background_charging")
  353. .tr(),
  354. secondary: Icon(
  355. Icons.charging_station,
  356. color: isChargingRequired ? activeColor : null,
  357. ),
  358. dense: true,
  359. activeColor: activeColor,
  360. value: isChargingRequired,
  361. onChanged: (isChecked) => ref
  362. .read(backupProvider.notifier)
  363. .configureBackgroundBackup(
  364. requireCharging: isChecked,
  365. onError: showErrorToUser,
  366. onBatteryInfo: showBatteryOptimizationInfoToUser,
  367. ),
  368. ),
  369. if (isBackgroundEnabled && Platform.isAndroid)
  370. ListTile(
  371. isThreeLine: false,
  372. dense: true,
  373. title: const Text(
  374. 'backup_controller_page_background_delay',
  375. style: TextStyle(
  376. fontWeight: FontWeight.bold,
  377. ),
  378. ).tr(
  379. args: [formatBackupDelaySliderValue(triggerDelay.value)],
  380. ),
  381. subtitle: Slider(
  382. value: triggerDelay.value,
  383. onChanged: (double v) => triggerDelay.value = v,
  384. onChangeEnd: (double v) => ref
  385. .read(backupProvider.notifier)
  386. .configureBackgroundBackup(
  387. triggerDelay: backupDelayToMilliseconds(v),
  388. onError: showErrorToUser,
  389. onBatteryInfo: showBatteryOptimizationInfoToUser,
  390. ),
  391. max: 3.0,
  392. divisions: 3,
  393. label: formatBackupDelaySliderValue(triggerDelay.value),
  394. activeColor: Theme.of(context).primaryColor,
  395. ),
  396. ),
  397. ElevatedButton(
  398. onPressed: () => ref
  399. .read(backupProvider.notifier)
  400. .configureBackgroundBackup(
  401. enabled: !isBackgroundEnabled,
  402. onError: showErrorToUser,
  403. onBatteryInfo: showBatteryOptimizationInfoToUser,
  404. ),
  405. child: Text(
  406. isBackgroundEnabled
  407. ? "backup_controller_page_background_turn_off"
  408. : "backup_controller_page_background_turn_on",
  409. style: const TextStyle(
  410. fontWeight: FontWeight.bold,
  411. fontSize: 12,
  412. ),
  413. ).tr(),
  414. ),
  415. ],
  416. ),
  417. ),
  418. if (isBackgroundEnabled && Platform.isIOS)
  419. FutureBuilder(
  420. future: ref
  421. .read(backgroundServiceProvider)
  422. .getIOSBackgroundAppRefreshEnabled(),
  423. builder: (context, snapshot) {
  424. final enabled = snapshot.data;
  425. // If it's not enabled, show them some kind of alert that says
  426. // background refresh is not enabled
  427. if (enabled != null && !enabled) {}
  428. // If it's enabled, no need to bother them
  429. return Container();
  430. },
  431. ),
  432. if (Platform.isIOS && isBackgroundEnabled && settings != null)
  433. IosDebugInfoTile(
  434. settings: settings,
  435. ),
  436. ],
  437. );
  438. }
  439. Widget buildBackgroundAppRefreshWarning() {
  440. return ListTile(
  441. isThreeLine: true,
  442. leading: const Icon(
  443. Icons.task_outlined,
  444. ),
  445. title: const Text(
  446. 'backup_controller_page_background_app_refresh_disabled_title',
  447. style: TextStyle(
  448. fontWeight: FontWeight.bold,
  449. fontSize: 14,
  450. ),
  451. ).tr(),
  452. subtitle: Column(
  453. crossAxisAlignment: CrossAxisAlignment.start,
  454. children: [
  455. Padding(
  456. padding: const EdgeInsets.symmetric(vertical: 8.0),
  457. child: const Text(
  458. 'backup_controller_page_background_app_refresh_disabled_content',
  459. ).tr(),
  460. ),
  461. ElevatedButton(
  462. onPressed: () => openAppSettings(),
  463. child: const Text(
  464. 'backup_controller_page_background_app_refresh_enable_button_text',
  465. style: TextStyle(
  466. fontWeight: FontWeight.bold,
  467. fontSize: 12,
  468. ),
  469. ).tr(),
  470. ),
  471. ],
  472. ),
  473. );
  474. }
  475. Widget buildSelectedAlbumName() {
  476. var text = "backup_controller_page_backup_selected".tr();
  477. var albums = ref.watch(backupProvider).selectedBackupAlbums;
  478. if (albums.isNotEmpty) {
  479. for (var album in albums) {
  480. if (album.name == "Recent" || album.name == "Recents") {
  481. text += "${album.name} (${'backup_all'.tr()}), ";
  482. } else {
  483. text += "${album.name}, ";
  484. }
  485. }
  486. return Padding(
  487. padding: const EdgeInsets.only(top: 8.0),
  488. child: Text(
  489. text.trim().substring(0, text.length - 2),
  490. style: TextStyle(
  491. color: Theme.of(context).primaryColor,
  492. fontSize: 12,
  493. fontWeight: FontWeight.bold,
  494. ),
  495. ),
  496. );
  497. } else {
  498. return Padding(
  499. padding: const EdgeInsets.only(top: 8.0),
  500. child: Text(
  501. "backup_controller_page_none_selected".tr(),
  502. style: TextStyle(
  503. color: Theme.of(context).primaryColor,
  504. fontSize: 12,
  505. fontWeight: FontWeight.bold,
  506. ),
  507. ),
  508. );
  509. }
  510. }
  511. Widget buildExcludedAlbumName() {
  512. var text = "backup_controller_page_excluded".tr();
  513. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  514. if (albums.isNotEmpty) {
  515. for (var album in albums) {
  516. text += "${album.name}, ";
  517. }
  518. return Padding(
  519. padding: const EdgeInsets.only(top: 8.0),
  520. child: Text(
  521. text.trim().substring(0, text.length - 2),
  522. style: TextStyle(
  523. color: Colors.red[300],
  524. fontSize: 12,
  525. fontWeight: FontWeight.bold,
  526. ),
  527. ),
  528. );
  529. } else {
  530. return const SizedBox();
  531. }
  532. }
  533. buildFolderSelectionTile() {
  534. return Card(
  535. shape: RoundedRectangleBorder(
  536. borderRadius: BorderRadius.circular(20),
  537. side: BorderSide(
  538. color: isDarkMode
  539. ? const Color.fromARGB(255, 56, 56, 56)
  540. : Colors.black12,
  541. width: 1,
  542. ),
  543. ),
  544. elevation: 0,
  545. borderOnForeground: false,
  546. child: ListTile(
  547. minVerticalPadding: 15,
  548. title: const Text(
  549. "backup_controller_page_albums",
  550. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
  551. ).tr(),
  552. subtitle: Padding(
  553. padding: const EdgeInsets.only(top: 8.0),
  554. child: Column(
  555. crossAxisAlignment: CrossAxisAlignment.start,
  556. children: [
  557. const Text(
  558. "backup_controller_page_to_backup",
  559. style: TextStyle(fontSize: 12),
  560. ).tr(),
  561. buildSelectedAlbumName(),
  562. buildExcludedAlbumName(),
  563. ],
  564. ),
  565. ),
  566. trailing: ElevatedButton(
  567. onPressed: () {
  568. AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
  569. },
  570. child: const Text(
  571. "backup_controller_page_select",
  572. style: TextStyle(
  573. fontWeight: FontWeight.bold,
  574. fontSize: 12,
  575. ),
  576. ).tr(),
  577. ),
  578. ),
  579. );
  580. }
  581. void startBackup() {
  582. ref.watch(errorBackupListProvider.notifier).empty();
  583. if (ref.watch(backupProvider).backupProgress !=
  584. BackUpProgressEnum.inBackground) {
  585. ref.watch(backupProvider.notifier).startBackupProcess();
  586. }
  587. }
  588. Widget buildBackupButton() {
  589. return Padding(
  590. padding: const EdgeInsets.only(
  591. top: 24,
  592. ),
  593. child: Container(
  594. child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
  595. backupState.backupProgress ==
  596. BackUpProgressEnum.manualInProgress
  597. ? ElevatedButton(
  598. style: ElevatedButton.styleFrom(
  599. foregroundColor: Colors.grey[50],
  600. backgroundColor: Colors.red[300],
  601. // padding: const EdgeInsets.all(14),
  602. ),
  603. onPressed: () {
  604. if (backupState.backupProgress ==
  605. BackUpProgressEnum.manualInProgress) {
  606. ref.read(manualUploadProvider.notifier).cancelBackup();
  607. } else {
  608. ref.read(backupProvider.notifier).cancelBackup();
  609. }
  610. },
  611. child: const Text(
  612. "backup_controller_page_cancel",
  613. style: TextStyle(
  614. fontSize: 14,
  615. fontWeight: FontWeight.bold,
  616. ),
  617. ).tr(),
  618. )
  619. : ElevatedButton(
  620. onPressed: shouldBackup ? startBackup : null,
  621. child: const Text(
  622. "backup_controller_page_start_backup",
  623. style: TextStyle(
  624. fontSize: 14,
  625. fontWeight: FontWeight.bold,
  626. ),
  627. ).tr(),
  628. ),
  629. ),
  630. );
  631. }
  632. buildBackgroundBackupInfo() {
  633. return const ListTile(
  634. leading: Icon(Icons.info_outline_rounded),
  635. title: Text(
  636. "Background backup is currently running, cannot start manual backup",
  637. ),
  638. );
  639. }
  640. return Scaffold(
  641. appBar: AppBar(
  642. elevation: 0,
  643. title: const Text(
  644. "backup_controller_page_backup",
  645. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  646. ).tr(),
  647. leading: IconButton(
  648. onPressed: () {
  649. ref.watch(websocketProvider.notifier).listenUploadEvent();
  650. AutoRouter.of(context).pop(true);
  651. },
  652. splashRadius: 24,
  653. icon: const Icon(
  654. Icons.arrow_back_ios_rounded,
  655. ),
  656. ),
  657. ),
  658. body: Padding(
  659. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
  660. child: ListView(
  661. // crossAxisAlignment: CrossAxisAlignment.start,
  662. children: [
  663. Padding(
  664. padding: const EdgeInsets.all(8.0),
  665. child: const Text(
  666. "backup_controller_page_info",
  667. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  668. ).tr(),
  669. ),
  670. buildFolderSelectionTile(),
  671. BackupInfoCard(
  672. title: "backup_controller_page_total".tr(),
  673. subtitle: "backup_controller_page_total_sub".tr(),
  674. info: ref.watch(backupProvider).availableAlbums.isEmpty
  675. ? "..."
  676. : "${backupState.allUniqueAssets.length}",
  677. ),
  678. BackupInfoCard(
  679. title: "backup_controller_page_backup".tr(),
  680. subtitle: "backup_controller_page_backup_sub".tr(),
  681. info: ref.watch(backupProvider).availableAlbums.isEmpty
  682. ? "..."
  683. : "${backupState.selectedAlbumsBackupAssetsIds.length}",
  684. ),
  685. BackupInfoCard(
  686. title: "backup_controller_page_remainder".tr(),
  687. subtitle: "backup_controller_page_remainder_sub".tr(),
  688. info: ref.watch(backupProvider).availableAlbums.isEmpty
  689. ? "..."
  690. : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  691. ),
  692. const Divider(),
  693. buildAutoBackupController(),
  694. const Divider(),
  695. AnimatedSwitcher(
  696. duration: const Duration(milliseconds: 500),
  697. child: Platform.isIOS
  698. ? (appRefreshDisabled
  699. ? buildBackgroundAppRefreshWarning()
  700. : buildBackgroundBackupController())
  701. : buildBackgroundBackupController(),
  702. ),
  703. if (showBackupFix) const Divider(),
  704. if (showBackupFix) buildCheckCorruptBackups(),
  705. const Divider(),
  706. const Divider(),
  707. const CurrentUploadingAssetInfoBox(),
  708. if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
  709. buildBackupButton(),
  710. ],
  711. ),
  712. ),
  713. );
  714. }
  715. }