halkyon/halkyon_learn_flutter/test/support/fake_card_gateway.dart
Tizian 「ティツィアーン」 6c9dfadf27
refactor(app): flashcards keep the order they came in, without Reorder
Studying breaks the order of due cards anyway; what follows the list
order is new cards and cramming, and that order comes from creating and
importing, with sub-studysets putting lessons in order. No one drags a
card among a thousand (decided 2026-10-08).

Gone: Reorder and the grip on flashcards, CardListEventMoved,
CardRepository.move and the gateway's updatePosition, with their tests.
Each card keeps its stored position, so the order stays the same on
every device. liftWhileDragged is Material's plain lift again, for the
lists that keep Reorder.

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>
2026-10-09 18:09:21 +02:00

170 lines
4.5 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/card/data/card_gateway.dart';
/// A [CardGateway] in memory. Ids sort by creation, as UUID v7 ids do: cards
/// are `card-0001`, `card-0002`, …, revisions `rev-0001`, …;
/// [receiveRemoteRevision] plays a sync that brought another device's edit.
class FakeCardGateway() implements CardGateway {
final _cards =
<
String,
({
String studysetId,
String position,
DateTime? buriedAt,
DateTime createdAt,
})
>{};
final _revisions = <String, CardRevisionRow>{};
final _remoteChanges = StreamController<void>.broadcast();
var _nextId = 1;
/// Every revision written on this device, in order.
final written = <CardRevisionRow>[];
/// The deletion steps, in order: `blank <card id>`, `delete <card id>`.
final deletions = <String>[];
/// When set, storing a card 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 cardId,
required Set<String> parents,
required ByteData payload,
}) {
_revisions[id] = (
id: id,
cardId: cardId,
parents: parents,
payload: payload,
);
_remoteChanges.add(null);
}
@override
Future<List<CardRow>> cardsOf(String studysetId) async => [
for (final MapEntry(key: id, value: card) in _cards.entries)
if (card.studysetId == studysetId)
(id: id, position: card.position, buriedAt: card.buriedAt),
];
@override
Future<List<CardStampRow>> allCards() async => [
for (final MapEntry(key: id, value: card) in _cards.entries)
(id: id, studysetId: card.studysetId, createdAt: card.createdAt),
];
@override
Future<List<CardRevisionRow>> revisionsOf(Set<String> cardIds) async => [
for (final revision in _revisions.values)
if (cardIds.contains(revision.cardId)) revision,
];
@override
Future<List<CardRevisionRow>> allRevisions() async => [..._revisions.values];
@override
Future<List<String>> insertCards(
String studysetId,
List<NewCardRow> cards,
) async => [
for (final card in cards)
await insertCard(
studysetId: studysetId,
position: card.position,
payload: card.payload,
),
];
@override
Future<String> insertCard({
required String studysetId,
required String position,
required ByteData payload,
}) async {
if (failWrites case final error?) {
throw error;
}
final id = _newId('card');
_cards[id] = (
studysetId: studysetId,
position: position,
buriedAt: null,
createdAt: clock.now().toUtc(),
);
await insertRevision(cardId: id, parents: const {}, payload: payload);
return id;
}
@override
Future<String> insertRevision({
required String cardId,
required Set<String> parents,
required ByteData payload,
}) async {
if (failWrites case final error?) {
throw error;
}
final revision = (
id: _newId('rev'),
cardId: cardId,
parents: parents,
payload: payload,
);
_revisions[revision.id] = revision;
written.add(revision);
return revision.id;
}
@override
Future<void> updateBuriedAt(String cardId, DateTime? buriedAt) async {
if (_cards[cardId] case final card?) {
_cards[cardId] = (
studysetId: card.studysetId,
position: card.position,
buriedAt: buriedAt?.toUtc(),
createdAt: card.createdAt,
);
}
}
@override
Future<void> blankRevisions(String cardId) async {
deletions.add('blank $cardId');
for (final revision in [..._revisions.values]) {
if (revision.cardId == cardId) {
_revisions[revision.id] = (
id: revision.id,
cardId: cardId,
parents: revision.parents,
payload: redactedPayload,
);
}
}
}
@override
Future<void> deleteCard(String cardId) async {
deletions.add('delete $cardId');
_cards.remove(cardId);
}
@override
Stream<void> get remoteChanges => _remoteChanges.stream;
String _newId(String kind) =>
'$kind-${(_nextId++).toString().padLeft(4, '0')}';
}