feat: Add color and icon properties to categories, enhance category management in UI
This commit is contained in:
@@ -15,6 +15,8 @@ class Categories extends Table {
|
|||||||
integer().named('server_version').withDefault(const Constant(0))();
|
integer().named('server_version').withDefault(const Constant(0))();
|
||||||
BoolColumn get isDeleted =>
|
BoolColumn get isDeleted =>
|
||||||
boolean().named('is_deleted').withDefault(const Constant(false))();
|
boolean().named('is_deleted').withDefault(const Constant(false))();
|
||||||
|
IntColumn get colorValue => integer().nullable().named('color_value')();
|
||||||
|
IntColumn get iconCodePoint => integer().nullable().named('icon_code_point')();
|
||||||
BoolColumn get isDirty =>
|
BoolColumn get isDirty =>
|
||||||
boolean().named('is_dirty').withDefault(const Constant(true))();
|
boolean().named('is_dirty').withDefault(const Constant(true))();
|
||||||
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
|
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
|
||||||
@@ -46,7 +48,7 @@ class Notes extends Table {
|
|||||||
@DriftDatabase(tables: [Notes, Categories])
|
@DriftDatabase(tables: [Notes, Categories])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -61,6 +63,12 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
await customStatement('UPDATE notes SET is_dirty = 0');
|
await customStatement('UPDATE notes SET is_dirty = 0');
|
||||||
await customStatement('UPDATE categories SET is_dirty = 0');
|
await customStatement('UPDATE categories SET is_dirty = 0');
|
||||||
}
|
}
|
||||||
|
if (from < 3) {
|
||||||
|
await migrator.addColumn(categories, categories.colorValue);
|
||||||
|
await migrator.addColumn(categories, categories.iconCodePoint);
|
||||||
|
await customStatement('UPDATE categories SET color_value = NULL');
|
||||||
|
await customStatement('UPDATE categories SET icon_code_point = NULL');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -682,6 +682,28 @@ class $CategoriesTable extends Categories
|
|||||||
),
|
),
|
||||||
defaultValue: const Constant(true),
|
defaultValue: const Constant(true),
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _colorValueMeta = const VerificationMeta(
|
||||||
|
'colorValue',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> colorValue = GeneratedColumn<int>(
|
||||||
|
'color_value',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _iconCodePointMeta = const VerificationMeta(
|
||||||
|
'iconCodePoint',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> iconCodePoint = GeneratedColumn<int>(
|
||||||
|
'icon_code_point',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
);
|
);
|
||||||
@@ -700,6 +722,8 @@ class $CategoriesTable extends Categories
|
|||||||
serverVersion,
|
serverVersion,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
isDirty,
|
isDirty,
|
||||||
|
colorValue,
|
||||||
|
iconCodePoint,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
@@ -751,6 +775,21 @@ class $CategoriesTable extends Categories
|
|||||||
isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta),
|
isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('color_value')) {
|
||||||
|
context.handle(
|
||||||
|
_colorValueMeta,
|
||||||
|
colorValue.isAcceptableOrUnknown(data['color_value']!, _colorValueMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('icon_code_point')) {
|
||||||
|
context.handle(
|
||||||
|
_iconCodePointMeta,
|
||||||
|
iconCodePoint.isAcceptableOrUnknown(
|
||||||
|
data['icon_code_point']!,
|
||||||
|
_iconCodePointMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('updated_at')) {
|
if (data.containsKey('updated_at')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_updatedAtMeta,
|
_updatedAtMeta,
|
||||||
@@ -788,6 +827,14 @@ class $CategoriesTable extends Categories
|
|||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_dirty'],
|
data['${effectivePrefix}is_dirty'],
|
||||||
)!,
|
)!,
|
||||||
|
colorValue: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}color_value'],
|
||||||
|
),
|
||||||
|
iconCodePoint: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}icon_code_point'],
|
||||||
|
),
|
||||||
updatedAt: attachedDatabase.typeMapping.read(
|
updatedAt: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}updated_at'],
|
data['${effectivePrefix}updated_at'],
|
||||||
@@ -807,6 +854,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
final int serverVersion;
|
final int serverVersion;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
final bool isDirty;
|
final bool isDirty;
|
||||||
|
final int? colorValue;
|
||||||
|
final int? iconCodePoint;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
const DbCategory({
|
const DbCategory({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -814,6 +863,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
required this.isDeleted,
|
required this.isDeleted,
|
||||||
required this.isDirty,
|
required this.isDirty,
|
||||||
|
this.colorValue,
|
||||||
|
this.iconCodePoint,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
@@ -824,6 +875,12 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
map['server_version'] = Variable<int>(serverVersion);
|
map['server_version'] = Variable<int>(serverVersion);
|
||||||
map['is_deleted'] = Variable<bool>(isDeleted);
|
map['is_deleted'] = Variable<bool>(isDeleted);
|
||||||
map['is_dirty'] = Variable<bool>(isDirty);
|
map['is_dirty'] = Variable<bool>(isDirty);
|
||||||
|
if (!nullToAbsent || colorValue != null) {
|
||||||
|
map['color_value'] = Variable<int>(colorValue);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || iconCodePoint != null) {
|
||||||
|
map['icon_code_point'] = Variable<int>(iconCodePoint);
|
||||||
|
}
|
||||||
map['updated_at'] = Variable<DateTime>(updatedAt);
|
map['updated_at'] = Variable<DateTime>(updatedAt);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -835,6 +892,12 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
serverVersion: Value(serverVersion),
|
serverVersion: Value(serverVersion),
|
||||||
isDeleted: Value(isDeleted),
|
isDeleted: Value(isDeleted),
|
||||||
isDirty: Value(isDirty),
|
isDirty: Value(isDirty),
|
||||||
|
colorValue: colorValue == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value<int?>(colorValue),
|
||||||
|
iconCodePoint: iconCodePoint == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value<int?>(iconCodePoint),
|
||||||
updatedAt: Value(updatedAt),
|
updatedAt: Value(updatedAt),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -850,6 +913,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
serverVersion: serializer.fromJson<int>(json['serverVersion']),
|
serverVersion: serializer.fromJson<int>(json['serverVersion']),
|
||||||
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
||||||
isDirty: serializer.fromJson<bool>(json['isDirty']),
|
isDirty: serializer.fromJson<bool>(json['isDirty']),
|
||||||
|
colorValue: serializer.fromJson<int?>(json['colorValue']),
|
||||||
|
iconCodePoint: serializer.fromJson<int?>(json['iconCodePoint']),
|
||||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -862,6 +927,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
'serverVersion': serializer.toJson<int>(serverVersion),
|
'serverVersion': serializer.toJson<int>(serverVersion),
|
||||||
'isDeleted': serializer.toJson<bool>(isDeleted),
|
'isDeleted': serializer.toJson<bool>(isDeleted),
|
||||||
'isDirty': serializer.toJson<bool>(isDirty),
|
'isDirty': serializer.toJson<bool>(isDirty),
|
||||||
|
'colorValue': serializer.toJson<int?>(colorValue),
|
||||||
|
'iconCodePoint': serializer.toJson<int?>(iconCodePoint),
|
||||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -872,6 +939,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
int? serverVersion,
|
int? serverVersion,
|
||||||
bool? isDeleted,
|
bool? isDeleted,
|
||||||
bool? isDirty,
|
bool? isDirty,
|
||||||
|
int? colorValue,
|
||||||
|
int? iconCodePoint,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
}) => DbCategory(
|
}) => DbCategory(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -879,6 +948,8 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
isDirty: isDirty ?? this.isDirty,
|
isDirty: isDirty ?? this.isDirty,
|
||||||
|
colorValue: colorValue ?? this.colorValue,
|
||||||
|
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
);
|
);
|
||||||
DbCategory copyWithCompanion(CategoriesCompanion data) {
|
DbCategory copyWithCompanion(CategoriesCompanion data) {
|
||||||
@@ -892,6 +963,12 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
: this.serverVersion,
|
: this.serverVersion,
|
||||||
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
|
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
|
||||||
isDirty: data.isDirty.present ? data.isDirty.value : this.isDirty,
|
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,
|
||||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -938,12 +1015,16 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
final Value<bool> isDirty;
|
final Value<bool> isDirty;
|
||||||
final Value<DateTime> updatedAt;
|
final Value<DateTime> updatedAt;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
|
final Value<int?> colorValue;
|
||||||
|
final Value<int?> iconCodePoint;
|
||||||
const CategoriesCompanion({
|
const CategoriesCompanion({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.encryptedName = const Value.absent(),
|
this.encryptedName = const Value.absent(),
|
||||||
this.serverVersion = const Value.absent(),
|
this.serverVersion = const Value.absent(),
|
||||||
this.isDeleted = const Value.absent(),
|
this.isDeleted = const Value.absent(),
|
||||||
this.isDirty = const Value.absent(),
|
this.isDirty = const Value.absent(),
|
||||||
|
this.colorValue = const Value.absent(),
|
||||||
|
this.iconCodePoint = const Value.absent(),
|
||||||
this.updatedAt = const Value.absent(),
|
this.updatedAt = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
});
|
});
|
||||||
@@ -953,6 +1034,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
this.serverVersion = const Value.absent(),
|
this.serverVersion = const Value.absent(),
|
||||||
this.isDeleted = const Value.absent(),
|
this.isDeleted = const Value.absent(),
|
||||||
this.isDirty = const Value.absent(),
|
this.isDirty = const Value.absent(),
|
||||||
|
this.colorValue = const Value.absent(),
|
||||||
|
this.iconCodePoint = const Value.absent(),
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
}) : id = Value(id),
|
}) : id = Value(id),
|
||||||
@@ -964,6 +1047,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
Expression<int>? serverVersion,
|
Expression<int>? serverVersion,
|
||||||
Expression<bool>? isDeleted,
|
Expression<bool>? isDeleted,
|
||||||
Expression<bool>? isDirty,
|
Expression<bool>? isDirty,
|
||||||
|
Expression<int>? colorValue,
|
||||||
|
Expression<int>? iconCodePoint,
|
||||||
Expression<DateTime>? updatedAt,
|
Expression<DateTime>? updatedAt,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
@@ -973,6 +1058,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
if (serverVersion != null) 'server_version': serverVersion,
|
if (serverVersion != null) 'server_version': serverVersion,
|
||||||
if (isDeleted != null) 'is_deleted': isDeleted,
|
if (isDeleted != null) 'is_deleted': isDeleted,
|
||||||
if (isDirty != null) 'is_dirty': isDirty,
|
if (isDirty != null) 'is_dirty': isDirty,
|
||||||
|
if (colorValue != null) 'color_value': colorValue,
|
||||||
|
if (iconCodePoint != null) 'icon_code_point': iconCodePoint,
|
||||||
if (updatedAt != null) 'updated_at': updatedAt,
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
});
|
});
|
||||||
@@ -984,6 +1071,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
Value<int>? serverVersion,
|
Value<int>? serverVersion,
|
||||||
Value<bool>? isDeleted,
|
Value<bool>? isDeleted,
|
||||||
Value<bool>? isDirty,
|
Value<bool>? isDirty,
|
||||||
|
Value<int>? colorValue,
|
||||||
|
Value<int>? iconCodePoint,
|
||||||
Value<DateTime>? updatedAt,
|
Value<DateTime>? updatedAt,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
@@ -993,6 +1082,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
isDirty: isDirty ?? this.isDirty,
|
isDirty: isDirty ?? this.isDirty,
|
||||||
|
colorValue: colorValue ?? this.colorValue,
|
||||||
|
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
);
|
);
|
||||||
@@ -1016,6 +1107,12 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
if (isDirty.present) {
|
if (isDirty.present) {
|
||||||
map['is_dirty'] = Variable<bool>(isDirty.value);
|
map['is_dirty'] = Variable<bool>(isDirty.value);
|
||||||
}
|
}
|
||||||
|
if (colorValue.present) {
|
||||||
|
map['color_value'] = Variable<int>(colorValue.value);
|
||||||
|
}
|
||||||
|
if (iconCodePoint.present) {
|
||||||
|
map['icon_code_point'] = Variable<int>(iconCodePoint.value);
|
||||||
|
}
|
||||||
if (updatedAt.present) {
|
if (updatedAt.present) {
|
||||||
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:notas/models/category.dart';
|
|||||||
|
|
||||||
import 'package:notas/data/note_encryption.dart';
|
import 'package:notas/data/note_encryption.dart';
|
||||||
import 'package:notas/widgets/sync_status.dart';
|
import 'package:notas/widgets/sync_status.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show debugPrint;
|
||||||
|
|
||||||
class NoteRepository {
|
class NoteRepository {
|
||||||
NoteRepository({
|
NoteRepository({
|
||||||
@@ -34,6 +35,56 @@ class NoteRepository {
|
|||||||
return _loadDeletedNotesFromDatabase();
|
return _loadDeletedNotesFromDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Category>> loadCategories() async {
|
||||||
|
final List<DbCategory> dbCategories = await _database.getAllCategories();
|
||||||
|
final List<Category> 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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,
|
||||||
|
updatedAt: category.updatedAt,
|
||||||
|
serverVersion: const Value(0),
|
||||||
|
isDeleted: const Value(false),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
colorValue: Value<int?>(category.colorValue),
|
||||||
|
iconCodePoint: Value<int?>(category.iconCodePoint),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('Category inserted to database');
|
||||||
|
}
|
||||||
|
|
||||||
Future<Note> createNote(Note note) async {
|
Future<Note> createNote(Note note) async {
|
||||||
await _database.insertNoteAtTop(
|
await _database.insertNoteAtTop(
|
||||||
NotesCompanion.insert(
|
NotesCompanion.insert(
|
||||||
@@ -60,7 +111,7 @@ class NoteRepository {
|
|||||||
|
|
||||||
final DbNote row =
|
final DbNote row =
|
||||||
existingNote ??
|
existingNote ??
|
||||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||||
|
|
||||||
await _database.updateNoteRow(
|
await _database.updateNoteRow(
|
||||||
DbNote(
|
DbNote(
|
||||||
@@ -92,7 +143,7 @@ class NoteRepository {
|
|||||||
|
|
||||||
final DbNote row =
|
final DbNote row =
|
||||||
existingNote ??
|
existingNote ??
|
||||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||||
|
|
||||||
if (row.isDeleted || note.isDeleted || note.isPermanentlyDeleted) {
|
if (row.isDeleted || note.isDeleted || note.isPermanentlyDeleted) {
|
||||||
await _database.permanentlyDeleteNote(row.id);
|
await _database.permanentlyDeleteNote(row.id);
|
||||||
@@ -111,7 +162,7 @@ class NoteRepository {
|
|||||||
|
|
||||||
final DbNote row =
|
final DbNote row =
|
||||||
existingNote ??
|
existingNote ??
|
||||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||||
|
|
||||||
await _database.moveNote(
|
await _database.moveNote(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -212,10 +263,7 @@ class NoteRepository {
|
|||||||
details.add('StackTrace: ${stackTrace.toString()}');
|
details.add('StackTrace: ${stackTrace.toString()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {'error': true, 'message': details.join('\n\n')};
|
||||||
'error': true,
|
|
||||||
'message': details.join('\n\n'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final SyncResponse response = syncResult['data'] as SyncResponse;
|
final SyncResponse response = syncResult['data'] as SyncResponse;
|
||||||
@@ -249,10 +297,7 @@ class NoteRepository {
|
|||||||
'categoriesCount': response.changes.categories.length,
|
'categoriesCount': response.changes.categories.length,
|
||||||
};
|
};
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
return {
|
return {'error': true, 'message': '$e\n\nStackTrace: $st'};
|
||||||
'error': true,
|
|
||||||
'message': '$e\n\nStackTrace: $st',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,21 +308,18 @@ class NoteRepository {
|
|||||||
// Apply categories from server
|
// Apply categories from server
|
||||||
for (final SyncCategoryResponse catResponse
|
for (final SyncCategoryResponse catResponse
|
||||||
in response.changes.categories) {
|
in response.changes.categories) {
|
||||||
final String categoryName =
|
// Store the encrypted blob received from the server directly in the DB.
|
||||||
catResponse.isDeleted || catResponse.encryptedName.isEmpty
|
// Decryption is only performed when loading categories for display.
|
||||||
? ''
|
final String encryptedBlob = catResponse.encryptedName;
|
||||||
: await NoteEncryption.decryptNote(
|
|
||||||
catResponse.encryptedName,
|
|
||||||
_masterKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await _database.upsertCategory(
|
await _database.upsertCategory(
|
||||||
CategoriesCompanion(
|
CategoriesCompanion(
|
||||||
id: Value(catResponse.id),
|
id: Value(catResponse.id),
|
||||||
encryptedName: Value(categoryName),
|
encryptedName: Value(encryptedBlob),
|
||||||
serverVersion: Value(catResponse.serverVersion),
|
serverVersion: Value(catResponse.serverVersion),
|
||||||
isDeleted: Value(catResponse.isDeleted),
|
isDeleted: Value(catResponse.isDeleted),
|
||||||
|
colorValue: Value<int?>(catResponse.colorValue),
|
||||||
|
iconCodePoint: Value<int?>(catResponse.iconCodePoint),
|
||||||
updatedAt: Value(catResponse.updatedAt),
|
updatedAt: Value(catResponse.updatedAt),
|
||||||
isDirty: const Value(false),
|
isDirty: const Value(false),
|
||||||
),
|
),
|
||||||
@@ -439,9 +481,10 @@ Future<List<SyncCategoryPayload>> _encryptCategories(
|
|||||||
final List<SyncCategoryPayload> payloads = [];
|
final List<SyncCategoryPayload> payloads = [];
|
||||||
|
|
||||||
for (final DbCategory row in categories) {
|
for (final DbCategory row in categories) {
|
||||||
final String encryptedName = row.encryptedName.isEmpty
|
// The DB already stores the encrypted name blob in `encryptedName`.
|
||||||
? ''
|
// Use it directly when building the sync payload and preserve
|
||||||
: await NoteEncryption.encryptNote(row.encryptedName, masterKey);
|
// color/icon values from the DB row so they are sent to the server.
|
||||||
|
final String encryptedName = row.encryptedName;
|
||||||
|
|
||||||
payloads.add(
|
payloads.add(
|
||||||
SyncCategoryPayload.fromCategory(
|
SyncCategoryPayload.fromCategory(
|
||||||
@@ -452,6 +495,8 @@ Future<List<SyncCategoryPayload>> _encryptCategories(
|
|||||||
isDeleted: row.isDeleted,
|
isDeleted: row.isDeleted,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
isDirty: row.isDirty,
|
isDirty: row.isDirty,
|
||||||
|
colorValue: row.colorValue,
|
||||||
|
iconCodePoint: row.iconCodePoint,
|
||||||
),
|
),
|
||||||
encryptedName: encryptedName,
|
encryptedName: encryptedName,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ class SyncCategoryPayload {
|
|||||||
required this.encryptedName,
|
required this.encryptedName,
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
this.isDeleted = false,
|
this.isDeleted = false,
|
||||||
|
this.colorValue,
|
||||||
|
this.iconCodePoint,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +104,8 @@ class SyncCategoryPayload {
|
|||||||
final String encryptedName;
|
final String encryptedName;
|
||||||
final int serverVersion;
|
final int serverVersion;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
|
final int? colorValue;
|
||||||
|
final int? iconCodePoint;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
|
||||||
factory SyncCategoryPayload.fromCategory(
|
factory SyncCategoryPayload.fromCategory(
|
||||||
@@ -113,6 +117,8 @@ class SyncCategoryPayload {
|
|||||||
encryptedName: encryptedName,
|
encryptedName: encryptedName,
|
||||||
serverVersion: category.serverVersion,
|
serverVersion: category.serverVersion,
|
||||||
isDeleted: category.isDeleted,
|
isDeleted: category.isDeleted,
|
||||||
|
colorValue: category.colorValue,
|
||||||
|
iconCodePoint: category.iconCodePoint,
|
||||||
updatedAt: category.updatedAt,
|
updatedAt: category.updatedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,6 +129,8 @@ class SyncCategoryPayload {
|
|||||||
'encrypted_name': encryptedName,
|
'encrypted_name': encryptedName,
|
||||||
'serverVersion': serverVersion,
|
'serverVersion': serverVersion,
|
||||||
'isDeleted': isDeleted,
|
'isDeleted': isDeleted,
|
||||||
|
if (colorValue != null) 'colorValue': colorValue!.toSigned(32),
|
||||||
|
if (iconCodePoint != null) 'iconCodePoint': iconCodePoint,
|
||||||
'updatedAt': updatedAt.toIso8601String(),
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -213,12 +221,16 @@ class SyncCategoryResponse {
|
|||||||
required this.encryptedName,
|
required this.encryptedName,
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
this.isDeleted = false,
|
this.isDeleted = false,
|
||||||
|
this.colorValue,
|
||||||
|
this.iconCodePoint,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
final String id;
|
final String id;
|
||||||
final String encryptedName;
|
final String encryptedName;
|
||||||
final int serverVersion;
|
final int serverVersion;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
|
final int? colorValue;
|
||||||
|
final int? iconCodePoint;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
|
||||||
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -227,6 +239,12 @@ class SyncCategoryResponse {
|
|||||||
encryptedName: _readStringValue(json['encrypted_name']),
|
encryptedName: _readStringValue(json['encrypted_name']),
|
||||||
serverVersion: _readIntValue(json['serverVersion']),
|
serverVersion: _readIntValue(json['serverVersion']),
|
||||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
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),
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -237,6 +255,8 @@ class SyncCategoryResponse {
|
|||||||
name: name,
|
name: name,
|
||||||
serverVersion: serverVersion,
|
serverVersion: serverVersion,
|
||||||
isDeleted: isDeleted,
|
isDeleted: isDeleted,
|
||||||
|
colorValue: colorValue,
|
||||||
|
iconCodePoint: iconCodePoint,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class Category {
|
|||||||
this.isDeleted = false,
|
this.isDeleted = false,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.isDirty = true,
|
this.isDirty = true,
|
||||||
|
this.colorValue,
|
||||||
|
this.iconCodePoint,
|
||||||
}) : id = id ?? Uuid().v4();
|
}) : id = id ?? Uuid().v4();
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@@ -16,6 +18,8 @@ class Category {
|
|||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final bool isDirty;
|
final bool isDirty;
|
||||||
|
final int? colorValue;
|
||||||
|
final int? iconCodePoint;
|
||||||
|
|
||||||
Category copyWith({
|
Category copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -24,6 +28,8 @@ class Category {
|
|||||||
bool? isDeleted,
|
bool? isDeleted,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
bool? isDirty,
|
bool? isDirty,
|
||||||
|
int? colorValue,
|
||||||
|
int? iconCodePoint,
|
||||||
}) {
|
}) {
|
||||||
return Category(
|
return Category(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -32,6 +38,8 @@ class Category {
|
|||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
isDirty: isDirty ?? this.isDirty,
|
isDirty: isDirty ?? this.isDirty,
|
||||||
|
colorValue: colorValue ?? this.colorValue,
|
||||||
|
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+264
-14
@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|||||||
import 'package:notas/data/note_repository.dart';
|
import 'package:notas/data/note_repository.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/screens/note_editor_screen.dart';
|
import 'package:notas/screens/note_editor_screen.dart';
|
||||||
|
import 'package:notas/models/category.dart';
|
||||||
import 'package:notas/widgets/menu_drawer.dart';
|
import 'package:notas/widgets/menu_drawer.dart';
|
||||||
import 'package:notas/widgets/note_card.dart';
|
import 'package:notas/widgets/note_card.dart';
|
||||||
import 'package:notas/widgets/search_app_bar.dart';
|
import 'package:notas/widgets/search_app_bar.dart';
|
||||||
@@ -49,6 +50,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
bool _isMenuOpen = false;
|
bool _isMenuOpen = false;
|
||||||
bool _showDeletedNotes = false;
|
bool _showDeletedNotes = false;
|
||||||
PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse;
|
PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse;
|
||||||
|
List<Category> _categories = <Category>[];
|
||||||
|
String? _selectedCategoryId;
|
||||||
|
|
||||||
void _openMenu() {
|
void _openMenu() {
|
||||||
if (_isMenuOpen) {
|
if (_isMenuOpen) {
|
||||||
@@ -80,6 +83,34 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadNotes();
|
_loadNotes();
|
||||||
|
_loadCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCategories() async {
|
||||||
|
try {
|
||||||
|
final List<Category> cats = await widget.repository.loadCategories();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_categories = cats;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to load categories: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Category? _currentCategory() {
|
||||||
|
final String? selectedCategoryId = _selectedCategoryId;
|
||||||
|
if (selectedCategoryId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Category category in _categories) {
|
||||||
|
if (category.id == selectedCategoryId) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -113,7 +144,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openNoteComposer() async {
|
Future<void> _openNoteComposer() async {
|
||||||
final dynamic result = await NoteEditorScreen.showDialog(context);
|
final dynamic result = await NoteEditorScreen.showDialog(
|
||||||
|
context,
|
||||||
|
categoryId: _showDeletedNotes ? null : _selectedCategoryId,
|
||||||
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return;
|
return;
|
||||||
@@ -188,12 +222,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Note> _getFilteredNotes() {
|
List<Note> _getFilteredNotes() {
|
||||||
|
Iterable<Note> notes = _notes;
|
||||||
|
|
||||||
|
if (_selectedCategoryId != null) {
|
||||||
|
notes = notes.where(
|
||||||
|
(Note note) => note.categoryId == _selectedCategoryId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_searchQuery.isEmpty) {
|
if (_searchQuery.isEmpty) {
|
||||||
return _notes;
|
return notes.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String query = _searchQuery.toLowerCase();
|
final String query = _searchQuery.toLowerCase();
|
||||||
return _notes
|
return notes
|
||||||
.where(
|
.where(
|
||||||
(Note note) =>
|
(Note note) =>
|
||||||
note.title.toLowerCase().contains(query) ||
|
note.title.toLowerCase().contains(query) ||
|
||||||
@@ -205,6 +247,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Future<void> _handleMenuItemTapped(String item) async {
|
Future<void> _handleMenuItemTapped(String item) async {
|
||||||
_closeMenu();
|
_closeMenu();
|
||||||
|
|
||||||
|
if (item == 'create_category') {
|
||||||
|
await _showCreateCategoryDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.startsWith('category_')) {
|
||||||
|
final String id = item.substring('category_'.length);
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = id;
|
||||||
|
_showDeletedNotes = false;
|
||||||
|
_searchQuery = '';
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
await _loadNotes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item == 'settings') {
|
if (item == 'settings') {
|
||||||
widget.onOpenSettings();
|
widget.onOpenSettings();
|
||||||
return;
|
return;
|
||||||
@@ -213,6 +272,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
if (item == 'deleted_notes') {
|
if (item == 'deleted_notes') {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showDeletedNotes = true;
|
_showDeletedNotes = true;
|
||||||
|
_selectedCategoryId = null;
|
||||||
_searchQuery = '';
|
_searchQuery = '';
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
@@ -223,6 +283,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
if (item == 'all_notes') {
|
if (item == 'all_notes') {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showDeletedNotes = false;
|
_showDeletedNotes = false;
|
||||||
|
_selectedCategoryId = null;
|
||||||
_searchQuery = '';
|
_searchQuery = '';
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
@@ -230,15 +291,185 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showCreateCategoryDialog() async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
Color? selectedColor;
|
||||||
|
IconData? selectedIcon;
|
||||||
|
|
||||||
|
final List<Color> palette = [
|
||||||
|
Colors.amber,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.green,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.red,
|
||||||
|
Colors.teal,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.grey,
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<IconData> icons = [
|
||||||
|
Icons.folder,
|
||||||
|
Icons.work,
|
||||||
|
Icons.star,
|
||||||
|
Icons.home,
|
||||||
|
Icons.school,
|
||||||
|
Icons.book,
|
||||||
|
Icons.music_note,
|
||||||
|
Icons.lightbulb,
|
||||||
|
];
|
||||||
|
|
||||||
|
final bool? result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (BuildContext ctx, StateSetter setState) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Crear categoría'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Nombre de la categoría',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Color',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: palette.map((Color color) {
|
||||||
|
final bool isSelected = selectedColor == color;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => selectedColor = color),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: Colors.white, width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Icono',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: icons.map((IconData icon) {
|
||||||
|
final bool isSelected = selectedIcon == icon;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => selectedIcon = icon),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white10
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: isSelected ? Colors.white : Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Crear'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != true || controller.text.trim().isEmpty) {
|
||||||
|
controller.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Category newCategory = Category(
|
||||||
|
name: controller.text.trim(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
colorValue: selectedColor?.toARGB32(),
|
||||||
|
iconCodePoint: selectedIcon?.codePoint,
|
||||||
|
);
|
||||||
|
debugPrint('Creating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}');
|
||||||
|
await widget.repository.createCategory(newCategory);
|
||||||
|
await _loadCategories();
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Categoría creada')));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await widget.onRequestSync();
|
||||||
|
} catch (_) {}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('ERROR creating category: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error al crear categoría: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double width = MediaQuery.of(context).size.width;
|
final double width = MediaQuery.of(context).size.width;
|
||||||
final int crossAxisCount = math.max((width / 250).floor().round(), 2);
|
final int crossAxisCount = math.max((width / 250).floor().round(), 2);
|
||||||
|
final List<Note> visibleNotes = _getFilteredNotes();
|
||||||
|
final Category? currentCategory = _currentCategory();
|
||||||
|
|
||||||
final Widget body = _isLoading
|
final Widget body = _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _notes.isEmpty
|
: visibleNotes.isEmpty
|
||||||
? _EmptyState(showDeletedNotes: _showDeletedNotes)
|
? _EmptyState(
|
||||||
|
showDeletedNotes: _showDeletedNotes,
|
||||||
|
categoryName: currentCategory?.name,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
)
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await widget.onRequestSync();
|
await widget.onRequestSync();
|
||||||
@@ -251,9 +482,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
mainAxisSpacing: 10,
|
mainAxisSpacing: 10,
|
||||||
crossAxisSpacing: 10,
|
crossAxisSpacing: 10,
|
||||||
itemCount: _getFilteredNotes().length,
|
itemCount: visibleNotes.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final List<Note> filteredNotes = _getFilteredNotes();
|
final List<Note> filteredNotes = visibleNotes;
|
||||||
return DragTarget<int>(
|
return DragTarget<int>(
|
||||||
onAcceptWithDetails: (DragTargetDetails<int> details) {
|
onAcceptWithDetails: (DragTargetDetails<int> details) {
|
||||||
final Note targetNote = filteredNotes[index];
|
final Note targetNote = filteredNotes[index];
|
||||||
@@ -357,7 +588,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -640,9 +870,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
elevation: 8,
|
elevation: 8,
|
||||||
child: MenuDrawer(
|
child: MenuDrawer(
|
||||||
onMenuItemTapped: _handleMenuItemTapped,
|
onMenuItemTapped: _handleMenuItemTapped,
|
||||||
selectedItem: _showDeletedNotes
|
selectedItem: _selectedCategoryId != null
|
||||||
? 'deleted_notes'
|
? 'category_${_selectedCategoryId}'
|
||||||
: 'all_notes',
|
: (_showDeletedNotes
|
||||||
|
? 'deleted_notes'
|
||||||
|
: 'all_notes'),
|
||||||
|
categories: _categories,
|
||||||
|
onCreateCategory: _showCreateCategoryDialog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -668,9 +902,15 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EmptyState extends StatelessWidget {
|
class _EmptyState extends StatelessWidget {
|
||||||
const _EmptyState({required this.showDeletedNotes});
|
const _EmptyState({
|
||||||
|
required this.showDeletedNotes,
|
||||||
|
this.categoryName,
|
||||||
|
this.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
final bool showDeletedNotes;
|
final bool showDeletedNotes;
|
||||||
|
final String? categoryName;
|
||||||
|
final String? searchQuery;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -681,7 +921,13 @@ class _EmptyState extends StatelessWidget {
|
|||||||
const Icon(Icons.note_add_outlined, color: Colors.white54, size: 48),
|
const Icon(Icons.note_add_outlined, color: Colors.white54, size: 48),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
showDeletedNotes ? 'No hay notas borradas' : 'Aún no hay notas',
|
searchQuery != null && searchQuery!.isNotEmpty
|
||||||
|
? 'No hay resultados'
|
||||||
|
: showDeletedNotes
|
||||||
|
? 'No hay notas borradas'
|
||||||
|
: categoryName != null
|
||||||
|
? 'No hay notas en esta categoría'
|
||||||
|
: 'Aún no hay notas',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -690,8 +936,12 @@ class _EmptyState extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
showDeletedNotes
|
searchQuery != null && searchQuery!.isNotEmpty
|
||||||
|
? 'Prueba a buscar con otro término o crea una nota nueva.'
|
||||||
|
: showDeletedNotes
|
||||||
? 'Las notas borradas aparecerán aquí para poder restaurarlas.'
|
? 'Las notas borradas aparecerán aquí para poder restaurarlas.'
|
||||||
|
: categoryName != null
|
||||||
|
? 'Pulsa el botón + para crear una nota en “$categoryName”.'
|
||||||
: 'Pulsa el botón + para crear la primera.',
|
: 'Pulsa el botón + para crear la primera.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
|||||||
@@ -14,15 +14,25 @@ import 'package:notas/platform/app_platform.dart';
|
|||||||
// the user confirmed deletion. `null` indicates the user closed without saving.
|
// the user confirmed deletion. `null` indicates the user closed without saving.
|
||||||
|
|
||||||
class NoteEditorScreen extends StatefulWidget {
|
class NoteEditorScreen extends StatefulWidget {
|
||||||
const NoteEditorScreen({super.key, required this.note, this.onComplete});
|
const NoteEditorScreen({
|
||||||
|
super.key,
|
||||||
|
required this.note,
|
||||||
|
this.categoryId,
|
||||||
|
this.onComplete,
|
||||||
|
});
|
||||||
|
|
||||||
final Note? note;
|
final Note? note;
|
||||||
|
final String? categoryId;
|
||||||
final ValueChanged<dynamic>? onComplete;
|
final ValueChanged<dynamic>? onComplete;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
||||||
|
|
||||||
static Future<dynamic> showDialog(BuildContext context, {Note? note}) {
|
static Future<dynamic> showDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
Note? note,
|
||||||
|
String? categoryId,
|
||||||
|
}) {
|
||||||
if (isAndroid || isIOS) {
|
if (isAndroid || isIOS) {
|
||||||
return showGeneralDialog<dynamic>(
|
return showGeneralDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -30,7 +40,7 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
barrierColor: Colors.transparent,
|
barrierColor: Colors.transparent,
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
return NoteEditorScreen(note: note);
|
return NoteEditorScreen(note: note, categoryId: categoryId);
|
||||||
},
|
},
|
||||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
return ScaleTransition(scale: animation, child: child);
|
return ScaleTransition(scale: animation, child: child);
|
||||||
@@ -46,7 +56,7 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
barrierColor: Colors.transparent,
|
barrierColor: Colors.transparent,
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
return NoteEditorScreen(note: note);
|
return NoteEditorScreen(note: note, categoryId: categoryId);
|
||||||
},
|
},
|
||||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
return ScaleTransition(scale: animation, child: child);
|
return ScaleTransition(scale: animation, child: child);
|
||||||
@@ -61,6 +71,7 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
builder: (BuildContext overlayContext) {
|
builder: (BuildContext overlayContext) {
|
||||||
return NoteEditorScreen(
|
return NoteEditorScreen(
|
||||||
note: note,
|
note: note,
|
||||||
|
categoryId: categoryId,
|
||||||
onComplete: (dynamic result) {
|
onComplete: (dynamic result) {
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
completer.complete(result);
|
completer.complete(result);
|
||||||
@@ -99,6 +110,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
position: 0,
|
position: 0,
|
||||||
|
categoryId: widget.categoryId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_currentNote = widget.note!;
|
_currentNote = widget.note!;
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/models/category.dart';
|
||||||
|
|
||||||
class MenuDrawer extends StatelessWidget {
|
class MenuDrawer extends StatelessWidget {
|
||||||
const MenuDrawer({
|
const MenuDrawer({
|
||||||
super.key,
|
super.key,
|
||||||
this.onMenuItemTapped,
|
this.onMenuItemTapped,
|
||||||
this.selectedItem,
|
this.selectedItem,
|
||||||
|
this.categories = const [],
|
||||||
|
this.onCreateCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ValueChanged<String>? onMenuItemTapped;
|
final ValueChanged<String>? onMenuItemTapped;
|
||||||
final String? selectedItem;
|
final String? selectedItem;
|
||||||
|
final List<Category> categories;
|
||||||
|
final VoidCallback? onCreateCategory;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,8 +36,36 @@ class MenuDrawer extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: const [
|
children: [
|
||||||
SizedBox.shrink(),
|
if (categories.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Column(
|
||||||
|
children: categories.map((category) {
|
||||||
|
final categoryId = 'category_${category.id}';
|
||||||
|
final IconData categoryIcon = category.iconCodePoint == null
|
||||||
|
? Icons.folder_outlined
|
||||||
|
: IconData(
|
||||||
|
category.iconCodePoint!,
|
||||||
|
fontFamily: 'MaterialIcons',
|
||||||
|
);
|
||||||
|
|
||||||
|
return _MenuItemTile(
|
||||||
|
icon: categoryIcon,
|
||||||
|
label: category.name,
|
||||||
|
selected: selectedItem == categoryId,
|
||||||
|
onTap: () => onMenuItemTapped?.call(categoryId),
|
||||||
|
iconColor: Color(category.colorValue ?? 0xFFFFC107),
|
||||||
|
textColor: Color(category.colorValue ?? 0xFFFFC107),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_MenuItemTile(
|
||||||
|
icon: Icons.add_circle_outline,
|
||||||
|
label: 'Crear categoría',
|
||||||
|
onTap: onCreateCategory,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -86,7 +119,6 @@ class _MenuItemTile extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
left: selected ? 0 : 8,
|
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 2,
|
top: 2,
|
||||||
bottom: 2,
|
bottom: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user