mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
adds syncing
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
import 'sync_enums.dart';
|
||||
|
||||
class Ingredient {
|
||||
final int? id;
|
||||
final String name; // e.g., "Vitamin K2", "Vitamin D3"
|
||||
final double amount; // e.g., 75, 20
|
||||
final String unit; // e.g., "mcg", "mg", "IU"
|
||||
|
||||
// Sync metadata
|
||||
final String syncId;
|
||||
final DateTime lastModified;
|
||||
final SyncStatus syncStatus;
|
||||
final bool isDeleted;
|
||||
|
||||
const Ingredient({
|
||||
this.id,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
required this.unit,
|
||||
required this.syncId,
|
||||
required this.lastModified,
|
||||
this.syncStatus = SyncStatus.pending,
|
||||
this.isDeleted = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -17,6 +29,10 @@ class Ingredient {
|
||||
'name': name,
|
||||
'amount': amount,
|
||||
'unit': unit,
|
||||
'syncId': syncId,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
'syncStatus': syncStatus.name,
|
||||
'isDeleted': isDeleted ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +42,17 @@ class Ingredient {
|
||||
name: map['name'],
|
||||
amount: map['amount']?.toDouble() ?? 0.0,
|
||||
unit: map['unit'],
|
||||
syncId: map['syncId'] ?? '',
|
||||
lastModified: map['lastModified'] != null
|
||||
? DateTime.parse(map['lastModified'])
|
||||
: DateTime.now(),
|
||||
syncStatus: map['syncStatus'] != null
|
||||
? SyncStatus.values.firstWhere(
|
||||
(e) => e.name == map['syncStatus'],
|
||||
orElse: () => SyncStatus.pending,
|
||||
)
|
||||
: SyncStatus.pending,
|
||||
isDeleted: (map['isDeleted'] ?? 0) == 1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,12 +61,20 @@ class Ingredient {
|
||||
String? name,
|
||||
double? amount,
|
||||
String? unit,
|
||||
String? syncId,
|
||||
DateTime? lastModified,
|
||||
SyncStatus? syncStatus,
|
||||
bool? isDeleted,
|
||||
}) {
|
||||
return Ingredient(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
amount: amount ?? this.amount,
|
||||
unit: unit ?? this.unit,
|
||||
syncId: syncId ?? this.syncId,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
syncStatus: syncStatus ?? this.syncStatus,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,11 +89,12 @@ class Ingredient {
|
||||
return other is Ingredient &&
|
||||
other.name == name &&
|
||||
other.amount == amount &&
|
||||
other.unit == unit;
|
||||
other.unit == unit &&
|
||||
other.syncId == syncId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return name.hashCode ^ amount.hashCode ^ unit.hashCode;
|
||||
return name.hashCode ^ amount.hashCode ^ unit.hashCode ^ syncId.hashCode;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import 'ingredient.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'ingredient.dart';
|
||||
import 'sync_enums.dart';
|
||||
|
||||
class Supplement {
|
||||
final int? id;
|
||||
final String name;
|
||||
@@ -14,6 +18,12 @@ class Supplement {
|
||||
final DateTime createdAt;
|
||||
final bool isActive;
|
||||
|
||||
// Sync metadata
|
||||
final String syncId;
|
||||
final DateTime lastModified;
|
||||
final SyncStatus syncStatus;
|
||||
final bool isDeleted;
|
||||
|
||||
Supplement({
|
||||
this.id,
|
||||
required this.name,
|
||||
@@ -26,7 +36,12 @@ class Supplement {
|
||||
this.notes,
|
||||
required this.createdAt,
|
||||
this.isActive = true,
|
||||
});
|
||||
String? syncId,
|
||||
DateTime? lastModified,
|
||||
this.syncStatus = SyncStatus.pending,
|
||||
this.isDeleted = false,
|
||||
}) : syncId = syncId ?? const Uuid().v4(),
|
||||
lastModified = lastModified ?? DateTime.now();
|
||||
|
||||
// Helper getters
|
||||
double get totalDosagePerIntake {
|
||||
@@ -40,7 +55,7 @@ class Supplement {
|
||||
if (ingredients.isEmpty) {
|
||||
return 'No ingredients specified';
|
||||
}
|
||||
return ingredients.map((ingredient) =>
|
||||
return ingredients.map((ingredient) =>
|
||||
'${ingredient.amount * numberOfUnits}${ingredient.unit} ${ingredient.name}'
|
||||
).join(', ');
|
||||
}
|
||||
@@ -66,12 +81,16 @@ class Supplement {
|
||||
'notes': notes,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'isActive': isActive ? 1 : 0,
|
||||
'syncId': syncId,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
'syncStatus': syncStatus.name,
|
||||
'isDeleted': isDeleted ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
factory Supplement.fromMap(Map<String, dynamic> map) {
|
||||
List<Ingredient> ingredients = [];
|
||||
|
||||
|
||||
// Try to parse ingredients if they exist
|
||||
if (map['ingredients'] != null && map['ingredients'].isNotEmpty) {
|
||||
try {
|
||||
@@ -98,6 +117,17 @@ class Supplement {
|
||||
notes: map['notes'],
|
||||
createdAt: DateTime.parse(map['createdAt']),
|
||||
isActive: map['isActive'] == 1,
|
||||
syncId: map['syncId'] ?? const Uuid().v4(),
|
||||
lastModified: map['lastModified'] != null
|
||||
? DateTime.parse(map['lastModified'])
|
||||
: DateTime.now(),
|
||||
syncStatus: map['syncStatus'] != null
|
||||
? SyncStatus.values.firstWhere(
|
||||
(e) => e.name == map['syncStatus'],
|
||||
orElse: () => SyncStatus.pending,
|
||||
)
|
||||
: SyncStatus.pending,
|
||||
isDeleted: (map['isDeleted'] ?? 0) == 1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,6 +143,10 @@ class Supplement {
|
||||
String? notes,
|
||||
DateTime? createdAt,
|
||||
bool? isActive,
|
||||
String? syncId,
|
||||
DateTime? lastModified,
|
||||
SyncStatus? syncStatus,
|
||||
bool? isDeleted,
|
||||
}) {
|
||||
return Supplement(
|
||||
id: id ?? this.id,
|
||||
@@ -126,6 +160,34 @@ class Supplement {
|
||||
notes: notes ?? this.notes,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
syncId: syncId ?? this.syncId,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
syncStatus: syncStatus ?? this.syncStatus,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new supplement with updated sync status and timestamp
|
||||
Supplement markAsModified() {
|
||||
return copyWith(
|
||||
lastModified: DateTime.now(),
|
||||
syncStatus: SyncStatus.modified,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new supplement marked as synced
|
||||
Supplement markAsSynced() {
|
||||
return copyWith(
|
||||
syncStatus: SyncStatus.synced,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new supplement marked for deletion
|
||||
Supplement markAsDeleted() {
|
||||
return copyWith(
|
||||
isDeleted: true,
|
||||
lastModified: DateTime.now(),
|
||||
syncStatus: SyncStatus.modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,7 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'sync_enums.dart';
|
||||
|
||||
class SupplementIntake {
|
||||
final int? id;
|
||||
final int supplementId;
|
||||
@@ -6,6 +10,12 @@ class SupplementIntake {
|
||||
final double unitsTaken; // Number of units taken (can be fractional)
|
||||
final String? notes;
|
||||
|
||||
// Sync metadata
|
||||
final String syncId;
|
||||
final DateTime lastModified;
|
||||
final SyncStatus syncStatus;
|
||||
final bool isDeleted;
|
||||
|
||||
SupplementIntake({
|
||||
this.id,
|
||||
required this.supplementId,
|
||||
@@ -13,7 +23,12 @@ class SupplementIntake {
|
||||
required this.dosageTaken,
|
||||
required this.unitsTaken,
|
||||
this.notes,
|
||||
});
|
||||
String? syncId,
|
||||
DateTime? lastModified,
|
||||
this.syncStatus = SyncStatus.pending,
|
||||
this.isDeleted = false,
|
||||
}) : syncId = syncId ?? const Uuid().v4(),
|
||||
lastModified = lastModified ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
@@ -23,6 +38,10 @@ class SupplementIntake {
|
||||
'dosageTaken': dosageTaken,
|
||||
'unitsTaken': unitsTaken,
|
||||
'notes': notes,
|
||||
'syncId': syncId,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
'syncStatus': syncStatus.name,
|
||||
'isDeleted': isDeleted ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +53,17 @@ class SupplementIntake {
|
||||
dosageTaken: map['dosageTaken'],
|
||||
unitsTaken: (map['unitsTaken'] ?? 1).toDouble(), // Default for backwards compatibility
|
||||
notes: map['notes'],
|
||||
syncId: map['syncId'] ?? const Uuid().v4(),
|
||||
lastModified: map['lastModified'] != null
|
||||
? DateTime.parse(map['lastModified'])
|
||||
: DateTime.now(),
|
||||
syncStatus: map['syncStatus'] != null
|
||||
? SyncStatus.values.firstWhere(
|
||||
(e) => e.name == map['syncStatus'],
|
||||
orElse: () => SyncStatus.pending,
|
||||
)
|
||||
: SyncStatus.pending,
|
||||
isDeleted: (map['isDeleted'] ?? 0) == 1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +74,10 @@ class SupplementIntake {
|
||||
double? dosageTaken,
|
||||
double? unitsTaken,
|
||||
String? notes,
|
||||
String? syncId,
|
||||
DateTime? lastModified,
|
||||
SyncStatus? syncStatus,
|
||||
bool? isDeleted,
|
||||
}) {
|
||||
return SupplementIntake(
|
||||
id: id ?? this.id,
|
||||
@@ -52,6 +86,26 @@ class SupplementIntake {
|
||||
dosageTaken: dosageTaken ?? this.dosageTaken,
|
||||
unitsTaken: unitsTaken ?? this.unitsTaken,
|
||||
notes: notes ?? this.notes,
|
||||
syncId: syncId ?? this.syncId,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
syncStatus: syncStatus ?? this.syncStatus,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new intake marked as synced
|
||||
SupplementIntake markAsSynced() {
|
||||
return copyWith(
|
||||
syncStatus: SyncStatus.synced,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new intake marked for deletion
|
||||
SupplementIntake markAsDeleted() {
|
||||
return copyWith(
|
||||
isDeleted: true,
|
||||
lastModified: DateTime.now(),
|
||||
syncStatus: SyncStatus.modified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
314
lib/models/sync_data.dart
Normal file
314
lib/models/sync_data.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
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)';
|
||||
}
|
||||
}
|
178
lib/models/sync_enums.dart
Normal file
178
lib/models/sync_enums.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
/// Enumeration for sync status of individual records
|
||||
enum SyncStatus {
|
||||
/// Record is newly created and needs to be synced
|
||||
pending,
|
||||
|
||||
/// Record is in sync with remote server
|
||||
synced,
|
||||
|
||||
/// Record has been modified locally and needs to be synced
|
||||
modified,
|
||||
|
||||
/// Record has a conflict that needs resolution
|
||||
conflict,
|
||||
|
||||
/// Record is being synced (temporary state)
|
||||
syncing,
|
||||
|
||||
/// Record sync failed and will be retried
|
||||
failed,
|
||||
}
|
||||
|
||||
/// Enumeration for overall sync operation status
|
||||
enum SyncOperationStatus {
|
||||
/// No sync operation in progress
|
||||
idle,
|
||||
|
||||
/// Currently syncing data
|
||||
syncing,
|
||||
|
||||
/// Last sync completed successfully
|
||||
success,
|
||||
|
||||
/// Last sync failed due to network issues
|
||||
networkError,
|
||||
|
||||
/// Last sync failed due to authentication issues
|
||||
authenticationError,
|
||||
|
||||
/// Last sync failed due to server issues
|
||||
serverError,
|
||||
|
||||
/// Last sync had conflicts that need resolution
|
||||
conflictsDetected,
|
||||
|
||||
/// Sync cancelled by user
|
||||
cancelled,
|
||||
}
|
||||
|
||||
/// Enumeration for conflict resolution strategies
|
||||
enum ConflictResolutionStrategy {
|
||||
/// Always prefer local changes
|
||||
preferLocal,
|
||||
|
||||
/// Always prefer remote changes
|
||||
preferRemote,
|
||||
|
||||
/// Prefer the most recently modified record
|
||||
preferNewer,
|
||||
|
||||
/// Ask user to resolve conflicts manually
|
||||
manual,
|
||||
}
|
||||
|
||||
/// Enumeration for sync frequency options
|
||||
enum SyncFrequency {
|
||||
/// Manual sync only
|
||||
manual,
|
||||
|
||||
/// Sync every 15 minutes
|
||||
every15Minutes,
|
||||
|
||||
/// Sync every hour
|
||||
hourly,
|
||||
|
||||
/// Sync every 6 hours
|
||||
every6Hours,
|
||||
|
||||
/// Sync once per day
|
||||
daily,
|
||||
}
|
||||
|
||||
/// Extension to get human-readable names for SyncStatus
|
||||
extension SyncStatusExtension on SyncStatus {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case SyncStatus.pending:
|
||||
return 'Pending Sync';
|
||||
case SyncStatus.synced:
|
||||
return 'Synced';
|
||||
case SyncStatus.modified:
|
||||
return 'Modified';
|
||||
case SyncStatus.conflict:
|
||||
return 'Conflict';
|
||||
case SyncStatus.syncing:
|
||||
return 'Syncing...';
|
||||
case SyncStatus.failed:
|
||||
return 'Sync Failed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to get human-readable names for SyncOperationStatus
|
||||
extension SyncOperationStatusExtension on SyncOperationStatus {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case SyncOperationStatus.idle:
|
||||
return 'Ready';
|
||||
case SyncOperationStatus.syncing:
|
||||
return 'Syncing...';
|
||||
case SyncOperationStatus.success:
|
||||
return 'Sync Complete';
|
||||
case SyncOperationStatus.networkError:
|
||||
return 'Network Error';
|
||||
case SyncOperationStatus.authenticationError:
|
||||
return 'Authentication Failed';
|
||||
case SyncOperationStatus.serverError:
|
||||
return 'Server Error';
|
||||
case SyncOperationStatus.conflictsDetected:
|
||||
return 'Conflicts Detected';
|
||||
case SyncOperationStatus.cancelled:
|
||||
return 'Sync Cancelled';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to get duration for SyncFrequency
|
||||
extension SyncFrequencyExtension on SyncFrequency {
|
||||
Duration get duration {
|
||||
switch (this) {
|
||||
case SyncFrequency.manual:
|
||||
return Duration.zero; // Manual sync only
|
||||
case SyncFrequency.every15Minutes:
|
||||
return const Duration(minutes: 15);
|
||||
case SyncFrequency.hourly:
|
||||
return const Duration(hours: 1);
|
||||
case SyncFrequency.every6Hours:
|
||||
return const Duration(hours: 6);
|
||||
case SyncFrequency.daily:
|
||||
return const Duration(days: 1);
|
||||
}
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case SyncFrequency.manual:
|
||||
return 'Manual Only';
|
||||
case SyncFrequency.every15Minutes:
|
||||
return 'Every 15 Minutes';
|
||||
case SyncFrequency.hourly:
|
||||
return 'Hourly';
|
||||
case SyncFrequency.every6Hours:
|
||||
return 'Every 6 Hours';
|
||||
case SyncFrequency.daily:
|
||||
return 'Daily';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants for sync operations
|
||||
class SyncConstants {
|
||||
static const String syncFileName = 'supplements_sync.json';
|
||||
static const String syncFileBackupName = 'supplements_sync_backup.json';
|
||||
static const int currentSyncVersion = 1;
|
||||
static const int maxRetryAttempts = 3;
|
||||
static const Duration networkTimeout = Duration(seconds: 30);
|
||||
static const Duration conflictResolutionTimeout = Duration(minutes: 5);
|
||||
|
||||
/// Default WebDAV paths for different cloud providers
|
||||
static const String nextcloudWebdavPath = '/remote.php/dav/files/';
|
||||
static const String owncloudWebdavPath = '/remote.php/webdav/';
|
||||
|
||||
/// Supported cloud providers
|
||||
static const List<String> supportedProviders = [
|
||||
'Nextcloud',
|
||||
'ownCloud',
|
||||
'Generic WebDAV',
|
||||
];
|
||||
}
|
Reference in New Issue
Block a user