What the dashboard shows, worked out from this device's data: cards rated (crammed included, fresh starts not) and cards written per local day; the streak, up to today or yesterday, rest days counting; the cards falling due per day, but those resting in archived studysets; the study goal's progress in its day or week. The goal, the rest days and the dashboard's arrangement live in the synced settings (payload schema 3). Cards tell when they were made by their UUID v7 ids; reviews report their changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
142 lines
3.9 KiB
Dart
142 lines
3.9 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 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),
|
|
];
|
|
|
|
@override
|
|
Future<List<CardStamp>> 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<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,
|
|
createdAt: clock.now().toUtc(),
|
|
);
|
|
await insertRevision(cardId: id, parents: const {}, payload: payload);
|
|
return id;
|
|
}
|
|
|
|
@override
|
|
Future<void> 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);
|
|
}
|
|
|
|
@override
|
|
Future<void> updatePosition(String cardId, String position) async {
|
|
if (_cards[cardId] case final card?) {
|
|
_cards[cardId] = (
|
|
studysetId: card.studysetId,
|
|
position: position,
|
|
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')}';
|
|
}
|