feat: 增加AI配置文件与TTS自动朗读,修复二级页面返回按钮与音频播放

This commit is contained in:
shenlei
2026-09-15 19:11:39 +09:00
parent 37c86f7ecb
commit 4341726162
18 changed files with 1322 additions and 397 deletions
@@ -101,7 +101,7 @@ class _LessonFlowState extends State<LessonFlow> {
state: widget.state,
initialText: activity.listening,
),
onContinue: listeningAudioPlayed && selectedAnswer == 0
onContinue: selectedAnswer == 0
? widget.state.completeListening
: null,
);
@@ -192,6 +192,7 @@ class _LessonFlowState extends State<LessonFlow> {
return _LessonScope(
title:
'${lesson.number} 课 · ${lesson.title} · 第 ${widget.state.activeSegmentIndexFor(lesson.id) + 1}/${lesson.segments.length}',
onExit: widget.onFinish,
child: content,
);
}
@@ -205,7 +206,21 @@ class _LessonScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) => AppPage(
appBar: AppBar(title: Text(_LessonScope.of(context))),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: "退出课程",
onPressed: () {
final onExit = _LessonScope.exitOf(context);
if (onExit != null) {
onExit();
} else if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
),
title: Text(_LessonScope.of(context)),
),
child: SpacedColumn(
spacing: 16,
children: [
@@ -231,16 +246,25 @@ class _LessonScaffold extends StatelessWidget {
}
class _LessonScope extends InheritedWidget {
const _LessonScope({required this.title, required super.child});
const _LessonScope({
required this.title,
this.onExit,
required super.child,
});
final String title;
final VoidCallback? onExit;
static String of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<_LessonScope>()?.title ??
'A0 课程练习';
static VoidCallback? exitOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<_LessonScope>()?.onExit;
@override
bool updateShouldNotify(_LessonScope oldWidget) => title != oldWidget.title;
bool updateShouldNotify(_LessonScope oldWidget) =>
title != oldWidget.title || onExit != oldWidget.onExit;
}
class _PreviewStep extends StatelessWidget {
@@ -357,7 +381,12 @@ class _ListeningStep extends StatelessWidget {
for (var index = 0; index < answers.length; index++)
SectionCard(
tint: selectedAnswer == index ? AppColors.softGreen : null,
onTap: audioPlayed ? () => onSelected(index) : null,
onTap: () {
onSelected(index);
if (!audioPlayed) {
onPlayed();
}
},
child: Row(
children: [
Icon(
@@ -374,7 +403,9 @@ class _ListeningStep extends StatelessWidget {
),
),
PrimaryButton(
label: audioPlayed ? '检查并继续' : '先播放音频',
label: selectedAnswer >= 0
? '检查并继续'
: (audioPlayed ? '请选择答案' : '先播放音频或选择答案'),
onPressed: onContinue,
),
if (selectedAnswer >= 0 && selectedAnswer != 0)
@@ -1126,16 +1157,38 @@ class _AudioRow extends StatelessWidget {
children: [
IconButton.filled(
onPressed: () async {
await VoiceService.instance.speak(speech ?? label);
onPlayed?.call();
try {
await VoiceService.instance.speak(speech ?? label);
} finally {
onPlayed?.call();
}
},
icon: const Icon(Icons.play_arrow),
),
const SizedBox(width: 10),
Expanded(child: Text(label)),
Expanded(
child: InkWell(
onTap: () async {
try {
await VoiceService.instance.speak(speech ?? label);
} finally {
onPlayed?.call();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(label),
),
),
),
TextButton(
onPressed: () =>
VoiceService.instance.speak(speech ?? label, slow: true),
onPressed: () async {
try {
await VoiceService.instance.speak(speech ?? label, slow: true);
} finally {
onPlayed?.call();
}
},
child: const Text('慢速'),
),
],