diff --git a/lib/data/app_database.dart b/lib/data/app_database.dart index 555b778..6637db1 100644 --- a/lib/data/app_database.dart +++ b/lib/data/app_database.dart @@ -10,7 +10,7 @@ part 'app_database.g.dart'; @DataClassName('DbCategory') class Categories extends Table { TextColumn get id => text()(); - TextColumn get encryptedName => text().named('encrypted_name')(); + TextColumn get name => text().named('name')(); IntColumn get serverVersion => integer().named('server_version').withDefault(const Constant(0))(); BoolColumn get isDeleted => diff --git a/lib/data/app_database.g.dart b/lib/data/app_database.g.dart index 3e990d5..bef2926 100644 --- a/lib/data/app_database.g.dart +++ b/lib/data/app_database.g.dart @@ -629,12 +629,10 @@ class $CategoriesTable extends Categories type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _encryptedNameMeta = const VerificationMeta( - 'encryptedName', - ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override - late final GeneratedColumn encryptedName = GeneratedColumn( - 'encrypted_name', + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, type: DriftSqlType.string, @@ -667,21 +665,6 @@ class $CategoriesTable extends Categories ), defaultValue: const Constant(false), ); - static const VerificationMeta _isDirtyMeta = const VerificationMeta( - 'isDirty', - ); - @override - late final GeneratedColumn isDirty = GeneratedColumn( - 'is_dirty', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_dirty" IN (0, 1))', - ), - defaultValue: const Constant(true), - ); static const VerificationMeta _colorValueMeta = const VerificationMeta( 'colorValue', ); @@ -704,6 +687,21 @@ class $CategoriesTable extends Categories type: DriftSqlType.int, requiredDuringInsert: false, ); + static const VerificationMeta _isDirtyMeta = const VerificationMeta( + 'isDirty', + ); + @override + late final GeneratedColumn isDirty = GeneratedColumn( + 'is_dirty', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_dirty" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); static const VerificationMeta _updatedAtMeta = const VerificationMeta( 'updatedAt', ); @@ -718,12 +716,12 @@ class $CategoriesTable extends Categories @override List get $columns => [ id, - encryptedName, + name, serverVersion, isDeleted, - isDirty, colorValue, iconCodePoint, + isDirty, updatedAt, ]; @override @@ -743,16 +741,13 @@ class $CategoriesTable extends Categories } else if (isInserting) { context.missing(_idMeta); } - if (data.containsKey('encrypted_name')) { + if (data.containsKey('name')) { context.handle( - _encryptedNameMeta, - encryptedName.isAcceptableOrUnknown( - data['encrypted_name']!, - _encryptedNameMeta, - ), + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), ); } else if (isInserting) { - context.missing(_encryptedNameMeta); + context.missing(_nameMeta); } if (data.containsKey('server_version')) { context.handle( @@ -769,12 +764,6 @@ class $CategoriesTable extends Categories isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta), ); } - if (data.containsKey('is_dirty')) { - context.handle( - _isDirtyMeta, - isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta), - ); - } if (data.containsKey('color_value')) { context.handle( _colorValueMeta, @@ -790,6 +779,12 @@ class $CategoriesTable extends Categories ), ); } + if (data.containsKey('is_dirty')) { + context.handle( + _isDirtyMeta, + isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta), + ); + } if (data.containsKey('updated_at')) { context.handle( _updatedAtMeta, @@ -811,9 +806,9 @@ class $CategoriesTable extends Categories DriftSqlType.string, data['${effectivePrefix}id'], )!, - encryptedName: attachedDatabase.typeMapping.read( + name: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}encrypted_name'], + data['${effectivePrefix}name'], )!, serverVersion: attachedDatabase.typeMapping.read( DriftSqlType.int, @@ -823,10 +818,6 @@ class $CategoriesTable extends Categories DriftSqlType.bool, data['${effectivePrefix}is_deleted'], )!, - isDirty: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}is_dirty'], - )!, colorValue: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}color_value'], @@ -835,6 +826,10 @@ class $CategoriesTable extends Categories DriftSqlType.int, data['${effectivePrefix}icon_code_point'], ), + isDirty: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_dirty'], + )!, updatedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}updated_at'], @@ -850,37 +845,37 @@ class $CategoriesTable extends Categories class DbCategory extends DataClass implements Insertable { final String id; - final String encryptedName; + final String name; final int serverVersion; final bool isDeleted; - final bool isDirty; final int? colorValue; final int? iconCodePoint; + final bool isDirty; final DateTime updatedAt; const DbCategory({ required this.id, - required this.encryptedName, + required this.name, required this.serverVersion, required this.isDeleted, - required this.isDirty, this.colorValue, this.iconCodePoint, + required this.isDirty, required this.updatedAt, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['encrypted_name'] = Variable(encryptedName); + map['name'] = Variable(name); map['server_version'] = Variable(serverVersion); map['is_deleted'] = Variable(isDeleted); - map['is_dirty'] = Variable(isDirty); if (!nullToAbsent || colorValue != null) { map['color_value'] = Variable(colorValue); } if (!nullToAbsent || iconCodePoint != null) { map['icon_code_point'] = Variable(iconCodePoint); } + map['is_dirty'] = Variable(isDirty); map['updated_at'] = Variable(updatedAt); return map; } @@ -888,16 +883,16 @@ class DbCategory extends DataClass implements Insertable { CategoriesCompanion toCompanion(bool nullToAbsent) { return CategoriesCompanion( id: Value(id), - encryptedName: Value(encryptedName), + name: Value(name), serverVersion: Value(serverVersion), isDeleted: Value(isDeleted), - isDirty: Value(isDirty), colorValue: colorValue == null && nullToAbsent ? const Value.absent() - : Value(colorValue), + : Value(colorValue), iconCodePoint: iconCodePoint == null && nullToAbsent ? const Value.absent() - : Value(iconCodePoint), + : Value(iconCodePoint), + isDirty: Value(isDirty), updatedAt: Value(updatedAt), ); } @@ -909,12 +904,12 @@ class DbCategory extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return DbCategory( id: serializer.fromJson(json['id']), - encryptedName: serializer.fromJson(json['encryptedName']), + name: serializer.fromJson(json['name']), serverVersion: serializer.fromJson(json['serverVersion']), isDeleted: serializer.fromJson(json['isDeleted']), - isDirty: serializer.fromJson(json['isDirty']), colorValue: serializer.fromJson(json['colorValue']), iconCodePoint: serializer.fromJson(json['iconCodePoint']), + isDirty: serializer.fromJson(json['isDirty']), updatedAt: serializer.fromJson(json['updatedAt']), ); } @@ -923,52 +918,52 @@ class DbCategory extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'encryptedName': serializer.toJson(encryptedName), + 'name': serializer.toJson(name), 'serverVersion': serializer.toJson(serverVersion), 'isDeleted': serializer.toJson(isDeleted), - 'isDirty': serializer.toJson(isDirty), 'colorValue': serializer.toJson(colorValue), 'iconCodePoint': serializer.toJson(iconCodePoint), + 'isDirty': serializer.toJson(isDirty), 'updatedAt': serializer.toJson(updatedAt), }; } DbCategory copyWith({ String? id, - String? encryptedName, + String? name, int? serverVersion, bool? isDeleted, + Value colorValue = const Value.absent(), + Value iconCodePoint = const Value.absent(), bool? isDirty, - int? colorValue, - int? iconCodePoint, DateTime? updatedAt, }) => DbCategory( id: id ?? this.id, - encryptedName: encryptedName ?? this.encryptedName, + name: name ?? this.name, serverVersion: serverVersion ?? this.serverVersion, isDeleted: isDeleted ?? this.isDeleted, + colorValue: colorValue.present ? colorValue.value : this.colorValue, + iconCodePoint: iconCodePoint.present + ? iconCodePoint.value + : this.iconCodePoint, isDirty: isDirty ?? this.isDirty, - colorValue: colorValue ?? this.colorValue, - iconCodePoint: iconCodePoint ?? this.iconCodePoint, updatedAt: updatedAt ?? this.updatedAt, ); DbCategory copyWithCompanion(CategoriesCompanion data) { return DbCategory( id: data.id.present ? data.id.value : this.id, - encryptedName: data.encryptedName.present - ? data.encryptedName.value - : this.encryptedName, + name: data.name.present ? data.name.value : this.name, serverVersion: data.serverVersion.present ? data.serverVersion.value : this.serverVersion, isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, - isDirty: data.isDirty.present ? data.isDirty.value : this.isDirty, colorValue: data.colorValue.present ? data.colorValue.value : this.colorValue, iconCodePoint: data.iconCodePoint.present ? data.iconCodePoint.value : this.iconCodePoint, + isDirty: data.isDirty.present ? data.isDirty.value : this.isDirty, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, ); } @@ -977,9 +972,11 @@ class DbCategory extends DataClass implements Insertable { String toString() { return (StringBuffer('DbCategory(') ..write('id: $id, ') - ..write('encryptedName: $encryptedName, ') + ..write('name: $name, ') ..write('serverVersion: $serverVersion, ') ..write('isDeleted: $isDeleted, ') + ..write('colorValue: $colorValue, ') + ..write('iconCodePoint: $iconCodePoint, ') ..write('isDirty: $isDirty, ') ..write('updatedAt: $updatedAt') ..write(')')) @@ -989,9 +986,11 @@ class DbCategory extends DataClass implements Insertable { @override int get hashCode => Object.hash( id, - encryptedName, + name, serverVersion, isDeleted, + colorValue, + iconCodePoint, isDirty, updatedAt, ); @@ -1000,66 +999,68 @@ class DbCategory extends DataClass implements Insertable { identical(this, other) || (other is DbCategory && other.id == this.id && - other.encryptedName == this.encryptedName && + other.name == this.name && other.serverVersion == this.serverVersion && other.isDeleted == this.isDeleted && + other.colorValue == this.colorValue && + other.iconCodePoint == this.iconCodePoint && other.isDirty == this.isDirty && other.updatedAt == this.updatedAt); } class CategoriesCompanion extends UpdateCompanion { final Value id; - final Value encryptedName; + final Value name; final Value serverVersion; final Value isDeleted; + final Value colorValue; + final Value iconCodePoint; final Value isDirty; final Value updatedAt; final Value rowid; - final Value colorValue; - final Value iconCodePoint; const CategoriesCompanion({ this.id = const Value.absent(), - this.encryptedName = const Value.absent(), + this.name = const Value.absent(), this.serverVersion = const Value.absent(), this.isDeleted = const Value.absent(), - this.isDirty = const Value.absent(), this.colorValue = const Value.absent(), this.iconCodePoint = const Value.absent(), + this.isDirty = const Value.absent(), this.updatedAt = const Value.absent(), this.rowid = const Value.absent(), }); CategoriesCompanion.insert({ required String id, - required String encryptedName, + required String name, this.serverVersion = const Value.absent(), this.isDeleted = const Value.absent(), - this.isDirty = const Value.absent(), this.colorValue = const Value.absent(), this.iconCodePoint = const Value.absent(), + this.isDirty = const Value.absent(), required DateTime updatedAt, this.rowid = const Value.absent(), }) : id = Value(id), - encryptedName = Value(encryptedName), + name = Value(name), updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, - Expression? encryptedName, + Expression? name, Expression? serverVersion, Expression? isDeleted, - Expression? isDirty, Expression? colorValue, Expression? iconCodePoint, + Expression? isDirty, Expression? updatedAt, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (encryptedName != null) 'encrypted_name': encryptedName, + if (name != null) 'name': name, if (serverVersion != null) 'server_version': serverVersion, if (isDeleted != null) 'is_deleted': isDeleted, - if (isDirty != null) 'is_dirty': isDirty, if (colorValue != null) 'color_value': colorValue, if (iconCodePoint != null) 'icon_code_point': iconCodePoint, + if (isDirty != null) 'is_dirty': isDirty, if (updatedAt != null) 'updated_at': updatedAt, if (rowid != null) 'rowid': rowid, }); @@ -1067,23 +1068,23 @@ class CategoriesCompanion extends UpdateCompanion { CategoriesCompanion copyWith({ Value? id, - Value? encryptedName, + Value? name, Value? serverVersion, Value? isDeleted, + Value? colorValue, + Value? iconCodePoint, Value? isDirty, - Value? colorValue, - Value? iconCodePoint, Value? updatedAt, Value? rowid, }) { return CategoriesCompanion( id: id ?? this.id, - encryptedName: encryptedName ?? this.encryptedName, + name: name ?? this.name, serverVersion: serverVersion ?? this.serverVersion, isDeleted: isDeleted ?? this.isDeleted, - isDirty: isDirty ?? this.isDirty, colorValue: colorValue ?? this.colorValue, iconCodePoint: iconCodePoint ?? this.iconCodePoint, + isDirty: isDirty ?? this.isDirty, updatedAt: updatedAt ?? this.updatedAt, rowid: rowid ?? this.rowid, ); @@ -1095,8 +1096,8 @@ class CategoriesCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (encryptedName.present) { - map['encrypted_name'] = Variable(encryptedName.value); + if (name.present) { + map['name'] = Variable(name.value); } if (serverVersion.present) { map['server_version'] = Variable(serverVersion.value); @@ -1104,15 +1105,15 @@ class CategoriesCompanion extends UpdateCompanion { if (isDeleted.present) { map['is_deleted'] = Variable(isDeleted.value); } - if (isDirty.present) { - map['is_dirty'] = Variable(isDirty.value); - } if (colorValue.present) { map['color_value'] = Variable(colorValue.value); } if (iconCodePoint.present) { map['icon_code_point'] = Variable(iconCodePoint.value); } + if (isDirty.present) { + map['is_dirty'] = Variable(isDirty.value); + } if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } @@ -1126,9 +1127,11 @@ class CategoriesCompanion extends UpdateCompanion { String toString() { return (StringBuffer('CategoriesCompanion(') ..write('id: $id, ') - ..write('encryptedName: $encryptedName, ') + ..write('name: $name, ') ..write('serverVersion: $serverVersion, ') ..write('isDeleted: $isDeleted, ') + ..write('colorValue: $colorValue, ') + ..write('iconCodePoint: $iconCodePoint, ') ..write('isDirty: $isDirty, ') ..write('updatedAt: $updatedAt, ') ..write('rowid: $rowid') @@ -1444,9 +1447,11 @@ typedef $$NotesTableProcessedTableManager = typedef $$CategoriesTableCreateCompanionBuilder = CategoriesCompanion Function({ required String id, - required String encryptedName, + required String name, Value serverVersion, Value isDeleted, + Value colorValue, + Value iconCodePoint, Value isDirty, required DateTime updatedAt, Value rowid, @@ -1454,9 +1459,11 @@ typedef $$CategoriesTableCreateCompanionBuilder = typedef $$CategoriesTableUpdateCompanionBuilder = CategoriesCompanion Function({ Value id, - Value encryptedName, + Value name, Value serverVersion, Value isDeleted, + Value colorValue, + Value iconCodePoint, Value isDirty, Value updatedAt, Value rowid, @@ -1476,8 +1483,8 @@ class $$CategoriesTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get encryptedName => $composableBuilder( - column: $table.encryptedName, + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column), ); @@ -1491,6 +1498,16 @@ class $$CategoriesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get colorValue => $composableBuilder( + column: $table.colorValue, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get iconCodePoint => $composableBuilder( + column: $table.iconCodePoint, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get isDirty => $composableBuilder( column: $table.isDirty, builder: (column) => ColumnFilters(column), @@ -1516,8 +1533,8 @@ class $$CategoriesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get encryptedName => $composableBuilder( - column: $table.encryptedName, + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column), ); @@ -1531,6 +1548,16 @@ class $$CategoriesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get colorValue => $composableBuilder( + column: $table.colorValue, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get iconCodePoint => $composableBuilder( + column: $table.iconCodePoint, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get isDirty => $composableBuilder( column: $table.isDirty, builder: (column) => ColumnOrderings(column), @@ -1554,10 +1581,8 @@ class $$CategoriesTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get encryptedName => $composableBuilder( - column: $table.encryptedName, - builder: (column) => column, - ); + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); GeneratedColumn get serverVersion => $composableBuilder( column: $table.serverVersion, @@ -1567,6 +1592,16 @@ class $$CategoriesTableAnnotationComposer GeneratedColumn get isDeleted => $composableBuilder(column: $table.isDeleted, builder: (column) => column); + GeneratedColumn get colorValue => $composableBuilder( + column: $table.colorValue, + builder: (column) => column, + ); + + GeneratedColumn get iconCodePoint => $composableBuilder( + column: $table.iconCodePoint, + builder: (column) => column, + ); + GeneratedColumn get isDirty => $composableBuilder(column: $table.isDirty, builder: (column) => column); @@ -1606,17 +1641,21 @@ class $$CategoriesTableTableManager updateCompanionCallback: ({ Value id = const Value.absent(), - Value encryptedName = const Value.absent(), + Value name = const Value.absent(), Value serverVersion = const Value.absent(), Value isDeleted = const Value.absent(), + Value colorValue = const Value.absent(), + Value iconCodePoint = const Value.absent(), Value isDirty = const Value.absent(), Value updatedAt = const Value.absent(), Value rowid = const Value.absent(), }) => CategoriesCompanion( id: id, - encryptedName: encryptedName, + name: name, serverVersion: serverVersion, isDeleted: isDeleted, + colorValue: colorValue, + iconCodePoint: iconCodePoint, isDirty: isDirty, updatedAt: updatedAt, rowid: rowid, @@ -1624,17 +1663,21 @@ class $$CategoriesTableTableManager createCompanionCallback: ({ required String id, - required String encryptedName, + required String name, Value serverVersion = const Value.absent(), Value isDeleted = const Value.absent(), + Value colorValue = const Value.absent(), + Value iconCodePoint = const Value.absent(), Value isDirty = const Value.absent(), required DateTime updatedAt, Value rowid = const Value.absent(), }) => CategoriesCompanion.insert( id: id, - encryptedName: encryptedName, + name: name, serverVersion: serverVersion, isDeleted: isDeleted, + colorValue: colorValue, + iconCodePoint: iconCodePoint, isDirty: isDirty, updatedAt: updatedAt, rowid: rowid, diff --git a/lib/data/note_repository.dart b/lib/data/note_repository.dart index ef795fa..0e017c4 100644 --- a/lib/data/note_repository.dart +++ b/lib/data/note_repository.dart @@ -39,41 +39,29 @@ class NoteRepository { final List dbCategories = await _database.getAllCategories(); final List categories = []; for (final DbCategory row in dbCategories) { - try { - final String decryptedName = await NoteEncryption.decryptNote( - row.encryptedName, - _masterKey, - ); - categories.add( - Category( - id: row.id, - name: decryptedName, - serverVersion: row.serverVersion, - isDeleted: row.isDeleted, - updatedAt: row.updatedAt, - isDirty: row.isDirty, - colorValue: row.colorValue, - iconCodePoint: row.iconCodePoint, - ), - ); - } catch (e) { - debugPrint('Error al desencriptar categoría: $e'); - } + categories.add( + Category( + id: row.id, + name: row.name, + serverVersion: row.serverVersion, + isDeleted: row.isDeleted, + updatedAt: row.updatedAt, + isDirty: row.isDirty, + colorValue: row.colorValue, + iconCodePoint: row.iconCodePoint, + ), + ); } return categories; } Future createCategory(Category category) async { debugPrint('createCategory called with: ${category.name}'); - final String encryptedName = await NoteEncryption.encryptNote( - category.name, - _masterKey, - ); - debugPrint('Category name encrypted'); + await _database.upsertCategory( CategoriesCompanion.insert( id: category.id, - encryptedName: encryptedName, + name: category.name, updatedAt: category.updatedAt, serverVersion: const Value(0), isDeleted: const Value(false), @@ -317,14 +305,15 @@ class NoteRepository { // Apply categories from server for (final SyncCategoryResponse catResponse in response.changes.categories) { - // Store the encrypted blob received from the server directly in the DB. - // Decryption is only performed when loading categories for display. - final String encryptedBlob = catResponse.encryptedName; + final String plainName = await _plainCategoryName( + catResponse.encryptedName, + _masterKey, + ); await _database.upsertCategory( CategoriesCompanion( id: Value(catResponse.id), - encryptedName: Value(encryptedBlob), + name: Value(plainName), serverVersion: Value(catResponse.serverVersion), isDeleted: Value(catResponse.isDeleted), colorValue: Value(catResponse.colorValue), @@ -490,16 +479,16 @@ Future> _encryptCategories( final List payloads = []; for (final DbCategory row in categories) { - // The DB already stores the encrypted name blob in `encryptedName`. + // The DB stores the plain category name locally. // Use it directly when building the sync payload and preserve // color/icon values from the DB row so they are sent to the server. - final String encryptedName = row.encryptedName; + final String plainName = row.name; payloads.add( SyncCategoryPayload.fromCategory( Category( id: row.id, - name: row.encryptedName, + name: plainName, serverVersion: row.serverVersion, isDeleted: row.isDeleted, updatedAt: row.updatedAt, @@ -507,7 +496,7 @@ Future> _encryptCategories( colorValue: row.colorValue, iconCodePoint: row.iconCodePoint, ), - encryptedName: encryptedName, + encryptedName: await NoteEncryption.encryptNote(plainName, masterKey), ), ); } @@ -709,3 +698,11 @@ int _parallelWorkerCount(int itemCount) { ); return math.max(1, math.min(itemCount, cappedByCpu)); } + +Future _plainCategoryName(String storedName, String masterKey) async { + try { + return await NoteEncryption.decryptNote(storedName, masterKey); + } catch (_) { + return storedName; + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index a14b9c6..029390c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/gestures.dart'; @@ -832,20 +833,26 @@ class _CategoryDialogState extends State<_CategoryDialog> { Color? _selectedColor; IconData? _selectedIcon; int _selectedSection = 0; + bool _nameHasError = false; + bool _isSaving = false; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.category?.name ?? ''); _selectedColor = - widget.category != null && widget.category!.colorValue != null - ? Color(widget.category!.colorValue!) - : null; + widget.category == null + ? CategoryStyle.colors.first + : widget.category!.colorValue != null + ? Color(widget.category!.colorValue!) + : null; if (widget.category != null && widget.category!.iconCodePoint != null) { _selectedIcon = CategoryStyle.icons.firstWhere( (IconData icon) => icon.codePoint == widget.category!.iconCodePoint, orElse: () => CategoryStyle.icons.first, ); + } else if (widget.category == null) { + _selectedIcon = CategoryStyle.icons.first; } } @@ -856,12 +863,24 @@ class _CategoryDialogState extends State<_CategoryDialog> { } Future _saveCategory() async { + if (_isSaving) { + return; + } + final String name = _controller.text.trim(); if (name.isEmpty) { + setState(() { + _nameHasError = true; + }); return; } try { + setState(() { + _nameHasError = false; + _isSaving = true; + }); + final Category newCategory = Category( id: widget.category?.id, name: name, @@ -881,16 +900,19 @@ class _CategoryDialogState extends State<_CategoryDialog> { await widget.repository.createCategory(newCategory); await widget.onCategoriesChanged(); - try { - await widget.onRequestSync(); - } catch (_) {} if (mounted) { - await widget.onCategoryDeleted(); - if (!mounted) return; Navigator.pop(context); } + + widget.onRequestSync().catchError((_) {}); + } catch (e) { debugPrint('ERROR creating category: $e'); + if (mounted) { + setState(() { + _isSaving = false; + }); + } if (mounted) { ScaffoldMessenger.of( context, @@ -899,6 +921,24 @@ class _CategoryDialogState extends State<_CategoryDialog> { } } + Future _runPostSaveCallbacks({ + required Future Function() onCategoriesChanged, + required Future Function() onRequestSync, + required Future Function() onCategoryDeleted, + }) async { + try { + await onCategoriesChanged(); + } catch (_) {} + + try { + await onCategoryDeleted(); + } catch (_) {} + + unawaited( + onRequestSync().catchError((_) {}), + ); + } + Future _deleteCategory() async { final bool? confirm = await showDialog( context: context, @@ -954,8 +994,17 @@ class _CategoryDialogState extends State<_CategoryDialog> { children: [ TextField( controller: _controller, + onChanged: (_) { + if (_nameHasError) { + setState(() { + _nameHasError = false; + }); + } + }, decoration: const InputDecoration( hintText: 'Nombre de la categoría', + ).copyWith( + errorText: _nameHasError ? 'El nombre es obligatorio' : null, ), ), const SizedBox(height: 16), @@ -1085,8 +1134,14 @@ class _CategoryDialogState extends State<_CategoryDialog> { child: const Text('Cancelar'), ), TextButton( - onPressed: _saveCategory, - child: Text(widget.category == null ? 'Crear' : 'Guardar'), + onPressed: _isSaving ? null : _saveCategory, + child: _isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(widget.category == null ? 'Crear' : 'Guardar'), ), ], );