backup_controller_page.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  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. showDialog<void>(
  153. context: context,
  154. barrierDismissible: false,
  155. builder: (BuildContext context) {
  156. return AlertDialog(
  157. title: const Text(
  158. 'backup_controller_page_background_battery_info_title',
  159. ).tr(),
  160. content: SingleChildScrollView(
  161. child: const Text(
  162. 'backup_controller_page_background_battery_info_message',
  163. ).tr(),
  164. ),
  165. actions: [
  166. ElevatedButton(
  167. onPressed: () => launchUrl(
  168. Uri.parse('https://dontkillmyapp.com'),
  169. mode: LaunchMode.externalApplication,
  170. ),
  171. child: const Text(
  172. "backup_controller_page_background_battery_info_link",
  173. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  174. ).tr(),
  175. ),
  176. ElevatedButton(
  177. child: const Text(
  178. 'backup_controller_page_background_battery_info_ok',
  179. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  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. Padding(
  214. padding: const EdgeInsets.symmetric(vertical: 8.0),
  215. child:
  216. const Text("backup_controller_page_background_description")
  217. .tr(),
  218. ),
  219. if (isBackgroundEnabled)
  220. SwitchListTile(
  221. title:
  222. const Text("backup_controller_page_background_wifi").tr(),
  223. secondary: Icon(
  224. Icons.wifi,
  225. color: isWifiRequired ? activeColor : null,
  226. ),
  227. dense: true,
  228. activeColor: activeColor,
  229. value: isWifiRequired,
  230. onChanged: hasExclusiveAccess
  231. ? (isChecked) => ref
  232. .read(backupProvider.notifier)
  233. .configureBackgroundBackup(
  234. requireWifi: isChecked,
  235. onError: _showErrorToUser,
  236. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  237. )
  238. : null,
  239. ),
  240. if (isBackgroundEnabled)
  241. SwitchListTile(
  242. title: const Text("backup_controller_page_background_charging")
  243. .tr(),
  244. secondary: Icon(
  245. Icons.charging_station,
  246. color: isChargingRequired ? activeColor : null,
  247. ),
  248. dense: true,
  249. activeColor: activeColor,
  250. value: isChargingRequired,
  251. onChanged: hasExclusiveAccess
  252. ? (isChecked) => ref
  253. .read(backupProvider.notifier)
  254. .configureBackgroundBackup(
  255. requireCharging: isChecked,
  256. onError: _showErrorToUser,
  257. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  258. )
  259. : null,
  260. ),
  261. ElevatedButton(
  262. onPressed: () =>
  263. ref.read(backupProvider.notifier).configureBackgroundBackup(
  264. enabled: !isBackgroundEnabled,
  265. onError: _showErrorToUser,
  266. onBatteryInfo: _showBatteryOptimizationInfoToUser,
  267. ),
  268. child: Text(
  269. isBackgroundEnabled
  270. ? "backup_controller_page_background_turn_off"
  271. : "backup_controller_page_background_turn_on",
  272. style:
  273. const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
  274. ).tr(),
  275. ),
  276. ],
  277. ),
  278. );
  279. }
  280. Widget _buildSelectedAlbumName() {
  281. var text = "backup_controller_page_backup_selected".tr();
  282. var albums = ref.watch(backupProvider).selectedBackupAlbums;
  283. if (albums.isNotEmpty) {
  284. for (var album in albums) {
  285. if (album.name == "Recent" || album.name == "Recents") {
  286. text += "${album.name} (${'backup_all'.tr()}), ";
  287. } else {
  288. text += "${album.name}, ";
  289. }
  290. }
  291. return Padding(
  292. padding: const EdgeInsets.only(top: 8.0),
  293. child: Text(
  294. text.trim().substring(0, text.length - 2),
  295. style: TextStyle(
  296. color: Theme.of(context).primaryColor,
  297. fontSize: 12,
  298. fontWeight: FontWeight.bold,
  299. ),
  300. ),
  301. );
  302. } else {
  303. return Padding(
  304. padding: const EdgeInsets.only(top: 8.0),
  305. child: Text(
  306. "backup_controller_page_none_selected".tr(),
  307. style: TextStyle(
  308. color: Theme.of(context).primaryColor,
  309. fontSize: 12,
  310. fontWeight: FontWeight.bold,
  311. ),
  312. ),
  313. );
  314. }
  315. }
  316. Widget _buildExcludedAlbumName() {
  317. var text = "backup_controller_page_excluded".tr();
  318. var albums = ref.watch(backupProvider).excludedBackupAlbums;
  319. if (albums.isNotEmpty) {
  320. for (var album in albums) {
  321. text += "${album.name}, ";
  322. }
  323. return Padding(
  324. padding: const EdgeInsets.only(top: 8.0),
  325. child: Text(
  326. text.trim().substring(0, text.length - 2),
  327. style: TextStyle(
  328. color: Colors.red[300],
  329. fontSize: 12,
  330. fontWeight: FontWeight.bold,
  331. ),
  332. ),
  333. );
  334. } else {
  335. return const SizedBox();
  336. }
  337. }
  338. _buildFolderSelectionTile() {
  339. return Card(
  340. shape: RoundedRectangleBorder(
  341. borderRadius: BorderRadius.circular(5), // if you need this
  342. side: const BorderSide(
  343. color: Colors.black12,
  344. width: 1,
  345. ),
  346. ),
  347. elevation: 0,
  348. borderOnForeground: false,
  349. child: ListTile(
  350. minVerticalPadding: 15,
  351. title: const Text(
  352. "backup_controller_page_albums",
  353. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
  354. ).tr(),
  355. subtitle: Padding(
  356. padding: const EdgeInsets.only(top: 8.0),
  357. child: Column(
  358. crossAxisAlignment: CrossAxisAlignment.start,
  359. children: [
  360. const Text(
  361. "backup_controller_page_to_backup",
  362. style: TextStyle(fontSize: 12),
  363. ).tr(),
  364. _buildSelectedAlbumName(),
  365. _buildExcludedAlbumName()
  366. ],
  367. ),
  368. ),
  369. trailing: ElevatedButton(
  370. onPressed: hasExclusiveAccess
  371. ? () {
  372. AutoRouter.of(context)
  373. .push(const BackupAlbumSelectionRoute());
  374. }
  375. : null,
  376. child: const Text(
  377. "backup_controller_page_select",
  378. style: TextStyle(
  379. fontWeight: FontWeight.bold,
  380. fontSize: 12,
  381. ),
  382. ).tr(),
  383. ),
  384. ),
  385. );
  386. }
  387. _buildCurrentBackupAssetInfoCard() {
  388. return ListTile(
  389. leading: Icon(
  390. Icons.info_outline_rounded,
  391. color: Theme.of(context).primaryColor,
  392. ),
  393. title: Row(
  394. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  395. children: [
  396. const Text(
  397. "backup_controller_page_uploading_file_info",
  398. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
  399. ).tr(),
  400. if (ref.watch(errorBackupListProvider).isNotEmpty)
  401. ActionChip(
  402. avatar: Icon(
  403. Icons.info,
  404. size: 24,
  405. color: Colors.red[400],
  406. ),
  407. elevation: 1,
  408. visualDensity: VisualDensity.compact,
  409. label: Text(
  410. "backup_controller_page_failed",
  411. style: TextStyle(
  412. color: Colors.red[400],
  413. fontWeight: FontWeight.bold,
  414. fontSize: 11,
  415. ),
  416. ).tr(
  417. args: [ref.watch(errorBackupListProvider).length.toString()],
  418. ),
  419. backgroundColor: Colors.white,
  420. onPressed: () {
  421. AutoRouter.of(context).push(const FailedBackupStatusRoute());
  422. },
  423. ),
  424. ],
  425. ),
  426. subtitle: Column(
  427. children: [
  428. Padding(
  429. padding: const EdgeInsets.only(top: 8.0),
  430. child: LinearPercentIndicator(
  431. padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
  432. barRadius: const Radius.circular(2),
  433. lineHeight: 10.0,
  434. trailing: Text(
  435. " ${backupState.progressInPercentage.toStringAsFixed(0)}%",
  436. style: const TextStyle(fontSize: 12),
  437. ),
  438. percent: backupState.progressInPercentage / 100.0,
  439. backgroundColor: Colors.grey,
  440. progressColor: Theme.of(context).primaryColor,
  441. ),
  442. ),
  443. Padding(
  444. padding: const EdgeInsets.only(top: 8.0),
  445. child: Table(
  446. border: TableBorder.all(
  447. color: Theme.of(context).primaryColorLight,
  448. width: 1,
  449. ),
  450. children: [
  451. TableRow(
  452. decoration: const BoxDecoration(
  453. // color: Colors.grey[100],
  454. ),
  455. children: [
  456. TableCell(
  457. verticalAlignment: TableCellVerticalAlignment.middle,
  458. child: Padding(
  459. padding: const EdgeInsets.all(6.0),
  460. child: const Text(
  461. 'backup_controller_page_filename',
  462. style: TextStyle(
  463. fontWeight: FontWeight.bold,
  464. fontSize: 10.0,
  465. ),
  466. ).tr(
  467. args: [
  468. backupState.currentUploadAsset.fileName,
  469. backupState.currentUploadAsset.fileType
  470. .toLowerCase()
  471. ],
  472. ),
  473. ),
  474. ),
  475. ],
  476. ),
  477. TableRow(
  478. decoration: const BoxDecoration(
  479. // color: Colors.grey[200],
  480. ),
  481. children: [
  482. TableCell(
  483. verticalAlignment: TableCellVerticalAlignment.middle,
  484. child: Padding(
  485. padding: const EdgeInsets.all(6.0),
  486. child: const Text(
  487. "backup_controller_page_created",
  488. style: TextStyle(
  489. fontWeight: FontWeight.bold,
  490. fontSize: 10.0,
  491. ),
  492. ).tr(
  493. args: [
  494. DateFormat.yMMMMd('en_US').format(
  495. DateTime.parse(
  496. backupState.currentUploadAsset.createdAt
  497. .toString(),
  498. ).toLocal(),
  499. )
  500. ],
  501. ),
  502. ),
  503. ),
  504. ],
  505. ),
  506. TableRow(
  507. decoration: const BoxDecoration(
  508. // color: Colors.grey[100],
  509. ),
  510. children: [
  511. TableCell(
  512. child: Padding(
  513. padding: const EdgeInsets.all(6.0),
  514. child: const Text(
  515. "backup_controller_page_id",
  516. style: TextStyle(
  517. fontWeight: FontWeight.bold,
  518. fontSize: 10.0,
  519. ),
  520. ).tr(args: [backupState.currentUploadAsset.id]),
  521. ),
  522. ),
  523. ],
  524. ),
  525. ],
  526. ),
  527. ),
  528. ],
  529. ),
  530. );
  531. }
  532. void startBackup() {
  533. ref.watch(errorBackupListProvider.notifier).empty();
  534. if (ref.watch(backupProvider).backupProgress !=
  535. BackUpProgressEnum.inBackground) {
  536. ref.watch(backupProvider.notifier).startBackupProcess();
  537. }
  538. }
  539. return Scaffold(
  540. appBar: AppBar(
  541. elevation: 0,
  542. title: const Text(
  543. "backup_controller_page_backup",
  544. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  545. ).tr(),
  546. leading: IconButton(
  547. onPressed: () {
  548. ref.watch(websocketProvider.notifier).listenUploadEvent();
  549. AutoRouter.of(context).pop(true);
  550. },
  551. splashRadius: 24,
  552. icon: const Icon(
  553. Icons.arrow_back_ios_rounded,
  554. ),
  555. ),
  556. ),
  557. body: Padding(
  558. padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
  559. child: ListView(
  560. // crossAxisAlignment: CrossAxisAlignment.start,
  561. children: [
  562. Padding(
  563. padding: const EdgeInsets.all(8.0),
  564. child: const Text(
  565. "backup_controller_page_info",
  566. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  567. ).tr(),
  568. ),
  569. hasExclusiveAccess
  570. ? const SizedBox.shrink()
  571. : Card(
  572. shape: RoundedRectangleBorder(
  573. borderRadius:
  574. BorderRadius.circular(5), // if you need this
  575. side: const BorderSide(
  576. color: Colors.black12,
  577. width: 1,
  578. ),
  579. ),
  580. elevation: 0,
  581. borderOnForeground: false,
  582. child: const Padding(
  583. padding: EdgeInsets.all(16.0),
  584. child: Text(
  585. "Background backup is currently running, some actions are disabled",
  586. style: TextStyle(fontWeight: FontWeight.bold),
  587. ),
  588. ),
  589. ),
  590. _buildFolderSelectionTile(),
  591. BackupInfoCard(
  592. title: "backup_controller_page_total".tr(),
  593. subtitle: "backup_controller_page_total_sub".tr(),
  594. info: "${backupState.allUniqueAssets.length}",
  595. ),
  596. BackupInfoCard(
  597. title: "backup_controller_page_backup".tr(),
  598. subtitle: "backup_controller_page_backup_sub".tr(),
  599. info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
  600. ),
  601. BackupInfoCard(
  602. title: "backup_controller_page_remainder".tr(),
  603. subtitle: "backup_controller_page_remainder_sub".tr(),
  604. info:
  605. "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
  606. ),
  607. const Divider(),
  608. _buildAutoBackupController(),
  609. if (Platform.isAndroid) const Divider(),
  610. if (Platform.isAndroid) _buildBackgroundBackupController(),
  611. const Divider(),
  612. _buildStorageInformation(),
  613. const Divider(),
  614. _buildCurrentBackupAssetInfoCard(),
  615. Padding(
  616. padding: const EdgeInsets.only(
  617. top: 24,
  618. ),
  619. child: Container(
  620. child:
  621. backupState.backupProgress == BackUpProgressEnum.inProgress
  622. ? ElevatedButton(
  623. style: ElevatedButton.styleFrom(
  624. foregroundColor: Colors.grey[50],
  625. backgroundColor: Colors.red[300],
  626. // padding: const EdgeInsets.all(14),
  627. ),
  628. onPressed: () {
  629. ref.read(backupProvider.notifier).cancelBackup();
  630. },
  631. child: const Text(
  632. "backup_controller_page_cancel",
  633. style: TextStyle(
  634. fontSize: 14,
  635. fontWeight: FontWeight.bold,
  636. ),
  637. ).tr(),
  638. )
  639. : ElevatedButton(
  640. onPressed: shouldBackup ? startBackup : null,
  641. child: const Text(
  642. "backup_controller_page_start_backup",
  643. style: TextStyle(
  644. fontSize: 14,
  645. fontWeight: FontWeight.bold,
  646. ),
  647. ).tr(),
  648. ),
  649. ),
  650. )
  651. ],
  652. ),
  653. ),
  654. );
  655. }
  656. }