With two notes or more, Reorder beside the tabs gives each note a grip in place of its options, as paths have, until Done; e.g. to follow a reading order. A note's position was stored already; the repository now moves it (only that column changes, and it syncs). The notes' bloc sits with the studyset's other lists now, above the tabs, so the button knows how many there are. The position helpers the paths kept to themselves are shared (core/sync/fractional_position.dart), and so is the grip (ReorderGrip). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
149 lines
4.3 KiB
Dart
149 lines
4.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:clock/clock.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
|
|
|
|
/// A [NoteGateway] in memory. Ids sort by creation, as UUID v7 ids do: notes
|
|
/// are `note-0001`, `note-0002`, …, revisions `nrev-0001`, …, each written
|
|
/// at the clock's time; [receiveRemoteRevision] plays a sync that brought
|
|
/// another device's edit.
|
|
class FakeNoteGateway() implements NoteGateway {
|
|
final _notes = <String, ({String studysetId, String position})>{};
|
|
final _revisions = <String, NoteRevisionRow>{};
|
|
final _remoteChanges = StreamController<void>.broadcast();
|
|
var _nextId = 1;
|
|
|
|
/// Every revision written on this device, in order.
|
|
final written = <NoteRevisionRow>[];
|
|
|
|
/// The deletion steps, in order: `blank <note id>`, `delete <note id>`.
|
|
final deletions = <String>[];
|
|
|
|
/// When set, storing a note or a revision fails with it.
|
|
Exception? failWrites;
|
|
|
|
/// The payload each revision holds now, by revision id.
|
|
Map<String, ByteData> get payloads => {
|
|
for (final revision in _revisions.values) revision.id: revision.payload,
|
|
};
|
|
|
|
/// Stores a revision as if another device had written it and a sync had
|
|
/// brought it here.
|
|
void receiveRemoteRevision({
|
|
required String id,
|
|
required String noteId,
|
|
required Set<String> parents,
|
|
required ByteData payload,
|
|
}) {
|
|
_revisions[id] = (
|
|
id: id,
|
|
noteId: noteId,
|
|
parents: parents,
|
|
payload: payload,
|
|
writtenAt: clock.now().toUtc(),
|
|
);
|
|
_remoteChanges.add(null);
|
|
}
|
|
|
|
/// Deletes the note [noteId] as if another device had done it and a sync
|
|
/// had brought it here.
|
|
void receiveRemoteDeletion(String noteId) {
|
|
_notes.remove(noteId);
|
|
_remoteChanges.add(null);
|
|
}
|
|
|
|
@override
|
|
Future<List<NoteRow>> notesOf(String studysetId) async => [
|
|
for (final MapEntry(key: id, value: note) in _notes.entries)
|
|
if (note.studysetId == studysetId) (id: id, position: note.position),
|
|
];
|
|
|
|
@override
|
|
Future<List<NoteStampRow>> allNotes() async => [
|
|
for (final MapEntry(key: id, value: note) in _notes.entries)
|
|
(id: id, studysetId: note.studysetId),
|
|
];
|
|
|
|
@override
|
|
Future<List<NoteRevisionRow>> revisionsOf(Set<String> noteIds) async => [
|
|
for (final revision in _revisions.values)
|
|
if (noteIds.contains(revision.noteId)) revision,
|
|
];
|
|
|
|
@override
|
|
Future<({String noteId, String revisionId})> insertNote({
|
|
required String studysetId,
|
|
required String position,
|
|
required ByteData payload,
|
|
}) async {
|
|
if (failWrites case final error?) {
|
|
throw error;
|
|
}
|
|
final id = _newId('note');
|
|
_notes[id] = (studysetId: studysetId, position: position);
|
|
final revisionId = await insertRevision(
|
|
noteId: id,
|
|
parents: const {},
|
|
payload: payload,
|
|
);
|
|
return (noteId: id, revisionId: revisionId);
|
|
}
|
|
|
|
@override
|
|
Future<String> insertRevision({
|
|
required String noteId,
|
|
required Set<String> parents,
|
|
required ByteData payload,
|
|
}) async {
|
|
if (failWrites case final error?) {
|
|
throw error;
|
|
}
|
|
final revision = (
|
|
id: _newId('nrev'),
|
|
noteId: noteId,
|
|
parents: parents,
|
|
payload: payload,
|
|
writtenAt: clock.now().toUtc(),
|
|
);
|
|
_revisions[revision.id] = revision;
|
|
written.add(revision);
|
|
return revision.id;
|
|
}
|
|
|
|
@override
|
|
Future<void> updatePosition(String noteId, String position) async {
|
|
if (_notes[noteId] case final note?) {
|
|
_notes[noteId] = (studysetId: note.studysetId, position: position);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> blankRevisions(String noteId) async {
|
|
deletions.add('blank $noteId');
|
|
for (final revision in [..._revisions.values]) {
|
|
if (revision.noteId == noteId) {
|
|
_revisions[revision.id] = (
|
|
id: revision.id,
|
|
noteId: noteId,
|
|
parents: revision.parents,
|
|
payload: redactedPayload,
|
|
writtenAt: revision.writtenAt,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteNote(String noteId) async {
|
|
deletions.add('delete $noteId');
|
|
_notes.remove(noteId);
|
|
}
|
|
|
|
@override
|
|
Stream<void> get remoteChanges => _remoteChanges.stream;
|
|
|
|
String _newId(String kind) =>
|
|
'$kind-${(_nextId++).toString().padLeft(4, '0')}';
|
|
}
|