Browse beside the flashcards' tab opens the studyset's cards over the tabs (with everything inside it when it has sub-studysets), laid out as the older design had it: - a grid of tiles (each card's two sides) or one card at a time beside the list (on a phone above a strip of them, swiped through), as last left for the studyset on this device; - a search over front, back, reading, meanings, hints, explanation and tag names, katakana found as hiragana and kana by romaji; - sorting by own order, newest, oldest, A–Z (words by reading), due first; a filter sheet by kind, tag and state (new, learning, due, buried, reported); - choosing cards (a toggle; shift-click or long-press for a range, select all) to move into another or a new studyset, add or remove tags, start afresh, or delete with Undo; the actions in the row on wide windows, in a bar at the foot below. Moving cards between studysets is new in the card repository and its gateway (studysetId and position written, named columns). 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>
186 lines
4.9 KiB
Dart
186 lines
4.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? 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> updateStudyset(
|
|
String cardId,
|
|
String studysetId,
|
|
String position,
|
|
) async {
|
|
if (_cards[cardId] case final card?) {
|
|
_cards[cardId] = (
|
|
studysetId: studysetId,
|
|
position: position,
|
|
buriedAt: card.buriedAt,
|
|
createdAt: card.createdAt,
|
|
);
|
|
}
|
|
}
|
|
|
|
@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')}';
|
|
}
|