feat(app): the card composer, for many cards in one sitting

Create flashcards, and tapping a card, open the composer: the stack of
cards on the left (New flashcard on top, the new ones, then the stored
ones; collapsible), the card being written on the right, saved as the
person leaves it (another card, the pane, the screen, the app). The
title says how saving stands; leaving with cards that couldn't be saved
asks first. A card has its tags and their pin, an action menu
(duplicate, clear, delete), the type switch (flashcard, vocabulary,
matching), the front, and below a line its answer side: one answer or
several to mark right or wrong, the reading and meanings, or categories
and items. Fields changed differently on two devices show every version
to keep one. The single-card editor goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-01 18:42:25 +02:00
commit 0afc22d454
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -6,9 +6,8 @@ import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/router/tab_visibility.dart';
import 'package:halkyon_learn_flutter/src/core/widget/found_by_id.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/widget/profile_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_edit_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_composer_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/widget/explore_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/widget/settings_screen.dart';
@ -17,8 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/library_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
// Material's Card widget would clash with the domain's Card.
import 'package:material_ui/material_ui.dart' hide Card;
import 'package:material_ui/material_ui.dart';
/// The router of one person's app: the four tabs in a shell, each with its own
/// stack of screens, and the focused surfaces over them. Every route is built
@ -102,39 +100,21 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
);
},
),
// Before the card route: its `new` would otherwise read as a card's id.
// The composer: writing a studyset's cards, opened on a new card or on
// one of them. Before the card route: its `new` would otherwise read as
// a card's id.
GoRoute(
path: '/studysets/:studysetId/cards/new',
builder: (context, state) => BlocProvider(
create: (context) => CardEditBloc(
PersonScope.of(context).cards,
PersonScope.of(context).tags,
state.pathParameters['studysetId']!,
null,
)..add(const CardEditEventStarted()),
child: const CardScreen(card: null),
),
builder: (context, state) =>
_composer(context, state.pathParameters['studysetId']!),
),
GoRoute(
path: '/studysets/:studysetId/cards/:cardId',
builder: (context, state) {
final person = PersonScope.of(context);
final studysetId = state.pathParameters['studysetId']!;
return FoundById<Card>(
id: state.pathParameters['cardId']!,
items: () => person.cards.watch(studysetId),
idOf: (card) => card.id,
builder: (card, _) => BlocProvider(
create: (context) => CardEditBloc(
person.cards,
person.tags,
studysetId,
card,
)..add(const CardEditEventStarted()),
child: CardScreen(card: card),
),
);
},
builder: (context, state) => _composer(
context,
state.pathParameters['studysetId']!,
cardId: state.pathParameters['cardId'],
),
),
GoRoute(
path: RoutePaths.settings,
@ -149,3 +129,23 @@ Widget _studysetScreen(BuildContext context, String id) => FoundById<Studyset>(
idOf: (studyset) => studyset.id,
builder: (studyset, all) => StudysetScreen(studyset: studyset, all: all),
);
/// The composer of the studyset [studysetId]'s cards, opened on [cardId] if
/// given; a studyset gone (deleted, or an old link) says so.
Widget _composer(BuildContext context, String studysetId, {String? cardId}) {
final person = PersonScope.of(context);
return FoundById<Studyset>(
id: studysetId,
items: person.studysets.watchAll,
idOf: (studyset) => studyset.id,
builder: (studyset, _) => BlocProvider(
create: (context) =>
CardComposerBloc(person.cards, person.tags, studysetId)
..add(const CardComposerEventStarted()),
child: CardComposerScreen(
studysetTitle: studyset.content.title,
initialCardId: cardId,
),
),
);
}