feat(app): undo deleting a card
Deleting a card, from the list or while studying, asks first ("Delete
flashcard?"), then offers Undo for a few seconds ("Flashcard deleted").
Meanwhile the card leaves every list and session; once the time to
undo is over, its revisions are blanked and it is deleted for good, as
notes and studysets already are (CardDeletions, a DelayedDeletions).
Undo while studying puts the card back into the round where it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c5cbc53bdd
commit
2b188641c3
6 changed files with 219 additions and 44 deletions
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
|
|||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
|
|
@ -62,6 +63,7 @@ PersonDependencies personDependencies({Future<void> Function()? onDispose}) {
|
|||
assets: assets,
|
||||
deletions: StudysetDeletions(studysets, cards, notes, paths, pdfSources),
|
||||
noteDeletions: NoteDeletions(notes),
|
||||
cardDeletions: CardDeletions(cards),
|
||||
pathDeletions: LearningPathDeletions(paths),
|
||||
stepDeletions: StepDeletions(paths),
|
||||
pdfDeletions: PdfDeletions(pdfSources),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
|
|
@ -22,14 +23,17 @@ void main() {
|
|||
late CardRepository cards;
|
||||
late TagRepository tags;
|
||||
late ReviewRepository reviews;
|
||||
late CardDeletions deletions;
|
||||
|
||||
setUp(() {
|
||||
gateway = FakeCardGateway();
|
||||
cards = CardRepository(gateway);
|
||||
tags = TagRepository(FakeTagGateway(), cardChanges: cards.changes);
|
||||
reviews = ReviewRepository(FakeReviewGateway());
|
||||
deletions = CardDeletions(cards, undoWindow: Duration.zero);
|
||||
});
|
||||
tearDown(() async {
|
||||
await deletions.dispose();
|
||||
await reviews.dispose();
|
||||
await tags.dispose();
|
||||
await cards.dispose();
|
||||
|
|
@ -40,7 +44,7 @@ void main() {
|
|||
'a card reported or buried shows so, and either is taken back',
|
||||
() async {
|
||||
final id = await cards.create('japanese', card('猫', 'cat'));
|
||||
final bloc = CardListBloc(cards, tags, reviews, 'japanese')
|
||||
final bloc = CardListBloc(cards, tags, reviews, deletions, 'japanese')
|
||||
..add(const CardListEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
Future<CardListStateLoaded> next(
|
||||
|
|
@ -70,7 +74,7 @@ void main() {
|
|||
|
||||
test('lists the cards and follows every change', () async {
|
||||
await cards.create('japanese', card('猫', 'cat'));
|
||||
final bloc = CardListBloc(cards, tags, reviews, 'japanese')
|
||||
final bloc = CardListBloc(cards, tags, reviews, deletions, 'japanese')
|
||||
..add(const CardListEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
|
||||
|
|
@ -100,7 +104,7 @@ void main() {
|
|||
test('moves a card', () async {
|
||||
await cards.create('japanese', card('a', ''));
|
||||
await cards.create('japanese', card('b', ''));
|
||||
final bloc = CardListBloc(cards, tags, reviews, 'japanese')
|
||||
final bloc = CardListBloc(cards, tags, reviews, deletions, 'japanese')
|
||||
..add(const CardListEventStarted());
|
||||
addTearDown(bloc.close);
|
||||
final loaded = await bloc.stream.firstWhere(
|
||||
|
|
@ -137,7 +141,7 @@ void main() {
|
|||
await tags.setTagsOf(ids[0], {noun});
|
||||
await tags.setTagsOf(ids[1], {noun});
|
||||
await tags.setTagsOf(ids[2], {verb});
|
||||
bloc = CardListBloc(cards, tags, reviews, 'japanese')
|
||||
bloc = CardListBloc(cards, tags, reviews, deletions, 'japanese')
|
||||
..add(const CardListEventStarted());
|
||||
});
|
||||
tearDown(() => bloc.close());
|
||||
|
|
|
|||
|
|
@ -110,10 +110,8 @@ void main() {
|
|||
expect(find.text('Changed on two devices'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a card's options delete it, after asking", (tester) async {
|
||||
final id = await cards.create('japanese', card('猫', 'cat'));
|
||||
await showList(tester);
|
||||
|
||||
/// Deletes the card shown through its options, saying yes.
|
||||
Future<void> delete(WidgetTester tester) async {
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(CardTile),
|
||||
|
|
@ -126,11 +124,38 @@ void main() {
|
|||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete flashcard?'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Delete'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
expect(gateway.deletions, ['blank $id', 'delete $id']);
|
||||
testWidgets("a card's options delete it, after asking; once the time to "
|
||||
'undo is over, for good', (tester) async {
|
||||
final id = await cards.create('japanese', card('猫', 'cat'));
|
||||
await showList(tester);
|
||||
|
||||
await delete(tester);
|
||||
|
||||
expect(find.text('Flashcard deleted'), findsOneWidget);
|
||||
expect(find.text('No flashcards yet'), findsOneWidget);
|
||||
expect(gateway.deletions, isEmpty);
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(gateway.deletions, ['blank $id', 'delete $id']);
|
||||
});
|
||||
|
||||
testWidgets('Undo keeps a card deleted a moment ago', (tester) async {
|
||||
await cards.create('japanese', card('猫', 'cat'));
|
||||
await showList(tester);
|
||||
|
||||
await delete(tester);
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('猫'), findsOneWidget);
|
||||
expect(gateway.deletions, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a card's options report it and bury it, each shown on the "
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
|
|
@ -35,6 +36,7 @@ void main() {
|
|||
late FakeStudysetGateway studysetGateway;
|
||||
late StudysetRepository studysets;
|
||||
late CardRepository cards;
|
||||
late CardDeletions deletions;
|
||||
late FakeReviewGateway reviewGateway;
|
||||
late ReviewRepository reviews;
|
||||
late TagRepository tags;
|
||||
|
|
@ -49,6 +51,7 @@ void main() {
|
|||
);
|
||||
studysets = StudysetRepository(studysetGateway);
|
||||
cards = CardRepository(FakeCardGateway());
|
||||
deletions = CardDeletions(cards, undoWindow: Duration.zero);
|
||||
reviewGateway = FakeReviewGateway();
|
||||
reviews = ReviewRepository(reviewGateway);
|
||||
tags = TagRepository(FakeTagGateway());
|
||||
|
|
@ -56,6 +59,7 @@ void main() {
|
|||
now = DateTime.utc(2026, 10, 5, 9);
|
||||
});
|
||||
tearDown(() async {
|
||||
await deletions.dispose();
|
||||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
await tags.dispose();
|
||||
|
|
@ -66,6 +70,7 @@ void main() {
|
|||
StudySessionBloc session() => StudySessionBloc(
|
||||
studysets,
|
||||
cards,
|
||||
deletions,
|
||||
reviews,
|
||||
tags,
|
||||
settings,
|
||||
|
|
@ -188,6 +193,8 @@ void main() {
|
|||
bloc.add(const StudySessionEventCardDeleted());
|
||||
expect((await showing(bloc, '犬')).pills, hasLength(1));
|
||||
});
|
||||
// Deleted for good once the time to undo it is over.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
(await cards.cardsOf('japanese')).map((each) => each.content.front),
|
||||
|
|
@ -794,6 +801,7 @@ void main() {
|
|||
StudySessionBloc focused(StudyFocus focus) => StudySessionBloc(
|
||||
studysets,
|
||||
cards,
|
||||
deletions,
|
||||
reviews,
|
||||
tags,
|
||||
settings,
|
||||
|
|
|
|||
|
|
@ -253,7 +253,11 @@ void main() {
|
|||
await tester.tap(find.widgetWithText(FilledButton, 'Delete'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Flashcard deleted'), findsOneWidget);
|
||||
expect(find.textContaining('犬', findRichText: true), findsOneWidget);
|
||||
// For good once the time to undo it is over.
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(await cards.cardsOf('japanese'), hasLength(1));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
|
||||
|
|
@ -170,6 +171,7 @@ Dependencies testDependencies({
|
|||
pdfSources,
|
||||
);
|
||||
final noteDeletions = NoteDeletions(notes);
|
||||
final cardDeletions = CardDeletions(cards);
|
||||
final pathDeletions = LearningPathDeletions(paths);
|
||||
final stepDeletions = StepDeletions(paths);
|
||||
final pdfDeletions = PdfDeletions(pdfSources);
|
||||
|
|
@ -187,6 +189,7 @@ Dependencies testDependencies({
|
|||
assets: assets,
|
||||
deletions: deletions,
|
||||
noteDeletions: noteDeletions,
|
||||
cardDeletions: cardDeletions,
|
||||
pathDeletions: pathDeletions,
|
||||
stepDeletions: stepDeletions,
|
||||
pdfDeletions: pdfDeletions,
|
||||
|
|
@ -194,6 +197,7 @@ Dependencies testDependencies({
|
|||
onDispose: () async {
|
||||
await deletions.dispose();
|
||||
await noteDeletions.dispose();
|
||||
await cardDeletions.dispose();
|
||||
await pathDeletions.dispose();
|
||||
await stepDeletions.dispose();
|
||||
await pdfDeletions.dispose();
|
||||
|
|
|
|||
Loading…
Reference in a new issue