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;
|
||||
|
||||
@@ -54,6 +54,14 @@
|
||||
<div class="tool-group" role="group" aria-label="模式">
|
||||
<button type="button" id="btn-loop" class="layout-btn" aria-pressed="false">循环</button>
|
||||
<button type="button" id="btn-bgm" class="layout-btn" aria-pressed="false">配乐</button>
|
||||
<label class="tool-name" for="timer-select">定时</label>
|
||||
<select id="timer-select" aria-label="定时停止">
|
||||
<option value="0">关闭</option>
|
||||
<option value="30">30 分钟</option>
|
||||
<option value="60">60 分钟</option>
|
||||
<option value="90">90 分钟</option>
|
||||
<option value="120">120 分钟</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="status" class="status" role="status"></div>
|
||||
<button type="button" id="btn-expand" class="expand-btn" aria-expanded="false" aria-label="展开设置" title="设置">
|
||||
|
||||
@@ -179,6 +179,23 @@ body {
|
||||
}
|
||||
#voice-select:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
|
||||
#timer-select {
|
||||
appearance: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.86rem;
|
||||
color: var(--ink-soft);
|
||||
letter-spacing: 0.08em;
|
||||
background:
|
||||
linear-gradient(45deg, transparent 50%, var(--gold) 50%) right 12px center / 6px 6px no-repeat,
|
||||
linear-gradient(135deg, var(--gold) 50%, transparent 50%) right 8px center / 6px 6px no-repeat,
|
||||
transparent;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 6px;
|
||||
padding: 6px 26px 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#timer-select:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
|
||||
/* ===== 字号 / 排版工具(并入控制栏) ===== */
|
||||
.tool-group { display: flex; align-items: center; gap: 8px; }
|
||||
.tool-name { font-size: 0.8rem; color: var(--ink-soft); letter-spacing: 0.2em; }
|
||||
|
||||
Reference in New Issue
Block a user