Files
App.K12.Student.Miniapp_Flu…/lib/features/recite/widgets/recite_audio.dart
T
崔宝新 27eba92315 feat(recite): 完成背诵模块基础框架与核心页面开发
本次提交实现了背诵模块的完整基础链路:
1. 新增词书、背单词接口层与对应数据模型
2. 完成背诵首页、章节列表页等核心页面开发
3. 实现音频播放、加载弹窗等通用组件
4. 配置路由跳转链路,替换旧版缓存传参方案
5. 对齐Vue端业务逻辑与UI样式
2026-09-11 14:38:47 +08:00

77 lines
2.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}