|
|
|
@ -61,6 +61,16 @@ |
|
|
|
<script> |
|
|
|
import config from '@/mixins/config.js' |
|
|
|
import audioManager from '@/utils/audioManager.js' |
|
|
|
import { |
|
|
|
splitTextIntelligently, |
|
|
|
formatTime, |
|
|
|
generateCacheKey, |
|
|
|
validateCacheData, |
|
|
|
findFirstNonLeadAudio, |
|
|
|
limitCacheSize, |
|
|
|
clearAudioCache, |
|
|
|
resetAudioState |
|
|
|
} from '@/utils/audioUtils.js' |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'AudioControls', |
|
|
|
@ -162,7 +172,7 @@ export default { |
|
|
|
|
|
|
|
// 检查当前页面是否有缓存的音频 |
|
|
|
hasCurrentPageCache() { |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const cachedData = this.audioCache[cacheKey]; |
|
|
|
|
|
|
|
// 更严格的缓存有效性检查 |
|
|
|
@ -170,20 +180,8 @@ export default { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查缓存的音色ID是否与当前音色匹配 |
|
|
|
if (cachedData.voiceId && cachedData.voiceId !== this.localVoiceId) { |
|
|
|
// console.warn('缓存音色不匹配:', cachedData.voiceId, '!=', this.localVoiceId); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查音频URL是否有效 |
|
|
|
const firstAudio = cachedData.audios[0]; |
|
|
|
if (!firstAudio || !firstAudio.url) { |
|
|
|
// console.warn('缓存音频数据无效'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
// 使用公共方法验证缓存数据 |
|
|
|
return validateCacheData(cachedData, this.currentPage); |
|
|
|
}, |
|
|
|
|
|
|
|
// 判断音频功能是否应该被禁用(会员限制页面且用户非会员) |
|
|
|
@ -200,7 +198,7 @@ export default { |
|
|
|
// 如果全局预加载状态为true,需要检查当前页面是否在预加载队列中 |
|
|
|
if (this.isPreloading) { |
|
|
|
// 检查当前页面是否有缓存(如果有缓存说明已经预加载完成) |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const hasCache = this.audioCache[cacheKey] && this.audioCache[cacheKey].audios && this.audioCache[cacheKey].audios.length > 0; |
|
|
|
|
|
|
|
// 如果没有缓存且正在预加载,说明当前页面可能正在预加载中 |
|
|
|
@ -272,24 +270,57 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
// ==================== 原有方法 ==================== |
|
|
|
|
|
|
|
// 辅助方法:查找第一个非导语音频的索引 |
|
|
|
findFirstNonLeadAudio() { |
|
|
|
if (!this.currentPageAudios || this.currentPageAudios.length === 0) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
return findFirstNonLeadAudio(this.currentPageAudios); |
|
|
|
}, |
|
|
|
|
|
|
|
// 从第一个音频开始查找非导语音频 |
|
|
|
for (let i = 0; i < this.currentPageAudios.length; i++) { |
|
|
|
const audioData = this.currentPageAudios[i]; |
|
|
|
if (audioData && !audioData.isLead) { |
|
|
|
console.log(`🎵 findFirstNonLeadAudio: 找到第一个非导语音频,索引=${i}, isLead=${audioData.isLead}`); |
|
|
|
return i; |
|
|
|
} |
|
|
|
// 格式化时间显示 |
|
|
|
formatTime(seconds) { |
|
|
|
return formatTime(seconds); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 重置音频播放状态 |
|
|
|
* @param {boolean} clearHighlight - 是否清除高亮索引,默认true |
|
|
|
* @param {boolean} emitEvent - 是否发送状态变化事件,默认true |
|
|
|
*/ |
|
|
|
resetPlaybackState(clearHighlight = true, emitEvent = true) { |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.sliderValue = 0; |
|
|
|
|
|
|
|
if (clearHighlight) { |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
} |
|
|
|
|
|
|
|
if (emitEvent) { |
|
|
|
this.$emit('audio-state-change', { |
|
|
|
hasAudioData: this.hasAudioData, |
|
|
|
isLoading: this.isAudioLoading, |
|
|
|
currentHighlightIndex: this.currentHighlightIndex |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 如果所有音频都是导语,返回 -1 表示不播放 |
|
|
|
console.log('🎵 findFirstNonLeadAudio: 所有音频都是导语,返回 -1 不播放'); |
|
|
|
return -1; |
|
|
|
/** |
|
|
|
* 清除高亮索引并发送事件 |
|
|
|
*/ |
|
|
|
clearHighlight() { |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.emitHighlightChange(-1); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化音频索引为第一个非导语音频 |
|
|
|
* @returns {number} 设置的音频索引 |
|
|
|
*/ |
|
|
|
initializeAudioIndex() { |
|
|
|
const firstNonLeadIndex = this.findFirstNonLeadAudio(); |
|
|
|
this.currentAudioIndex = firstNonLeadIndex >= 0 ? firstNonLeadIndex : 0; |
|
|
|
return this.currentAudioIndex; |
|
|
|
}, |
|
|
|
|
|
|
|
// 检查并自动加载预加载完成的音频 |
|
|
|
@ -312,9 +343,11 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查当前页面是否有缓存的音频数据 |
|
|
|
const pageKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const pageKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const cachedAudio = this.audioCache[pageKey]; |
|
|
|
|
|
|
|
console.log(`🎵 checkAndLoadPreloadedAudio: 检查缓存,页面=${this.currentPage}, 缓存键=${pageKey}, 有缓存=${!!cachedAudio}`); |
|
|
|
|
|
|
|
if (cachedAudio && cachedAudio.audios && cachedAudio.audios.length > 0) { |
|
|
|
// 有缓存:直接显示控制栏并自动播放 |
|
|
|
this.currentPageAudios = cachedAudio.audios; |
|
|
|
@ -323,12 +356,21 @@ export default { |
|
|
|
this.isAudioLoading = false; |
|
|
|
|
|
|
|
// 初始化音频索引为第一个非导语音频 |
|
|
|
const firstNonLeadIndex = this.findFirstNonLeadAudio(); |
|
|
|
this.currentAudioIndex = firstNonLeadIndex; |
|
|
|
this.initializeAudioIndex(); |
|
|
|
this.currentTime = 0; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.clearHighlight(); |
|
|
|
|
|
|
|
console.log(`🎵 checkAndLoadPreloadedAudio: 从缓存加载音频,页面=${this.currentPage}, 音频数量=${this.currentPageAudios.length}`); |
|
|
|
console.log(`🎵 checkAndLoadPreloadedAudio: 从缓存加载音频,页面=${this.currentPage}, 音频数量=${this.currentPageAudios.length}, 缓存页面=${cachedAudio.pageNumber || '未知'}`); |
|
|
|
|
|
|
|
// 验证缓存数据是否属于当前页面 |
|
|
|
if (cachedAudio.pageNumber && cachedAudio.pageNumber !== this.currentPage) { |
|
|
|
console.error(`🚨 缓存数据页面不匹配!当前页面=${this.currentPage}, 缓存页面=${cachedAudio.pageNumber}`); |
|
|
|
// 清除错误的缓存数据 |
|
|
|
delete this.audioCache[pageKey]; |
|
|
|
// 重新加载正确的音频 |
|
|
|
this.getCurrentPageAudio(true); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 通知父组件音频状态变化 |
|
|
|
this.$emit('audio-state-change', { |
|
|
|
@ -372,91 +414,9 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 智能分割文本,按句号和逗号分割中英文文本 |
|
|
|
splitTextIntelligently(text) { |
|
|
|
if (!text || typeof text !== 'string') { |
|
|
|
return [text]; |
|
|
|
} |
|
|
|
|
|
|
|
// 判断是否为中文文本(包含中文字符) |
|
|
|
const isChinese = /[\u4e00-\u9fa5]/.test(text); |
|
|
|
const maxLength = isChinese ? 100 : 200; |
|
|
|
|
|
|
|
// 如果文本长度不超过限制,直接返回 |
|
|
|
if (text.length <= maxLength) { |
|
|
|
return [{ |
|
|
|
text: text, |
|
|
|
startIndex: 0, |
|
|
|
endIndex: text.length - 1 |
|
|
|
}]; |
|
|
|
} |
|
|
|
|
|
|
|
const segments = []; |
|
|
|
let currentText = text; |
|
|
|
let globalStartIndex = 0; |
|
|
|
|
|
|
|
while (currentText.length > 0) { |
|
|
|
if (currentText.length <= maxLength) { |
|
|
|
// 剩余文本不超过限制,直接添加 |
|
|
|
segments.push({ |
|
|
|
text: currentText, |
|
|
|
startIndex: globalStartIndex, |
|
|
|
endIndex: globalStartIndex + currentText.length - 1 |
|
|
|
}); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
// 在限制长度内寻找最佳分割点 |
|
|
|
let splitIndex = maxLength; |
|
|
|
let bestSplitIndex = -1; |
|
|
|
|
|
|
|
// 优先寻找句号 |
|
|
|
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) { |
|
|
|
const char = currentText[i]; |
|
|
|
if (char === '。' || char === '.') { |
|
|
|
bestSplitIndex = i + 1; // 包含句号 |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果没找到句号,寻找逗号 |
|
|
|
if (bestSplitIndex === -1) { |
|
|
|
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) { |
|
|
|
const char = currentText[i]; |
|
|
|
if (char === ',' || char === ',' || char === ';' || char === ';') { |
|
|
|
bestSplitIndex = i + 1; // 包含标点符号 |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果还是没找到合适的分割点,使用默认长度 |
|
|
|
if (bestSplitIndex === -1) { |
|
|
|
bestSplitIndex = maxLength; |
|
|
|
} |
|
|
|
|
|
|
|
// 提取当前段落 |
|
|
|
const segment = currentText.substring(0, bestSplitIndex).trim(); |
|
|
|
if (segment.length > 0) { |
|
|
|
segments.push({ |
|
|
|
text: segment, |
|
|
|
startIndex: globalStartIndex, |
|
|
|
endIndex: globalStartIndex + segment.length - 1 |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 更新剩余文本和全局索引 |
|
|
|
currentText = currentText.substring(bestSplitIndex).trim(); |
|
|
|
globalStartIndex += bestSplitIndex; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return segments; |
|
|
|
}, |
|
|
|
|
|
|
|
// 分批次请求音频 |
|
|
|
async requestAudioInBatches(text, voiceType) { |
|
|
|
const segments = this.splitTextIntelligently(text); |
|
|
|
const segments = splitTextIntelligently(text); |
|
|
|
const audioSegments = []; |
|
|
|
let totalDuration = 0; |
|
|
|
const requestId = this.currentRequestId; // 保存当前请求ID |
|
|
|
@ -585,7 +545,7 @@ export default { |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.currentTime = 0; |
|
|
|
this.totalTime = 0; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.clearHighlight(); |
|
|
|
|
|
|
|
// 通知父组件音频状态变化 |
|
|
|
this.$emit('audio-state-change', { |
|
|
|
@ -609,7 +569,7 @@ export default { |
|
|
|
console.log('this.audioCache:', this.audioCache); |
|
|
|
|
|
|
|
// 检查缓存中是否已有当前页面的音频数据 |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
if (this.audioCache[cacheKey]) { |
|
|
|
|
|
|
|
// 从缓存加载音频数据 |
|
|
|
@ -617,10 +577,8 @@ export default { |
|
|
|
this.totalTime = this.audioCache[cacheKey].totalDuration; |
|
|
|
|
|
|
|
// 初始化音频索引为第一个非导语音频 |
|
|
|
const firstNonLeadIndex = this.findFirstNonLeadAudio(); |
|
|
|
this.currentAudioIndex = firstNonLeadIndex; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
this.initializeAudioIndex(); |
|
|
|
this.resetPlaybackState(false, false); |
|
|
|
this.hasAudioData = true; |
|
|
|
this.isAudioLoading = false; |
|
|
|
|
|
|
|
@ -796,11 +754,12 @@ export default { |
|
|
|
await this.calculateTotalDuration(); |
|
|
|
|
|
|
|
// 将音频数据保存到缓存中 |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
this.audioCache[cacheKey] = { |
|
|
|
audios: [...this.currentPageAudios], // 深拷贝音频数组 |
|
|
|
totalDuration: this.totalTime, |
|
|
|
voiceId: this.localVoiceId, // 保存音色ID用于验证 |
|
|
|
pageNumber: this.currentPage, // 保存页面号码用于验证 |
|
|
|
timestamp: Date.now() // 保存时间戳 |
|
|
|
}; |
|
|
|
|
|
|
|
@ -871,7 +830,7 @@ export default { |
|
|
|
// 重置失败状态 |
|
|
|
this.audioLoadFailed = false; |
|
|
|
// 清除当前页面的音频缓存 |
|
|
|
const pageKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const pageKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
if (this.audioCache[pageKey]) { |
|
|
|
delete this.audioCache[pageKey]; |
|
|
|
|
|
|
|
@ -889,15 +848,11 @@ export default { |
|
|
|
audioManager.stopCurrentAudio(); |
|
|
|
this.currentAudio = null; |
|
|
|
|
|
|
|
// 重置播放状态 |
|
|
|
this.currentAudioIndex = 0; |
|
|
|
this.isPlaying = false; |
|
|
|
this.currentTime = 0; |
|
|
|
// 使用公共方法重置播放状态 |
|
|
|
this.resetPlaybackState(true, false); |
|
|
|
this.totalTime = 0; |
|
|
|
this.sliderValue = 0; |
|
|
|
this.isAudioLoading = false; |
|
|
|
this.audioLoadFailed = false; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.playSpeed = 1.0; |
|
|
|
|
|
|
|
// 页面切换时,始终清空当前音频数据,避免数据错乱 |
|
|
|
@ -916,14 +871,14 @@ export default { |
|
|
|
|
|
|
|
// 加载缓存的音频数据并显示播放控制栏 |
|
|
|
loadCachedAudioData() { |
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const cachedData = this.audioCache[cacheKey]; |
|
|
|
|
|
|
|
// 严格验证缓存数据 |
|
|
|
if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) { |
|
|
|
console.warn('缓存数据不存在或为空:', cacheKey); |
|
|
|
// 使用公共方法验证缓存数据 |
|
|
|
if (!validateCacheData(cachedData, this.currentPage)) { |
|
|
|
console.warn('缓存数据不存在或无效:', cacheKey); |
|
|
|
uni.showToast({ |
|
|
|
title: '缓存音频数据不存在', |
|
|
|
title: '缓存音频数据无效', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
@ -961,7 +916,7 @@ export default { |
|
|
|
this.hasAudioData = true; |
|
|
|
this.isAudioLoading = false; |
|
|
|
this.audioLoadFailed = false; |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.clearHighlight(); |
|
|
|
|
|
|
|
// 通知父组件音频状态变化 |
|
|
|
this.$emit('audio-state-change', { |
|
|
|
@ -1178,7 +1133,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查音频数据是否属于当前页面 |
|
|
|
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const currentPageCache = this.audioCache[audioCacheKey]; |
|
|
|
|
|
|
|
if (!currentPageCache || !currentPageCache.audios.includes(currentAudioData)) { |
|
|
|
@ -1210,7 +1165,7 @@ export default { |
|
|
|
audioManager.pause(); |
|
|
|
this.isPlaying = false; |
|
|
|
// 暂停时清除高亮 |
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
this.clearHighlight(); |
|
|
|
// 通知父组件高亮状态变化 |
|
|
|
this.emitHighlightChange(-1); |
|
|
|
}, |
|
|
|
@ -1608,7 +1563,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查音频数据是否属于当前页面,防止页面切换时的数据错乱 |
|
|
|
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const currentPageCache = this.audioCache[audioCacheKey]; |
|
|
|
|
|
|
|
// 如果当前音频数据不属于当前页面,则不更新高亮 |
|
|
|
@ -1676,7 +1631,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查音频数据是否属于当前页面,防止页面切换时的数据错乱 |
|
|
|
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`; |
|
|
|
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const currentPageCache = this.audioCache[audioCacheKey]; |
|
|
|
|
|
|
|
// 如果当前音频数据不属于当前页面,则不发送滚动事件 |
|
|
|
@ -2259,12 +2214,6 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
formatTime(seconds) { |
|
|
|
const mins = Math.floor(seconds / 60); |
|
|
|
const secs = Math.floor(seconds % 60); |
|
|
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|
|
|
}, |
|
|
|
|
|
|
|
// 清理音频缓存 |
|
|
|
clearAudioCache() { |
|
|
|
this.audioCache = {}; |
|
|
|
@ -2679,7 +2628,7 @@ export default { |
|
|
|
// 检查页面是否有文本内容且未缓存 |
|
|
|
if (pageData && pageData.length > 0) { |
|
|
|
const hasTextContent = pageData.some(item => item.type === 'text' && item.content); |
|
|
|
const cacheKey = `${this.courseId}_${i + 1}_${this.voiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, i + 1, this.localVoiceId); |
|
|
|
const isAlreadyCached = this.audioCache[cacheKey]; |
|
|
|
|
|
|
|
if (hasTextContent && !isAlreadyCached) { |
|
|
|
@ -2696,7 +2645,7 @@ export default { |
|
|
|
|
|
|
|
// 预加载单个页面的音频 |
|
|
|
async preloadPageAudio(pageIndex, pageData) { |
|
|
|
const cacheKey = `${this.courseId}_${pageIndex + 1}_${this.voiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, pageIndex + 1, this.localVoiceId); |
|
|
|
|
|
|
|
// 检查是否已经缓存 |
|
|
|
if (this.audioCache[cacheKey]) { |
|
|
|
@ -2773,6 +2722,7 @@ export default { |
|
|
|
audios: audioArray, |
|
|
|
totalDuration: totalDuration, |
|
|
|
voiceId: this.localVoiceId, // 保存音色ID用于验证 |
|
|
|
pageNumber: pageIndex + 1, // 保存页面号码用于验证 |
|
|
|
timestamp: Date.now() // 保存时间戳 |
|
|
|
}; |
|
|
|
|
|
|
|
@ -2785,13 +2735,13 @@ export default { |
|
|
|
|
|
|
|
// 检查指定页面是否有音频缓存 |
|
|
|
checkAudioCache(pageNumber) { |
|
|
|
const cacheKey = `${this.courseId}_${pageNumber}_${this.localVoiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, pageNumber, this.localVoiceId); |
|
|
|
const cachedData = this.audioCache[cacheKey]; |
|
|
|
|
|
|
|
if (cachedData && cachedData.audios && cachedData.audios.length > 0) { |
|
|
|
|
|
|
|
return true; |
|
|
|
if (!validateCacheData(cachedData, pageNumber)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
@ -2806,11 +2756,10 @@ export default { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const cacheKey = `${this.courseId}_${this.currentPage}_${this.voiceId}`; |
|
|
|
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId); |
|
|
|
const cachedData = this.audioCache[cacheKey]; |
|
|
|
|
|
|
|
if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) { |
|
|
|
|
|
|
|
if (!validateCacheData(cachedData, this.currentPage)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
|