mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
wip
This commit is contained in:
parent
429d339c6d
commit
f87ae08cd1
14
mobile/lib/domain/interfaces/backup.interface.dart
Normal file
14
mobile/lib/domain/interfaces/backup.interface.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
|
||||||
|
abstract interface class IBackupRepository implements IDatabaseRepository {
|
||||||
|
Future<List<LocalAsset>> getAssets(String albumId);
|
||||||
|
|
||||||
|
Future<List<String>> getAssetIds(String albumId);
|
||||||
|
|
||||||
|
/// Returns the total number of assets that are selected for backup.
|
||||||
|
Future<int> getTotalCount(BackupSelection selection);
|
||||||
|
|
||||||
|
Future<int> getBackupCount();
|
||||||
|
}
|
114
mobile/lib/infrastructure/repositories/backup.repository.dart
Normal file
114
mobile/lib/infrastructure/repositories/backup.repository.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/backup.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
final backupRepositoryProvider = Provider<IBackupRepository>(
|
||||||
|
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class DriftBackupRepository extends DriftDatabaseRepository
|
||||||
|
implements IBackupRepository {
|
||||||
|
final Drift _db;
|
||||||
|
final Platform _platform;
|
||||||
|
const DriftBackupRepository(this._db, {Platform? platform})
|
||||||
|
: _platform = platform ?? const LocalPlatform(),
|
||||||
|
super(_db);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LocalAsset>> getAssets(String albumId) {
|
||||||
|
final query = _db.localAlbumAssetEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getAssetIds(String albumId) {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId));
|
||||||
|
return query
|
||||||
|
.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> getTotalCount(BackupSelection selection) {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection.equals(selection.index),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> getBackupCount() {
|
||||||
|
final query = _db.localAlbumEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)..where(
|
||||||
|
_db.localAlbumEntity.backupSelection.equals(
|
||||||
|
BackupSelection.selected.index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on LocalAlbumEntityData {
|
||||||
|
LocalAlbum toDto({int assetCount = 0}) {
|
||||||
|
return LocalAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
assetCount: assetCount,
|
||||||
|
backupSelection: backupSelection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on LocalAssetEntityData {
|
||||||
|
LocalAsset toDto() {
|
||||||
|
return LocalAsset(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
checksum: checksum,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,15 +10,14 @@ import 'package:immich_mobile/domain/models/local_album.model.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_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/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/exp_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/exp_upload_option_toggle.dart';
|
import 'package:immich_mobile/widgets/backup/exp_upload_option_toggle.dart';
|
||||||
@ -43,6 +42,14 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
ref.read(expBackupProvider.notifier).getBackupStatus();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
// Update the background settings information just to make sure we
|
// Update the background settings information just to make sure we
|
||||||
@ -88,131 +95,6 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
[backupState.backupProgress],
|
[backupState.backupProgress],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget buildSelectedAlbumName() {
|
|
||||||
String text = "backup_controller_page_backup_selected".tr();
|
|
||||||
final albums = ref
|
|
||||||
.watch(backupAlbumProvider)
|
|
||||||
.where(
|
|
||||||
(album) => album.backupSelection == BackupSelection.selected,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
|
||||||
for (var album in albums) {
|
|
||||||
if (album.name == "Recent" || album.name == "Recents") {
|
|
||||||
text += "${album.name} (${'all'.tr()}), ";
|
|
||||||
} else {
|
|
||||||
text += "${album.name}, ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
text.trim().substring(0, text.length - 2),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
"backup_controller_page_none_selected".tr(),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildExcludedAlbumName() {
|
|
||||||
String text = "backup_controller_page_excluded".tr();
|
|
||||||
final albums = ref
|
|
||||||
.watch(backupAlbumProvider)
|
|
||||||
.where(
|
|
||||||
(album) => album.backupSelection == BackupSelection.excluded,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
|
||||||
for (var album in albums) {
|
|
||||||
text += "${album.name}, ";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
text.trim().substring(0, text.length - 2),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
|
||||||
color: Colors.red[300],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFolderSelectionTile() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Card(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
side: BorderSide(
|
|
||||||
color: context.colorScheme.outlineVariant,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
borderOnForeground: false,
|
|
||||||
child: ListTile(
|
|
||||||
minVerticalPadding: 18,
|
|
||||||
title: Text(
|
|
||||||
"backup_controller_page_albums",
|
|
||||||
style: context.textTheme.titleMedium,
|
|
||||||
).tr(),
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"backup_controller_page_to_backup",
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
buildSelectedAlbumName(),
|
|
||||||
buildExcludedAlbumName(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await context.pushRoute(const ExpBackupAlbumSelectionRoute());
|
|
||||||
// waited until returning from selection
|
|
||||||
await ref
|
|
||||||
.read(backupProvider.notifier)
|
|
||||||
.backupAlbumSelectionDone();
|
|
||||||
// waited until backup albums are stored in DB
|
|
||||||
ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"select",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void startBackup() {
|
void startBackup() {
|
||||||
ref.watch(errorBackupListProvider.notifier).empty();
|
ref.watch(errorBackupListProvider.notifier).empty();
|
||||||
if (ref.watch(backupProvider).backupProgress !=
|
if (ref.watch(backupProvider).backupProgress !=
|
||||||
@ -329,61 +211,17 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
onToggle: () =>
|
onToggle: () =>
|
||||||
context.replaceRoute(const BackupControllerRoute()),
|
context.replaceRoute(const BackupControllerRoute()),
|
||||||
),
|
),
|
||||||
buildFolderSelectionTile(),
|
const SizedBox(height: 8),
|
||||||
BackupInfoCard(
|
const BackupAlbumSelectionCard(),
|
||||||
title: "total".tr(),
|
const TotalCard(),
|
||||||
subtitle: "backup_controller_page_total_sub".tr(),
|
const RemainderCard(),
|
||||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
|
||||||
? "..."
|
|
||||||
: "${backupState.allUniqueAssets.length}",
|
|
||||||
),
|
|
||||||
BackupInfoCard(
|
|
||||||
title: "backup_controller_page_backup".tr(),
|
|
||||||
subtitle: "backup_controller_page_backup_sub".tr(),
|
|
||||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
|
||||||
? "..."
|
|
||||||
: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
|
||||||
),
|
|
||||||
BackupInfoCard(
|
|
||||||
title: "backup_controller_page_remainder".tr(),
|
|
||||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
|
||||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
|
||||||
? "..."
|
|
||||||
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
|
|
||||||
),
|
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const CurrentUploadingAssetInfoBox(),
|
const CurrentUploadingAssetInfoBox(),
|
||||||
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
|
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
|
||||||
buildBackupButton(),
|
buildBackupButton(),
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref.watch(uploadServiceProvider).getRecords();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"get record",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.watch(uploadServiceProvider)
|
|
||||||
.deleteAllUploadTasks();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"clear records",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref.watch(uploadServiceProvider).cancelAllUpload();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"cancel all uploads",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
buildFolderSelectionTile(),
|
const BackupAlbumSelectionCard(),
|
||||||
if (!didGetBackupInfo.value) buildLoadingIndicator(),
|
if (!didGetBackupInfo.value) buildLoadingIndicator(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -393,3 +231,156 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BackupAlbumSelectionCard extends ConsumerWidget {
|
||||||
|
const BackupAlbumSelectionCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Widget buildSelectedAlbumName() {
|
||||||
|
String text = "backup_controller_page_backup_selected".tr();
|
||||||
|
final albums = ref
|
||||||
|
.watch(backupAlbumProvider)
|
||||||
|
.where(
|
||||||
|
(album) => album.backupSelection == BackupSelection.selected,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
if (album.name == "Recent" || album.name == "Recents") {
|
||||||
|
text += "${album.name} (${'all'.tr()}), ";
|
||||||
|
} else {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"backup_controller_page_none_selected".tr(),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildExcludedAlbumName() {
|
||||||
|
String text = "backup_controller_page_excluded".tr();
|
||||||
|
final albums = ref
|
||||||
|
.watch(backupAlbumProvider)
|
||||||
|
.where(
|
||||||
|
(album) => album.backupSelection == BackupSelection.excluded,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: Colors.red[300],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: ListTile(
|
||||||
|
minVerticalPadding: 18,
|
||||||
|
title: Text(
|
||||||
|
"backup_controller_page_albums",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
).tr(),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"backup_controller_page_to_backup",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
buildSelectedAlbumName(),
|
||||||
|
buildExcludedAlbumName(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await context.pushRoute(const ExpBackupAlbumSelectionRoute());
|
||||||
|
ref.read(expBackupProvider.notifier).getBackupStatus();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"select",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TotalCard extends ConsumerWidget {
|
||||||
|
const TotalCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final totalCount = ref.watch(expBackupProvider.select((p) => p.totalCount));
|
||||||
|
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "total".tr(),
|
||||||
|
subtitle: "backup_controller_page_total_sub".tr(),
|
||||||
|
info: totalCount.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemainderCard extends ConsumerWidget {
|
||||||
|
const RemainderCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final backupState = ref.watch(backupProvider);
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "backup_controller_page_remainder".tr(),
|
||||||
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
|
info: backupState.availableAlbums.isEmpty
|
||||||
|
? "..."
|
||||||
|
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,7 +19,6 @@ class BackupAlbumNotifier extends StateNotifier<List<LocalAlbum>> {
|
|||||||
|
|
||||||
Future<void> getAll() async {
|
Future<void> getAll() async {
|
||||||
state = await _localAlbumService.getAll();
|
state = await _localAlbumService.getAll();
|
||||||
print("Backup albums loaded: ${state.length}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectAlbum(LocalAlbum album) async {
|
Future<void> selectAlbum(LocalAlbum album) async {
|
||||||
|
73
mobile/lib/providers/backup/exp_backup.provider.dart
Normal file
73
mobile/lib/providers/backup/exp_backup.provider.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/services/exp_backup.service.dart';
|
||||||
|
|
||||||
|
class ExpBackupState {
|
||||||
|
final int totalCount;
|
||||||
|
ExpBackupState({
|
||||||
|
required this.totalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
ExpBackupState copyWith({
|
||||||
|
int? totalCount,
|
||||||
|
}) {
|
||||||
|
return ExpBackupState(
|
||||||
|
totalCount: totalCount ?? this.totalCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'totalCount': totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ExpBackupState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ExpBackupState(
|
||||||
|
totalCount: map['totalCount'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory ExpBackupState.fromJson(String source) =>
|
||||||
|
ExpBackupState.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ExpBackupState(totalCount: $totalCount)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant ExpBackupState other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.totalCount == totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => totalCount.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
final expBackupProvider =
|
||||||
|
StateNotifierProvider<ExpBackupNotifier, ExpBackupState>((ref) {
|
||||||
|
return ExpBackupNotifier(ref.watch(expBackupServiceProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
|
||||||
|
ExpBackupNotifier(this._backupService)
|
||||||
|
: super(
|
||||||
|
ExpBackupState(
|
||||||
|
totalCount: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ExpBackupService _backupService;
|
||||||
|
|
||||||
|
Future<void> getBackupStatus() async {
|
||||||
|
final totalCount = await _backupService.getTotalCount();
|
||||||
|
|
||||||
|
state = state.copyWith(totalCount: totalCount);
|
||||||
|
}
|
||||||
|
}
|
@ -336,5 +336,9 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: ExpBackupRoute.page,
|
page: ExpBackupRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: ExpBackupAlbumSelectionRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
31
mobile/lib/services/exp_backup.service.dart
Normal file
31
mobile/lib/services/exp_backup.service.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/backup.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
final expBackupServiceProvider = Provider<ExpBackupService>(
|
||||||
|
(ref) => ExpBackupService(
|
||||||
|
ref.watch(backupRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ExpBackupService {
|
||||||
|
ExpBackupService(this._backupRepository);
|
||||||
|
|
||||||
|
final IBackupRepository _backupRepository;
|
||||||
|
|
||||||
|
Future<int> getTotalCount() async {
|
||||||
|
final [selectedCount, excludedCount] = await Future.wait([
|
||||||
|
_backupRepository.getTotalCount(BackupSelection.selected),
|
||||||
|
_backupRepository.getTotalCount(BackupSelection.excluded),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return selectedCount - excludedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getBackupCount() {
|
||||||
|
return _backupRepository.getBackupCount();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user