Compare commits

...

2 Commits

6 changed files with 244 additions and 99 deletions
+122 -48
View File
@@ -5,6 +5,8 @@ import 'package:drift/native.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:notas/data/note_positioning.dart';
part 'app_database.g.dart'; part 'app_database.g.dart';
@DataClassName('DbCategory') @DataClassName('DbCategory')
@@ -16,7 +18,8 @@ class Categories extends Table {
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 colorValue => integer().nullable().named('color_value')();
IntColumn get iconCodePoint => integer().nullable().named('icon_code_point')(); 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')();
@@ -38,17 +41,17 @@ class Notes extends Table {
BoolColumn get isDeleted => BoolColumn get isDeleted =>
boolean().named('is_deleted').withDefault(const Constant(false))(); boolean().named('is_deleted').withDefault(const Constant(false))();
TextColumn get categoryId => text().nullable().named('category_id')(); TextColumn get categoryId => text().nullable().named('category_id')();
BoolColumn get isDirty => BoolColumn get isDirty =>
boolean().named('is_dirty').withDefault(const Constant(true))(); boolean().named('is_dirty').withDefault(const Constant(true))();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@DriftDatabase(tables: [Notes, Categories]) @DriftDatabase(tables: [Notes, Categories])
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
@override @override
int get schemaVersion => 3; int get schemaVersion => 4;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -69,6 +72,30 @@ class AppDatabase extends _$AppDatabase {
await customStatement('UPDATE categories SET color_value = NULL'); await customStatement('UPDATE categories SET color_value = NULL');
await customStatement('UPDATE categories SET icon_code_point = NULL'); await customStatement('UPDATE categories SET icon_code_point = NULL');
} }
if (from < 4) {
final List<DbNote> activeNotes =
await (select(notes)
..where((n) => n.isDeleted.equals(false))
..orderBy([
(note) => OrderingTerm(expression: note.sortIndex),
]))
.get();
final List<int> rebalancedPositions = rebalanceNotePositions(
activeNotes.length,
);
for (var index = 0; index < activeNotes.length; index += 1) {
final DbNote row = activeNotes[index];
await (update(notes)..where((n) => n.id.equals(row.id))).write(
NotesCompanion(
sortIndex: Value(rebalancedPositions[index]),
updatedAt: Value(DateTime.now()),
isDirty: const Value(true),
),
);
}
}
}, },
); );
@@ -97,17 +124,38 @@ class AppDatabase extends _$AppDatabase {
// ========== Notes ========== // ========== Notes ==========
Future<List<DbNote>> getAllNotes() { Future<List<DbNote>> getAllNotes() {
return (select(notes) return (select(notes)
..orderBy([(note) => OrderingTerm(expression: note.sortIndex)]) ..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
])
..where((n) => n.isDeleted.equals(false))) ..where((n) => n.isDeleted.equals(false)))
.get(); .get();
} }
Future<int> insertNoteAtTop(NotesCompanion note) { Future<int> insertNoteAtTop(NotesCompanion note) {
return transaction(() async { return transaction(() async {
await customStatement( final DbNote? topNote =
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE is_deleted = 0', await (select(notes)
); ..where((n) => n.isDeleted.equals(false))
return into(notes).insert(note.copyWith(sortIndex: const Value<int>(0))); ..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
])
..limit(1))
.getSingleOrNull();
final int nextSortIndex = topNote == null
? 0
: topNote.sortIndex + notePositionStep;
await into(
notes,
).insert(note.copyWith(sortIndex: Value<int>(nextSortIndex)));
return nextSortIndex;
}); });
} }
@@ -133,11 +181,6 @@ class AppDatabase extends _$AppDatabase {
isDirty: const Value(true), isDirty: const Value(true),
), ),
); );
await customStatement(
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND is_deleted = 0',
[removedIndex],
);
} }
Future<void> deleteNoteAndShift({ Future<void> deleteNoteAndShift({
@@ -165,44 +208,67 @@ class AppDatabase extends _$AppDatabase {
required int oldIndex, required int oldIndex,
required int newIndex, required int newIndex,
}) { }) {
if (oldIndex == newIndex) {
return Future<void>.value();
}
return transaction(() async { return transaction(() async {
final List<DbNote> all = await (select( final List<DbNote> orderedNotes =
notes, await (select(notes)
)..where((n) => n.isDeleted.equals(false))).get(); ..where((n) => n.isDeleted.equals(false))
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
]))
.get();
final int count = all.length; final int currentIndex = orderedNotes.indexWhere(
if (count == 0) { (DbNote row) => row.id == id,
);
if (currentIndex == -1) {
return; return;
} }
final int maxIndex = count - 1; final int safeNewIndex = newIndex.clamp(0, orderedNotes.length - 1);
if (currentIndex == safeNewIndex) {
final int safeOld = oldIndex.clamp(0, maxIndex);
final int safeNew = newIndex.clamp(0, maxIndex);
if (safeOld == safeNew) {
return; return;
} }
if (safeOld < safeNew) { final DbNote movedNote = orderedNotes.removeAt(currentIndex);
await customStatement( orderedNotes.insert(safeNewIndex, movedNote);
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0',
[safeOld, safeNew], final int? newStoredPosition;
); if (safeNewIndex == 0) {
newStoredPosition = orderedNotes[1].sortIndex + notePositionStep;
} else if (safeNewIndex == orderedNotes.length - 1) {
newStoredPosition =
orderedNotes[orderedNotes.length - 2].sortIndex - notePositionStep;
} else { } else {
await customStatement( newStoredPosition = midpointNotePosition(
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0', higherPosition: orderedNotes[safeNewIndex - 1].sortIndex,
[safeNew, safeOld], lowerPosition: orderedNotes[safeNewIndex + 1].sortIndex,
); );
} }
if (newStoredPosition == null) {
final List<int> rebalancedPositions = rebalanceNotePositions(
orderedNotes.length,
);
for (var index = 0; index < orderedNotes.length; index += 1) {
final DbNote row = orderedNotes[index];
await (update(notes)..where((n) => n.id.equals(row.id))).write(
NotesCompanion(
sortIndex: Value<int>(rebalancedPositions[index]),
updatedAt: Value<DateTime>(DateTime.now()),
isDirty: const Value(true),
),
);
}
return;
}
await (update(notes)..where((n) => n.id.equals(id))).write( await (update(notes)..where((n) => n.id.equals(id))).write(
NotesCompanion( NotesCompanion(
sortIndex: Value<int>(safeNew), sortIndex: Value<int>(newStoredPosition),
updatedAt: Value<DateTime>(DateTime.now()), updatedAt: Value<DateTime>(DateTime.now()),
isDirty: const Value(true), isDirty: const Value(true),
), ),
@@ -217,15 +283,23 @@ class AppDatabase extends _$AppDatabase {
} }
Future<List<DbNote>> getDeletedNotes() { Future<List<DbNote>> getDeletedNotes() {
// A note is considered deleted (in the trash) when `is_deleted` is true // A note is considered deleted (in the trash) when `is_deleted` is true
// and at least one of `title` or `body` is not empty. Previously the // and at least one of `title` or `body` is not empty. Previously the
// query required both title AND body to be non-empty which excluded // query required both title AND body to be non-empty which excluded
// notes that had an empty body (common) from appearing in the trash. // notes that had an empty body (common) from appearing in the trash.
return (select(notes)..where( return (select(notes)
(n) => n.isDeleted.equals(true) & ..where(
(n.title.isNotValue('') | n.body.isNotValue('')), (n) =>
)) n.isDeleted.equals(true) &
.get(); (n.title.isNotValue('') | n.body.isNotValue('')),
)
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
]))
.get();
} }
Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) { Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
+46
View File
@@ -0,0 +1,46 @@
const int notePositionScale = 10;
const int notePositionStep = 1000 * notePositionScale;
const int notePositionRebalanceThreshold = 1;
int toStoredNotePosition(double position) {
return (position * notePositionScale).round();
}
double fromStoredNotePosition(int storedPosition) {
return storedPosition / notePositionScale;
}
int nextTopNotePosition(Iterable<int> storedPositions) {
int? highestPosition;
for (final int position in storedPositions) {
if (highestPosition == null || position > highestPosition) {
highestPosition = position;
}
}
if (highestPosition == null) {
return 0;
}
return highestPosition + notePositionStep;
}
int? midpointNotePosition({
required int higherPosition,
required int lowerPosition,
}) {
final int gap = higherPosition - lowerPosition;
if (gap <= notePositionRebalanceThreshold) {
return null;
}
return lowerPosition + (gap ~/ 2);
}
List<int> rebalanceNotePositions(int itemCount) {
return List<int>.generate(
itemCount,
(int index) => (itemCount - 1 - index) * notePositionStep,
);
}
+15 -11
View File
@@ -6,6 +6,7 @@ import 'dart:io' show Platform;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:notas/data/app_database.dart'; import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart'; import 'package:notas/data/api_client.dart';
import 'package:notas/data/note_positioning.dart';
import 'package:notas/data/sync_models.dart'; import 'package:notas/data/sync_models.dart';
import 'package:notas/models/note.dart'; import 'package:notas/models/note.dart';
import 'package:notas/models/category.dart'; import 'package:notas/models/category.dart';
@@ -58,9 +59,9 @@ class NoteRepository {
Future<void> createCategory(Category category) async { Future<void> createCategory(Category category) async {
debugPrint('createCategory called with: ${category.name}'); debugPrint('createCategory called with: ${category.name}');
final DbCategory? existingCategory = await ( final DbCategory? existingCategory = await (_database.select(
_database.select(_database.categories)..where((c) => c.id.equals(category.id)) _database.categories,
).getSingleOrNull(); )..where((c) => c.id.equals(category.id))).getSingleOrNull();
final int effectiveServerVersion = math.max( final int effectiveServerVersion = math.max(
category.serverVersion, category.serverVersion,
existingCategory?.serverVersion ?? category.serverVersion, existingCategory?.serverVersion ?? category.serverVersion,
@@ -91,7 +92,7 @@ class NoteRepository {
} }
Future<Note> createNote(Note note) async { Future<Note> createNote(Note note) async {
await _database.insertNoteAtTop( final int storedPosition = await _database.insertNoteAtTop(
NotesCompanion.insert( NotesCompanion.insert(
id: note.id, id: note.id,
title: note.title, title: note.title,
@@ -106,7 +107,10 @@ class NoteRepository {
), ),
); );
return note.copyWith(position: 0, isDirty: true); return note.copyWith(
position: fromStoredNotePosition(storedPosition),
isDirty: true,
);
} }
Future<Note> updateNote(Note note) async { Future<Note> updateNote(Note note) async {
@@ -137,7 +141,7 @@ class NoteRepository {
isDeleted: false, isDeleted: false,
isPermanentlyDeleted: false, isPermanentlyDeleted: false,
isDirty: true, isDirty: true,
position: row.sortIndex.toDouble(), position: fromStoredNotePosition(row.sortIndex),
); );
} }
@@ -364,7 +368,7 @@ class NoteRepository {
body: isPermanentlyDeleted ? '' : decryptedBody, body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt, createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt, updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position.round(), sortIndex: toStoredNotePosition(noteResponse.position),
serverVersion: noteResponse.serverVersion, serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted, isDeleted: noteResponse.isDeleted,
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId, categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
@@ -382,7 +386,7 @@ class NoteRepository {
body: Value(isPermanentlyDeleted ? '' : decryptedBody), body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt), createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt), updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position.round()), sortIndex: Value(toStoredNotePosition(noteResponse.position)),
serverVersion: Value(noteResponse.serverVersion), serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted), isDeleted: Value(noteResponse.isDeleted),
categoryId: Value( categoryId: Value(
@@ -412,7 +416,7 @@ class NoteRepository {
body: row.body, body: row.body,
createdAt: row.createdAt, createdAt: row.createdAt,
updatedAt: row.updatedAt, updatedAt: row.updatedAt,
position: row.sortIndex.toDouble(), position: fromStoredNotePosition(row.sortIndex),
serverVersion: row.serverVersion, serverVersion: row.serverVersion,
isDeleted: row.isDeleted, isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row), isPermanentlyDeleted: _isPermanentlyDeleted(row),
@@ -585,7 +589,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
'updatedAt': row.updatedAt.toIso8601String(), 'updatedAt': row.updatedAt.toIso8601String(),
'categoryId': row.categoryId, 'categoryId': row.categoryId,
'serverVersion': row.serverVersion, 'serverVersion': row.serverVersion,
'position': row.sortIndex, 'position': fromStoredNotePosition(row.sortIndex),
'isDeleted': row.isDeleted, 'isDeleted': row.isDeleted,
'isPermanentlyDeleted': isPermanentlyDeleted, 'isPermanentlyDeleted': isPermanentlyDeleted,
}; };
@@ -635,7 +639,7 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
'encryptedTitle': encryptedTitle, 'encryptedTitle': encryptedTitle,
'encryptedBody': encryptedBody, 'encryptedBody': encryptedBody,
'serverVersion': note['serverVersion']! as int, 'serverVersion': note['serverVersion']! as int,
'position': note['position']! as int, 'position': (note['position'] as num).toDouble(),
'isDeleted': note['isDeleted']! as bool, 'isDeleted': note['isDeleted']! as bool,
'isPermanentlyDeleted': isPermanentlyDeleted, 'isPermanentlyDeleted': isPermanentlyDeleted,
'updatedAt': note['updatedAt']! as String, 'updatedAt': note['updatedAt']! as String,
+27 -36
View File
@@ -87,7 +87,9 @@ class _HomeScreenState extends State<HomeScreen> {
_loadNotesAndCategories(); _loadNotesAndCategories();
} }
Future<void> _loadNotesAndCategories({bool showLoadingIndicator = true}) async { Future<void> _loadNotesAndCategories({
bool showLoadingIndicator = true,
}) async {
if (showLoadingIndicator) { if (showLoadingIndicator) {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@@ -97,8 +99,8 @@ class _HomeScreenState extends State<HomeScreen> {
final Future<List<Note>> notesFuture = _showDeletedNotes final Future<List<Note>> notesFuture = _showDeletedNotes
? widget.repository.loadDeletedNotes() ? widget.repository.loadDeletedNotes()
: widget.repository.loadNotes(); : widget.repository.loadNotes();
final Future<List<Category>> categoriesFuture = final Future<List<Category>> categoriesFuture = widget.repository
widget.repository.loadCategories(); .loadCategories();
List<Note> notesResult = <Note>[]; List<Note> notesResult = <Note>[];
List<Category> categoriesResult = <Category>[]; List<Category> categoriesResult = <Category>[];
@@ -362,7 +364,8 @@ class _HomeScreenState extends State<HomeScreen> {
final Category? currentCategory = _currentCategory(); final Category? currentCategory = _currentCategory();
final Map<String, Color> categoryBorderColors = <String, Color>{ final Map<String, Color> categoryBorderColors = <String, Color>{
for (final Category category in _categories) for (final Category category in _categories)
if (category.colorValue != null) category.id: Color(category.colorValue!), if (category.colorValue != null)
category.id: Color(category.colorValue!),
}; };
final Widget body = _isLoading final Widget body = _isLoading
@@ -421,9 +424,12 @@ class _HomeScreenState extends State<HomeScreen> {
_lastPointerKind, _lastPointerKind,
); );
final Widget draggableNote = _DraggableNote( final Widget
draggableNote = _DraggableNote(
note: filteredNotes[index], note: filteredNotes[index],
borderColor: categoryBorderColors[filteredNotes[index].categoryId], borderColor:
categoryBorderColors[filteredNotes[index]
.categoryId],
dataIndex: _notes.indexOf( dataIndex: _notes.indexOf(
filteredNotes[index], filteredNotes[index],
), ),
@@ -582,7 +588,7 @@ class _HomeScreenState extends State<HomeScreen> {
child: MenuDrawer( child: MenuDrawer(
onMenuItemTapped: _handleMenuItemTapped, onMenuItemTapped: _handleMenuItemTapped,
selectedItem: _selectedCategoryId != null selectedItem: _selectedCategoryId != null
? 'category_$_selectedCategoryId' ? 'category_$_selectedCategoryId'
: (_showDeletedNotes : (_showDeletedNotes
? 'deleted_notes' ? 'deleted_notes'
: 'all_notes'), : 'all_notes'),
@@ -840,12 +846,11 @@ class _CategoryDialogState extends State<_CategoryDialog> {
void initState() { void initState() {
super.initState(); super.initState();
_controller = TextEditingController(text: widget.category?.name ?? ''); _controller = TextEditingController(text: widget.category?.name ?? '');
_selectedColor = _selectedColor = widget.category == null
widget.category == null
? CategoryStyle.colors.first ? CategoryStyle.colors.first
: widget.category!.colorValue != null : widget.category!.colorValue != null
? Color(widget.category!.colorValue!) ? Color(widget.category!.colorValue!)
: null; : null;
if (widget.category != null && widget.category!.iconCodePoint != null) { if (widget.category != null && widget.category!.iconCodePoint != null) {
_selectedIcon = CategoryStyle.icons.firstWhere( _selectedIcon = CategoryStyle.icons.firstWhere(
(IconData icon) => icon.codePoint == widget.category!.iconCodePoint, (IconData icon) => icon.codePoint == widget.category!.iconCodePoint,
@@ -906,7 +911,6 @@ class _CategoryDialogState extends State<_CategoryDialog> {
} }
widget.onRequestSync().catchError((_) {}); widget.onRequestSync().catchError((_) {});
} catch (e) { } catch (e) {
debugPrint('ERROR creating category: $e'); debugPrint('ERROR creating category: $e');
if (mounted) { if (mounted) {
@@ -922,24 +926,6 @@ class _CategoryDialogState extends State<_CategoryDialog> {
} }
} }
Future<void> _runPostSaveCallbacks({
required Future<void> Function() onCategoriesChanged,
required Future<void> Function() onRequestSync,
required Future<void> Function() onCategoryDeleted,
}) async {
try {
await onCategoriesChanged();
} catch (_) {}
try {
await onCategoryDeleted();
} catch (_) {}
unawaited(
onRequestSync().catchError((_) {}),
);
}
Future<void> _deleteCategory() async { Future<void> _deleteCategory() async {
final bool? confirm = await showDialog<bool>( final bool? confirm = await showDialog<bool>(
context: context, context: context,
@@ -1002,11 +988,14 @@ class _CategoryDialogState extends State<_CategoryDialog> {
}); });
} }
}, },
decoration: const InputDecoration( decoration:
hintText: 'Nombre de la categoría', const InputDecoration(
).copyWith( hintText: 'Nombre de la categoría',
errorText: _nameHasError ? 'El nombre es obligatorio' : null, ).copyWith(
), errorText: _nameHasError
? 'El nombre es obligatorio'
: null,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Container( Container(
@@ -1084,7 +1073,9 @@ class _CategoryDialogState extends State<_CategoryDialog> {
key: const ValueKey<String>('icons'), key: const ValueKey<String>('icons'),
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
children: CategoryStyle.icons.map((IconData icon) { children: CategoryStyle.icons.map((
IconData icon,
) {
final bool isSelected = _selectedIcon == icon; final bool isSelected = _selectedIcon == icon;
return GestureDetector( return GestureDetector(
onTap: () => setState(() { onTap: () => setState(() {
+14 -4
View File
@@ -336,7 +336,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
Widget _buildCategorySelectorBox({Category? category}) { Widget _buildCategorySelectorBox({Category? category}) {
final String label = category?.name ?? 'Sin categoría'; final String label = category?.name ?? 'Sin categoría';
final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint); final IconData icon = CategoryStyle.iconForCodePoint(
category?.iconCodePoint,
);
final Color backgroundColor = _categoryBackgroundColor(category); final Color backgroundColor = _categoryBackgroundColor(category);
final Color foregroundColor = _categoryForegroundColor(category); final Color foregroundColor = _categoryForegroundColor(category);
@@ -475,7 +477,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
}) { }) {
final Color backgroundColor = _categoryBackgroundColor(category); final Color backgroundColor = _categoryBackgroundColor(category);
final Color foregroundColor = _categoryForegroundColor(category); final Color foregroundColor = _categoryForegroundColor(category);
final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint); final IconData icon = CategoryStyle.iconForCodePoint(
category?.iconCodePoint,
);
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
@@ -510,8 +514,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
), ),
), ),
), ),
if (isSelected) if (isSelected) Icon(Icons.check, color: foregroundColor, size: 18),
Icon(Icons.check, color: foregroundColor, size: 18),
], ],
), ),
), ),
@@ -565,6 +568,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(
'Posicion: ${_currentNote.position}',
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
Text( Text(
'Creado: ${_formatDate(_currentNote.createdAt)}', 'Creado: ${_formatDate(_currentNote.createdAt)}',
style: const TextStyle( style: const TextStyle(
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:notas/data/note_positioning.dart';
void main() {
test('converts between stored and displayed positions', () {
expect(toStoredNotePosition(1500.5), 15005);
expect(fromStoredNotePosition(15005), 1500.5);
});
test('supports descending gaps and rebalance', () {
expect(nextTopNotePosition(<int>[0, 10000, 20000]), 30000);
expect(
midpointNotePosition(higherPosition: 20000, lowerPosition: 10000),
15000,
);
expect(midpointNotePosition(higherPosition: 2, lowerPosition: 1), isNull);
expect(rebalanceNotePositions(3), <int>[20000, 10000, 0]);
});
}