backup_controller_page.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. import 'dart:io';
  2. import 'package:auto_route/auto_route.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/modules/backup/providers/error_backup_list.provider.dart';
  8. import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
  9. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  10. import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
  11. import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
  12. import 'package:immich_mobile/routing/router.dart';
  13. import 'package:immich_mobile/shared/providers/websocket.provider.dart';
  14. import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
  15. import 'package:percent_indicator/linear_percent_indicator.dart';
  16. import 'package:url_launcher/url_launcher.dart';
  17. class BackupControllerPage extends HookConsumerWidget {
  18. const BackupControllerPage({Key? key}) : super(key: key);
  19. @override
  20. Widget build(BuildContext context, WidgetRef ref) {
  21. BackUpState backupState = ref.watch(backupProvider);
  22. AuthenticationState authenticationState = ref.watch(authenticationProvider);
  23. bool hasExclusiveAccess =
  24. backupState.backupProgress != BackUpProgressEnum.inBackground;
  25. bool shouldBackup = backupState.allUniqueAssets.length -
  26. backupState.selectedAlbumsBackupAssetsIds.length ==
  27. 0 ||
  28. !hasExclusiveAccess
  29. ? false
  30. : true;
  31. useEffect(
  32. () {
  33. if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
  34. ref.watch(backupProvider.notifier).getBackupInfo();
  35. }
  36. ref
  37. .watch(websocketProvider.notifier)
  38. .stopListenToEvent('on_upload_success');
  39. return null;
  40. },
  41. [],
  42. );
  43. Widget _buildStorageInformation() {
  44. return ListTile(
  45. leading: Icon(
  46. Icons.storage_rounded,
  47. color: Theme.of(context).primaryColor,
  48. ),
  49. title: const Text(
  50. "backup_controller_page_server_storage",
  51. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  52. ).tr(),
  53. subtitle: Padding(
  54. padding: const EdgeInsets.only(top: 8.0),
  55. child: Column(
  56. crossAxisAlignment: CrossAxisAlignment.start,
  57. children: [
  58. Padding(
  59. padding: const EdgeInsets.only(top: 8.0),
  60. child: LinearPercentIndicator(
  61. padding:
  62. const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
  63. barRadius: const Radius.circular(2),
  64. lineHeight: 10.0,
  65. percent: backupState.serverInfo.diskUsagePercentage / 100.0,
  66. backgroundColor: Colors.grey,
  67. progressColor: Theme.of(context).primaryColor,
  68. ),
  69. ),
  70. Padding(
  71. padding: const EdgeInsets.only(top: 12.0),
  72. child: const Text('backup_controller_page_storage_format').tr(
  73. args: [
  74. backupState.serverInfo.diskUse,
  75. backupState.serverInfo.diskSize
  76. ],
  77. ),
  78. ),
  79. ],
  80. ),
  81. ),
  82. );
  83. }
  84. ListTile _buildAutoBackupController() {
  85. var backUpOption = authenticationState.deviceInfo.isAutoBackup
  86. ? "backup_controller_page_status_on".tr()
  87. : "backup_controller_page_status_off".tr();
  88. var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
  89. var backupBtnText = authenticationState.deviceInfo.isAutoBackup
  90. ? "backup_controller_page_turn_off".tr()
  91. : "backup_controller_page_turn_on".tr();
  92. return ListTile(
  93. isThreeLine: true,
  94. leading: isAutoBackup
  95. ? Icon(
  96. Icons.cloud_done_rounded,
  97. color: Theme.of(context).primaryColor,
  98. )
  99. : const Icon(Icons.cloud_off_rounded),
  100. title: Text(
  101. backUpOption,
  102. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  103. ),
  104. subtitle: Padding(
  105. padding: const EdgeInsets.symmetric(vertical: 8.0),
  106. child: Column(
  107. crossAxisAlignment: CrossAxisAlignment.start,
  108. children: [
  109. if (!isAutoBackup)
  110. const Text(
  111. "backup_controller_page_desc_backup",
  112. style: TextStyle(fontSize: 14),
  113. ).tr(),
  114. Padding(
  115. padding: const EdgeInsets.only(top: 8.0),
  116. child: ElevatedButton(
  117. onPressed: () {
  118. if (isAutoBackup) {
  119. ref
  120. .read(authenticationProvider.notifier)
  121. .setAutoBackup(false);
  122. } else {
  123. ref
  124. .read(authenticationProvider.notifier)
  125. .setAutoBackup(true);
  126. }
  127. },
  128. child: Text(
  129. backupBtnText,
  130. style: const TextStyle(
  131. fontWeight: FontWeight.bold,
  132. fontSize: 12,
  133. ),
  134. ),
  135. ),
  136. )
  137. ],
  138. ),
  139. ),
  140. );
  141. }
  142. void _showErrorToUser(String msg) {
  143. final snackBar = SnackBar(
  144. content: Text(
  145. msg.tr(),
  146. ),
  147. backgroundColor: Colors.red,
  148. );
  149. ScaffoldMessenger.of(context).showSnackBar(snackBar);
  150. }
  151. void _showBatteryOptimizationInfoToUser() {
  152. final buttonTextColor = Theme.of(context).primaryColor;
  153. showDialog<void>(
  154. context: context,
  155. barrierDismissible: false,
  156. builder: (BuildContext context) {
  157. return AlertDialog(
  158. title: const Text(
  159. 'backup_controller_page_background_battery_info_title',
  160. ).tr(),
  161. content: SingleChildScrollView(
  162. child: const Text(
  163. 'backup_controller_page_background_battery_info_message',
  164. ).tr(),
  165. ),
  166. actions: [
  167. TextButton(
  168. onPressed: () => launchUrl(
  169. Uri.parse('https://dontkillmyapp.com'),
  170. mode: LaunchMode.externalApplication),
  171. child: Text(
  172. "backup_controller_page_background_battery_info_link",
  173. style: TextStyle(color: buttonTextColor),
  174. ).tr(),
  175. ),
  176. TextButton(
  177. child: Text(
  178. 'backup_controller_page_background_battery_info_ok',
  179. style: TextStyle(color: buttonTextColor),
  180. ).tr(),
  181. onPressed: () {
  182. Navigator.of(context).pop();
  183. },
  184. ),
  185. ],
  186. );
  187. },
  188. );
  189. }
  190. ListTile _buildBackgroundBackupController() {
  191. final bool isBackgroundEnabled = backupState.backgroundBackup;
  192. final bool isWifiRequired = backupState.backupRequireWifi;
  193. final bool isChargingRequired = backupState.backupRequireCharging;
  194. final Color activeColor = Theme.of(context).primaryColor;
  195. return ListTile(
  196. isThreeLine: true,
  197. leading: isBackgroundEnabled
  198. ? Icon(
  199. Icons.cloud_sync_rounded,
  200. color: activeColor,
  201. )
  202. : const Icon(Icons.cloud_sync_rounded),
  203. title: Text(
  204. isBackgroundEnabled
  205. ? "backup_controller_page_background_is_on"
  206. : "backup_controller_page_background_is_off",
  207. style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  208. ).tr(),
  209. subtitle: Column(
  210. crossAxisAlignment: CrossAxisAlignment.start,
  211. children: [
  212. if (!isBackgroundEnabled)
  213. const Text("backup_controller_page_background_description").tr(),
  214. if (isBackgroundEnabled)
  215. SwitchListTile(
  216. title:
  217. const Text("backup_controller_page_background_wifi").tr(),
  218. secondary: Icon(
  219. Icons.wifi,
  220. color: isWifiRequired ? activeColor : null,
  221. ),
  222. dense: true,
  223. activeColor: activeColor,
  224. value: isWifiRequired,
  225. onChanged: hasExclusiveAccess
  226. ? (isChecked) => ref
  227. .read(backupProvider.notifier)
  228. .configureBackgroundBackup(
  229. requireWifi: isChecked,
  230. onError: _showErrorToUser,
  231. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  232. )
  233. : null,
  234. ),
  235. if (isBackgroundEnabled)
  236. SwitchListTile(
  237. title: const Text("backup_controller_page_background_charging")
  238. .tr(),
  239. secondary: Icon(
  240. Icons.charging_station,
  241. color: isChargingRequired ? activeColor : null,
  242. ),
  243. dense: true,
  244. activeColor: activeColor,
  245. value: isChargingRequired,
  246. onChanged: hasExclusiveAccess
  247. ? (isChecked) => ref
  248. .read(backupProvider.notifier)
  249. .configureBackgroundBackup(
  250. requireCharging: isChecked,
  251. onError: _showErrorToUser,
  252. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  253. )
  254. : null,
  255. ),
  256. ElevatedButton(
  257. onPressed: () =>
  258. ref.read(backupProvider.notifier).configureBackgroundBackup(
  259. enabled: !isBackgroundEnabled,
  260. onError: _showErrorToUser,
  261. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  262. ),
  263. child: Text(
  264. isBackgroundEnabled
  265. ? "backup_controller_page_background_turn_off"
  266. : "backup_controller_page_background_turn_on",
  267. style:
  268. const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  269. ).tr(),
  270. ),
  271. ],
  272. ),
  273. );
  274. }
  275. Widget _buildSelectedAlbumName() {
  276. var text = "backup_controller_page_backup_selected".tr();
  277. var albums = ref.watch(backupProvider).selectedBackupAlbums;
  278. if (albums.isNotEmpty) {
  279. for (var album in albums) {
  280. if (album.name == "Recent" || album.name == "Recents") {
  281. text += "${album.name} (${'backup_all'.tr()}), ";
  282. } else {
  283. text += "${album.name}, ";
  284. }
  285. }
  286. return Padding(
  287. padding: const EdgeInsets.only(top: 8.0),
  288. child: Text(
  289. text.trim().substring(0, text.length - 2),
  290. style: TextStyle(
  291. color: Theme.of(context).primaryColor,
  292. fontSize: 12,
  293. fontWeight: FontWeight.bold,
  294. ),
  295. ),
  296. );
  297. } else {
  298. return Padding(
  299. padding: const EdgeInsets.only(top: 8.0),
  300. child: Text(
  301. "backup_controller_page_none_selected".tr(),
  302. style: TextStyle(
  303. color: Theme.of(context).primaryColor,
  304. fontSize: 12,
  305. fontWeight: FontWeight.bold,
  306. ),
  307. ),
  308. );
  309. }
  310. }
  311. Widget _buildExcludedAlbumName() {
  312. var text = "backup_controller_page_excluded".tr();
  313. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  314. if (albums.isNotEmpty) {
  315. for (var album in albums) {
  316. text += "${album.name}, ";
  317. }
  318. return Padding(
  319. padding: const EdgeInsets.only(top: 8.0),
  320. child: Text(
  321. text.trim().substring(0, text.length - 2),
  322. style: TextStyle(
  323. color: Colors.red[300],
  324. fontSize: 12,
  325. fontWeight: FontWeight.bold,
  326. ),
  327. ),
  328. );
  329. } else {
  330. return const SizedBox();
  331. }
  332. }
  333. _buildFolderSelectionTile() {
  334. return Card(
  335. shape: RoundedRectangleBorder(
  336. borderRadius: BorderRadius.circular(5), // if you need this
  337. side: const BorderSide(
  338. color: Colors.black12,
  339. width: 1,
  340. ),
  341. ),
  342. elevation: 0,
  343. borderOnForeground: false,
  344. child: ListTile(
  345. minVerticalPadding: 15,
  346. title: const Text(
  347. "backup_controller_page_albums",
  348. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
  349. ).tr(),
  350. subtitle: Padding(
  351. padding: const EdgeInsets.only(top: 8.0),
  352. child: Column(
  353. crossAxisAlignment: CrossAxisAlignment.start,
  354. children: [
  355. const Text(
  356. "backup_controller_page_to_backup",
  357. style: TextStyle(fontSize: 12),
  358. ).tr(),
  359. _buildSelectedAlbumName(),
  360. _buildExcludedAlbumName()
  361. ],
  362. ),
  363. ),
  364. trailing: ElevatedButton(
  365. onPressed: hasExclusiveAccess
  366. ? () {
  367. AutoRouter.of(context)
  368. .push(const BackupAlbumSelectionRoute());
  369. }
  370. : null,
  371. child: const Text(
  372. "backup_controller_page_select",
  373. style: TextStyle(
  374. fontWeight: FontWeight.bold,
  375. fontSize: 12,
  376. ),
  377. ).tr(),
  378. ),
  379. ),
  380. );
  381. }
  382. _buildCurrentBackupAssetInfoCard() {
  383. return ListTile(
  384. leading: Icon(
  385. Icons.info_outline_rounded,
  386. color: Theme.of(context).primaryColor,
  387. ),
  388. title: Row(
  389. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  390. children: [
  391. const Text(
  392. "backup_controller_page_uploading_file_info",
  393. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  394. ).tr(),
  395. if (ref.watch(errorBackupListProvider).isNotEmpty)
  396. ActionChip(
  397. avatar: Icon(
  398. Icons.info,
  399. size: 24,
  400. color: Colors.red[400],
  401. ),
  402. elevation: 1,
  403. visualDensity: VisualDensity.compact,
  404. label: Text(
  405. "backup_controller_page_failed",
  406. style: TextStyle(
  407. color: Colors.red[400],
  408. fontWeight: FontWeight.bold,
  409. fontSize: 11,
  410. ),
  411. ).tr(
  412. args: [ref.watch(errorBackupListProvider).length.toString()],
  413. ),
  414. backgroundColor: Colors.white,
  415. onPressed: () {
  416. AutoRouter.of(context).push(const FailedBackupStatusRoute());
  417. },
  418. ),
  419. ],
  420. ),
  421. subtitle: Column(
  422. children: [
  423. Padding(
  424. padding: const EdgeInsets.only(top: 8.0),
  425. child: LinearPercentIndicator(
  426. padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
  427. barRadius: const Radius.circular(2),
  428. lineHeight: 10.0,
  429. trailing: Text(
  430. " ${backupState.progressInPercentage.toStringAsFixed(0)}%",
  431. style: const TextStyle(fontSize: 12),
  432. ),
  433. percent: backupState.progressInPercentage / 100.0,
  434. backgroundColor: Colors.grey,
  435. progressColor: Theme.of(context).primaryColor,
  436. ),
  437. ),
  438. Padding(
  439. padding: const EdgeInsets.only(top: 8.0),
  440. child: Table(
  441. border: TableBorder.all(
  442. color: Theme.of(context).primaryColorLight,
  443. width: 1,
  444. ),
  445. children: [
  446. TableRow(
  447. decoration: const BoxDecoration(
  448. // color: Colors.grey[100],
  449. ),
  450. children: [
  451. TableCell(
  452. verticalAlignment: TableCellVerticalAlignment.middle,
  453. child: Padding(
  454. padding: const EdgeInsets.all(6.0),
  455. child: const Text(
  456. 'backup_controller_page_filename',
  457. style: TextStyle(
  458. fontWeight: FontWeight.bold,
  459. fontSize: 10.0,
  460. ),
  461. ).tr(
  462. args: [
  463. backupState.currentUploadAsset.fileName,
  464. backupState.currentUploadAsset.fileType
  465. .toLowerCase()
  466. ],
  467. ),
  468. ),
  469. ),
  470. ],
  471. ),
  472. TableRow(
  473. decoration: const BoxDecoration(
  474. // color: Colors.grey[200],
  475. ),
  476. children: [
  477. TableCell(
  478. verticalAlignment: TableCellVerticalAlignment.middle,
  479. child: Padding(
  480. padding: const EdgeInsets.all(6.0),
  481. child: const Text(
  482. "backup_controller_page_created",
  483. style: TextStyle(
  484. fontWeight: FontWeight.bold,
  485. fontSize: 10.0,
  486. ),
  487. ).tr(
  488. args: [
  489. DateFormat.yMMMMd('en_US').format(
  490. DateTime.parse(
  491. backupState.currentUploadAsset.createdAt
  492. .toString(),
  493. ),
  494. )
  495. ],
  496. ),
  497. ),
  498. ),
  499. ],
  500. ),
  501. TableRow(
  502. decoration: const BoxDecoration(
  503. // color: Colors.grey[100],
  504. ),
  505. children: [
  506. TableCell(
  507. child: Padding(
  508. padding: const EdgeInsets.all(6.0),
  509. child: const Text(
  510. "backup_controller_page_id",
  511. style: TextStyle(
  512. fontWeight: FontWeight.bold,
  513. fontSize: 10.0,
  514. ),
  515. ).tr(args: [backupState.currentUploadAsset.id]),
  516. ),
  517. ),
  518. ],
  519. ),
  520. ],
  521. ),
  522. ),
  523. ],
  524. ),
  525. );
  526. }
  527. void startBackup() {
  528. ref.watch(errorBackupListProvider.notifier).empty();
  529. if (ref.watch(backupProvider).backupProgress !=
  530. BackUpProgressEnum.inBackground) {
  531. ref.watch(backupProvider.notifier).startBackupProcess();
  532. }
  533. }
  534. return Scaffold(
  535. appBar: AppBar(
  536. elevation: 0,
  537. title: const Text(
  538. "backup_controller_page_backup",
  539. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  540. ).tr(),
  541. leading: IconButton(
  542. onPressed: () {
  543. ref.watch(websocketProvider.notifier).listenUploadEvent();
  544. AutoRouter.of(context).pop(true);
  545. },
  546. splashRadius: 24,
  547. icon: const Icon(
  548. Icons.arrow_back_ios_rounded,
  549. ),
  550. ),
  551. ),
  552. body: Padding(
  553. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
  554. child: ListView(
  555. // crossAxisAlignment: CrossAxisAlignment.start,
  556. children: [
  557. Padding(
  558. padding: const EdgeInsets.all(8.0),
  559. child: const Text(
  560. "backup_controller_page_info",
  561. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  562. ).tr(),
  563. ),
  564. hasExclusiveAccess
  565. ? const SizedBox.shrink()
  566. : Card(
  567. shape: RoundedRectangleBorder(
  568. borderRadius:
  569. BorderRadius.circular(5), // if you need this
  570. side: const BorderSide(
  571. color: Colors.black12,
  572. width: 1,
  573. ),
  574. ),
  575. elevation: 0,
  576. borderOnForeground: false,
  577. child: const Padding(
  578. padding: EdgeInsets.all(16.0),
  579. child: Text(
  580. "Background backup is currently running, some actions are disabled",
  581. style: TextStyle(fontWeight: FontWeight.bold),
  582. ),
  583. ),
  584. ),
  585. _buildFolderSelectionTile(),
  586. BackupInfoCard(
  587. title: "backup_controller_page_total".tr(),
  588. subtitle: "backup_controller_page_total_sub".tr(),
  589. info: "${backupState.allUniqueAssets.length}",
  590. ),
  591. BackupInfoCard(
  592. title: "backup_controller_page_backup".tr(),
  593. subtitle: "backup_controller_page_backup_sub".tr(),
  594. info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
  595. ),
  596. BackupInfoCard(
  597. title: "backup_controller_page_remainder".tr(),
  598. subtitle: "backup_controller_page_remainder_sub".tr(),
  599. info:
  600. "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  601. ),
  602. const Divider(),
  603. _buildAutoBackupController(),
  604. if (Platform.isAndroid) const Divider(),
  605. if (Platform.isAndroid) _buildBackgroundBackupController(),
  606. const Divider(),
  607. _buildStorageInformation(),
  608. const Divider(),
  609. _buildCurrentBackupAssetInfoCard(),
  610. Padding(
  611. padding: const EdgeInsets.only(
  612. top: 24,
  613. ),
  614. child: Container(
  615. child:
  616. backupState.backupProgress == BackUpProgressEnum.inProgress
  617. ? ElevatedButton(
  618. style: ElevatedButton.styleFrom(
  619. primary: Colors.red[300],
  620. onPrimary: Colors.grey[50],
  621. // padding: const EdgeInsets.all(14),
  622. ),
  623. onPressed: () {
  624. ref.read(backupProvider.notifier).cancelBackup();
  625. },
  626. child: const Text(
  627. "backup_controller_page_cancel",
  628. style: TextStyle(
  629. fontSize: 14,
  630. fontWeight: FontWeight.bold,
  631. ),
  632. ).tr(),
  633. )
  634. : ElevatedButton(
  635. onPressed: shouldBackup ? startBackup : null,
  636. child: const Text(
  637. "backup_controller_page_start_backup",
  638. style: TextStyle(
  639. fontSize: 14,
  640. fontWeight: FontWeight.bold,
  641. ),
  642. ).tr(),
  643. ),
  644. ),
  645. )
  646. ],
  647. ),
  648. ),
  649. );
  650. }
  651. }