feat: 定时停止(30/60/90/120 分钟)与循环联动
- 设置区新增"定时"下拉,到点自动停止播放并淡出配乐; 截止时间判定走 timeupdate,手机锁屏/后台也能按时停 - 设定定时自动带开循环(播到点为止);定时关闭或到点后 收回自动循环,手动开启的循环不受影响 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -104,6 +104,7 @@ const btnPrev = document.getElementById("btn-prev");
|
||||
const btnNext = document.getElementById("btn-next");
|
||||
const btnLoop = document.getElementById("btn-loop");
|
||||
const btnBgm = document.getElementById("btn-bgm");
|
||||
const timerSelect = document.getElementById("timer-select");
|
||||
const pageInfo = document.getElementById("page-info");
|
||||
|
||||
/** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */
|
||||
@@ -113,6 +114,9 @@ let playing = false;
|
||||
let inflight = null; // 进行中的 fetch AbortController
|
||||
let loopMode = false; // 循环诵读开关:播完全文回到开头继续
|
||||
let bgmOn = false; // 背景音乐开关
|
||||
let sleepEndAt = 0; // 定时停止的截止时间戳(毫秒),0 = 未开启
|
||||
let sleepTimer = null; // 定时器句柄(前台兜底;后台由 onTimeUpdate 判定截止时间)
|
||||
let loopAutoByTimer = false; // 循环是否为设定定时时自动打开的(是则定时结束一并关回)
|
||||
let segTimes = []; // 合并音频中各段的起始秒
|
||||
let currentSeg = -1; // 正在朗读的段号
|
||||
|
||||
@@ -663,8 +667,12 @@ async function buildCombined(blobs) {
|
||||
return { url: URL.createObjectURL(new Blob([out], { type: "audio/wav" })), times };
|
||||
}
|
||||
|
||||
/** 播放进度回调:按当前时间定位段号,高亮并自动翻页滚动。 */
|
||||
/** 播放进度回调:按当前时间定位段号,高亮并自动翻页滚动;顺带判定定时停止。 */
|
||||
function onTimeUpdate() {
|
||||
if (sleepEndAt && Date.now() >= sleepEndAt) {
|
||||
fireSleepTimer();
|
||||
return;
|
||||
}
|
||||
const t = audioEl.currentTime;
|
||||
let i = segTimes.length - 1;
|
||||
while (i > 0 && segTimes[i] > t) i -= 1;
|
||||
@@ -727,15 +735,62 @@ async function playFrom(index) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 循环诵读开关:开时读完全文自动回到第一段继续。 */
|
||||
/** 设置循环开关状态并同步按钮外观(供点击与定时联动共用)。 */
|
||||
function setLoopMode(on) {
|
||||
loopMode = on;
|
||||
audioEl.loop = on;
|
||||
btnLoop.classList.toggle("active", on);
|
||||
btnLoop.setAttribute("aria-pressed", String(on));
|
||||
}
|
||||
|
||||
/** 循环诵读开关:开时读完全文自动回到第一段继续;定时关闭时即无限循环。 */
|
||||
btnLoop.addEventListener("click", () => {
|
||||
loopMode = !loopMode;
|
||||
audioEl.loop = loopMode; // 播放中切换立即生效
|
||||
btnLoop.classList.toggle("active", loopMode);
|
||||
btnLoop.setAttribute("aria-pressed", String(loopMode));
|
||||
setLoopMode(!loopMode);
|
||||
loopAutoByTimer = false; // 用户手动干预后以手动状态为准
|
||||
setStatus(loopMode ? "循环诵读:开" : "循环诵读:关");
|
||||
});
|
||||
|
||||
/** 定时停止到期:停止播放、下拉复位,并收回定时自动带开的循环。 */
|
||||
function fireSleepTimer() {
|
||||
sleepEndAt = 0;
|
||||
sleepTimer = null;
|
||||
timerSelect.value = "0";
|
||||
if (loopAutoByTimer) {
|
||||
loopAutoByTimer = false;
|
||||
setLoopMode(false);
|
||||
}
|
||||
if (playing || inflight) {
|
||||
stopPlayback();
|
||||
setStatus("定时结束,已停止播放");
|
||||
}
|
||||
}
|
||||
|
||||
/** 定时停止:选 30/60/90/120 分钟后到点自动停播;选"关闭"取消。 */
|
||||
timerSelect.addEventListener("change", () => {
|
||||
if (sleepTimer) {
|
||||
clearTimeout(sleepTimer);
|
||||
sleepTimer = null;
|
||||
}
|
||||
const mins = Number(timerSelect.value);
|
||||
if (!mins) {
|
||||
sleepEndAt = 0;
|
||||
if (loopAutoByTimer) {
|
||||
// 定时带开的循环随"关闭"一并收回;用户手动开的循环不动
|
||||
loopAutoByTimer = false;
|
||||
setLoopMode(false);
|
||||
}
|
||||
setStatus("定时停止:关");
|
||||
return;
|
||||
}
|
||||
sleepEndAt = Date.now() + mins * 60000;
|
||||
sleepTimer = setTimeout(fireSleepTimer, mins * 60000);
|
||||
if (!loopMode) {
|
||||
setLoopMode(true); // 定时场景自动开启循环,播到点为止
|
||||
loopAutoByTimer = true;
|
||||
}
|
||||
setStatus(`定时 ${mins} 分钟后停止 · 循环已开`);
|
||||
});
|
||||
|
||||
/** 背景音乐开关:播放中开/关即时淡入淡出;未播放时只记录状态,下次播放生效。 */
|
||||
btnBgm.addEventListener("click", () => {
|
||||
bgmOn = !bgmOn;
|
||||
|
||||
Reference in New Issue
Block a user