feat: add local backup export import core

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:23:30 +02:00
parent b256595e08
commit c3d5273869
6 changed files with 1380 additions and 4 deletions

View File

@ -1,3 +1,5 @@
import 'dart:typed_data';
import '../domain/domain.dart';
abstract interface class Clock {
@ -34,6 +36,213 @@ final class TagUsage {
final int count;
}
enum LocalBackupImportMode { merge, replaceAll }
enum LocalBackupValidationError {
invalidFile,
incompatibleFormat,
newerVersion,
corrupted,
activeWorkoutInProgress,
importFailedNoMutation,
}
final class LocalBackupException implements Exception {
const LocalBackupException(this.error);
final LocalBackupValidationError error;
@override
String toString() => error.name;
}
final class LocalBackupDocument {
const LocalBackupDocument({required this.fileName, required this.bytes});
final String fileName;
final Uint8List bytes;
}
final class LocalBackupCounts {
const LocalBackupCounts({
required this.exercises,
required this.programs,
required this.workoutTemplates,
required this.workoutHistories,
required this.mediaAssets,
required this.embeddedMediaFiles,
});
final int exercises;
final int programs;
final int workoutTemplates;
final int workoutHistories;
final int mediaAssets;
final int embeddedMediaFiles;
}
final class LocalBackupPreview {
const LocalBackupPreview({
required this.exportedAt,
required this.counts,
required this.hasEmbeddedMedia,
required this.missingMediaCount,
required this.hasNameDuplicates,
});
final DateTime exportedAt;
final LocalBackupCounts counts;
final bool hasEmbeddedMedia;
final int missingMediaCount;
final bool hasNameDuplicates;
}
final class LocalBackupImportResult {
const LocalBackupImportResult({
required this.insertedCount,
required this.updatedCount,
required this.ignoredOlderCount,
required this.deletedByReplaceCount,
required this.missingMediaCount,
});
final int insertedCount;
final int updatedCount;
final int ignoredOlderCount;
final int deletedByReplaceCount;
final int missingMediaCount;
LocalBackupImportResult copyWith({int? missingMediaCount}) {
return LocalBackupImportResult(
insertedCount: insertedCount,
updatedCount: updatedCount,
ignoredOlderCount: ignoredOlderCount,
deletedByReplaceCount: deletedByReplaceCount,
missingMediaCount: missingMediaCount ?? this.missingMediaCount,
);
}
}
final class LocalBackupResource {
const LocalBackupResource({
required this.id,
required this.updatedAt,
required this.payload,
});
final String id;
final DateTime updatedAt;
final Map<String, Object?> payload;
LocalBackupResource copyWith({Map<String, Object?>? payload}) {
return LocalBackupResource(
id: id,
updatedAt: updatedAt,
payload: payload ?? this.payload,
);
}
}
final class LocalDataExportSnapshot {
const LocalDataExportSnapshot({
required this.exportedAt,
required this.appSchemaVersion,
required this.originDeviceId,
required this.mediaAssets,
required this.exercises,
required this.programs,
required this.workoutTemplates,
required this.workoutHistories,
this.mediaFiles = const [],
});
final DateTime exportedAt;
final int appSchemaVersion;
final String originDeviceId;
final List<LocalBackupResource> mediaAssets;
final List<LocalBackupResource> exercises;
final List<LocalBackupResource> programs;
final List<LocalBackupResource> workoutTemplates;
final List<LocalBackupResource> workoutHistories;
final List<EmbeddedBackupMediaFile> mediaFiles;
LocalBackupCounts get counts => LocalBackupCounts(
exercises: exercises.length,
programs: programs.length,
workoutTemplates: workoutTemplates.length,
workoutHistories: workoutHistories.length,
mediaAssets: mediaAssets.length,
embeddedMediaFiles: mediaFiles.length,
);
LocalDataExportSnapshot copyWith({
List<LocalBackupResource>? mediaAssets,
List<EmbeddedBackupMediaFile>? mediaFiles,
}) {
return LocalDataExportSnapshot(
exportedAt: exportedAt,
appSchemaVersion: appSchemaVersion,
originDeviceId: originDeviceId,
mediaAssets: mediaAssets ?? this.mediaAssets,
exercises: exercises,
programs: programs,
workoutTemplates: workoutTemplates,
workoutHistories: workoutHistories,
mediaFiles: mediaFiles ?? this.mediaFiles,
);
}
}
final class EmbeddedBackupMediaFile {
const EmbeddedBackupMediaFile({
required this.mediaAssetId,
required this.role,
required this.fileName,
required this.mimeType,
required this.sizeBytes,
required this.base64,
});
final String mediaAssetId;
final String role;
final String fileName;
final String? mimeType;
final int sizeBytes;
final String base64;
}
final class RestoredMediaFile {
const RestoredMediaFile({
required this.mediaAssetId,
required this.localUri,
required this.sizeBytes,
});
final String mediaAssetId;
final String localUri;
final int sizeBytes;
}
abstract interface class LocalDataBackupRepository {
Future<LocalDataExportSnapshot> readExportSnapshot(DateTime exportedAt);
Future<bool> hasOpenActiveWorkoutSession();
Future<bool> hasAnyUserData();
Future<LocalBackupImportResult> applyImportSnapshot({
required LocalDataExportSnapshot snapshot,
required LocalBackupImportMode mode,
required DateTime importedAt,
});
}
abstract interface class LocalBackupMediaStore {
Future<List<EmbeddedBackupMediaFile>> readEmbeddableFiles(
List<LocalBackupResource> mediaAssets,
);
Future<Map<String, RestoredMediaFile>> restoreEmbeddedFiles(
List<EmbeddedBackupMediaFile> files,
);
}
enum ProgressionPeriod { fourWeeks, threeMonths, all }
enum ProgressionMeasure { manualScore, stopwatchScore, reps, time }