feat: complete kouyu_english app codebase, A0 specifications and .gitignore

This commit is contained in:
shen
2026-09-15 15:58:33 +08:00
parent b8072673d8
commit 37c86f7ecb
136 changed files with 19042 additions and 0 deletions
@@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import '../../core/app_state.dart';
import '../../core/app_theme.dart';
import '../../core/models.dart';
import '../../core/seed_courses.dart';
import '../../core/assessment_bank.dart';
import '../../widgets/app_widgets.dart';
import '../dialogue/dialogue_flow.dart';
import '../assessment/assessment_page.dart';
import '../home/home_page.dart';
import '../lesson/lesson_flow.dart';
import '../progress/progress_pages.dart';
import '../review/review_page.dart';
class LearningShell extends StatefulWidget {
const LearningShell({super.key, required this.state});
final AppState state;
@override
State<LearningShell> createState() => _LearningShellState();
}
class _LearningShellState extends State<LearningShell> {
AppTab tab = AppTab.home;
var route = _ShellRoute.tab;
bool dialogueInLesson = false;
AssessmentPack? assessmentPack;
DialogueSummaryData? dialogueSummary;
void showTab(AppTab value) => setState(() {
tab = value;
route = _ShellRoute.tab;
});
void showLesson() => setState(() => route = _ShellRoute.lesson);
void showDialogueScene() => setState(() => route = _ShellRoute.scene);
void showDialogue({bool inLesson = false}) => setState(() {
dialogueInLesson = inLesson;
route = _ShellRoute.dialogue;
});
void showSummary(DialogueSummaryData summary) => setState(() {
dialogueSummary = summary;
route = _ShellRoute.summary;
});
void showSettings() => setState(() => route = _ShellRoute.settings);
void showAssessment(AssessmentPack pack) => setState(() {
assessmentPack = pack;
route = _ShellRoute.assessmentPreparation;
});
void startAssessment() => setState(() => route = _ShellRoute.assessment);
void showAdaptiveLesson() =>
setState(() => route = _ShellRoute.adaptiveLesson);
@override
Widget build(BuildContext context) {
Widget body;
switch (route) {
case _ShellRoute.lesson:
body = LessonFlow(
state: widget.state,
onOpenDialogue: () => showDialogue(inLesson: true),
onFinish: () => showTab(AppTab.home),
);
case _ShellRoute.scene:
body = DialogueScenePage(onStart: showDialogue);
case _ShellRoute.dialogue:
body = DialoguePage(
state: widget.state,
isLessonDialogue: dialogueInLesson,
onFinished: dialogueInLesson
? (_) => showLesson()
: (summary) {
if (summary != null) showSummary(summary);
},
);
case _ShellRoute.summary:
body = DialogueSummaryPage(
summary: dialogueSummary!,
onHome: () => showTab(AppTab.home),
onLesson: showLesson,
onRetry: showDialogue,
);
case _ShellRoute.settings:
body = SettingsPage(state: widget.state);
case _ShellRoute.adaptiveLesson:
body = AdaptiveLessonPage(
state: widget.state,
onFinished: () => showTab(AppTab.review),
);
case _ShellRoute.assessment:
body = AssessmentPage(
state: widget.state,
pack: assessmentPack!,
onFinished: () => showTab(AppTab.progress),
onStartReplacement: showAssessment,
);
case _ShellRoute.assessmentPreparation:
body = AssessmentPreparationPage(
state: widget.state,
pack: assessmentPack!,
onStart: startAssessment,
onBack: () => showTab(AppTab.progress),
);
case _ShellRoute.tab:
body = _tabContent();
}
return AnimatedBuilder(
animation: widget.state,
builder: (context, _) => Scaffold(
body: body,
bottomNavigationBar: route == _ShellRoute.tab
? NavigationBar(
selectedIndex: tab.index,
height: 70,
indicatorColor: AppColors.softGreen,
onDestinationSelected: (index) => showTab(AppTab.values[index]),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.menu_book_outlined),
selectedIcon: Icon(Icons.menu_book),
label: '学习',
),
NavigationDestination(
icon: Icon(Icons.chat_bubble_outline),
selectedIcon: Icon(Icons.chat_bubble),
label: '对话',
),
NavigationDestination(
icon: Icon(Icons.refresh_outlined),
selectedIcon: Icon(Icons.refresh),
label: '复习',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
)
: null,
),
);
}
Widget _tabContent() {
switch (tab) {
case AppTab.home:
return HomePage(
state: widget.state,
onStartPrimaryTask: widget.state.reviewIsPrimary
? () => showTab(AppTab.review)
: showLesson,
onOpenDialogue: showDialogue,
onResumeLessonDialogue: () => showDialogue(inLesson: true),
);
case AppTab.learn:
return _LearningMap(
onOpenLesson: (id) {
widget.state.openLesson(id);
showLesson();
},
onStartReinforcement: () {
widget.state.scheduleA0Reinforcement();
showTab(AppTab.review);
},
onOpenReview: () => showTab(AppTab.review),
state: widget.state,
);
case AppTab.dialogue:
return DialogueScenePage(onStart: showDialogue);
case AppTab.review:
return ReviewPage(
state: widget.state,
onFinished: () => showTab(AppTab.home),
onOpenAdaptiveLesson: showAdaptiveLesson,
);
case AppTab.progress:
return ProgressPage(
state: widget.state,
onSettings: showSettings,
onOpenAssessment: showAssessment,
);
}
}
}
enum _ShellRoute {
tab,
lesson,
scene,
dialogue,
summary,
settings,
adaptiveLesson,
assessmentPreparation,
assessment,
}
class _LearningMap extends StatelessWidget {
const _LearningMap({
required this.onOpenLesson,
required this.onStartReinforcement,
required this.onOpenReview,
required this.state,
});
final ValueChanged<String> onOpenLesson;
final VoidCallback onStartReinforcement;
final VoidCallback onOpenReview;
final AppState state;
@override
Widget build(BuildContext context) {
return AppPage(
child: SpacedColumn(
children: [
const Eyebrow('学习地图 · 按掌握状态推进'),
Text('从认识到开口', style: Theme.of(context).textTheme.headlineMedium),
const Text('每节课都围绕一个能完成的小任务。'),
if (state.reviewBacklog)
SectionCard(
tint: AppColors.warm,
child: SpacedColumn(
children: [
Text(
'复习已有积压,今天先完成 ${state.dueReviewCount} 项到期复习,再开启新课。',
style: const TextStyle(color: AppColors.warmInk),
),
SecondaryButton(label: '先去复习', onPressed: onOpenReview),
],
),
),
for (final lesson in a0SeedLessons)
SectionCard(
tint: lesson.id == state.activeLessonId
? AppColors.softGreen
: null,
onTap:
state.isLessonUnlocked(lesson.id) &&
(!state.reviewBacklog ||
state.completedLessonIds.contains(lesson.id))
? () => onOpenLesson(lesson.id)
: null,
child: Row(
children: [
Icon(
state.completedLessonIds.contains(lesson.id)
? Icons.check_circle
: state.reviewBacklog
? Icons.lock_outline
: state.isLessonUnlocked(lesson.id)
? Icons.play_circle_outline
: Icons.lock_outline,
color: state.completedLessonIds.contains(lesson.id)
? AppColors.green
: AppColors.muted,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${lesson.number} 课 · ${lesson.title}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 3),
Text(
lesson.segments.length > 1
? '${lesson.outcome} · 小段 ${lesson.segments.where((segment) => state.isSegmentComplete(segment.id)).length}/${lesson.segments.length}'
: lesson.outcome,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
],
),
),
if (state.completedLessonIds.length == a0SeedLessons.length)
SectionCard(
tint: AppColors.softGreen,
child: SpacedColumn(
children: [
const Text(
'A0 巩固变式',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Text('换一个人物、地点或情境,继续巩固尚未稳定的核心表达。'),
PrimaryButton(
label: '安排一题巩固练习',
onPressed: onStartReinforcement,
),
],
),
),
],
),
);
}
}