Skip to content

Commit e877279

Browse files
committed
FEAT: 完善精听手动模式控制
1 parent cfa6ae5 commit e877279

16 files changed

Lines changed: 317 additions & 105 deletions

‎TASKS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
## 本次完成
66

7+
- [x] 精听讲解页按模式显示“继续”或“下一句”(2026-10-18):自动模式保留“继续”重播与自动推进;手动模式使用共享页脚现有下一句导航,推进时沿用原切句动画和手动播放设置。新增精听页交互回归测试,并将 Provider 测试更新为覆盖手动从讲解页切句。播放器 Provider 与页面定向测试共 106 项通过;3 个相关 Dart 文件定向分析无问题,格式检查与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动限于精听讲解页控件选择和现有导航复用。未运行 Maestro:Android ADB server 启动被 `Operation not permitted` 阻止。**完成时间**: 2026-10-18 18:30
8+
- [x] 精听手动模式支持循环次数和停顿设置(2026-10-18):设置面板在手动模式下继续复用现有循环、停顿控件与偏好;精听沿用 `BlindPracticeFlowEngine`,按所选次数循环并在循环间停顿,结束后停在当前句等待用户切句,重听从第一遍开始,无限循环可由用户切句打断。共享遍数提示统一显示手动/自动模式与当前遍数,本地化说明同步更新。新增设置面板、播放器流程、遍数提示及精听页回归测试。4 个定向测试文件共 86 项通过,精听页测试 51 项通过,13 个相关 Dart 文件 `flutter analyze --no-pub` 无问题,`flutter gen-l10n` 与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动范围局限于精听设置和共享循环/提示组件,定向检查已覆盖。未运行 Maestro:Android ADB 服务启动被 `Operation not permitted` 阻止;iOS Maestro CLI 初始化因无法修改工作区外依赖权限而失败,且现有 Flow 会清理应用状态。**完成时间**: 2026-10-18 17:51
79
- [x] 修复逐句播放中调整速度未即时生效(2026-10-18):`MediaSentencePlaybackDriver.setSpeed` 更新保存速度的同时调用 MediaEngine,使当前播放立即采用新速度,后续句子继续沿用该速度。新增回归测试覆盖播放中变速和下一句速度继承。`test/providers/media_engine/media_engine_provider_test.dart` 25 项通过,两个相关 Dart 文件定向分析无问题,格式与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动局限于播放速度驱动与对应回归测试,已完成定向验证。
810
- [x] 社区合集按地区使用中国镜像 URL(2026-10-18):App 解析并缓存 API 返回的 `coverUrlChina` / `mediaUrlChina`;中国用户在镜像 URL 可用时使用中国地址,否则使用全球地址,其他地区继续使用全球地址。已加入合集持久化中国封面 URL,数据库升级至 v59;媒体下载按地区选地址,失败时不跨区重试。社区合集 API、DTO、缓存、封面、下载、同步和 v58→v59 迁移相关测试通过(首次 71 项;后续 UI 回归 40 项),23 个相关 Dart 文件定向分析通过,`git diff --check` 通过。未运行 `scripts/check.sh`:改动局限于社区合集 URL 与封面数据链路,定向测试和分析已覆盖。未运行 Maestro:当前环境 `adb` server 启动被 `Operation not permitted` 阻止;现有 Flow 会清理 App 状态。**完成时间**: 2026-10-18
911
- 审查修复(2026-10-18):SharedPreferences 迁移解析 `coverUrlChina` 改用字符串模式匹配,畸形非字符串值安全回退为 null;新增回归测试。`test/models/collection_test.dart` 16 项通过,相关 Dart 分析无问题。

‎lib/l10n/app_en.arb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@
804804
"intensiveListenSettingsTemporaryHint": "Settings are remembered for next time",
805805
"intensiveListenPauseSmartDesc": "Auto-adjusted based on difficulty, sentence length, and learning stage",
806806
"intensiveListenControlModeAutoDesc": "Auto-loop, auto-pause, auto-next",
807-
"intensiveListenControlModeManualDesc": "Tap to replay, tap next",
807+
"intensiveListenControlModeManualDesc": "Repeat as configured, tap next",
808808
"intensiveListenPauseFixedUnit": "{seconds}s",
809809
"@intensiveListenPauseFixedUnit": {
810810
"placeholders": {

‎lib/l10n/app_localizations.dart‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2675,7 +2675,7 @@ abstract class AppLocalizations {
26752675
/// No description provided for @intensiveListenControlModeManualDesc.
26762676
///
26772677
/// In en, this message translates to:
2678-
/// **'Tap to replay, tap next'**
2678+
/// **'Repeat as configured, tap next'**
26792679
String get intensiveListenControlModeManualDesc;
26802680

26812681
/// No description provided for @intensiveListenPauseFixedUnit.

‎lib/l10n/app_localizations_en.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1477,7 +1477,8 @@ class AppLocalizationsEn extends AppLocalizations {
14771477
'Auto-loop, auto-pause, auto-next';
14781478

14791479
@override
1480-
String get intensiveListenControlModeManualDesc => 'Tap to replay, tap next';
1480+
String get intensiveListenControlModeManualDesc =>
1481+
'Repeat as configured, tap next';
14811482

14821483
@override
14831484
String intensiveListenPauseFixedUnit(int seconds) {

‎lib/l10n/app_localizations_zh.dart‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1427,7 +1427,7 @@ class AppLocalizationsZh extends AppLocalizations {
14271427
String get intensiveListenControlModeAutoDesc => '自动循环、自动停顿、自动下一句';
14281428

14291429
@override
1430-
String get intensiveListenControlModeManualDesc => '手动重听、手动下一句';
1430+
String get intensiveListenControlModeManualDesc => '按设置循环,手动下一句';
14311431

14321432
@override
14331433
String intensiveListenPauseFixedUnit(int seconds) {

‎lib/l10n/app_zh.arb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@
597597
"intensiveListenSettingsTemporaryHint": "设置会被记住,下次自动沿用",
598598
"intensiveListenPauseSmartDesc": "根据难度、句子长度和学习阶段自动调整",
599599
"intensiveListenControlModeAutoDesc": "自动循环、自动停顿、自动下一句",
600-
"intensiveListenControlModeManualDesc": "手动重听、手动下一句",
600+
"intensiveListenControlModeManualDesc": "按设置循环,手动下一句",
601601
"intensiveListenPauseFixedUnit": "{seconds}秒",
602602
"@intensiveListenPauseFixedUnit": {
603603
"placeholders": {

‎lib/providers/blind_flow/blind_practice_flow_engine.dart‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class BlindPracticeFlowConfig {
2929
/// 每成功播放一遍后的回调
3030
final void Function(Sentence sentence)? onSentencePlayed;
3131

32-
/// 是否为手动模式。手动模式下当前句播完后直接进入等待态。
32+
/// 是否为手动模式。手动模式完成设置的循环后等待用户切句。
3333
final bool Function() isManualMode;
3434

3535
const BlindPracticeFlowConfig({
@@ -149,13 +149,19 @@ class BlindPracticeFlowEngine {
149149
AppLogger.log(logTag, '-> WaitingForUser (from ${phase.runtimeType})');
150150
}
151151

152+
/// 重播当前句;手动模式从本轮第一遍重新开始。
152153
Future<void> replayCurrentSentence() async {
153154
if (_disposed) return;
155+
final sentence = currentSentence;
154156
_waitAfterCurrentPrompt = false;
155157
_stopActiveResources();
156158
_updateState(
157159
_state.copyWith(
158160
phase: const BlindIdle(),
161+
repeatIndex: _config.isManualMode() ? 0 : _state.repeatIndex,
162+
totalRepeats: sentence == null
163+
? _state.totalRepeats
164+
: _config.getRepeatCount(sentence),
159165
flowToken: _state.flowToken + 1,
160166
),
161167
);
@@ -364,18 +370,18 @@ class BlindPracticeFlowEngine {
364370
return;
365371
}
366372

367-
if (_config.isManualMode()) {
368-
_updateState(
369-
_state.copyWith(
370-
repeatIndex: 0,
371-
totalRepeats: _config.getRepeatCount(sentence),
372-
phase: const BlindWaitingForUser(BlindWaitingReason.userInteraction),
373-
),
374-
);
375-
return;
376-
}
377-
378373
if (_state.isLastRepeat) {
374+
if (_config.isManualMode()) {
375+
// 手动模式完成所有循环后停在当前句,不触发句间倒计时或自动推进。
376+
_updateState(
377+
_state.copyWith(
378+
phase: const BlindWaitingForUser(
379+
BlindWaitingReason.userInteraction,
380+
),
381+
),
382+
);
383+
return;
384+
}
379385
unawaited(_startSentenceInterval(sentence));
380386
return;
381387
}

‎lib/providers/learning_session/intensive_listen_player_provider.dart‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -868,8 +868,7 @@ class IntensiveListenPlayer extends _$IntensiveListenPlayer {
868868
sentences: _sentences,
869869
startIndex: startIndex ?? state.currentSentenceIndex,
870870
config: BlindPracticeFlowConfig(
871-
getRepeatCount: (_) =>
872-
state.settings.isManualMode ? 1 : state.settings.repeatCount,
871+
getRepeatCount: (_) => state.settings.repeatCount,
873872
getRepeatIntervalDuration: (sentence) =>
874873
calculatePauseDuration(sentence.duration, state.settings),
875874
getSentenceIntervalDuration: (sentence) =>

‎lib/screens/intensive_listen_player_screen.dart‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,9 +1057,7 @@ class _IntensiveListenPlayerScreenState
10571057
currentCount:
10581058
playerState.currentPlayCount,
10591059
totalCount:
1060-
playerState.settings.isManualMode
1061-
? 1
1062-
: playerState.settings.repeatCount,
1060+
playerState.settings.repeatCount,
10631061
),
10641062
statusSuffixText: _formatSpeed(
10651063
playerState.settings.playbackSpeed,
@@ -1099,8 +1097,10 @@ class _AnnotationContent extends StatelessWidget {
10991097
}
11001098
}
11011099

1100+
/// 自动模式在讲解页显示“继续”;手动模式沿用页脚的下一句导航。
11021101
bool _showContinueButton(IntensiveListenState playerState) {
11031102
return playerState.isAnnotationMode &&
1103+
!playerState.settings.isManualMode &&
11041104
!playerState.isAnnotationReplay &&
11051105
!playerState.isPauseBetweenSentences &&
11061106
playerState.annotationState?.phase is! WaitingAnnotationPageTransition;

‎lib/widgets/intensive_listen/intensive_listen_settings_sheet.dart‎

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,47 +54,40 @@ class _IntensiveListenSettingsSheet extends ConsumerWidget {
5454
// 播放速度(手动/自动模式下都生效)
5555
_buildPlaybackSpeedSection(l10n, theme, settings, ref),
5656

57-
// 循环次数和停顿设置仅在自动模式下显示
58-
if (!settings.isManualMode) ...[
59-
const SizedBox(height: AppSpacing.l),
57+
const SizedBox(height: AppSpacing.l),
6058

61-
// 每句循环次数
62-
_buildRepeatCountRow(l10n, theme, settings, ref),
63-
if (settings.repeatCount != 1)
64-
SwitchListTile(
65-
contentPadding: EdgeInsets.zero,
66-
title: Text(
67-
l10n.intensiveListenAnnotationReplayUsesRepeatCount,
68-
),
69-
value: settings.annotationReplayUsesRepeatCount,
70-
onChanged: (value) {
71-
ref
72-
.read(intensiveListenPlayerProvider.notifier)
73-
.updateSettings(
74-
settings.copyWith(
75-
annotationReplayUsesRepeatCount: value,
76-
),
77-
);
78-
},
79-
),
80-
const SizedBox(height: AppSpacing.l),
59+
// 两种控制模式共用循环次数与停顿设置。
60+
_buildRepeatCountRow(l10n, theme, settings, ref),
61+
if (settings.repeatCount != 1)
62+
SwitchListTile(
63+
contentPadding: EdgeInsets.zero,
64+
title: Text(l10n.intensiveListenAnnotationReplayUsesRepeatCount),
65+
value: settings.annotationReplayUsesRepeatCount,
66+
onChanged: (value) {
67+
ref
68+
.read(intensiveListenPlayerProvider.notifier)
69+
.updateSettings(
70+
settings.copyWith(annotationReplayUsesRepeatCount: value),
71+
);
72+
},
73+
),
74+
const SizedBox(height: AppSpacing.l),
8175

82-
// 句间停顿
83-
Text(
84-
l10n.intensiveListenPauseLabel,
85-
style: theme.textTheme.titleSmall?.copyWith(
86-
fontWeight: FontWeight.w600,
87-
),
76+
// 句间停顿
77+
Text(
78+
l10n.intensiveListenPauseLabel,
79+
style: theme.textTheme.titleSmall?.copyWith(
80+
fontWeight: FontWeight.w600,
8881
),
89-
const SizedBox(height: AppSpacing.s),
82+
),
83+
const SizedBox(height: AppSpacing.s),
9084

91-
// 模式切换
92-
_buildPauseModeSelector(l10n, settings, ref),
93-
const SizedBox(height: AppSpacing.m),
85+
// 模式切换
86+
_buildPauseModeSelector(l10n, settings, ref),
87+
const SizedBox(height: AppSpacing.m),
9488

95-
// 模式详情
96-
_buildPauseModeDetail(l10n, theme, settings, ref),
97-
],
89+
// 模式详情
90+
_buildPauseModeDetail(l10n, theme, settings, ref),
9891
],
9992
),
10093
);

0 commit comments

Comments
 (0)