@@ -54,9 +54,18 @@ class BlindPracticeFlowCallbacks {
5454 )
5555 playSentence;
5656
57+ /// 从暂停位置继续播放当前句;未提供时退回普通起播。
58+ final Future <SentencePlaybackResult > Function (
59+ Sentence sentence,
60+ int flowToken,
61+ Duration startAt,
62+ )?
63+ resumeSentence;
64+
5765 const BlindPracticeFlowCallbacks ({
5866 required this .pauseAudio,
5967 required this .playSentence,
68+ this .resumeSentence,
6069 });
6170}
6271
@@ -168,6 +177,20 @@ class BlindPracticeFlowEngine {
168177 await _playCurrentSentence ();
169178 }
170179
180+ /// 从给定媒体位置继续当前句,保留本轮重复次数和流程状态。
181+ Future <void > resumeCurrentSentence (Duration startAt) async {
182+ if (_disposed || currentSentence == null ) return ;
183+ _waitAfterCurrentPrompt = false ;
184+ _stopActiveResources ();
185+ _updateState (
186+ _state.copyWith (
187+ phase: const BlindIdle (),
188+ flowToken: _state.flowToken + 1 ,
189+ ),
190+ );
191+ await _playCurrentSentence (startAt: startAt);
192+ }
193+
171194 Future <void > nextSentence () async {
172195 if (_disposed) return ;
173196 if (_state.isLastSentence) return ;
@@ -258,10 +281,13 @@ class BlindPracticeFlowEngine {
258281 _sentences = [];
259282 }
260283
261- Future <void > _playCurrentSentence () async {
284+ /// 播放当前句,并在异步前置检查后校验本次 flowToken。
285+ /// [startAt] 仅属于本次启动,避免切句后串用旧的恢复位置。
286+ Future <void > _playCurrentSentence ({Duration ? startAt}) async {
262287 if (_disposed) return ;
263288 final sentence = currentSentence;
264289 if (sentence == null ) return ;
290+ final initialFlowToken = _state.flowToken;
265291
266292 if (sentence.duration <= Duration .zero) {
267293 await _advanceToNextSentence ();
@@ -270,7 +296,7 @@ class BlindPracticeFlowEngine {
270296
271297 final canStart =
272298 await _config.onBeforeSentenceStart? .call (_state.sentenceIndex) ?? true ;
273- if (_disposed) return ;
299+ if (_disposed || initialFlowToken != _state.flowToken ) return ;
274300 if (! canStart) {
275301 await _skipCurrentSentence (reason: 'before-sentence-start-rejected' );
276302 return ;
@@ -286,7 +312,10 @@ class BlindPracticeFlowEngine {
286312 'flowToken=$token ' ,
287313 );
288314
289- final result = await callbacks.playSentence (sentence, token);
315+ final resumeSentence = callbacks.resumeSentence;
316+ final result = startAt != null && resumeSentence != null
317+ ? await resumeSentence (sentence, token, startAt)
318+ : await callbacks.playSentence (sentence, token);
290319 if (_disposed) return ;
291320 final currentFlowToken = _state.flowToken;
292321 AppLogger .log (
0 commit comments