diff --git a/app.js b/app.js index d7414f4..b1f100e 100644 --- a/app.js +++ b/app.js @@ -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; diff --git a/index.html b/index.html index 0595472..181ce5a 100644 --- a/index.html +++ b/index.html @@ -54,6 +54,14 @@
+ +