feat: save repeat audio on mic record and rename button to play repeat
This commit is contained in:
@@ -377,6 +377,7 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
aiVoiceRecording = false;
|
||||
listening = false;
|
||||
transcribing = true;
|
||||
recordingPath = path;
|
||||
});
|
||||
if (path != null) {
|
||||
final config = widget.state.aiConfig;
|
||||
@@ -489,8 +490,12 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
final path = recordingPath;
|
||||
if (path == null) return;
|
||||
setState(() => playingRecording = true);
|
||||
await VoiceService.instance.playRecording(path);
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
await VoiceService.instance.playRecording(
|
||||
path,
|
||||
onComplete: () {
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecording() async {
|
||||
|
||||
@@ -438,9 +438,7 @@ class _SpeakingStep extends StatefulWidget {
|
||||
|
||||
class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
bool listening = false;
|
||||
bool recording = false;
|
||||
bool transcribing = false;
|
||||
bool aiVoiceRecording = false;
|
||||
bool playingRecording = false;
|
||||
String transcript = '';
|
||||
String? recordingPath;
|
||||
@@ -455,13 +453,13 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
}
|
||||
|
||||
Future<void> _toggleMic() async {
|
||||
if (aiVoiceRecording) {
|
||||
if (listening) {
|
||||
final path = await VoiceService.instance.stopRecording();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
aiVoiceRecording = false;
|
||||
listening = false;
|
||||
transcribing = true;
|
||||
recordingPath = path;
|
||||
});
|
||||
if (path != null) {
|
||||
final config = widget.state.aiConfig;
|
||||
@@ -480,7 +478,7 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
});
|
||||
if (text == null || text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('未识别到清晰声音,请重试或点击“播放示范音”。')),
|
||||
const SnackBar(content: Text('未识别到清晰发音,请重试或点击“播放示范音”。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -490,83 +488,62 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listening) {
|
||||
await VoiceService.instance.stopListening();
|
||||
if (mounted) setState(() => listening = false);
|
||||
return;
|
||||
await VoiceService.instance.stopRecordingPlayback();
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
if (!widget.keepRecording) {
|
||||
await VoiceService.instance.deleteRecording(recordingPath);
|
||||
}
|
||||
|
||||
final ready = await VoiceService.instance.startListening(
|
||||
(text, _) {
|
||||
if (mounted) setState(() => transcript = text);
|
||||
},
|
||||
onStatus: (status) {
|
||||
if (mounted && (status == 'notListening' || status == 'done')) {
|
||||
setState(() => listening = false);
|
||||
}
|
||||
},
|
||||
onError: (err) {
|
||||
if (mounted) {
|
||||
setState(() => listening = false);
|
||||
final recordStarted = await VoiceService.instance.startRecording();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
listening = recordStarted;
|
||||
if (recordStarted) {
|
||||
recordingPath = null;
|
||||
}
|
||||
});
|
||||
if (recordStarted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('已启动麦克风录音,跟读完成后再次点击,AI 将自动转写发音。')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('无法访问麦克风,请检查手机录音权限。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _togglePlayRecording() async {
|
||||
if (playingRecording) {
|
||||
await VoiceService.instance.stopRecordingPlayback();
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
return;
|
||||
}
|
||||
if (recordingPath == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请先使用麦克风跟读,录音完成后即可播放。')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => playingRecording = true);
|
||||
await VoiceService.instance.playRecording(
|
||||
recordingPath!,
|
||||
onComplete: () {
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
},
|
||||
);
|
||||
|
||||
if (!ready) {
|
||||
final recordStarted = await VoiceService.instance.startRecording();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
aiVoiceRecording = recordStarted;
|
||||
listening = recordStarted;
|
||||
});
|
||||
if (recordStarted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('已启动麦克风录音,跟读完成后再次点击,AI 将自动转写发音。')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('无法访问麦克风,请检查手机录音权限。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) setState(() => listening = ready);
|
||||
}
|
||||
|
||||
Future<void> _toggleRecording() async {
|
||||
if (recording) {
|
||||
final path = await VoiceService.instance.stopRecording();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
recording = false;
|
||||
recordingPath = path;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
final ready = await VoiceService.instance.startRecording();
|
||||
if (!mounted) return;
|
||||
setState(() => recording = ready);
|
||||
if (!ready) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('无法使用麦克风录音;请检查系统权限。')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _playRecording() async {
|
||||
final path = recordingPath;
|
||||
if (path == null) return;
|
||||
setState(() => playingRecording = true);
|
||||
await VoiceService.instance.playRecording(path);
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecording() async {
|
||||
await VoiceService.instance.stopRecordingPlayback();
|
||||
await VoiceService.instance.deleteRecording(recordingPath);
|
||||
if (mounted) setState(() => recordingPath = null);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
recordingPath = null;
|
||||
playingRecording = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -604,8 +581,8 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
onPressed: transcribing ? null : _toggleMic,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: recording ? '停止本机录音' : '录音后回听',
|
||||
onPressed: listening ? null : _toggleRecording,
|
||||
label: playingRecording ? '停止播放' : '播放跟读',
|
||||
onPressed: (listening || transcribing) ? null : _togglePlayRecording,
|
||||
),
|
||||
if (recordingPath != null)
|
||||
SectionCard(
|
||||
@@ -613,20 +590,20 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
child: SpacedColumn(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(widget.keepRecording ? '录音已保存在本机。' : '本次录音仅在离开此步骤前保留。'),
|
||||
Text(widget.keepRecording ? '录音已保存在本机。' : '本次跟读录音仅在离开此步骤前保留。'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: playingRecording ? null : _playRecording,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text('回听'),
|
||||
onPressed: (listening || transcribing) ? null : _togglePlayRecording,
|
||||
icon: Icon(playingRecording ? Icons.stop : Icons.play_arrow),
|
||||
label: Text(playingRecording ? '停止播放' : '播放跟读'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
tooltip: '删除录音',
|
||||
onPressed: _deleteRecording,
|
||||
onPressed: (listening || transcribing) ? null : _deleteRecording,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
@@ -635,7 +612,9 @@ class _SpeakingStepState extends State<_SpeakingStep> {
|
||||
),
|
||||
),
|
||||
if (transcript.isNotEmpty)
|
||||
SectionCard(child: Text('设备转写:$transcript\n请确认它是否接近你刚才说的内容。')),
|
||||
SectionCard(
|
||||
child: Text("设备转写:$transcript\n请确认它是否接近你刚才说的内容。"),
|
||||
),
|
||||
const SectionCard(
|
||||
child: Text('转写不确定或与原句不符时,可重说或继续文字练习;这一步只算跟读练习,不作为独立口语证据。'),
|
||||
),
|
||||
@@ -1036,8 +1015,12 @@ class _IndependentStepState extends State<_IndependentStep> {
|
||||
final path = recordingPath;
|
||||
if (path == null) return;
|
||||
setState(() => playingRecording = true);
|
||||
await VoiceService.instance.playRecording(path);
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
await VoiceService.instance.playRecording(
|
||||
path,
|
||||
onComplete: () {
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecording() async {
|
||||
@@ -1053,6 +1036,7 @@ class _IndependentStepState extends State<_IndependentStep> {
|
||||
aiVoiceRecording = false;
|
||||
listening = false;
|
||||
transcribing = true;
|
||||
recordingPath = path;
|
||||
});
|
||||
if (path != null) {
|
||||
final config = widget.state.aiConfig;
|
||||
|
||||
@@ -540,8 +540,12 @@ class _AdaptiveLessonPageState extends State<AdaptiveLessonPage> {
|
||||
final path = recordingPath;
|
||||
if (path == null) return;
|
||||
setState(() => playingRecording = true);
|
||||
await VoiceService.instance.playRecording(path);
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
await VoiceService.instance.playRecording(
|
||||
path,
|
||||
onComplete: () {
|
||||
if (mounted) setState(() => playingRecording = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecording() async {
|
||||
|
||||
Reference in New Issue
Block a user