backup_controller_page.dart 27 KB

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