mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-12-08 06:02:34 +00:00
315 lines
9.4 KiB
Dart
315 lines
9.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'supplement.dart';
|
|
import 'supplement_intake.dart';
|
|
import 'sync_enums.dart';
|
|
|
|
/// Model representing the complete sync data structure
|
|
/// This is what gets serialized to JSON and synced via WebDAV
|
|
class SyncData {
|
|
final int version;
|
|
final String deviceId;
|
|
final String deviceName;
|
|
final DateTime syncTimestamp;
|
|
final List<Supplement> supplements;
|
|
final List<SupplementIntake> intakes;
|
|
final Map<String, dynamic> metadata;
|
|
|
|
const SyncData({
|
|
required this.version,
|
|
required this.deviceId,
|
|
required this.deviceName,
|
|
required this.syncTimestamp,
|
|
required this.supplements,
|
|
required this.intakes,
|
|
this.metadata = const {},
|
|
});
|
|
|
|
/// Convert sync data to JSON map
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'version': version,
|
|
'deviceId': deviceId,
|
|
'deviceName': deviceName,
|
|
'syncTimestamp': syncTimestamp.toIso8601String(),
|
|
'supplements': supplements.map((s) => s.toMap()).toList(),
|
|
'intakes': intakes.map((i) => i.toMap()).toList(),
|
|
'metadata': metadata,
|
|
};
|
|
}
|
|
|
|
/// Create sync data from JSON map
|
|
factory SyncData.fromJson(Map<String, dynamic> json) {
|
|
return SyncData(
|
|
version: json['version'] ?? 1,
|
|
deviceId: json['deviceId'] ?? '',
|
|
deviceName: json['deviceName'] ?? 'Unknown Device',
|
|
syncTimestamp: json['syncTimestamp'] != null
|
|
? DateTime.parse(json['syncTimestamp'])
|
|
: DateTime.now(),
|
|
supplements: (json['supplements'] as List<dynamic>? ?? [])
|
|
.map((s) => Supplement.fromMap(s as Map<String, dynamic>))
|
|
.toList(),
|
|
intakes: (json['intakes'] as List<dynamic>? ?? [])
|
|
.map((i) => SupplementIntake.fromMap(i as Map<String, dynamic>))
|
|
.toList(),
|
|
metadata: json['metadata'] as Map<String, dynamic>? ?? {},
|
|
);
|
|
}
|
|
|
|
/// Convert sync data to JSON string
|
|
String toJsonString() {
|
|
return jsonEncode(toJson());
|
|
}
|
|
|
|
/// Create sync data from JSON string
|
|
factory SyncData.fromJsonString(String jsonString) {
|
|
final json = jsonDecode(jsonString) as Map<String, dynamic>;
|
|
return SyncData.fromJson(json);
|
|
}
|
|
|
|
/// Create a copy with updated values
|
|
SyncData copyWith({
|
|
int? version,
|
|
String? deviceId,
|
|
String? deviceName,
|
|
DateTime? syncTimestamp,
|
|
List<Supplement>? supplements,
|
|
List<SupplementIntake>? intakes,
|
|
Map<String, dynamic>? metadata,
|
|
}) {
|
|
return SyncData(
|
|
version: version ?? this.version,
|
|
deviceId: deviceId ?? this.deviceId,
|
|
deviceName: deviceName ?? this.deviceName,
|
|
syncTimestamp: syncTimestamp ?? this.syncTimestamp,
|
|
supplements: supplements ?? this.supplements,
|
|
intakes: intakes ?? this.intakes,
|
|
metadata: metadata ?? this.metadata,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'SyncData(version: $version, deviceId: $deviceId, '
|
|
'deviceName: $deviceName, syncTimestamp: $syncTimestamp, '
|
|
'supplements: ${supplements.length}, intakes: ${intakes.length})';
|
|
}
|
|
}
|
|
|
|
/// Model representing a sync conflict
|
|
class SyncConflict {
|
|
final String syncId;
|
|
final ConflictType type;
|
|
final DateTime localTimestamp;
|
|
final DateTime remoteTimestamp;
|
|
final Map<String, dynamic> localData;
|
|
final Map<String, dynamic> remoteData;
|
|
final ConflictResolutionStrategy? suggestedResolution;
|
|
|
|
const SyncConflict({
|
|
required this.syncId,
|
|
required this.type,
|
|
required this.localTimestamp,
|
|
required this.remoteTimestamp,
|
|
required this.localData,
|
|
required this.remoteData,
|
|
this.suggestedResolution,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'syncId': syncId,
|
|
'type': type.name,
|
|
'localTimestamp': localTimestamp.toIso8601String(),
|
|
'remoteTimestamp': remoteTimestamp.toIso8601String(),
|
|
'localData': localData,
|
|
'remoteData': remoteData,
|
|
'suggestedResolution': suggestedResolution?.name,
|
|
};
|
|
}
|
|
|
|
factory SyncConflict.fromJson(Map<String, dynamic> json) {
|
|
return SyncConflict(
|
|
syncId: json['syncId'],
|
|
type: ConflictType.values.firstWhere(
|
|
(e) => e.name == json['type'],
|
|
orElse: () => ConflictType.modification,
|
|
),
|
|
localTimestamp: DateTime.parse(json['localTimestamp']),
|
|
remoteTimestamp: DateTime.parse(json['remoteTimestamp']),
|
|
localData: json['localData'] as Map<String, dynamic>,
|
|
remoteData: json['remoteData'] as Map<String, dynamic>,
|
|
suggestedResolution: json['suggestedResolution'] != null
|
|
? ConflictResolutionStrategy.values.firstWhere(
|
|
(e) => e.name == json['suggestedResolution'],
|
|
orElse: () => ConflictResolutionStrategy.manual,
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
/// Get human-readable description of the conflict
|
|
String get description {
|
|
switch (type) {
|
|
case ConflictType.modification:
|
|
return 'Item modified on both devices';
|
|
case ConflictType.deletion:
|
|
return 'Item deleted on one device, modified on another';
|
|
case ConflictType.creation:
|
|
return 'Item created with same ID on both devices';
|
|
}
|
|
}
|
|
|
|
/// Determine if local version is newer
|
|
bool get isLocalNewer => localTimestamp.isAfter(remoteTimestamp);
|
|
|
|
/// Determine if remote version is newer
|
|
bool get isRemoteNewer => remoteTimestamp.isAfter(localTimestamp);
|
|
|
|
/// Check if timestamps are identical
|
|
bool get haveSameTimestamp => localTimestamp.isAtSameMomentAs(remoteTimestamp);
|
|
}
|
|
|
|
/// Types of sync conflicts
|
|
enum ConflictType {
|
|
/// Both local and remote versions were modified
|
|
modification,
|
|
|
|
/// One version was deleted while the other was modified
|
|
deletion,
|
|
|
|
/// Same item was created on both devices
|
|
creation,
|
|
}
|
|
|
|
/// Model for sync operation results
|
|
class SyncResult {
|
|
final bool success;
|
|
final DateTime timestamp;
|
|
final SyncOperationStatus status;
|
|
final String? error;
|
|
final List<SyncConflict> conflicts;
|
|
final SyncStatistics statistics;
|
|
|
|
const SyncResult({
|
|
required this.success,
|
|
required this.timestamp,
|
|
required this.status,
|
|
this.error,
|
|
this.conflicts = const [],
|
|
this.statistics = const SyncStatistics(),
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'success': success,
|
|
'timestamp': timestamp.toIso8601String(),
|
|
'status': status.name,
|
|
'error': error,
|
|
'conflicts': conflicts.map((c) => c.toJson()).toList(),
|
|
'statistics': statistics.toJson(),
|
|
};
|
|
}
|
|
|
|
factory SyncResult.fromJson(Map<String, dynamic> json) {
|
|
return SyncResult(
|
|
success: json['success'] ?? false,
|
|
timestamp: DateTime.parse(json['timestamp']),
|
|
status: SyncOperationStatus.values.firstWhere(
|
|
(e) => e.name == json['status'],
|
|
orElse: () => SyncOperationStatus.idle,
|
|
),
|
|
error: json['error'],
|
|
conflicts: (json['conflicts'] as List<dynamic>? ?? [])
|
|
.map((c) => SyncConflict.fromJson(c as Map<String, dynamic>))
|
|
.toList(),
|
|
statistics: json['statistics'] != null
|
|
? SyncStatistics.fromJson(json['statistics'] as Map<String, dynamic>)
|
|
: const SyncStatistics(),
|
|
);
|
|
}
|
|
|
|
/// Create a failed sync result
|
|
factory SyncResult.failure({
|
|
required String error,
|
|
required SyncOperationStatus status,
|
|
List<SyncConflict> conflicts = const [],
|
|
}) {
|
|
return SyncResult(
|
|
success: false,
|
|
timestamp: DateTime.now(),
|
|
status: status,
|
|
error: error,
|
|
conflicts: conflicts,
|
|
);
|
|
}
|
|
|
|
/// Create a successful sync result
|
|
factory SyncResult.success({
|
|
List<SyncConflict> conflicts = const [],
|
|
SyncStatistics statistics = const SyncStatistics(),
|
|
}) {
|
|
return SyncResult(
|
|
success: true,
|
|
timestamp: DateTime.now(),
|
|
status: conflicts.isEmpty
|
|
? SyncOperationStatus.success
|
|
: SyncOperationStatus.conflictsDetected,
|
|
conflicts: conflicts,
|
|
statistics: statistics,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Statistics about sync operations
|
|
class SyncStatistics {
|
|
final int supplementsUploaded;
|
|
final int supplementsDownloaded;
|
|
final int intakesUploaded;
|
|
final int intakesDownloaded;
|
|
final int conflictsResolved;
|
|
final Duration syncDuration;
|
|
|
|
const SyncStatistics({
|
|
this.supplementsUploaded = 0,
|
|
this.supplementsDownloaded = 0,
|
|
this.intakesUploaded = 0,
|
|
this.intakesDownloaded = 0,
|
|
this.conflictsResolved = 0,
|
|
this.syncDuration = Duration.zero,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'supplementsUploaded': supplementsUploaded,
|
|
'supplementsDownloaded': supplementsDownloaded,
|
|
'intakesUploaded': intakesUploaded,
|
|
'intakesDownloaded': intakesDownloaded,
|
|
'conflictsResolved': conflictsResolved,
|
|
'syncDurationMs': syncDuration.inMilliseconds,
|
|
};
|
|
}
|
|
|
|
factory SyncStatistics.fromJson(Map<String, dynamic> json) {
|
|
return SyncStatistics(
|
|
supplementsUploaded: json['supplementsUploaded'] ?? 0,
|
|
supplementsDownloaded: json['supplementsDownloaded'] ?? 0,
|
|
intakesUploaded: json['intakesUploaded'] ?? 0,
|
|
intakesDownloaded: json['intakesDownloaded'] ?? 0,
|
|
conflictsResolved: json['conflictsResolved'] ?? 0,
|
|
syncDuration: Duration(milliseconds: json['syncDurationMs'] ?? 0),
|
|
);
|
|
}
|
|
|
|
int get totalUploaded => supplementsUploaded + intakesUploaded;
|
|
int get totalDownloaded => supplementsDownloaded + intakesDownloaded;
|
|
int get totalSynced => totalUploaded + totalDownloaded;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'SyncStatistics(uploaded: $totalUploaded, downloaded: $totalDownloaded, '
|
|
'conflicts: $conflictsResolved, duration: ${syncDuration.inSeconds}s)';
|
|
}
|
|
}
|