Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View file

@ -0,0 +1,185 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoCard({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken);
ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
buildSelectedTextBox() {
if (isSelected) {
return Chip(
visualDensity: VisualDensity.compact,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
label: Text(
"album_info_card_backup_album_included",
style: TextStyle(
fontSize: 10,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: context.primaryColor,
);
} else if (isExcluded) {
return Chip(
visualDensity: VisualDensity.compact,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
label: Text(
"album_info_card_backup_album_excluded",
style: TextStyle(
fontSize: 10,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: Colors.red[300],
);
}
return const SizedBox();
}
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
return GestureDetector(
onTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
if (syncAlbum) {
ref.read(albumProvider.notifier).createSyncAlbum(album.name);
}
}
},
onDoubleTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: Card(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(12), // if you need this
),
side: BorderSide(
color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
ColorFiltered(
colorFilter: buildImageFilter(),
child: const Image(
width: double.infinity,
height: double.infinity,
image: AssetImage('assets/immich-logo.png'),
fit: BoxFit.cover,
),
),
Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
album.name,
style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
),
),
IconButton(
onPressed: () {
context.pushRoute(AlbumPreviewRoute(album: album.album));
},
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,98 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoListTile({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
buildTileColor() {
if (isSelected) {
return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25);
} else if (isExcluded) {
return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
}
buildIcon() {
if (isSelected) {
return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary);
}
if (isExcluded) {
return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error);
}
return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest);
}
return GestureDetector(
onDoubleTap: () {
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
if (syncAlbum) {
ref.read(albumProvider.notifier).createSyncAlbum(album.name);
}
}
},
leading: buildIcon(),
title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
subtitle: Text(album.assetCount.toString()),
trailing: IconButton(
onPressed: () {
context.pushRoute(AlbumPreviewRoute(album: album.album));
},
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
),
);
}
}

View file

@ -0,0 +1,105 @@
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 isUploadInProgress = ref.watch(
backupProvider.select(
(value) =>
value.backupProgress == BackUpProgressEnum.inProgress ||
value.backupProgress == BackUpProgressEnum.inBackground ||
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(
namedArgs: isUploadInProgress
? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()}
: {'filename': "-", 'size': "-"},
),
),
),
],
),
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(namedArgs: {'date': isUploadInProgress ? _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(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}),
),
),
],
),
],
),
);
}
@pragma('vm:prefer-inline')
String _getAssetCreationDate(CurrentUploadAsset asset) {
return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal());
}
}

View file

@ -0,0 +1,106 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
class BackupInfoCard extends StatelessWidget {
final String title;
final String subtitle;
final String info;
final VoidCallback? onTap;
final bool isLoading;
const BackupInfoCard({
super.key,
required this.title,
required this.subtitle,
required this.info,
this.onTap,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(20), // if you need this
),
side: BorderSide(color: context.colorScheme.outlineVariant, width: 1),
),
elevation: 0,
borderOnForeground: false,
child: Column(
children: [
ListTile(
minVerticalPadding: 18,
isThreeLine: true,
title: Text(title, style: context.textTheme.titleMedium),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0, right: 18.0),
child: Text(
subtitle,
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
children: [
Text(
info,
style: context.textTheme.titleLarge?.copyWith(
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
fontFeatures: const [FontFeature.tabularFigures()],
),
),
if (isLoading)
Positioned.fill(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: context.colorScheme.onSurface.withAlpha(150),
),
),
),
),
],
),
Text(
"backup_info_card_assets",
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
),
).tr(),
],
),
),
if (onTap != null) ...[
const Divider(height: 0),
ListTile(
enableFeedback: true,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
),
onTap: onTap,
title: Text(
"view_details".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant),
),
],
],
),
);
}
}

View file

@ -0,0 +1,37 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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 StatelessWidget {
const CurrentUploadingAssetInfoBox({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
isThreeLine: true,
leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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(),
],
),
);
}
}

View file

@ -0,0 +1,96 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class DriftAlbumInfoListTile extends HookConsumerWidget {
final LocalAlbum album;
const DriftAlbumInfoListTile({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected = album.backupSelection == BackupSelection.selected;
final bool isExcluded = album.backupSelection == BackupSelection.excluded;
buildTileColor() {
if (isSelected) {
return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25);
} else if (isExcluded) {
return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
}
buildIcon() {
if (isSelected) {
return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary);
}
if (isExcluded) {
return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error);
}
return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest);
}
Widget buildSubtitle() {
return Text(
album.isIosSharedAlbum ? '${album.assetCount} (iCloud Shared Album)' : album.assetCount.toString(),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
);
}
return GestureDetector(
onDoubleTap: () {
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
} else {
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(backupAlbumProvider.notifier).excludeAlbum(album);
}
},
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
} else {
ref.read(backupAlbumProvider.notifier).selectAlbum(album);
}
},
leading: buildIcon(),
title: Text(album.name, style: context.textTheme.titleSmall),
subtitle: buildSubtitle(),
trailing: IconButton(
onPressed: () {
context.pushRoute(LocalTimelineRoute(album: album));
},
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
),
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/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()),
);
}
}

View file

@ -0,0 +1,22 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/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(namedArgs: {'count': count.toString()});
}
}

View file

@ -0,0 +1,43 @@
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)),
],
),
);
}
}

View file

@ -0,0 +1,49 @@
import 'package:intl/intl.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/translate_extensions.dart';
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
/// This is a simple debug widget which should be removed later on when we are
/// more confident about background sync
class IosDebugInfoTile extends HookConsumerWidget {
final IOSBackgroundSettings settings;
const IosDebugInfoTile({super.key, required this.settings});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fetch = settings.timeOfLastFetch;
final processing = settings.timeOfLastProcessing;
final processes = settings.numberOfBackgroundTasksQueued;
final String title;
if (processes == 0) {
title = 'ios_debug_info_no_processes_queued'.t(context: context);
} else {
title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes});
}
final df = DateFormat.yMd().add_jm();
final String subtitle;
if (fetch == null && processing == null) {
subtitle = 'ios_debug_info_no_sync_yet'.t(context: context);
} else if (fetch != null && processing == null) {
subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)});
} else if (processing != null && fetch == null) {
subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)});
} else {
final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)});
}
return ListTile(
title: Text(
title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor),
),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)),
leading: Icon(Icons.bug_report, color: context.primaryColor),
);
}
}

View file

@ -0,0 +1,45 @@
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: "GoogleSansCode"),
),
],
),
);
}
}

View file

@ -0,0 +1,51 @@
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: "GoogleSansCode")),
Text(
_formatUploadFileSpeed(uploadFileSpeed),
style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"),
),
],
),
);
}
@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';
}
}
}