backup_controller_page.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. import 'dart:io';
  2. import 'package:connectivity_plus/connectivity_plus.dart';
  3. import 'package:easy_localization/easy_localization.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_hooks/flutter_hooks.dart';
  6. import 'package:hooks_riverpod/hooks_riverpod.dart';
  7. import 'package:immich_mobile/extensions/build_context_extensions.dart';
  8. import 'package:immich_mobile/modules/album/providers/album.provider.dart';
  9. import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
  10. import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
  11. import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
  12. import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
  13. import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
  14. import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
  15. import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
  16. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  17. import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
  18. import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
  19. import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
  20. import 'package:immich_mobile/routing/router.dart';
  21. import 'package:immich_mobile/shared/models/asset.dart';
  22. import 'package:immich_mobile/shared/providers/asset.provider.dart';
  23. import 'package:immich_mobile/shared/providers/websocket.provider.dart';
  24. import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
  25. import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
  26. import 'package:immich_mobile/shared/ui/immich_toast.dart';
  27. import 'package:permission_handler/permission_handler.dart';
  28. import 'package:url_launcher/url_launcher.dart';
  29. import 'package:wakelock_plus/wakelock_plus.dart';
  30. class BackupControllerPage extends HookConsumerWidget {
  31. const BackupControllerPage({Key? key}) : super(key: key);
  32. @override
  33. Widget build(BuildContext context, WidgetRef ref) {
  34. BackUpState backupState = ref.watch(backupProvider);
  35. final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
  36. final settingsService = ref.watch(appSettingsServiceProvider);
  37. final showBackupFix = Platform.isAndroid &&
  38. settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting);
  39. final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
  40. final appRefreshDisabled =
  41. Platform.isIOS && settings?.appRefreshEnabled != true;
  42. bool hasExclusiveAccess =
  43. backupState.backupProgress != BackUpProgressEnum.inBackground;
  44. bool shouldBackup = backupState.allUniqueAssets.length -
  45. backupState.selectedAlbumsBackupAssetsIds.length ==
  46. 0 ||
  47. !hasExclusiveAccess
  48. ? false
  49. : true;
  50. final checkInProgress = useState(false);
  51. useEffect(
  52. () {
  53. if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
  54. backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
  55. ref.watch(backupProvider.notifier).getBackupInfo();
  56. }
  57. // Update the background settings information just to make sure we
  58. // have the latest, since the platform channel will not update
  59. // automatically
  60. if (Platform.isIOS) {
  61. ref.watch(iOSBackgroundSettingsProvider.notifier).refresh();
  62. }
  63. ref
  64. .watch(websocketProvider.notifier)
  65. .stopListenToEvent('on_upload_success');
  66. return null;
  67. },
  68. [],
  69. );
  70. Future<void> performDeletion(List<Asset> assets) async {
  71. try {
  72. checkInProgress.value = true;
  73. ImmichToast.show(
  74. context: context,
  75. msg: "Deleting ${assets.length} assets on the server...",
  76. );
  77. await ref
  78. .read(assetProvider.notifier)
  79. .deleteAssets(assets, force: true);
  80. ImmichToast.show(
  81. context: context,
  82. msg: "Deleted ${assets.length} assets on the server. "
  83. "You can now start a manual backup",
  84. toastType: ToastType.success,
  85. );
  86. } finally {
  87. checkInProgress.value = false;
  88. }
  89. }
  90. void performBackupCheck() async {
  91. try {
  92. checkInProgress.value = true;
  93. if (backupState.allUniqueAssets.length >
  94. backupState.selectedAlbumsBackupAssetsIds.length) {
  95. ImmichToast.show(
  96. context: context,
  97. msg: "Backup all assets before starting this check!",
  98. toastType: ToastType.error,
  99. );
  100. return;
  101. }
  102. final connection = await Connectivity().checkConnectivity();
  103. if (connection != ConnectivityResult.wifi) {
  104. ImmichToast.show(
  105. context: context,
  106. msg: "Make sure to be connected to unmetered Wi-Fi",
  107. toastType: ToastType.error,
  108. );
  109. return;
  110. }
  111. WakelockPlus.enable();
  112. const limit = 100;
  113. final toDelete = await ref
  114. .read(backupVerificationServiceProvider)
  115. .findWronglyBackedUpAssets(limit: limit);
  116. if (toDelete.isEmpty) {
  117. ImmichToast.show(
  118. context: context,
  119. msg: "Did not find any corrupt asset backups!",
  120. toastType: ToastType.success,
  121. );
  122. } else {
  123. await showDialog(
  124. context: context,
  125. builder: (context) => ConfirmDialog(
  126. onOk: () => performDeletion(toDelete),
  127. title: "Corrupt backups!",
  128. ok: "Delete",
  129. content:
  130. "Found ${toDelete.length} (max $limit at once) corrupt asset backups. "
  131. "Run the check again to find more.\n"
  132. "Do you want to delete the corrupt asset backups now?",
  133. ),
  134. );
  135. }
  136. } finally {
  137. WakelockPlus.disable();
  138. checkInProgress.value = false;
  139. }
  140. }
  141. Widget buildCheckCorruptBackups() {
  142. return ListTile(
  143. leading: Icon(
  144. Icons.warning_rounded,
  145. color: context.primaryColor,
  146. ),
  147. title: const Text(
  148. "Check for corrupt asset backups",
  149. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  150. ),
  151. isThreeLine: true,
  152. subtitle: Column(
  153. crossAxisAlignment: CrossAxisAlignment.start,
  154. children: [
  155. const Text("Run this check only over Wi-Fi and once all assets "
  156. "have been backed-up. The procedure might take a few minutes."),
  157. ElevatedButton(
  158. onPressed: checkInProgress.value ? null : performBackupCheck,
  159. child: checkInProgress.value
  160. ? const CircularProgressIndicator()
  161. : const Text("Perform check"),
  162. ),
  163. ],
  164. ),
  165. );
  166. }
  167. ListTile buildAutoBackupController() {
  168. final isAutoBackup = backupState.autoBackup;
  169. final backUpOption = isAutoBackup
  170. ? "backup_controller_page_status_on".tr()
  171. : "backup_controller_page_status_off".tr();
  172. final backupBtnText = isAutoBackup
  173. ? "backup_controller_page_turn_off".tr()
  174. : "backup_controller_page_turn_on".tr();
  175. return ListTile(
  176. isThreeLine: true,
  177. leading: isAutoBackup
  178. ? Icon(
  179. Icons.cloud_done_rounded,
  180. color: context.primaryColor,
  181. )
  182. : const Icon(Icons.cloud_off_rounded),
  183. title: Text(
  184. backUpOption,
  185. style: context.textTheme.titleSmall,
  186. ),
  187. subtitle: Padding(
  188. padding: const EdgeInsets.symmetric(vertical: 8.0),
  189. child: Column(
  190. crossAxisAlignment: CrossAxisAlignment.start,
  191. children: [
  192. if (!isAutoBackup)
  193. const Text(
  194. "backup_controller_page_desc_backup",
  195. style: TextStyle(fontSize: 14),
  196. ).tr(),
  197. Padding(
  198. padding: const EdgeInsets.only(top: 8.0),
  199. child: ElevatedButton(
  200. onPressed: () => ref
  201. .read(backupProvider.notifier)
  202. .setAutoBackup(!isAutoBackup),
  203. child: Text(
  204. backupBtnText,
  205. style: context.textTheme.labelLarge?.copyWith(
  206. color: context.isDarkTheme ? Colors.black : Colors.white,
  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. 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 = 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: context.textTheme.titleSmall,
  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: 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: context.textTheme.labelLarge?.copyWith(
  410. color: context.isDarkTheme ? Colors.black : Colors.white,
  411. ),
  412. ).tr(),
  413. ),
  414. ],
  415. ),
  416. ),
  417. if (isBackgroundEnabled && Platform.isIOS)
  418. FutureBuilder(
  419. future: ref
  420. .read(backgroundServiceProvider)
  421. .getIOSBackgroundAppRefreshEnabled(),
  422. builder: (context, snapshot) {
  423. final enabled = snapshot.data;
  424. // If it's not enabled, show them some kind of alert that says
  425. // background refresh is not enabled
  426. if (enabled != null && !enabled) {}
  427. // If it's enabled, no need to bother them
  428. return Container();
  429. },
  430. ),
  431. if (Platform.isIOS && isBackgroundEnabled && settings != null)
  432. IosDebugInfoTile(
  433. settings: settings,
  434. ),
  435. ],
  436. );
  437. }
  438. Widget buildBackgroundAppRefreshWarning() {
  439. return ListTile(
  440. isThreeLine: true,
  441. leading: const Icon(
  442. Icons.task_outlined,
  443. ),
  444. title: const Text(
  445. 'backup_controller_page_background_app_refresh_disabled_title',
  446. style: TextStyle(
  447. fontWeight: FontWeight.bold,
  448. fontSize: 14,
  449. ),
  450. ).tr(),
  451. subtitle: Column(
  452. crossAxisAlignment: CrossAxisAlignment.start,
  453. children: [
  454. Padding(
  455. padding: const EdgeInsets.symmetric(vertical: 8.0),
  456. child: const Text(
  457. 'backup_controller_page_background_app_refresh_disabled_content',
  458. ).tr(),
  459. ),
  460. ElevatedButton(
  461. onPressed: () => openAppSettings(),
  462. child: const Text(
  463. 'backup_controller_page_background_app_refresh_enable_button_text',
  464. style: TextStyle(
  465. fontWeight: FontWeight.bold,
  466. fontSize: 12,
  467. ),
  468. ).tr(),
  469. ),
  470. ],
  471. ),
  472. );
  473. }
  474. Widget buildSelectedAlbumName() {
  475. var text = "backup_controller_page_backup_selected".tr();
  476. var albums = ref.watch(backupProvider).selectedBackupAlbums;
  477. if (albums.isNotEmpty) {
  478. for (var album in albums) {
  479. if (album.name == "Recent" || album.name == "Recents") {
  480. text += "${album.name} (${'backup_all'.tr()}), ";
  481. } else {
  482. text += "${album.name}, ";
  483. }
  484. }
  485. return Padding(
  486. padding: const EdgeInsets.only(top: 8.0),
  487. child: Text(
  488. text.trim().substring(0, text.length - 2),
  489. style: context.textTheme.labelLarge?.copyWith(
  490. color: context.primaryColor,
  491. ),
  492. ),
  493. );
  494. } else {
  495. return Padding(
  496. padding: const EdgeInsets.only(top: 8.0),
  497. child: Text(
  498. "backup_controller_page_none_selected".tr(),
  499. style: context.textTheme.labelLarge?.copyWith(
  500. color: context.primaryColor,
  501. ),
  502. ),
  503. );
  504. }
  505. }
  506. Widget buildExcludedAlbumName() {
  507. var text = "backup_controller_page_excluded".tr();
  508. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  509. if (albums.isNotEmpty) {
  510. for (var album in albums) {
  511. text += "${album.name}, ";
  512. }
  513. return Padding(
  514. padding: const EdgeInsets.only(top: 8.0),
  515. child: Text(
  516. text.trim().substring(0, text.length - 2),
  517. style: context.textTheme.labelLarge?.copyWith(
  518. color: Colors.red[300],
  519. ),
  520. ),
  521. );
  522. } else {
  523. return const SizedBox();
  524. }
  525. }
  526. buildFolderSelectionTile() {
  527. return Padding(
  528. padding: const EdgeInsets.only(top: 8.0),
  529. child: Card(
  530. shape: RoundedRectangleBorder(
  531. borderRadius: BorderRadius.circular(20),
  532. side: BorderSide(
  533. color: context.isDarkTheme
  534. ? const Color.fromARGB(255, 56, 56, 56)
  535. : Colors.black12,
  536. width: 1,
  537. ),
  538. ),
  539. elevation: 0,
  540. borderOnForeground: false,
  541. child: ListTile(
  542. minVerticalPadding: 18,
  543. title: Text(
  544. "backup_controller_page_albums",
  545. style: context.textTheme.titleMedium,
  546. ).tr(),
  547. subtitle: Padding(
  548. padding: const EdgeInsets.only(top: 8.0),
  549. child: Column(
  550. crossAxisAlignment: CrossAxisAlignment.start,
  551. children: [
  552. Text(
  553. "backup_controller_page_to_backup",
  554. style: context.textTheme.bodyMedium,
  555. ).tr(),
  556. buildSelectedAlbumName(),
  557. buildExcludedAlbumName(),
  558. ],
  559. ),
  560. ),
  561. trailing: ElevatedButton(
  562. onPressed: () async {
  563. await context.autoPush(const BackupAlbumSelectionRoute());
  564. // waited until returning from selection
  565. await ref
  566. .read(backupProvider.notifier)
  567. .backupAlbumSelectionDone();
  568. // waited until backup albums are stored in DB
  569. ref.read(albumProvider.notifier).getDeviceAlbums();
  570. },
  571. child: const Text(
  572. "backup_controller_page_select",
  573. style: TextStyle(
  574. fontWeight: FontWeight.bold,
  575. ),
  576. ).tr(),
  577. ),
  578. ),
  579. ),
  580. );
  581. }
  582. void startBackup() {
  583. ref.watch(errorBackupListProvider.notifier).empty();
  584. if (ref.watch(backupProvider).backupProgress !=
  585. BackUpProgressEnum.inBackground) {
  586. ref.watch(backupProvider.notifier).startBackupProcess();
  587. }
  588. }
  589. Widget buildBackupButton() {
  590. return Padding(
  591. padding: const EdgeInsets.only(
  592. top: 24,
  593. ),
  594. child: Container(
  595. child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
  596. backupState.backupProgress ==
  597. BackUpProgressEnum.manualInProgress
  598. ? ElevatedButton(
  599. style: ElevatedButton.styleFrom(
  600. foregroundColor: Colors.grey[50],
  601. backgroundColor: Colors.red[300],
  602. // padding: const EdgeInsets.all(14),
  603. ),
  604. onPressed: () {
  605. if (backupState.backupProgress ==
  606. BackUpProgressEnum.manualInProgress) {
  607. ref.read(manualUploadProvider.notifier).cancelBackup();
  608. } else {
  609. ref.read(backupProvider.notifier).cancelBackup();
  610. }
  611. },
  612. child: const Text(
  613. "backup_controller_page_cancel",
  614. style: TextStyle(
  615. fontSize: 14,
  616. fontWeight: FontWeight.bold,
  617. ),
  618. ).tr(),
  619. )
  620. : ElevatedButton(
  621. onPressed: shouldBackup ? startBackup : null,
  622. child: const Text(
  623. "backup_controller_page_start_backup",
  624. style: TextStyle(
  625. fontSize: 16,
  626. fontWeight: FontWeight.bold,
  627. ),
  628. ).tr(),
  629. ),
  630. ),
  631. );
  632. }
  633. buildBackgroundBackupInfo() {
  634. return const ListTile(
  635. leading: Icon(Icons.info_outline_rounded),
  636. title: Text(
  637. "Background backup is currently running, cannot start manual backup",
  638. ),
  639. );
  640. }
  641. return Scaffold(
  642. appBar: AppBar(
  643. elevation: 0,
  644. title: const Text(
  645. "backup_controller_page_backup",
  646. ).tr(),
  647. leading: IconButton(
  648. onPressed: () {
  649. ref.watch(websocketProvider.notifier).listenUploadEvent();
  650. context.autoPop(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: hasAnyAlbum
  663. ? [
  664. buildFolderSelectionTile(),
  665. BackupInfoCard(
  666. title: "backup_controller_page_total".tr(),
  667. subtitle: "backup_controller_page_total_sub".tr(),
  668. info: ref.watch(backupProvider).availableAlbums.isEmpty
  669. ? "..."
  670. : "${backupState.allUniqueAssets.length}",
  671. ),
  672. BackupInfoCard(
  673. title: "backup_controller_page_backup".tr(),
  674. subtitle: "backup_controller_page_backup_sub".tr(),
  675. info: ref.watch(backupProvider).availableAlbums.isEmpty
  676. ? "..."
  677. : "${backupState.selectedAlbumsBackupAssetsIds.length}",
  678. ),
  679. BackupInfoCard(
  680. title: "backup_controller_page_remainder".tr(),
  681. subtitle: "backup_controller_page_remainder_sub".tr(),
  682. info: ref.watch(backupProvider).availableAlbums.isEmpty
  683. ? "..."
  684. : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  685. ),
  686. const Divider(),
  687. buildAutoBackupController(),
  688. const Divider(),
  689. AnimatedSwitcher(
  690. duration: const Duration(milliseconds: 500),
  691. child: Platform.isIOS
  692. ? (appRefreshDisabled
  693. ? buildBackgroundAppRefreshWarning()
  694. : buildBackgroundBackupController())
  695. : buildBackgroundBackupController(),
  696. ),
  697. if (showBackupFix) const Divider(),
  698. if (showBackupFix) buildCheckCorruptBackups(),
  699. const Divider(),
  700. const CurrentUploadingAssetInfoBox(),
  701. if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
  702. buildBackupButton(),
  703. ]
  704. : [buildFolderSelectionTile()],
  705. ),
  706. ),
  707. );
  708. }
  709. }