diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index d63928b5b8..a49e783602 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -21,6 +21,7 @@ const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); const Color blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0); +const Color red400 = Color(0xFFEF5350); final Map _themePresetsMap = { ImmichColorPreset.indigo: ImmichTheme( diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart index 9a761c9e4a..787f117269 100644 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ b/mobile/lib/models/backup/current_upload_asset.model.dart @@ -18,6 +18,9 @@ class CurrentUploadAsset { this.iCloudAsset, }); + @pragma('vm:prefer-inline') + bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; + CurrentUploadAsset copyWith({ String? id, DateTime? fileCreatedAt, diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart new file mode 100644 index 0000000000..bbcbbe375f --- /dev/null +++ b/mobile/lib/widgets/backup/asset_info_table.dart @@ -0,0 +1,102 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/models/backup/backup_state.model.dart'; +import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; + +class BackupAssetInfoTable extends ConsumerWidget { + const BackupAssetInfoTable({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isManualUpload = ref.watch( + backupProvider.select( + (value) => value.backupProgress == BackUpProgressEnum.manualInProgress, + ), + ); + + final asset = isManualUpload + ? ref.watch( + manualUploadProvider.select((value) => value.currentUploadAsset), + ) + : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Table( + border: TableBorder.all( + color: context.colorScheme.outlineVariant, + width: 1, + ), + children: [ + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + 'backup_controller_page_filename', + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.bold, + fontSize: 10.0, + ), + ).tr( + args: [asset.fileName, asset.fileType.toLowerCase()], + ), + ), + ), + ], + ), + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + "backup_controller_page_created", + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.bold, + fontSize: 10.0, + ), + ).tr( + args: [_getAssetCreationDate(asset)], + ), + ), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + "backup_controller_page_id", + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.bold, + fontSize: 10.0, + ), + ).tr(args: [asset.id]), + ), + ), + ], + ), + ], + ), + ); + } + + @pragma('vm:prefer-inline') + String _getAssetCreationDate(CurrentUploadAsset asset) { + return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); + } +} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index f2f84e271f..b6d0edb200 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -1,296 +1,43 @@ import 'dart:io'; -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; +import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; +import 'package:immich_mobile/widgets/backup/error_chip.dart'; +import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; +import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; +import 'package:immich_mobile/widgets/backup/upload_stats.dart'; -class CurrentUploadingAssetInfoBox extends HookConsumerWidget { +class CurrentUploadingAssetInfoBox extends StatelessWidget { const CurrentUploadingAssetInfoBox({super.key}); + @override - Widget build(BuildContext context, WidgetRef ref) { - var isManualUpload = ref.watch(backupProvider).backupProgress == - BackUpProgressEnum.manualInProgress; - var asset = !isManualUpload - ? ref.watch(backupProvider).currentUploadAsset - : ref.watch(manualUploadProvider).currentUploadAsset; - var uploadProgress = !isManualUpload - ? ref.watch(backupProvider).progressInPercentage - : ref.watch(manualUploadProvider).progressInPercentage; - var uploadFileProgress = !isManualUpload - ? ref.watch(backupProvider).progressInFileSize - : ref.watch(manualUploadProvider).progressInFileSize; - var uploadFileSpeed = !isManualUpload - ? ref.watch(backupProvider).progressInFileSpeed - : ref.watch(manualUploadProvider).progressInFileSpeed; - var iCloudDownloadProgress = - ref.watch(backupProvider).iCloudDownloadProgress; - final isShowThumbnail = useState(false); - - String formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } - - String getAssetCreationDate() { - return DateFormat.yMMMMd().format( - DateTime.parse( - asset.fileCreatedAt.toString(), - ).toLocal(), - ); - } - - Widget buildErrorChip() { - return ActionChip( - avatar: Icon( - Icons.info, - color: Colors.red[400], - ), - elevation: 1, - visualDensity: VisualDensity.compact, - label: Text( - "backup_controller_page_failed", - style: TextStyle( - color: Colors.red[400], - fontWeight: FontWeight.bold, - fontSize: 11, - ), - ).tr( - args: [ref.watch(errorBackupListProvider).length.toString()], - ), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } - - Widget buildAssetInfoTable() { - return Table( - border: TableBorder.all( - color: context.colorScheme.outlineVariant, - width: 1, - ), + Widget build(BuildContext context) { + return ListTile( + isThreeLine: true, + leading: Icon( + Icons.image_outlined, + color: context.primaryColor, + size: 30, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - args: [asset.fileName, asset.fileType.toLowerCase()], - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - args: [getAssetCreationDate()], - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(args: [asset.id]), - ), - ), - ], - ), + Text( + "backup_controller_page_uploading_file_info", + style: context.textTheme.titleSmall, + ).tr(), + const BackupErrorChip(), + ], + ), + subtitle: Column( + children: [ + if (Platform.isIOS) const IcloudDownloadProgressBar(), + const BackupUploadProgressBar(), + const BackupUploadStats(), + const BackupAssetInfoTable(), ], - ); - } - - buildiCloudDownloadProgerssBar() { - if (asset.iCloudAsset != null && asset.iCloudAsset!) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox( - width: 110, - child: Text( - "iCloud Download", - style: context.textTheme.labelSmall, - ), - ), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${iCloudDownloadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12), - ), - ], - ), - ); - } - - return const SizedBox(); - } - - buildUploadProgressBar() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (asset.iCloudAsset != null && asset.iCloudAsset!) - SizedBox( - width: 110, - child: Text( - "Immich Upload", - style: context.textTheme.labelSmall, - ), - ), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"), - ), - ], - ), - ); - } - - buildUploadStats() { - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - uploadFileProgress, - style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), - ), - Text( - formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), - ), - ], - ), - ); - } - - return FutureBuilder( - future: ref.read(assetMediaRepositoryProvider).get(asset.id), - builder: (context, thumbnail) => ListTile( - isThreeLine: true, - leading: AnimatedCrossFade( - alignment: Alignment.centerLeft, - firstChild: GestureDetector( - onTap: () => isShowThumbnail.value = false, - child: thumbnail.hasData - ? ClipRRect( - borderRadius: BorderRadius.circular(5), - child: ImmichThumbnail( - asset: thumbnail.data, - width: 50, - height: 50, - ), - ) - : const SizedBox( - width: 50, - height: 50, - child: Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator.adaptive( - strokeWidth: 1, - ), - ), - ), - ), - secondChild: GestureDetector( - onTap: () => isShowThumbnail.value = true, - child: Icon( - Icons.image_outlined, - color: context.primaryColor, - size: 30, - ), - ), - crossFadeState: isShowThumbnail.value - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "backup_controller_page_uploading_file_info", - style: context.textTheme.titleSmall, - ).tr(), - if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) buildiCloudDownloadProgerssBar(), - buildUploadProgressBar(), - buildUploadStats(), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: buildAssetInfoTable(), - ), - ], - ), ), ); } diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart new file mode 100644 index 0000000000..4bbc040d4d --- /dev/null +++ b/mobile/lib/widgets/backup/error_chip.dart @@ -0,0 +1,32 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; + +class BackupErrorChip extends ConsumerWidget { + const BackupErrorChip({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasErrors = + ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); + if (!hasErrors) { + return const SizedBox(); + } + + return ActionChip( + avatar: const Icon( + Icons.info, + color: red400, + ), + elevation: 1, + visualDensity: VisualDensity.compact, + label: const BackupErrorChipText(), + backgroundColor: Colors.white, + onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), + ); + } +} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart new file mode 100644 index 0000000000..94148da176 --- /dev/null +++ b/mobile/lib/widgets/backup/error_chip_text.dart @@ -0,0 +1,28 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; + +class BackupErrorChipText extends ConsumerWidget { + const BackupErrorChipText({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final count = ref.watch(errorBackupListProvider).length; + if (count == 0) { + return const SizedBox(); + } + + return const Text( + "backup_controller_page_failed", + style: TextStyle( + color: red400, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ).tr( + args: [count.toString()], + ); + } +} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart new file mode 100644 index 0000000000..c61fb1a0d1 --- /dev/null +++ b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/backup/backup_state.model.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; + +class IcloudDownloadProgressBar extends ConsumerWidget { + const IcloudDownloadProgressBar({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final isManualUpload = ref.watch( + backupProvider.select( + (value) => value.backupProgress == BackUpProgressEnum.manualInProgress, + ), + ); + + final isIcloudAsset = isManualUpload + ? ref.watch( + manualUploadProvider + .select((value) => value.currentUploadAsset.isIcloudAsset), + ) + : ref.watch( + backupProvider + .select((value) => value.currentUploadAsset.isIcloudAsset), + ); + + if (!isIcloudAsset) { + return const SizedBox(); + } + + final iCloudDownloadProgress = ref + .watch(backupProvider.select((value) => value.iCloudDownloadProgress)); + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + SizedBox( + width: 110, + child: Text( + "iCloud Download", + style: context.textTheme.labelSmall, + ), + ), + Expanded( + child: LinearProgressIndicator( + minHeight: 10.0, + value: iCloudDownloadProgress / 100.0, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + ), + ), + Text( + " ${iCloudDownloadProgress ~/ 1}%", + style: const TextStyle(fontSize: 12), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart new file mode 100644 index 0000000000..9281914d9c --- /dev/null +++ b/mobile/lib/widgets/backup/upload_progress_bar.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/backup/backup_state.model.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; + +class BackupUploadProgressBar extends ConsumerWidget { + const BackupUploadProgressBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isManualUpload = ref.watch( + backupProvider.select( + (value) => value.backupProgress == BackUpProgressEnum.manualInProgress, + ), + ); + + final isIcloudAsset = isManualUpload + ? ref.watch( + manualUploadProvider + .select((value) => value.currentUploadAsset.isIcloudAsset), + ) + : ref.watch( + backupProvider + .select((value) => value.currentUploadAsset.isIcloudAsset), + ); + + final uploadProgress = isManualUpload + ? ref.watch( + manualUploadProvider.select((value) => value.progressInPercentage), + ) + : ref.watch( + backupProvider.select((value) => value.progressInPercentage), + ); + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + if (isIcloudAsset) + SizedBox( + width: 110, + child: Text( + "Immich Upload", + style: context.textTheme.labelSmall, + ), + ), + Expanded( + child: LinearProgressIndicator( + minHeight: 10.0, + value: uploadProgress / 100.0, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + ), + ), + Text( + " ${uploadProgress.toStringAsFixed(0)}%", + style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart new file mode 100644 index 0000000000..965202ce33 --- /dev/null +++ b/mobile/lib/widgets/backup/upload_stats.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/backup/backup_state.model.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; + +class BackupUploadStats extends ConsumerWidget { + const BackupUploadStats({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isManualUpload = ref.watch( + backupProvider.select( + (value) => value.backupProgress == BackUpProgressEnum.manualInProgress, + ), + ); + + final uploadFileProgress = isManualUpload + ? ref.watch( + manualUploadProvider.select((value) => value.progressInFileSize), + ) + : ref.watch(backupProvider.select((value) => value.progressInFileSize)); + + final uploadFileSpeed = isManualUpload + ? ref.watch( + manualUploadProvider.select((value) => value.progressInFileSpeed), + ) + : ref.watch( + backupProvider.select((value) => value.progressInFileSpeed), + ); + + return Padding( + padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + uploadFileProgress, + style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), + ), + Text( + _formatUploadFileSpeed(uploadFileSpeed), + style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), + ), + ], + ), + ); + } + + @pragma('vm:prefer-inline') + String _formatUploadFileSpeed(double uploadFileSpeed) { + if (uploadFileSpeed < 1024) { + return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; + } else if (uploadFileSpeed < 1024 * 1024) { + return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; + } else if (uploadFileSpeed < 1024 * 1024 * 1024) { + return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; + } else { + return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; + } + } +}