feat: Implement note positioning logic and tests for position conversion
This commit is contained in:
+103
-36
@@ -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')
|
||||||
@@ -48,7 +50,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 => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -69,6 +71,29 @@ 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 +122,37 @@ 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 = await (select(notes)
|
||||||
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE is_deleted = 0',
|
..where((n) => n.isDeleted.equals(false))
|
||||||
|
..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 into(notes).insert(note.copyWith(sortIndex: const Value<int>(0)));
|
return nextSortIndex;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +178,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 +205,64 @@ 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 = await (select(notes)
|
||||||
notes,
|
..where((n) => n.isDeleted.equals(false))
|
||||||
)..where((n) => n.isDeleted.equals(false))).get();
|
..orderBy([
|
||||||
|
(note) => OrderingTerm(
|
||||||
|
expression: note.sortIndex,
|
||||||
|
mode: OrderingMode.desc,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
|
|
||||||
final int count = all.length;
|
final int currentIndex = orderedNotes.indexWhere((DbNote row) => row.id == id);
|
||||||
if (count == 0) {
|
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),
|
||||||
),
|
),
|
||||||
@@ -221,10 +281,17 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
// 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)
|
||||||
|
..where(
|
||||||
(n) => n.isDeleted.equals(true) &
|
(n) => n.isDeleted.equals(true) &
|
||||||
(n.title.isNotValue('') | n.body.isNotValue('')),
|
(n.title.isNotValue('') | n.body.isNotValue('')),
|
||||||
))
|
)
|
||||||
|
..orderBy([
|
||||||
|
(note) => OrderingTerm(
|
||||||
|
expression: note.sortIndex,
|
||||||
|
mode: OrderingMode.desc,
|
||||||
|
),
|
||||||
|
]))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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],
|
||||||
),
|
),
|
||||||
@@ -840,8 +846,7 @@ 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!)
|
||||||
@@ -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,10 +988,13 @@ class _CategoryDialogState extends State<_CategoryDialog> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration:
|
||||||
|
const InputDecoration(
|
||||||
hintText: 'Nombre de la categoría',
|
hintText: 'Nombre de la categoría',
|
||||||
).copyWith(
|
).copyWith(
|
||||||
errorText: _nameHasError ? 'El nombre es obligatorio' : null,
|
errorText: _nameHasError
|
||||||
|
? 'El nombre es obligatorio'
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -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(() {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user