320 lines
8.6 KiB
Dart
320 lines
8.6 KiB
Dart
import 'package:notas/data/note_encryption.dart';
|
|
import 'package:notas/models/note.dart';
|
|
import 'package:notas/models/category.dart';
|
|
import 'dart:convert';
|
|
|
|
// DTOs para sincronización con el servidor
|
|
|
|
class SyncRequest {
|
|
SyncRequest({DateTime? lastSyncAt, required this.changes})
|
|
: lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
|
|
|
final DateTime lastSyncAt;
|
|
final SyncChanges changes;
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'lastSyncAt': lastSyncAt.toIso8601String(),
|
|
'changes': changes.toJson(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class SyncChanges {
|
|
const SyncChanges({this.categories = const [], this.notes = const []});
|
|
|
|
final List<SyncCategoryPayload> categories;
|
|
final List<SyncNotePayload> notes;
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
if (categories.isNotEmpty)
|
|
'categories': categories.map((c) => c.toJson()).toList(),
|
|
if (notes.isNotEmpty) 'notes': notes.map((n) => n.toJson()).toList(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class SyncChangesResponse {
|
|
const SyncChangesResponse({
|
|
this.categories = const [],
|
|
this.notes = const [],
|
|
});
|
|
|
|
final List<SyncCategoryResponse> categories;
|
|
final List<SyncNoteResponse> notes;
|
|
|
|
factory SyncChangesResponse.fromJson(Map<String, dynamic> json) {
|
|
final List<dynamic> categoriesJson =
|
|
json['categories'] as List<dynamic>? ?? [];
|
|
final List<dynamic> notesJson = json['notes'] as List<dynamic>? ?? [];
|
|
|
|
return SyncChangesResponse(
|
|
categories: categoriesJson
|
|
.map((c) => SyncCategoryResponse.fromJson(c as Map<String, dynamic>))
|
|
.toList(),
|
|
notes: notesJson
|
|
.map((n) => SyncNoteResponse.fromJson(n as Map<String, dynamic>))
|
|
.toList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _readStringValue(dynamic value) {
|
|
if (value is String) {
|
|
return value;
|
|
}
|
|
if (value == null) {
|
|
throw FormatException('Expected String value but found null');
|
|
}
|
|
return jsonEncode(value);
|
|
}
|
|
|
|
String _readOptionalStringValue(dynamic value) {
|
|
if (value == null) {
|
|
return '';
|
|
}
|
|
return _readStringValue(value);
|
|
}
|
|
|
|
int _readIntValue(dynamic value) {
|
|
if (value is int) {
|
|
return value;
|
|
}
|
|
if (value is String) {
|
|
final int? parsed = int.tryParse(value);
|
|
if (parsed != null) {
|
|
return parsed;
|
|
}
|
|
}
|
|
throw FormatException('Expected int value but found $value');
|
|
}
|
|
|
|
class SyncCategoryPayload {
|
|
const SyncCategoryPayload({
|
|
required this.id,
|
|
required this.encryptedName,
|
|
required this.serverVersion,
|
|
this.isDeleted = false,
|
|
this.colorValue,
|
|
this.iconCodePoint,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
final String id;
|
|
final String encryptedName;
|
|
final int serverVersion;
|
|
final bool isDeleted;
|
|
final int? colorValue;
|
|
final int? iconCodePoint;
|
|
final DateTime updatedAt;
|
|
|
|
static Future<SyncCategoryPayload> fromCategory(
|
|
Category category, {
|
|
required String masterKey,
|
|
}) async {
|
|
return SyncCategoryPayload(
|
|
id: category.id,
|
|
encryptedName: await NoteEncryption.encryptNote(category.name, masterKey),
|
|
serverVersion: category.serverVersion,
|
|
isDeleted: category.isDeleted,
|
|
colorValue: category.colorValue,
|
|
iconCodePoint: category.iconCodePoint,
|
|
updatedAt: category.updatedAt,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'encrypted_name': encryptedName,
|
|
'serverVersion': serverVersion,
|
|
'isDeleted': isDeleted,
|
|
if (colorValue != null) 'colorValue': colorValue!.toSigned(32),
|
|
if (iconCodePoint != null) 'iconCodePoint': iconCodePoint,
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class SyncNotePayload {
|
|
const SyncNotePayload({
|
|
required this.id,
|
|
this.categoryId,
|
|
required this.encryptedTitle,
|
|
required this.encryptedBody,
|
|
required this.serverVersion,
|
|
this.position = 0.0,
|
|
this.isDeleted = false,
|
|
this.isPermanentlyDeleted = false,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
final String id;
|
|
final String? categoryId;
|
|
final String encryptedTitle;
|
|
final String encryptedBody;
|
|
final int serverVersion;
|
|
final double position;
|
|
final bool isDeleted;
|
|
final bool isPermanentlyDeleted;
|
|
final DateTime updatedAt;
|
|
|
|
factory SyncNotePayload.fromNote(
|
|
Note note, {
|
|
required String encryptedTitle,
|
|
required String encryptedBody,
|
|
bool isPermanentlyDeleted = false,
|
|
}) {
|
|
return SyncNotePayload(
|
|
id: note.id,
|
|
categoryId: note.categoryId,
|
|
encryptedTitle: encryptedTitle,
|
|
encryptedBody: encryptedBody,
|
|
serverVersion: note.serverVersion,
|
|
position: note.position,
|
|
isDeleted: note.isDeleted,
|
|
isPermanentlyDeleted: isPermanentlyDeleted,
|
|
updatedAt: note.updatedAt,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
if (categoryId != null) 'categoryId': categoryId,
|
|
'encrypted_title': encryptedTitle,
|
|
'encrypted_body': encryptedBody,
|
|
'serverVersion': serverVersion,
|
|
if (position != 0) 'position': position,
|
|
if (isDeleted) 'isDeleted': isDeleted,
|
|
if (isPermanentlyDeleted) 'isPermanentlyDeleted': isPermanentlyDeleted,
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class SyncResponse {
|
|
const SyncResponse({
|
|
required this.serverTimestamp,
|
|
required this.synced,
|
|
required this.changes,
|
|
});
|
|
|
|
final DateTime serverTimestamp;
|
|
final bool synced;
|
|
final SyncChangesResponse changes;
|
|
|
|
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
|
return SyncResponse(
|
|
serverTimestamp: DateTime.parse(json['serverTimestamp'] as String),
|
|
synced: json['synced'] as bool? ?? false,
|
|
changes: SyncChangesResponse.fromJson(
|
|
json['changes'] as Map<String, dynamic>? ?? {},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SyncCategoryResponse {
|
|
const SyncCategoryResponse({
|
|
required this.id,
|
|
required this.encryptedName,
|
|
required this.serverVersion,
|
|
this.isDeleted = false,
|
|
this.colorValue,
|
|
this.iconCodePoint,
|
|
required this.updatedAt,
|
|
});
|
|
final String id;
|
|
final String encryptedName;
|
|
final int serverVersion;
|
|
final bool isDeleted;
|
|
final int? colorValue;
|
|
final int? iconCodePoint;
|
|
final DateTime updatedAt;
|
|
|
|
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
|
return SyncCategoryResponse(
|
|
id: _readStringValue(json['id']),
|
|
encryptedName: _readStringValue(json['encrypted_name']),
|
|
serverVersion: _readIntValue(json['serverVersion']),
|
|
isDeleted: json['isDeleted'] as bool? ?? false,
|
|
colorValue: json['colorValue'] == null
|
|
? null
|
|
: _readIntValue(json['colorValue']),
|
|
iconCodePoint: json['iconCodePoint'] == null
|
|
? null
|
|
: _readIntValue(json['iconCodePoint']),
|
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
|
);
|
|
}
|
|
|
|
Future<Category> toCategory({required String masterKey}) async {
|
|
return Category(
|
|
id: id,
|
|
name: await NoteEncryption.decrypt(encryptedName, masterKey),
|
|
serverVersion: serverVersion,
|
|
isDeleted: isDeleted,
|
|
colorValue: colorValue,
|
|
iconCodePoint: iconCodePoint,
|
|
updatedAt: updatedAt,
|
|
isDirty: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SyncNoteResponse {
|
|
const SyncNoteResponse({
|
|
required this.id,
|
|
this.categoryId,
|
|
required this.encryptedTitle,
|
|
required this.encryptedBody,
|
|
required this.serverVersion,
|
|
this.position = 0.0,
|
|
this.isDeleted = false,
|
|
this.isPermanentlyDeleted = false,
|
|
required this.updatedAt,
|
|
});
|
|
final String id;
|
|
final String? categoryId;
|
|
final String encryptedTitle;
|
|
final String encryptedBody;
|
|
final int serverVersion;
|
|
final double position;
|
|
final bool isDeleted;
|
|
final bool isPermanentlyDeleted;
|
|
final DateTime updatedAt;
|
|
|
|
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
|
return SyncNoteResponse(
|
|
id: _readStringValue(json['id']),
|
|
categoryId: json['categoryId'] == null
|
|
? null
|
|
: _readStringValue(json['categoryId']),
|
|
encryptedTitle: _readOptionalStringValue(json['encrypted_title']),
|
|
encryptedBody: _readOptionalStringValue(json['encrypted_body']),
|
|
serverVersion: _readIntValue(json['serverVersion']),
|
|
position: (json['position'] as num?)?.toDouble() ?? 0,
|
|
isDeleted: json['isDeleted'] as bool? ?? false,
|
|
isPermanentlyDeleted: json['isPermanentlyDeleted'] as bool? ?? false,
|
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
|
);
|
|
}
|
|
|
|
Note toNote() {
|
|
return Note(
|
|
id: id,
|
|
title: isPermanentlyDeleted ? '' : 'Encrypted',
|
|
body: isPermanentlyDeleted ? '' : 'Encrypted',
|
|
createdAt: updatedAt,
|
|
updatedAt: updatedAt,
|
|
position: position,
|
|
serverVersion: serverVersion,
|
|
isDeleted: isDeleted,
|
|
categoryId: categoryId,
|
|
isDirty: false,
|
|
);
|
|
}
|
|
}
|