308 lines
10 KiB
Dart
308 lines
10 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|