feat: Optimize note encryption and decryption processes with parallel execution
This commit is contained in:
+270
-50
@@ -1,3 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:notas/data/app_database.dart';
|
import 'package:notas/data/app_database.dart';
|
||||||
import 'package:notas/data/api_client.dart';
|
import 'package:notas/data/api_client.dart';
|
||||||
@@ -126,8 +131,7 @@ class NoteRepository {
|
|||||||
final int totalNotesToEncrypt = unsyncedNotes.length;
|
final int totalNotesToEncrypt = unsyncedNotes.length;
|
||||||
|
|
||||||
// Build sync request (note: we send encrypted data, but locally we have plaintext)
|
// Build sync request (note: we send encrypted data, but locally we have plaintext)
|
||||||
// Encrypt all notes before sending
|
// Encrypt all notes before sending.
|
||||||
final List<SyncNotePayload> encryptedNotesPayload = [];
|
|
||||||
if (totalNotesToEncrypt == 0) {
|
if (totalNotesToEncrypt == 0) {
|
||||||
onProgress?.call(
|
onProgress?.call(
|
||||||
SyncStatus.encrypting,
|
SyncStatus.encrypting,
|
||||||
@@ -136,32 +140,21 @@ class NoteRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var index = 0; index < unsyncedNotes.length; index += 1) {
|
final List<SyncNotePayload>
|
||||||
final DbNote dbNote = unsyncedNotes[index];
|
encryptedNotesPayload = await _encryptNotesInParallel(
|
||||||
final note = _fromDbNote(dbNote);
|
unsyncedNotes,
|
||||||
final encryptedTitle = await NoteEncryption.encryptNote(
|
masterKey: _masterKey,
|
||||||
note.title,
|
onProgress: (int encryptedCount) {
|
||||||
_masterKey,
|
onProgress?.call(
|
||||||
);
|
SyncStatus.encrypting,
|
||||||
final encryptedBody = await NoteEncryption.encryptNote(
|
progress: totalNotesToEncrypt == 0
|
||||||
note.body,
|
? 1.0
|
||||||
_masterKey,
|
: encryptedCount / totalNotesToEncrypt,
|
||||||
);
|
message:
|
||||||
encryptedNotesPayload.add(
|
'Encriptando notas para subir: $encryptedCount de $totalNotesToEncrypt',
|
||||||
SyncNotePayload.fromNote(
|
);
|
||||||
note,
|
},
|
||||||
encryptedTitle: encryptedTitle,
|
);
|
||||||
encryptedBody: encryptedBody,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
onProgress?.call(
|
|
||||||
SyncStatus.encrypting,
|
|
||||||
progress: (index + 1) / totalNotesToEncrypt,
|
|
||||||
message:
|
|
||||||
'Encriptando notas para subir: ${index + 1} de $totalNotesToEncrypt',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
|
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
|
||||||
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
|
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
|
||||||
@@ -241,35 +234,33 @@ class NoteRepository {
|
|||||||
|
|
||||||
// Apply notes from server
|
// Apply notes from server
|
||||||
final int totalNotesToDecrypt = response.changes.notes.length;
|
final int totalNotesToDecrypt = response.changes.notes.length;
|
||||||
for (var index = 0; index < response.changes.notes.length; index += 1) {
|
final List<Map<String, Object?>> decryptedNotes =
|
||||||
final SyncNoteResponse noteResponse = response.changes.notes[index];
|
await _decryptResponseNotesInParallel(
|
||||||
|
response.changes.notes,
|
||||||
|
masterKey: _masterKey,
|
||||||
|
onProgress: onDecryptProgress == null
|
||||||
|
? null
|
||||||
|
: (processed) =>
|
||||||
|
onDecryptProgress(processed, totalNotesToDecrypt),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var index = 0; index < decryptedNotes.length; index += 1) {
|
||||||
|
final Map<String, Object?> decryptedNote = decryptedNotes[index];
|
||||||
|
final String noteId = decryptedNote['id']! as String;
|
||||||
final existingNote = await (_database.select(
|
final existingNote = await (_database.select(
|
||||||
_database.notes,
|
_database.notes,
|
||||||
)..where((n) => n.uuid.equals(noteResponse.id))).getSingleOrNull();
|
)..where((n) => n.uuid.equals(noteId))).getSingleOrNull();
|
||||||
|
|
||||||
// Decrypt note content
|
final String decryptedTitle = decryptedNote['title']! as String;
|
||||||
String decryptedTitle = 'Encrypted';
|
final String decryptedBody = decryptedNote['body']! as String;
|
||||||
String decryptedBody = 'Encrypted';
|
final SyncNoteResponse noteResponse = response.changes.notes[index];
|
||||||
try {
|
|
||||||
decryptedTitle = await NoteEncryption.decryptNote(
|
|
||||||
noteResponse.encryptedTitle,
|
|
||||||
_masterKey,
|
|
||||||
);
|
|
||||||
decryptedBody = await NoteEncryption.decryptNote(
|
|
||||||
noteResponse.encryptedBody,
|
|
||||||
_masterKey,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// If decryption fails, keep default encrypted placeholders
|
|
||||||
print('Failed to decrypt note ${noteResponse.id}: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingNote != null) {
|
if (existingNote != null) {
|
||||||
// Update existing note
|
// Update existing note
|
||||||
await _database.updateNoteRow(
|
await _database.updateNoteRow(
|
||||||
DbNote(
|
DbNote(
|
||||||
id: existingNote.id,
|
id: existingNote.id,
|
||||||
uuid: noteResponse.id,
|
uuid: noteId,
|
||||||
title: decryptedTitle,
|
title: decryptedTitle,
|
||||||
body: decryptedBody,
|
body: decryptedBody,
|
||||||
createdAt: existingNote.createdAt,
|
createdAt: existingNote.createdAt,
|
||||||
@@ -298,8 +289,6 @@ class NoteRepository {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDecryptProgress?.call(index + 1, totalNotesToDecrypt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,3 +322,234 @@ class NoteRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Future<List<Map<String, Object?>>>> _encryptNoteBatches(
|
||||||
|
List<DbNote> notes, {
|
||||||
|
required String masterKey,
|
||||||
|
}) {
|
||||||
|
if (notes.isEmpty) {
|
||||||
|
return <Future<List<Map<String, Object?>>>>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final int workerCount = _parallelWorkerCount(notes.length);
|
||||||
|
final int batchSize = (notes.length / workerCount).ceil();
|
||||||
|
|
||||||
|
final List<Future<List<Map<String, Object?>>>> batchFutures = [];
|
||||||
|
for (var start = 0; start < notes.length; start += batchSize) {
|
||||||
|
final int end = math.min(start + batchSize, notes.length);
|
||||||
|
final List<Map<String, Object?>> batchNotes = [];
|
||||||
|
for (var index = start; index < end; index += 1) {
|
||||||
|
batchNotes.add(_dbNoteToEncryptionInput(notes[index], index));
|
||||||
|
}
|
||||||
|
|
||||||
|
batchFutures.add(
|
||||||
|
Isolate.run(
|
||||||
|
() => _encryptNoteBatch({'masterKey': masterKey, 'notes': batchNotes}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return batchFutures;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SyncNotePayload>> _encryptNotesInParallel(
|
||||||
|
List<DbNote> notes, {
|
||||||
|
required String masterKey,
|
||||||
|
void Function(int encryptedCount)? onProgress,
|
||||||
|
}) async {
|
||||||
|
final List<Future<List<Map<String, Object?>>>> batchFutures =
|
||||||
|
_encryptNoteBatches(notes, masterKey: masterKey);
|
||||||
|
final List<SyncNotePayload?> orderedPayloads = List<SyncNotePayload?>.filled(
|
||||||
|
notes.length,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
var encryptedCount = 0;
|
||||||
|
|
||||||
|
await for (final List<Map<String, Object?>> batchResult in Stream.fromFutures(
|
||||||
|
batchFutures,
|
||||||
|
)) {
|
||||||
|
for (final Map<String, Object?> row in batchResult) {
|
||||||
|
final int index = row['index']! as int;
|
||||||
|
orderedPayloads[index] = _syncNotePayloadFromEncryptionResult(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedCount += batchResult.length;
|
||||||
|
onProgress?.call(encryptedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedPayloads.cast<SyncNotePayload>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Future<List<Map<String, Object?>>>> _decryptNoteBatches(
|
||||||
|
List<SyncNoteResponse> notes, {
|
||||||
|
required String masterKey,
|
||||||
|
}) {
|
||||||
|
if (notes.isEmpty) {
|
||||||
|
return <Future<List<Map<String, Object?>>>>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final int workerCount = _parallelWorkerCount(notes.length);
|
||||||
|
final int batchSize = (notes.length / workerCount).ceil();
|
||||||
|
|
||||||
|
final List<Future<List<Map<String, Object?>>>> batchFutures = [];
|
||||||
|
for (var start = 0; start < notes.length; start += batchSize) {
|
||||||
|
final int end = math.min(start + batchSize, notes.length);
|
||||||
|
final List<Map<String, Object?>> batchNotes = [];
|
||||||
|
for (var index = start; index < end; index += 1) {
|
||||||
|
batchNotes.add(_syncNoteToDecryptionInput(notes[index], index));
|
||||||
|
}
|
||||||
|
|
||||||
|
batchFutures.add(
|
||||||
|
Isolate.run(
|
||||||
|
() => _decryptNoteBatch({'masterKey': masterKey, 'notes': batchNotes}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return batchFutures;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> _decryptResponseNotesInParallel(
|
||||||
|
List<SyncNoteResponse> notes, {
|
||||||
|
required String masterKey,
|
||||||
|
void Function(int processed)? onProgress,
|
||||||
|
}) async {
|
||||||
|
final List<Future<List<Map<String, Object?>>>> batchFutures =
|
||||||
|
_decryptNoteBatches(notes, masterKey: masterKey);
|
||||||
|
final List<Map<String, Object?>?> decryptedNotes =
|
||||||
|
List<Map<String, Object?>?>.filled(notes.length, null);
|
||||||
|
var processed = 0;
|
||||||
|
|
||||||
|
await for (final List<Map<String, Object?>> batchResult in Stream.fromFutures(
|
||||||
|
batchFutures,
|
||||||
|
)) {
|
||||||
|
for (final Map<String, Object?> row in batchResult) {
|
||||||
|
final int index = row['index']! as int;
|
||||||
|
decryptedNotes[index] = row;
|
||||||
|
}
|
||||||
|
processed += batchResult.length;
|
||||||
|
onProgress?.call(processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedNotes.cast<Map<String, Object?>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> _syncNoteToDecryptionInput(
|
||||||
|
SyncNoteResponse row,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
return <String, Object?>{
|
||||||
|
'index': index,
|
||||||
|
'id': row.id,
|
||||||
|
'encryptedTitle': row.encryptedTitle,
|
||||||
|
'encryptedBody': row.encryptedBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
|
||||||
|
return <String, Object?>{
|
||||||
|
'index': index,
|
||||||
|
'uuid': row.uuid,
|
||||||
|
'title': row.title,
|
||||||
|
'body': row.body,
|
||||||
|
'createdAt': row.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': row.updatedAt.toIso8601String(),
|
||||||
|
'categoryId': row.categoryId,
|
||||||
|
'serverVersion': row.serverVersion,
|
||||||
|
'position': row.sortIndex,
|
||||||
|
'isDeleted': row.isDeleted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
|
||||||
|
return SyncNotePayload(
|
||||||
|
id: row['id']! as String,
|
||||||
|
categoryId: row['categoryId'] as String?,
|
||||||
|
encryptedTitle: row['encryptedTitle']! as String,
|
||||||
|
encryptedBody: row['encryptedBody']! as String,
|
||||||
|
serverVersion: row['serverVersion']! as int,
|
||||||
|
position: row['position']! as int,
|
||||||
|
isDeleted: row['isDeleted']! as bool,
|
||||||
|
updatedAt: DateTime.parse(row['updatedAt']! as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> _encryptNoteBatch(
|
||||||
|
Map<String, Object?> request,
|
||||||
|
) async {
|
||||||
|
final String masterKey = request['masterKey']! as String;
|
||||||
|
final List<Map<String, Object?>> notes = (request['notes']! as List)
|
||||||
|
.cast<Map<String, Object?>>();
|
||||||
|
|
||||||
|
final List<Map<String, Object?>> encryptedNotes = [];
|
||||||
|
for (final Map<String, Object?> note in notes) {
|
||||||
|
final String title = note['title']! as String;
|
||||||
|
final String body = note['body']! as String;
|
||||||
|
|
||||||
|
final String encryptedTitle = await NoteEncryption.encryptNote(
|
||||||
|
title,
|
||||||
|
masterKey,
|
||||||
|
);
|
||||||
|
final String encryptedBody = await NoteEncryption.encryptNote(
|
||||||
|
body,
|
||||||
|
masterKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
encryptedNotes.add(<String, Object?>{
|
||||||
|
'index': note['index'] as int,
|
||||||
|
'id': note['uuid'] as String,
|
||||||
|
'categoryId': note['categoryId'] as String?,
|
||||||
|
'encryptedTitle': encryptedTitle,
|
||||||
|
'encryptedBody': encryptedBody,
|
||||||
|
'serverVersion': note['serverVersion']! as int,
|
||||||
|
'position': note['position']! as int,
|
||||||
|
'isDeleted': note['isDeleted']! as bool,
|
||||||
|
'updatedAt': note['updatedAt']! as String,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptedNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> _decryptNoteBatch(
|
||||||
|
Map<String, Object?> request,
|
||||||
|
) async {
|
||||||
|
final String masterKey = request['masterKey']! as String;
|
||||||
|
final List<Map<String, Object?>> notes = (request['notes']! as List)
|
||||||
|
.cast<Map<String, Object?>>();
|
||||||
|
|
||||||
|
final List<Map<String, Object?>> decryptedNotes = [];
|
||||||
|
for (final Map<String, Object?> note in notes) {
|
||||||
|
String decryptedTitle = 'Encrypted';
|
||||||
|
String decryptedBody = 'Encrypted';
|
||||||
|
try {
|
||||||
|
decryptedTitle = await NoteEncryption.decryptNote(
|
||||||
|
note['encryptedTitle']! as String,
|
||||||
|
masterKey,
|
||||||
|
);
|
||||||
|
decryptedBody = await NoteEncryption.decryptNote(
|
||||||
|
note['encryptedBody']! as String,
|
||||||
|
masterKey,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to decrypt note ${note['id']}: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedNotes.add(<String, Object?>{
|
||||||
|
'index': note['index'] as int,
|
||||||
|
'id': note['id'] as String,
|
||||||
|
'title': decryptedTitle,
|
||||||
|
'body': decryptedBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _parallelWorkerCount(int itemCount) {
|
||||||
|
final int cappedByCpu = math.max(
|
||||||
|
1,
|
||||||
|
(Platform.numberOfProcessors * 0.6).floor(),
|
||||||
|
);
|
||||||
|
return math.max(1, math.min(itemCount, cappedByCpu));
|
||||||
|
}
|
||||||
|
|||||||
+45
-23
@@ -1,13 +1,12 @@
|
|||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/models/category.dart';
|
import 'package:notas/models/category.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
// DTOs para sincronización con el servidor
|
// DTOs para sincronización con el servidor
|
||||||
|
|
||||||
class SyncRequest {
|
class SyncRequest {
|
||||||
SyncRequest({
|
SyncRequest({DateTime? lastSyncAt, required this.changes})
|
||||||
DateTime? lastSyncAt,
|
: lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
||||||
required this.changes,
|
|
||||||
}) : lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
|
||||||
|
|
||||||
final DateTime lastSyncAt;
|
final DateTime lastSyncAt;
|
||||||
final SyncChanges changes;
|
final SyncChanges changes;
|
||||||
@@ -21,10 +20,7 @@ class SyncRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SyncChanges {
|
class SyncChanges {
|
||||||
const SyncChanges({
|
const SyncChanges({this.categories = const [], this.notes = const []});
|
||||||
this.categories = const [],
|
|
||||||
this.notes = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<SyncCategoryPayload> categories;
|
final List<SyncCategoryPayload> categories;
|
||||||
final List<SyncNotePayload> notes;
|
final List<SyncNotePayload> notes;
|
||||||
@@ -33,8 +29,7 @@ class SyncChanges {
|
|||||||
return {
|
return {
|
||||||
if (categories.isNotEmpty)
|
if (categories.isNotEmpty)
|
||||||
'categories': categories.map((c) => c.toJson()).toList(),
|
'categories': categories.map((c) => c.toJson()).toList(),
|
||||||
if (notes.isNotEmpty)
|
if (notes.isNotEmpty) 'notes': notes.map((n) => n.toJson()).toList(),
|
||||||
'notes': notes.map((n) => n.toJson()).toList(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +44,8 @@ class SyncChangesResponse {
|
|||||||
final List<SyncNoteResponse> notes;
|
final List<SyncNoteResponse> notes;
|
||||||
|
|
||||||
factory SyncChangesResponse.fromJson(Map<String, dynamic> json) {
|
factory SyncChangesResponse.fromJson(Map<String, dynamic> json) {
|
||||||
final List<dynamic> categoriesJson = json['categories'] as List<dynamic>? ?? [];
|
final List<dynamic> categoriesJson =
|
||||||
|
json['categories'] as List<dynamic>? ?? [];
|
||||||
final List<dynamic> notesJson = json['notes'] as List<dynamic>? ?? [];
|
final List<dynamic> notesJson = json['notes'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
return SyncChangesResponse(
|
return SyncChangesResponse(
|
||||||
@@ -62,6 +58,30 @@ class SyncChangesResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _readStringValue(dynamic value) {
|
||||||
|
if (value is String) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
throw FormatException('Expected String value but found null');
|
||||||
|
}
|
||||||
|
return jsonEncode(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 {
|
class SyncCategoryPayload {
|
||||||
const SyncCategoryPayload({
|
const SyncCategoryPayload({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -163,11 +183,11 @@ class SyncResponse {
|
|||||||
|
|
||||||
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return SyncResponse(
|
return SyncResponse(
|
||||||
serverTimestamp:
|
serverTimestamp: DateTime.parse(json['serverTimestamp'] as String),
|
||||||
DateTime.parse(json['serverTimestamp'] as String),
|
|
||||||
synced: json['synced'] as bool? ?? false,
|
synced: json['synced'] as bool? ?? false,
|
||||||
changes: SyncChangesResponse.fromJson(
|
changes: SyncChangesResponse.fromJson(
|
||||||
json['changes'] as Map<String, dynamic>? ?? {}),
|
json['changes'] as Map<String, dynamic>? ?? {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +209,9 @@ class SyncCategoryResponse {
|
|||||||
|
|
||||||
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return SyncCategoryResponse(
|
return SyncCategoryResponse(
|
||||||
id: json['id'] as String,
|
id: _readStringValue(json['id']),
|
||||||
encryptedName: json['encrypted_name'] as String,
|
encryptedName: _readStringValue(json['encrypted_name']),
|
||||||
serverVersion: json['serverVersion'] as int,
|
serverVersion: _readIntValue(json['serverVersion']),
|
||||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
);
|
);
|
||||||
@@ -231,11 +251,13 @@ class SyncNoteResponse {
|
|||||||
|
|
||||||
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return SyncNoteResponse(
|
return SyncNoteResponse(
|
||||||
id: json['id'] as String,
|
id: _readStringValue(json['id']),
|
||||||
categoryId: json['categoryId'] as String?,
|
categoryId: json['categoryId'] == null
|
||||||
encryptedTitle: json['encrypted_title'] as String,
|
? null
|
||||||
encryptedBody: json['encrypted_body'] as String,
|
: _readStringValue(json['categoryId']),
|
||||||
serverVersion: json['serverVersion'] as int,
|
encryptedTitle: _readStringValue(json['encrypted_title']),
|
||||||
|
encryptedBody: _readStringValue(json['encrypted_body']),
|
||||||
|
serverVersion: _readIntValue(json['serverVersion']),
|
||||||
position: json['position'] as int? ?? 0,
|
position: json['position'] as int? ?? 0,
|
||||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.cloud_download_outlined,
|
icon: Icons.cloud_download_outlined,
|
||||||
color: const Color.fromARGB(255, 154, 194, 112),
|
color: const Color.fromARGB(255, 154, 194, 112),
|
||||||
determinate: false,
|
determinate: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user