refactor: Update Note and Category models to use 'id' instead of 'uuid', and adjust related database operations

- Changed 'uuid' to 'id' in Note and Category models for consistency.
- Updated database operations in NoteRepository to reflect the new 'id' field.
- Modified sync models to accommodate changes in Note and Category structures.
- Adjusted the handling of notes and categories during synchronization.
- Refactored the note editor and home screen to use the new 'id' field.
- Ensured that the 'isDirty' flag is properly set and utilized across models.
This commit is contained in:
2026-05-20 11:05:30 +02:00
parent 34f45a912f
commit def755e1c5
10 changed files with 520 additions and 323 deletions
+2 -2
View File
@@ -753,14 +753,14 @@ class _NotesAppState extends State<NotesApp>
}
});
}
} catch (e) {
} catch (e, st) {
if (!mounted) {
return;
}
setState(() {
_syncStatus = SyncStatus.error;
_syncErrorMessage = e.toString();
_syncErrorMessage = '$e\n\nStackTrace: $st';
_syncProgress = null;
_syncDetailMessage = null;
});
+27 -5
View File
@@ -277,9 +277,23 @@ class AuthApi {
debugPrint('Response body: ${res.body}');
if (res.statusCode >= 200 && res.statusCode < 300) {
final Map<String, dynamic> json =
jsonDecode(res.body) as Map<String, dynamic>;
return {'error': false, 'data': SyncResponse.fromJson(json)};
try {
final Map<String, dynamic> json =
jsonDecode(res.body) as Map<String, dynamic>;
return {'error': false, 'data': SyncResponse.fromJson(json)};
} catch (e, st) {
debugPrint('SYNC PARSE ERROR -> $e');
debugPrint(st.toString());
debugPrint('SYNC PARSE RAW BODY -> ${res.body}');
return {
'error': true,
'message': 'Error parseando respuesta de sync: $e',
'exception': e.toString(),
'stackTrace': st.toString(),
'body': res.body,
'status': res.statusCode,
};
}
}
// If token expired (401), try to refresh
@@ -298,8 +312,16 @@ class AuthApi {
try {
final dynamic decoded = jsonDecode(res.body);
return {'error': true, 'status': res.statusCode, 'body': decoded};
} catch (_) {
return {'error': true, 'status': res.statusCode, 'body': res.body};
} catch (e, st) {
debugPrint('SYNC HTTP ERROR PARSE FAILED -> $e');
debugPrint(st.toString());
return {
'error': true,
'status': res.statusCode,
'body': res.body,
'exception': e.toString(),
'stackTrace': st.toString(),
};
}
}
+52 -26
View File
@@ -9,22 +9,23 @@ part 'app_database.g.dart';
@DataClassName('DbCategory')
class Categories extends Table {
TextColumn get uuid => text().unique()();
TextColumn get id => text()();
TextColumn get encryptedName => text().named('encrypted_name')();
IntColumn get serverVersion =>
integer().named('server_version').withDefault(const Constant(0))();
BoolColumn get isDeleted =>
boolean().named('is_deleted').withDefault(const Constant(false))();
BoolColumn get isDirty =>
boolean().named('is_dirty').withDefault(const Constant(true))();
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
@override
Set<Column> get primaryKey => {uuid};
Set<Column> get primaryKey => {id};
}
@DataClassName('DbNote')
class Notes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get uuid => text().unique()();
TextColumn get id => text().named('id')();
TextColumn get title => text()();
TextColumn get body => text()();
DateTimeColumn get createdAt => dateTime().named('created_at')();
@@ -35,12 +36,34 @@ class Notes extends Table {
BoolColumn get isDeleted =>
boolean().named('is_deleted').withDefault(const Constant(false))();
TextColumn get categoryId => text().nullable().named('category_id')();
BoolColumn get isDirty =>
boolean().named('is_dirty').withDefault(const Constant(true))();
@override
Set<Column> get primaryKey => {id};
}
@DriftDatabase(tables: [Notes, Categories])
class AppDatabase extends _$AppDatabase {
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator migrator) async {
await migrator.createAll();
},
onUpgrade: (Migrator migrator, int from, int to) async {
if (from < 2) {
await migrator.addColumn(notes, notes.isDirty);
await migrator.addColumn(categories, categories.isDirty);
await customStatement('UPDATE notes SET is_dirty = 0');
await customStatement('UPDATE categories SET is_dirty = 0');
}
},
);
AppDatabase({required String encryptionKey})
: super(_openConnection(encryptionKey));
@@ -53,8 +76,8 @@ class AppDatabase extends _$AppDatabase {
return into(categories).insertOnConflictUpdate(category);
}
Future<void> deleteCategory(String uuid) {
return (update(categories)..where((c) => c.uuid.equals(uuid))).write(
Future<void> deleteCategory(String id) {
return (update(categories)..where((c) => c.id.equals(id))).write(
CategoriesCompanion(isDeleted: Value(true)),
);
}
@@ -70,7 +93,7 @@ class AppDatabase extends _$AppDatabase {
Future<int> insertNoteAtTop(NotesCompanion note) {
return transaction(() async {
await customStatement(
'UPDATE notes SET sort_index = sort_index + 1 WHERE is_deleted = 0',
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE is_deleted = 0',
);
return into(notes).insert(note.copyWith(sortIndex: const Value<int>(0)));
});
@@ -90,28 +113,29 @@ class AppDatabase extends _$AppDatabase {
return update(notes).replace(note);
}
Future<void> deleteNote(int id, int removedIndex) async {
Future<void> deleteNote(String id, int removedIndex) async {
await (update(notes)..where((n) => n.id.equals(id))).write(
NotesCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
isDirty: const Value(true),
),
);
await customStatement(
'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ? AND is_deleted = 0',
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND is_deleted = 0',
[removedIndex],
);
}
Future<void> deleteNoteAndShift({
required int id,
required String id,
required int removedIndex,
}) {
return deleteNote(id, removedIndex);
}
Future<void> permanentlyDeleteNote(int id) async {
Future<void> permanentlyDeleteNote(String id) async {
await (update(notes)..where((n) => n.id.equals(id))).write(
NotesCompanion(
title: const Value(''),
@@ -119,12 +143,13 @@ class AppDatabase extends _$AppDatabase {
categoryId: const Value(null),
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
isDirty: const Value(true),
),
);
}
Future<void> moveNote({
required int id,
required String id,
required int oldIndex,
required int newIndex,
}) {
@@ -133,9 +158,9 @@ class AppDatabase extends _$AppDatabase {
}
return transaction(() async {
final List<DbNote> all = await (select(notes)
..where((n) => n.isDeleted.equals(false)))
.get();
final List<DbNote> all = await (select(
notes,
)..where((n) => n.isDeleted.equals(false))).get();
final int count = all.length;
if (count == 0) {
@@ -153,12 +178,12 @@ class AppDatabase extends _$AppDatabase {
if (safeOld < safeNew) {
await customStatement(
'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0',
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0',
[safeOld, safeNew],
);
} else {
await customStatement(
'UPDATE notes SET sort_index = sort_index + 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0',
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0',
[safeNew, safeOld],
);
}
@@ -167,6 +192,7 @@ class AppDatabase extends _$AppDatabase {
NotesCompanion(
sortIndex: Value<int>(safeNew),
updatedAt: Value<DateTime>(DateTime.now()),
isDirty: const Value(true),
),
);
});
@@ -179,8 +205,12 @@ class AppDatabase extends _$AppDatabase {
}
Future<List<DbNote>> getDeletedNotes() {
return (select(notes)
..where((n) => n.isDeleted.equals(true) & n.title.isNotValue('') & n.body.isNotValue('')))
return (select(notes)..where(
(n) =>
n.isDeleted.equals(true) &
n.title.isNotValue('') &
n.body.isNotValue(''),
))
.get();
}
@@ -192,15 +222,11 @@ class AppDatabase extends _$AppDatabase {
// ========== Sync helpers ==========
Future<List<DbNote>> getUnsyncedNotes() {
return (select(notes)
..where((n) => n.isDeleted.equals(true) | n.serverVersion.equals(0)))
.get();
return (select(notes)..where((n) => n.isDirty.equals(true))).get();
}
Future<List<DbCategory>> getUnsyncedCategories() {
return (select(categories)
..where((c) => c.isDeleted.equals(true) | c.serverVersion.equals(0)))
.get();
return (select(categories)..where((c) => c.isDirty.equals(true))).get();
}
}
File diff suppressed because it is too large Load Diff
+144 -82
View File
@@ -35,9 +35,9 @@ class NoteRepository {
}
Future<Note> createNote(Note note) async {
final int id = await _database.insertNoteAtTop(
await _database.insertNoteAtTop(
NotesCompanion.insert(
uuid: note.uuid,
id: note.id,
title: note.title,
body: note.body,
createdAt: note.createdAt,
@@ -45,56 +45,77 @@ class NoteRepository {
sortIndex: 0,
serverVersion: const Value(0),
isDeleted: const Value(false),
categoryId: const Value(null),
categoryId: Value(note.categoryId),
isDirty: const Value(true),
),
);
return note.copyWith(id: id, index: 0);
return note.copyWith(position: 0, isDirty: true);
}
Future<Note> updateNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to update a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
await _database.updateNoteRow(
DbNote(
id: noteId,
uuid: note.uuid,
id: row.id,
title: note.title,
body: note.body,
createdAt: note.createdAt,
createdAt: row.createdAt,
updatedAt: note.updatedAt,
sortIndex: note.index,
sortIndex: row.sortIndex,
serverVersion: note.serverVersion,
isDeleted: false,
categoryId: note.categoryId,
isDirty: true,
),
);
return note.copyWith(isDeleted: false, isPermanentlyDeleted: false);
return note.copyWith(
isDeleted: false,
isPermanentlyDeleted: false,
isDirty: true,
position: row.sortIndex.toDouble(),
);
}
Future<void> deleteNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to delete a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
if (note.isDeleted || note.isPermanentlyDeleted) {
await _database.permanentlyDeleteNote(noteId);
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
if (row.isDeleted || note.isDeleted || note.isPermanentlyDeleted) {
await _database.permanentlyDeleteNote(row.id);
} else {
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
await _database.deleteNoteAndShift(
id: row.id,
removedIndex: row.sortIndex,
);
}
}
Future<void> moveNote(Note note, int newIndex) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to reorder a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
await _database.moveNote(
id: noteId,
oldIndex: note.index,
id: row.id,
oldIndex: row.sortIndex,
newIndex: newIndex,
);
}
@@ -120,26 +141,17 @@ class NoteRepository {
? DateTime.utc(1970, 1, 1)
: lastSync;
// Collect pending local changes.
// If we already synced at least once, use updatedAt to avoid re-sending
// old notes that were already uploaded.
final List<DbNote> unsyncedNotes;
final List<DbCategory> unsyncedCategories;
if (forceFull || lastSync == null) {
unsyncedNotes = await _database.getUnsyncedNotes();
unsyncedCategories = await _database.getUnsyncedCategories();
} else {
unsyncedNotes = await _database.getNotesChangedSince(lastSync);
unsyncedCategories = await _database.getCategoriesChangedSince(
lastSync,
);
}
// Collect pending local changes. Dirty flags are the source of truth for
// outbound sync; `lastSyncAt` is only used for asking the server what it
// changed since our previous successful sync.
final List<DbNote> unsyncedNotes = await _database.getUnsyncedNotes();
final List<DbCategory> unsyncedCategories = await _database
.getUnsyncedCategories();
final int totalNotesToEncrypt = unsyncedNotes.length;
// Build sync request (note: we send encrypted data, but locally we have plaintext)
// Encrypt notes that still contain content before sending.
// Build sync request: local data is plaintext, encryption happens only
// for the outbound payload.
if (totalNotesToEncrypt == 0) {
onProgress?.call(
SyncStatus.encrypting,
@@ -164,9 +176,8 @@ class NoteRepository {
},
);
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
.toList();
final List<SyncCategoryPayload> categoriesPayload =
await _encryptCategories(unsyncedCategories, masterKey: _masterKey);
final SyncRequest syncRequest = SyncRequest(
lastSyncAt: lastSyncForRequest,
@@ -184,7 +195,27 @@ class NoteRepository {
final Map<String, dynamic> syncResult = await _authApi.sync(syncRequest);
if (syncResult['error'] == true) {
return {'error': true, 'message': syncResult['body']};
final List<String> details = [];
final Object? message = syncResult['message'];
final Object? exception = syncResult['exception'];
final Object? stackTrace = syncResult['stackTrace'];
final Object? body = syncResult['body'];
if (message != null) details.add(message.toString());
if (exception != null && exception.toString() != details.firstOrNull) {
details.add('Exception: ${exception.toString()}');
}
if (body != null) {
details.add('Body: ${body.toString()}');
}
if (stackTrace != null) {
details.add('StackTrace: ${stackTrace.toString()}');
}
return {
'error': true,
'message': details.join('\n\n'),
};
}
final SyncResponse response = syncResult['data'] as SyncResponse;
@@ -217,8 +248,11 @@ class NoteRepository {
'notesCount': response.changes.notes.length,
'categoriesCount': response.changes.categories.length,
};
} catch (e) {
return {'error': true, 'message': e.toString()};
} catch (e, st) {
return {
'error': true,
'message': '$e\n\nStackTrace: $st',
};
}
}
@@ -229,13 +263,23 @@ class NoteRepository {
// Apply categories from server
for (final SyncCategoryResponse catResponse
in response.changes.categories) {
final String categoryName =
catResponse.isDeleted || catResponse.encryptedName.isEmpty
? ''
: await NoteEncryption.decryptNote(
catResponse.encryptedName,
_masterKey,
);
await _database.upsertCategory(
CategoriesCompanion(
uuid: Value(catResponse.id),
encryptedName: Value(catResponse.encryptedName),
id: Value(catResponse.id),
encryptedName: Value(categoryName),
serverVersion: Value(catResponse.serverVersion),
isDeleted: Value(catResponse.isDeleted),
updatedAt: Value(catResponse.updatedAt),
isDirty: const Value(false),
),
);
}
@@ -254,14 +298,14 @@ class NoteRepository {
for (var index = 0; index < decryptedNotes.length; index += 1) {
final Map<String, Object?> decryptedNote = decryptedNotes[index];
final String noteId = decryptedNote['id']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
final String noteId = (decryptedNote['id'] as String?) ?? noteResponse.id;
final existingNote = await (_database.select(
_database.notes,
)..where((n) => n.uuid.equals(noteId))).getSingleOrNull();
)..where((n) => n.id.equals(noteId))).getSingleOrNull();
final String decryptedTitle = decryptedNote['title']! as String;
final String decryptedBody = decryptedNote['body']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
final String decryptedTitle = (decryptedNote['title'] as String?) ?? '';
final String decryptedBody = (decryptedNote['body'] as String?) ?? '';
final bool isPermanentlyDeleted = noteResponse.isPermanentlyDeleted;
if (existingNote != null) {
@@ -269,15 +313,15 @@ class NoteRepository {
await _database.updateNoteRow(
DbNote(
id: existingNote.id,
uuid: noteId,
title: isPermanentlyDeleted ? '' : decryptedTitle,
body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position,
sortIndex: noteResponse.position.round(),
serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted,
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
isDirty: false,
),
);
} else {
@@ -286,15 +330,18 @@ class NoteRepository {
.into(_database.notes)
.insert(
NotesCompanion(
uuid: Value(noteResponse.id),
id: Value(noteResponse.id),
title: Value(isPermanentlyDeleted ? '' : decryptedTitle),
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position),
sortIndex: Value(noteResponse.position.round()),
serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted),
categoryId: Value(isPermanentlyDeleted ? null : noteResponse.categoryId),
categoryId: Value(
isPermanentlyDeleted ? null : noteResponse.categoryId,
),
isDirty: const Value(false),
),
);
}
@@ -314,26 +361,16 @@ class NoteRepository {
Note _fromDbNote(DbNote row) {
return Note(
id: row.id,
uuid: row.uuid,
title: row.title,
body: row.body,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
index: row.sortIndex,
position: row.sortIndex.toDouble(),
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row),
categoryId: row.categoryId,
);
}
Category _fromDbCategory(DbCategory row) {
return Category(
uuid: row.uuid,
encryptedName: row.encryptedName,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
updatedAt: row.updatedAt,
isDirty: row.isDirty,
);
}
}
@@ -395,6 +432,35 @@ Future<List<SyncNotePayload>> _encryptNotesInParallel(
return orderedPayloads.cast<SyncNotePayload>();
}
Future<List<SyncCategoryPayload>> _encryptCategories(
List<DbCategory> categories, {
required String masterKey,
}) async {
final List<SyncCategoryPayload> payloads = [];
for (final DbCategory row in categories) {
final String encryptedName = row.encryptedName.isEmpty
? ''
: await NoteEncryption.encryptNote(row.encryptedName, masterKey);
payloads.add(
SyncCategoryPayload.fromCategory(
Category(
id: row.id,
name: row.encryptedName,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
updatedAt: row.updatedAt,
isDirty: row.isDirty,
),
encryptedName: encryptedName,
),
);
}
return payloads;
}
List<Future<List<Map<String, Object?>>>> _decryptNoteBatches(
List<SyncNoteResponse> notes, {
required String masterKey,
@@ -467,7 +533,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
return <String, Object?>{
'index': index,
'uuid': row.uuid,
'id': row.id,
'title': row.title,
'body': row.body,
'createdAt': row.createdAt.toIso8601String(),
@@ -487,7 +553,7 @@ SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
encryptedTitle: row['encryptedTitle']! as String,
encryptedBody: row['encryptedBody']! as String,
serverVersion: row['serverVersion']! as int,
position: row['position']! as int,
position: (row['position']! as num).toDouble(),
isDeleted: row['isDeleted']! as bool,
isPermanentlyDeleted: row['isPermanentlyDeleted']! as bool,
updatedAt: DateTime.parse(row['updatedAt']! as String),
@@ -513,19 +579,13 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
encryptedTitle = '';
encryptedBody = '';
} else {
encryptedTitle = await NoteEncryption.encryptNote(
title,
masterKey,
);
encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
encryptedTitle = await NoteEncryption.encryptNote(title, masterKey);
encryptedBody = await NoteEncryption.encryptNote(body, masterKey);
}
encryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['uuid'] as String,
'id': note['id'] as String,
'categoryId': note['categoryId'] as String?,
'encryptedTitle': encryptedTitle,
'encryptedBody': encryptedBody,
@@ -570,9 +630,11 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
decryptedBody = '';
}
final String noteId = note['id']! as String;
decryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['id'] as String,
'id': noteId,
'title': decryptedTitle,
'body': decryptedBody,
'isPermanentlyDeleted': isPermanentlyDeleted,
+23 -21
View File
@@ -98,16 +98,19 @@ class SyncCategoryPayload {
required this.updatedAt,
});
final String id; // uuid
final String id;
final String encryptedName;
final int serverVersion;
final bool isDeleted;
final DateTime updatedAt;
factory SyncCategoryPayload.fromCategory(Category category) {
factory SyncCategoryPayload.fromCategory(
Category category, {
required String encryptedName,
}) {
return SyncCategoryPayload(
id: category.uuid,
encryptedName: category.encryptedName,
id: category.id,
encryptedName: encryptedName,
serverVersion: category.serverVersion,
isDeleted: category.isDeleted,
updatedAt: category.updatedAt,
@@ -132,18 +135,18 @@ class SyncNotePayload {
required this.encryptedTitle,
required this.encryptedBody,
required this.serverVersion,
this.position = 0,
this.position = 0.0,
this.isDeleted = false,
this.isPermanentlyDeleted = false,
required this.updatedAt,
});
final String id; // uuid
final String id;
final String? categoryId;
final String encryptedTitle;
final String encryptedBody;
final int serverVersion;
final int position;
final double position;
final bool isDeleted;
final bool isPermanentlyDeleted;
final DateTime updatedAt;
@@ -155,12 +158,12 @@ class SyncNotePayload {
bool isPermanentlyDeleted = false,
}) {
return SyncNotePayload(
id: note.uuid,
id: note.id,
categoryId: note.categoryId,
encryptedTitle: encryptedTitle,
encryptedBody: encryptedBody,
serverVersion: note.serverVersion,
position: note.index,
position: note.position,
isDeleted: note.isDeleted,
isPermanentlyDeleted: isPermanentlyDeleted,
updatedAt: note.updatedAt,
@@ -212,8 +215,7 @@ class SyncCategoryResponse {
this.isDeleted = false,
required this.updatedAt,
});
final String id; // uuid
final String id;
final String encryptedName;
final int serverVersion;
final bool isDeleted;
@@ -229,10 +231,10 @@ class SyncCategoryResponse {
);
}
Category toCategory() {
Category toCategory({required String name}) {
return Category(
uuid: id,
encryptedName: encryptedName,
id: id,
name: name,
serverVersion: serverVersion,
isDeleted: isDeleted,
updatedAt: updatedAt,
@@ -247,18 +249,17 @@ class SyncNoteResponse {
required this.encryptedTitle,
required this.encryptedBody,
required this.serverVersion,
this.position = 0,
this.position = 0.0,
this.isDeleted = false,
this.isPermanentlyDeleted = false,
required this.updatedAt,
});
final String id; // uuid
final String id;
final String? categoryId;
final String encryptedTitle;
final String encryptedBody;
final int serverVersion;
final int position;
final double position;
final bool isDeleted;
final bool isPermanentlyDeleted;
final DateTime updatedAt;
@@ -272,7 +273,7 @@ class SyncNoteResponse {
encryptedTitle: _readOptionalStringValue(json['encrypted_title']),
encryptedBody: _readOptionalStringValue(json['encrypted_body']),
serverVersion: _readIntValue(json['serverVersion']),
position: json['position'] as int? ?? 0,
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),
@@ -281,15 +282,16 @@ class SyncNoteResponse {
Note toNote() {
return Note(
uuid: id,
id: id,
title: isPermanentlyDeleted ? '' : 'Encrypted',
body: isPermanentlyDeleted ? '' : 'Encrypted',
createdAt: updatedAt,
updatedAt: updatedAt,
index: position,
position: position,
serverVersion: serverVersion,
isDeleted: isDeleted,
categoryId: categoryId,
isDirty: false,
);
}
}
+15 -11
View File
@@ -2,32 +2,36 @@ import 'package:uuid/uuid.dart';
class Category {
Category({
String? uuid,
required this.encryptedName,
String? id,
required this.name,
this.serverVersion = 0,
this.isDeleted = false,
required this.updatedAt,
}) : uuid = uuid ?? Uuid().v4();
this.isDirty = true,
}) : id = id ?? Uuid().v4();
final String uuid;
final String encryptedName;
final String id;
final String name;
final int serverVersion;
final bool isDeleted;
final DateTime updatedAt;
final bool isDirty;
Category copyWith({
String? uuid,
String? encryptedName,
String? id,
String? name,
int? serverVersion,
bool? isDeleted,
DateTime? updatedAt,
bool? isDirty,
}) {
return Category(
uuid: uuid ?? this.uuid,
encryptedName: encryptedName ?? this.encryptedName,
id: id ?? this.id,
name: name ?? this.name,
serverVersion: serverVersion ?? this.serverVersion,
isDeleted: isDeleted ?? this.isDeleted,
updatedAt: updatedAt ?? this.updatedAt,
isDirty: isDirty ?? this.isDirty,
);
}
@@ -37,9 +41,9 @@ class Category {
return true;
}
return other is Category && uuid == other.uuid;
return other is Category && id == other.id;
}
@override
int get hashCode => uuid.hashCode;
int get hashCode => id.hashCode;
}
+19 -24
View File
@@ -2,63 +2,58 @@ import 'package:uuid/uuid.dart';
// Model: Note
// - Representa una nota guardada en la app.
// - `id` es el identificador local de SQLite (autoincrement).
// - `uuid` es el identificador global sincronizado con el servidor.
// - `index` representa el orden visual dentro de la lista.
// - `serverVersion` se usa para resolver conflictos en sync.
// - `isDeleted` marca eliminaciones blandas.
class Note {
Note({
this.id,
String? uuid,
String? id,
required this.title,
required this.body,
required this.createdAt,
required this.updatedAt,
required this.index,
required this.position,
this.categoryId,
this.serverVersion = 0,
this.isDeleted = false,
this.isPermanentlyDeleted = false,
this.categoryId,
}) : uuid = uuid ?? Uuid().v4();
this.isDirty = true,
}) : id = id ?? Uuid().v4();
final int? id;
final String uuid;
final String id;
final String title;
final String body;
final DateTime createdAt;
final DateTime updatedAt;
final int index;
final double position;
final String? categoryId;
final int serverVersion;
final bool isDeleted;
final bool isPermanentlyDeleted;
final String? categoryId;
final bool isDirty;
Note copyWith({
int? id,
String? uuid,
String? id,
String? title,
String? body,
DateTime? createdAt,
DateTime? updatedAt,
int? index,
double? position,
String? categoryId,
int? serverVersion,
bool? isDeleted,
bool? isPermanentlyDeleted,
String? categoryId,
bool? isDirty,
}) {
return Note(
id: id ?? this.id,
uuid: uuid ?? this.uuid,
title: title ?? this.title,
body: body ?? this.body,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
index: index ?? this.index,
position: position ?? this.position,
categoryId: categoryId ?? this.categoryId,
serverVersion: serverVersion ?? this.serverVersion,
isDeleted: isDeleted ?? this.isDeleted,
isPermanentlyDeleted: isPermanentlyDeleted ?? this.isPermanentlyDeleted,
categoryId: categoryId ?? this.categoryId,
isDirty: isDirty ?? this.isDirty,
);
}
@@ -68,9 +63,9 @@ class Note {
return true;
}
return other is Note && uuid == other.uuid;
return other is Note && id == other.id;
}
@override
int get hashCode => uuid.hashCode;
}
int get hashCode => id.hashCode;
}
+7 -9
View File
@@ -154,9 +154,9 @@ class _HomeScreenState extends State<HomeScreen> {
// Don't let DB errors cause the app to reset the vault automatically.
debugPrint('Failed to move note: $e\n$st');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error al reordenar la nota: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error al reordenar la nota: $e')));
}
}
@@ -385,9 +385,8 @@ class _HomeScreenState extends State<HomeScreen> {
borderRadius: BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<int>(
filteredNotes[index].id ??
filteredNotes[index].index,
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () =>
@@ -510,9 +509,8 @@ class _HomeScreenState extends State<HomeScreen> {
borderRadius: BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<int>(
filteredNotes[index].id ??
filteredNotes[index].index,
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () =>
+3 -2
View File
@@ -89,7 +89,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
@override
void initState() {
super.initState();
_isNewNote = widget.note?.id == null;
_isNewNote = widget.note == null;
if (_isNewNote) {
final DateTime now = DateTime.now();
@@ -98,7 +98,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
body: '',
createdAt: now,
updatedAt: now,
index: 0,
position: 0,
);
} else {
_currentNote = widget.note!;
@@ -143,6 +143,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
title: title.isEmpty ? 'Sin título' : title,
body: body,
updatedAt: DateTime.now(),
isDirty: true,
);
_complete(updatedNote);