backup_controller_page.dart 28 KB

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