feat(recite): 完成背诵模块基础框架与核心页面开发
本次提交实现了背诵模块的完整基础链路: 1. 新增词书、背单词接口层与对应数据模型 2. 完成背诵首页、章节列表页等核心页面开发 3. 实现音频播放、加载弹窗等通用组件 4. 配置路由跳转链路,替换旧版缓存传参方案 5. 对齐Vue端业务逻辑与UI样式
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【ReciteAudioManager】背诵模块单例音频管理
|
||||
/// —— 对标 Vue utils/audioManager
|
||||
///
|
||||
/// 规则(与 Vue 完全一致):
|
||||
/// 1. 全局同时只播放一个音频:新 play 会先 stop 旧的;
|
||||
/// 2. hasActive 为 true 时页面层直接忽略点击;
|
||||
/// 3. 播放结束/失败/取消都清空 playingToken(驱动 4 帧动画停在第 1 帧);
|
||||
/// 4. 离开答题/小结页调 stopAll() 释放(对标 cleanupAllAudio)。
|
||||
/// ============================================================
|
||||
class ReciteAudioManager {
|
||||
ReciteAudioManager._() {
|
||||
_playerSub = _player.playerStateStream.listen((state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
playingToken.value = null;
|
||||
currentUrl = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static final ReciteAudioManager instance = ReciteAudioManager._();
|
||||
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
StreamSubscription<PlayerState>? _playerSub;
|
||||
|
||||
/// 当前播放令牌(自增),null = 无播放;4 帧按钮据此判断是否该动
|
||||
final ValueNotifier<int?> playingToken = ValueNotifier<int?>(null);
|
||||
|
||||
/// 当前播放的音频 URL,null = 无播放
|
||||
/// —— VoiceAnimButton 用它判断"当前播的是不是我的音频",
|
||||
/// 比 token 归属更可靠(自动播放 / 手动播放 / 跨组件触发都能联动)。
|
||||
String? currentUrl;
|
||||
|
||||
int _seq = 0;
|
||||
|
||||
bool get hasActive => playingToken.value != null;
|
||||
|
||||
/// 播放网络音频,返回本次播放令牌(失败返回 null)
|
||||
Future<int?> play(String url) async {
|
||||
if (url.isEmpty) return null;
|
||||
final token = ++_seq;
|
||||
try {
|
||||
await _player.stop();
|
||||
currentUrl = url;
|
||||
playingToken.value = token;
|
||||
await _player.setUrl(url);
|
||||
await _player.play();
|
||||
return token;
|
||||
} catch (e) {
|
||||
talker.error('背诵音频播放失败: $e');
|
||||
if (playingToken.value == token) {
|
||||
playingToken.value = null;
|
||||
currentUrl = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止并清空播放态(页面 dispose 时必须调)
|
||||
Future<void> stopAll() async {
|
||||
await _player.stop();
|
||||
playingToken.value = null;
|
||||
currentUrl = null;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_playerSub?.cancel();
|
||||
_player.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user