backup_controller_page.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  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/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. final checkInProgress = useState(false);
  49. useEffect(
  50. () {
  51. if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
  52. backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
  53. ref.watch(backupProvider.notifier).getBackupInfo();
  54. }
  55. // Update the background settings information just to make sure we
  56. // have the latest, since the platform channel will not update
  57. // automatically
  58. if (Platform.isIOS) {
  59. ref.watch(iOSBackgroundSettingsProvider.notifier).refresh();
  60. }
  61. ref
  62. .watch(websocketProvider.notifier)
  63. .stopListenToEvent('on_upload_success');
  64. return null;
  65. },
  66. [],
  67. );
  68. Future<void> performDeletion(List<Asset> assets) async {
  69. try {
  70. checkInProgress.value = true;
  71. ImmichToast.show(
  72. context: context,
  73. msg: "Deleting ${assets.length} assets on the server...",
  74. );
  75. await ref
  76. .read(assetProvider.notifier)
  77. .deleteAssets(assets, force: true);
  78. ImmichToast.show(
  79. context: context,
  80. msg: "Deleted ${assets.length} assets on the server. "
  81. "You can now start a manual backup",
  82. toastType: ToastType.success,
  83. );
  84. } finally {
  85. checkInProgress.value = false;
  86. }
  87. }
  88. void performBackupCheck() async {
  89. try {
  90. checkInProgress.value = true;
  91. if (backupState.allUniqueAssets.length >
  92. backupState.selectedAlbumsBackupAssetsIds.length) {
  93. ImmichToast.show(
  94. context: context,
  95. msg: "Backup all assets before starting this check!",
  96. toastType: ToastType.error,
  97. );
  98. return;
  99. }
  100. final connection = await Connectivity().checkConnectivity();
  101. if (connection != ConnectivityResult.wifi) {
  102. ImmichToast.show(
  103. context: context,
  104. msg: "Make sure to be connected to unmetered Wi-Fi",
  105. toastType: ToastType.error,
  106. );
  107. return;
  108. }
  109. WakelockPlus.enable();
  110. const limit = 100;
  111. final toDelete = await ref
  112. .read(backupVerificationServiceProvider)
  113. .findWronglyBackedUpAssets(limit: limit);
  114. if (toDelete.isEmpty) {
  115. ImmichToast.show(
  116. context: context,
  117. msg: "Did not find any corrupt asset backups!",
  118. toastType: ToastType.success,
  119. );
  120. } else {
  121. await showDialog(
  122. context: context,
  123. builder: (context) => ConfirmDialog(
  124. onOk: () => performDeletion(toDelete),
  125. title: "Corrupt backups!",
  126. ok: "Delete",
  127. content:
  128. "Found ${toDelete.length} (max $limit at once) corrupt asset backups. "
  129. "Run the check again to find more.\n"
  130. "Do you want to delete the corrupt asset backups now?",
  131. ),
  132. );
  133. }
  134. } finally {
  135. WakelockPlus.disable();
  136. checkInProgress.value = false;
  137. }
  138. }
  139. Widget buildCheckCorruptBackups() {
  140. return ListTile(
  141. leading: Icon(
  142. Icons.warning_rounded,
  143. color: context.primaryColor,
  144. ),
  145. title: const Text(
  146. "Check for corrupt asset backups",
  147. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  148. ),
  149. isThreeLine: true,
  150. subtitle: Column(
  151. crossAxisAlignment: CrossAxisAlignment.start,
  152. children: [
  153. const Text("Run this check only over Wi-Fi and once all assets "
  154. "have been backed-up. The procedure might take a few minutes."),
  155. ElevatedButton(
  156. onPressed: checkInProgress.value ? null : performBackupCheck,
  157. child: checkInProgress.value
  158. ? const CircularProgressIndicator()
  159. : const Text("Perform check"),
  160. ),
  161. ],
  162. ),
  163. );
  164. }
  165. ListTile buildAutoBackupController() {
  166. final isAutoBackup = backupState.autoBackup;
  167. final backUpOption = isAutoBackup
  168. ? "backup_controller_page_status_on".tr()
  169. : "backup_controller_page_status_off".tr();
  170. final backupBtnText = isAutoBackup
  171. ? "backup_controller_page_turn_off".tr()
  172. : "backup_controller_page_turn_on".tr();
  173. return ListTile(
  174. isThreeLine: true,
  175. leading: isAutoBackup
  176. ? Icon(
  177. Icons.cloud_done_rounded,
  178. color: context.primaryColor,
  179. )
  180. : const Icon(Icons.cloud_off_rounded),
  181. title: Text(
  182. backUpOption,
  183. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  184. ),
  185. subtitle: Padding(
  186. padding: const EdgeInsets.symmetric(vertical: 8.0),
  187. child: Column(
  188. crossAxisAlignment: CrossAxisAlignment.start,
  189. children: [
  190. if (!isAutoBackup)
  191. const Text(
  192. "backup_controller_page_desc_backup",
  193. style: TextStyle(fontSize: 14),
  194. ).tr(),
  195. Padding(
  196. padding: const EdgeInsets.only(top: 8.0),
  197. child: ElevatedButton(
  198. onPressed: () => ref
  199. .read(backupProvider.notifier)
  200. .setAutoBackup(!isAutoBackup),
  201. child: Text(
  202. backupBtnText,
  203. style: const TextStyle(
  204. fontWeight: FontWeight.bold,
  205. fontSize: 12,
  206. ),
  207. ),
  208. ),
  209. ),
  210. ],
  211. ),
  212. ),
  213. );
  214. }
  215. void showErrorToUser(String msg) {
  216. final snackBar = SnackBar(
  217. content: Text(
  218. msg.tr(),
  219. ),
  220. backgroundColor: Colors.red,
  221. );
  222. ScaffoldMessenger.of(context).showSnackBar(snackBar);
  223. }
  224. void showBatteryOptimizationInfoToUser() {
  225. showDialog<void>(
  226. context: context,
  227. barrierDismissible: false,
  228. builder: (BuildContext context) {
  229. return AlertDialog(
  230. title: const Text(
  231. 'backup_controller_page_background_battery_info_title',
  232. ).tr(),
  233. content: SingleChildScrollView(
  234. child: const Text(
  235. 'backup_controller_page_background_battery_info_message',
  236. ).tr(),
  237. ),
  238. actions: [
  239. ElevatedButton(
  240. onPressed: () => launchUrl(
  241. Uri.parse('https://dontkillmyapp.com'),
  242. mode: LaunchMode.externalApplication,
  243. ),
  244. child: const Text(
  245. "backup_controller_page_background_battery_info_link",
  246. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  247. ).tr(),
  248. ),
  249. ElevatedButton(
  250. child: const Text(
  251. 'backup_controller_page_background_battery_info_ok',
  252. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  253. ).tr(),
  254. onPressed: () {
  255. context.pop();
  256. },
  257. ),
  258. ],
  259. );
  260. },
  261. );
  262. }
  263. Widget buildBackgroundBackupController() {
  264. final bool isBackgroundEnabled = backupState.backgroundBackup;
  265. final bool isWifiRequired = backupState.backupRequireWifi;
  266. final bool isChargingRequired = backupState.backupRequireCharging;
  267. final Color activeColor = context.primaryColor;
  268. String formatBackupDelaySliderValue(double v) {
  269. if (v == 0.0) {
  270. return 'setting_notifications_notify_seconds'.tr(args: const ['5']);
  271. } else if (v == 1.0) {
  272. return 'setting_notifications_notify_seconds'.tr(args: const ['30']);
  273. } else if (v == 2.0) {
  274. return 'setting_notifications_notify_minutes'.tr(args: const ['2']);
  275. } else {
  276. return 'setting_notifications_notify_minutes'.tr(args: const ['10']);
  277. }
  278. }
  279. int backupDelayToMilliseconds(double v) {
  280. if (v == 0.0) {
  281. return 5000;
  282. } else if (v == 1.0) {
  283. return 30000;
  284. } else if (v == 2.0) {
  285. return 120000;
  286. } else {
  287. return 600000;
  288. }
  289. }
  290. double backupDelayToSliderValue(int ms) {
  291. if (ms == 5000) {
  292. return 0.0;
  293. } else if (ms == 30000) {
  294. return 1.0;
  295. } else if (ms == 120000) {
  296. return 2.0;
  297. } else {
  298. return 3.0;
  299. }
  300. }
  301. final triggerDelay =
  302. useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
  303. return Column(
  304. children: [
  305. ListTile(
  306. isThreeLine: true,
  307. leading: isBackgroundEnabled
  308. ? Icon(
  309. Icons.cloud_sync_rounded,
  310. color: activeColor,
  311. )
  312. : const Icon(Icons.cloud_sync_rounded),
  313. title: Text(
  314. isBackgroundEnabled
  315. ? "backup_controller_page_background_is_on"
  316. : "backup_controller_page_background_is_off",
  317. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  318. ).tr(),
  319. subtitle: Column(
  320. crossAxisAlignment: CrossAxisAlignment.start,
  321. children: [
  322. if (!isBackgroundEnabled)
  323. Padding(
  324. padding: const EdgeInsets.symmetric(vertical: 8.0),
  325. child: const Text(
  326. "backup_controller_page_background_description",
  327. ).tr(),
  328. ),
  329. if (isBackgroundEnabled && Platform.isAndroid)
  330. SwitchListTile.adaptive(
  331. title: const Text("backup_controller_page_background_wifi")
  332. .tr(),
  333. secondary: Icon(
  334. Icons.wifi,
  335. color: isWifiRequired ? activeColor : null,
  336. ),
  337. dense: true,
  338. activeColor: activeColor,
  339. value: isWifiRequired,
  340. onChanged: (isChecked) => ref
  341. .read(backupProvider.notifier)
  342. .configureBackgroundBackup(
  343. requireWifi: isChecked,
  344. onError: showErrorToUser,
  345. onBatteryInfo: showBatteryOptimizationInfoToUser,
  346. ),
  347. ),
  348. if (isBackgroundEnabled)
  349. SwitchListTile.adaptive(
  350. title:
  351. const Text("backup_controller_page_background_charging")
  352. .tr(),
  353. secondary: Icon(
  354. Icons.charging_station,
  355. color: isChargingRequired ? activeColor : null,
  356. ),
  357. dense: true,
  358. activeColor: activeColor,
  359. value: isChargingRequired,
  360. onChanged: (isChecked) => ref
  361. .read(backupProvider.notifier)
  362. .configureBackgroundBackup(
  363. requireCharging: isChecked,
  364. onError: showErrorToUser,
  365. onBatteryInfo: showBatteryOptimizationInfoToUser,
  366. ),
  367. ),
  368. if (isBackgroundEnabled && Platform.isAndroid)
  369. ListTile(
  370. isThreeLine: false,
  371. dense: true,
  372. title: const Text(
  373. 'backup_controller_page_background_delay',
  374. style: TextStyle(
  375. fontWeight: FontWeight.bold,
  376. ),
  377. ).tr(
  378. args: [formatBackupDelaySliderValue(triggerDelay.value)],
  379. ),
  380. subtitle: Slider(
  381. value: triggerDelay.value,
  382. onChanged: (double v) => triggerDelay.value = v,
  383. onChangeEnd: (double v) => ref
  384. .read(backupProvider.notifier)
  385. .configureBackgroundBackup(
  386. triggerDelay: backupDelayToMilliseconds(v),
  387. onError: showErrorToUser,
  388. onBatteryInfo: showBatteryOptimizationInfoToUser,
  389. ),
  390. max: 3.0,
  391. divisions: 3,
  392. label: formatBackupDelaySliderValue(triggerDelay.value),
  393. activeColor: context.primaryColor,
  394. ),
  395. ),
  396. ElevatedButton(
  397. onPressed: () => ref
  398. .read(backupProvider.notifier)
  399. .configureBackgroundBackup(
  400. enabled: !isBackgroundEnabled,
  401. onError: showErrorToUser,
  402. onBatteryInfo: showBatteryOptimizationInfoToUser,
  403. ),
  404. child: Text(
  405. isBackgroundEnabled
  406. ? "backup_controller_page_background_turn_off"
  407. : "backup_controller_page_background_turn_on",
  408. style: const TextStyle(
  409. fontWeight: FontWeight.bold,
  410. fontSize: 12,
  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: TextStyle(
  490. color: context.primaryColor,
  491. fontSize: 12,
  492. fontWeight: FontWeight.bold,
  493. ),
  494. ),
  495. );
  496. } else {
  497. return Padding(
  498. padding: const EdgeInsets.only(top: 8.0),
  499. child: Text(
  500. "backup_controller_page_none_selected".tr(),
  501. style: TextStyle(
  502. color: context.primaryColor,
  503. fontSize: 12,
  504. fontWeight: FontWeight.bold,
  505. ),
  506. ),
  507. );
  508. }
  509. }
  510. Widget buildExcludedAlbumName() {
  511. var text = "backup_controller_page_excluded".tr();
  512. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  513. if (albums.isNotEmpty) {
  514. for (var album in albums) {
  515. text += "${album.name}, ";
  516. }
  517. return Padding(
  518. padding: const EdgeInsets.only(top: 8.0),
  519. child: Text(
  520. text.trim().substring(0, text.length - 2),
  521. style: TextStyle(
  522. color: Colors.red[300],
  523. fontSize: 12,
  524. fontWeight: FontWeight.bold,
  525. ),
  526. ),
  527. );
  528. } else {
  529. return const SizedBox();
  530. }
  531. }
  532. buildFolderSelectionTile() {
  533. return Card(
  534. shape: RoundedRectangleBorder(
  535. borderRadius: BorderRadius.circular(20),
  536. side: BorderSide(
  537. color: context.isDarkTheme
  538. ? const Color.fromARGB(255, 56, 56, 56)
  539. : Colors.black12,
  540. width: 1,
  541. ),
  542. ),
  543. elevation: 0,
  544. borderOnForeground: false,
  545. child: ListTile(
  546. minVerticalPadding: 15,
  547. title: const Text(
  548. "backup_controller_page_albums",
  549. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
  550. ).tr(),
  551. subtitle: Padding(
  552. padding: const EdgeInsets.only(top: 8.0),
  553. child: Column(
  554. crossAxisAlignment: CrossAxisAlignment.start,
  555. children: [
  556. const Text(
  557. "backup_controller_page_to_backup",
  558. style: TextStyle(fontSize: 12),
  559. ).tr(),
  560. buildSelectedAlbumName(),
  561. buildExcludedAlbumName(),
  562. ],
  563. ),
  564. ),
  565. trailing: ElevatedButton(
  566. onPressed: () {
  567. context.autoPush(const BackupAlbumSelectionRoute());
  568. },
  569. child: const Text(
  570. "backup_controller_page_select",
  571. style: TextStyle(
  572. fontWeight: FontWeight.bold,
  573. fontSize: 12,
  574. ),
  575. ).tr(),
  576. ),
  577. ),
  578. );
  579. }
  580. void startBackup() {
  581. ref.watch(errorBackupListProvider.notifier).empty();
  582. if (ref.watch(backupProvider).backupProgress !=
  583. BackUpProgressEnum.inBackground) {
  584. ref.watch(backupProvider.notifier).startBackupProcess();
  585. }
  586. }
  587. Widget buildBackupButton() {
  588. return Padding(
  589. padding: const EdgeInsets.only(
  590. top: 24,
  591. ),
  592. child: Container(
  593. child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
  594. backupState.backupProgress ==
  595. BackUpProgressEnum.manualInProgress
  596. ? ElevatedButton(
  597. style: ElevatedButton.styleFrom(
  598. foregroundColor: Colors.grey[50],
  599. backgroundColor: Colors.red[300],
  600. // padding: const EdgeInsets.all(14),
  601. ),
  602. onPressed: () {
  603. if (backupState.backupProgress ==
  604. BackUpProgressEnum.manualInProgress) {
  605. ref.read(manualUploadProvider.notifier).cancelBackup();
  606. } else {
  607. ref.read(backupProvider.notifier).cancelBackup();
  608. }
  609. },
  610. child: const Text(
  611. "backup_controller_page_cancel",
  612. style: TextStyle(
  613. fontSize: 14,
  614. fontWeight: FontWeight.bold,
  615. ),
  616. ).tr(),
  617. )
  618. : ElevatedButton(
  619. onPressed: shouldBackup ? startBackup : null,
  620. child: const Text(
  621. "backup_controller_page_start_backup",
  622. style: TextStyle(
  623. fontSize: 14,
  624. fontWeight: FontWeight.bold,
  625. ),
  626. ).tr(),
  627. ),
  628. ),
  629. );
  630. }
  631. buildBackgroundBackupInfo() {
  632. return const ListTile(
  633. leading: Icon(Icons.info_outline_rounded),
  634. title: Text(
  635. "Background backup is currently running, cannot start manual backup",
  636. ),
  637. );
  638. }
  639. return Scaffold(
  640. appBar: AppBar(
  641. elevation: 0,
  642. title: const Text(
  643. "backup_controller_page_backup",
  644. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  645. ).tr(),
  646. leading: IconButton(
  647. onPressed: () {
  648. ref.watch(websocketProvider.notifier).listenUploadEvent();
  649. context.autoPop(true);
  650. },
  651. splashRadius: 24,
  652. icon: const Icon(
  653. Icons.arrow_back_ios_rounded,
  654. ),
  655. ),
  656. ),
  657. body: Padding(
  658. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
  659. child: ListView(
  660. // crossAxisAlignment: CrossAxisAlignment.start,
  661. children: [
  662. Padding(
  663. padding: const EdgeInsets.all(8.0),
  664. child: const Text(
  665. "backup_controller_page_info",
  666. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  667. ).tr(),
  668. ),
  669. buildFolderSelectionTile(),
  670. BackupInfoCard(
  671. title: "backup_controller_page_total".tr(),
  672. subtitle: "backup_controller_page_total_sub".tr(),
  673. info: ref.watch(backupProvider).availableAlbums.isEmpty
  674. ? "..."
  675. : "${backupState.allUniqueAssets.length}",
  676. ),
  677. BackupInfoCard(
  678. title: "backup_controller_page_backup".tr(),
  679. subtitle: "backup_controller_page_backup_sub".tr(),
  680. info: ref.watch(backupProvider).availableAlbums.isEmpty
  681. ? "..."
  682. : "${backupState.selectedAlbumsBackupAssetsIds.length}",
  683. ),
  684. BackupInfoCard(
  685. title: "backup_controller_page_remainder".tr(),
  686. subtitle: "backup_controller_page_remainder_sub".tr(),
  687. info: ref.watch(backupProvider).availableAlbums.isEmpty
  688. ? "..."
  689. : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  690. ),
  691. const Divider(),
  692. buildAutoBackupController(),
  693. const Divider(),
  694. AnimatedSwitcher(
  695. duration: const Duration(milliseconds: 500),
  696. child: Platform.isIOS
  697. ? (appRefreshDisabled
  698. ? buildBackgroundAppRefreshWarning()
  699. : buildBackgroundBackupController())
  700. : buildBackgroundBackupController(),
  701. ),
  702. if (showBackupFix) const Divider(),
  703. if (showBackupFix) buildCheckCorruptBackups(),
  704. const Divider(),
  705. const Divider(),
  706. const CurrentUploadingAssetInfoBox(),
  707. if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
  708. buildBackupButton(),
  709. ],
  710. ),
  711. ),
  712. );
  713. }
  714. }