111 lines
3.6 KiB
Dart
111 lines
3.6 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../core/app_state.dart';
|
||
import '../core/app_theme.dart';
|
||
import '../core/courses/courses.dart';
|
||
import 'app_widgets.dart';
|
||
|
||
/// What a taught pattern is *for*, next to what it means. Knowing that
|
||
/// `How are you?` means 你怎么样 does not tell a learner that it is not the
|
||
/// way to ask someone's name, so the course states the situation, the reply
|
||
/// it invites, and the items it is easiest to mix up with.
|
||
///
|
||
/// The card renders nothing when the pack explains nothing about the item,
|
||
/// which is the normal case for a plain word.
|
||
class UsageCard extends StatelessWidget {
|
||
const UsageCard({
|
||
super.key,
|
||
required this.itemId,
|
||
required this.state,
|
||
this.compact = false,
|
||
});
|
||
|
||
final String itemId;
|
||
final AppState state;
|
||
|
||
/// Inside a task step, only the situation and the difference are shown —
|
||
/// the learner is mid-exercise, not reading a reference card.
|
||
final bool compact;
|
||
|
||
/// A pattern counts as taught once it has a mastery row, which
|
||
/// `AppState` creates when the lesson first presents it.
|
||
bool _isTaught(String id) => state.mastery.containsKey(id);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final usage = coreUsage(itemId);
|
||
if (usage == null) return const SizedBox.shrink();
|
||
final lines = <({String label, String text})>[
|
||
if (usage.when.isNotEmpty) (label: '什么时候用', text: usage.when),
|
||
if (!compact && usage.reply.isNotEmpty)
|
||
(label: '对方会怎么答', text: usage.reply),
|
||
if (!compact && usage.swap.isNotEmpty) (label: '换个说法', text: usage.swap),
|
||
// Mid-exercise only the nearest confusion is worth a line; the full
|
||
// list belongs on the preview and lookup cards.
|
||
for (final other in visibleConfusables(
|
||
itemId,
|
||
isTaught: _isTaught,
|
||
).take(compact ? 1 : 3))
|
||
if (other.note.isNotEmpty)
|
||
(label: '别和 ${coreItemSpoken(other.id)} 弄混', text: other.note),
|
||
];
|
||
if (lines.isEmpty) return const SizedBox.shrink();
|
||
return SectionCard(
|
||
tint: AppColors.warm,
|
||
child: SpacedColumn(
|
||
spacing: 8,
|
||
children: [
|
||
for (final line in lines)
|
||
RichText(
|
||
text: TextSpan(
|
||
style: DefaultTextStyle.of(
|
||
context,
|
||
).style.copyWith(color: AppColors.warmInk, height: 1.45),
|
||
children: [
|
||
TextSpan(
|
||
text: '${line.label}:',
|
||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||
),
|
||
TextSpan(text: line.text),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Every usage note a teaching segment carries, in course order. Shown in the
|
||
/// speaking and writing steps, where a learner is about to produce the pattern
|
||
/// and the question is which one this situation calls for.
|
||
class SegmentUsage extends StatelessWidget {
|
||
const SegmentUsage({
|
||
super.key,
|
||
required this.segmentId,
|
||
required this.state,
|
||
this.compact = true,
|
||
});
|
||
|
||
final String segmentId;
|
||
final AppState state;
|
||
final bool compact;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ids = findLessonSegment(segmentId)?.targetItemIds ?? const <String>[];
|
||
final explained = [
|
||
for (final id in ids)
|
||
if (coreUsage(id) != null) id,
|
||
];
|
||
if (explained.isEmpty) return const SizedBox.shrink();
|
||
return SpacedColumn(
|
||
spacing: 8,
|
||
children: [
|
||
for (final id in explained)
|
||
UsageCard(itemId: id, state: state, compact: compact),
|
||
],
|
||
);
|
||
}
|
||
}
|