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>
170 lines
4.5 KiB
Dart
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')}';
|
|
}
|