本次提交实现了背诵模块的完整基础链路: 1. 新增词书、背单词接口层与对应数据模型 2. 完成背诵首页、章节列表页等核心页面开发 3. 实现音频播放、加载弹窗等通用组件 4. 配置路由跳转链路,替换旧版缓存传参方案 5. 对齐Vue端业务逻辑与UI样式
77 lines
2.4 KiB
Dart
77 lines
2.4 KiB
Dart
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();
|
||
}
|
||
}
|