您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自定义提示音间隔,自定义声音,自动保存设置,支持通知与静音等功能。
// ==UserScript== // @name 提示音定时器(自定义间隔 + 保存设置 + 自定义声音) // @namespace http://tampermonkey.net/ // @version 2.1 // @description 自定义提示音间隔,自定义声音,自动保存设置,支持通知与静音等功能。 // @author Knan // @match *://*/* // @grant none // @license MIT // ==/UserScript== (function () { "use strict"; // 防止重复注入 if (window.__soundTimerInjected) return; window.__soundTimerInjected = true; // 默认设置 const DEFAULT_SETTINGS = { A_MIN: 3, A_MAX: 5, B_INTERVAL: 90, B_PAUSE: 20, A_PAUSE: 10, A_URL: "https://actions.google.com/sounds/v1/alarms/beep_short.ogg", B_URL: "https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg", PANEL_X: null, PANEL_Y: null, }; // 状态管理 const state = { settings: { ...DEFAULT_SETTINGS }, isRunning: false, isMuted: false, isPaused: false, isNotifyEnabled: true, aTimer: null, bTimer: null, countdownInterval: null, nextATime: null, nextBTime: null, currentAudio: null, isDragging: false, dragOffsetX: 0, dragOffsetY: 0, isMinimized: false, }; // DOM元素引用 const dom = { panel: null, nextADisplay: null, nextBDisplay: null, inputs: {}, }; // 初始化函数 function init() { loadSettings(); createUI(); // 直接最小化 toggleMinimize(true); setupEventListeners(); } // 加载设置 function loadSettings() { try { const saved = localStorage.getItem("sound_timer_settings"); if (saved) { Object.assign(state.settings, JSON.parse(saved)); } } catch (e) { console.warn("加载设置失败", e); } } // 保存设置 function saveSettings() { localStorage.setItem( "sound_timer_settings", JSON.stringify(state.settings) ); } // 创建UI界面 function createUI() { // 主面板 dom.panel = document.createElement("div"); dom.panel.style.cssText = ` position: fixed; background: white; border: 1px solid #ccc; padding: 10px; z-index: 999999; font-size: 14px; font-family: sans-serif; box-shadow: 0 0 10px rgba(0,0,0,0.2); max-width: 280px; cursor: move; user-select: none; touch-action: none; `; // 设置初始位置 if (state.settings.PANEL_X !== null && state.settings.PANEL_Y !== null) { dom.panel.style.left = `${state.settings.PANEL_X}px`; dom.panel.style.top = `${state.settings.PANEL_Y}px`; } else { dom.panel.style.bottom = "20px"; dom.panel.style.right = "20px"; } // 面板HTML内容 dom.panel.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;"> <button id="minimizeBtn">🔽</button> <strong>提示音定时器</strong> <button id="closeBtn" style="color: red;">✖</button> </div> <button id="startBtn">开始</button> <button id="stopBtn">停止</button> <button id="pauseBtn">⏸ 暂停</button> <button id="resetBtn">重置</button> <button id="muteBtn">🔈 静音</button> <button id="notifyBtn">🔔 通知</button> <hr/> <div> <b>A 音间隔设置:</b><br/> A 音间隔:最小 <input id="aMin" type="number" style="width: 40px;" min="1" /> ~ 最大 <input id="aMax" type="number" style="width: 40px;" min="1" /> 分钟<br/> 播放后暂停:<input id="aPause" type="number" style="width: 50px;" min="1" /> 秒 </div> <hr/> <div> <b>B 音间隔设置:</b><br/> B 音间隔 <input id="bInt" type="number" style="width: 50px;" min="1" /> 分钟<br/> 播放后暂停 <input id="bPause" type="number" style="width: 50px;" min="1" /> 分钟 </div> <hr/> <div> <b>声音链接(可选):</b><br/> A 音 URL:<input id="aUrl" type="text" placeholder="mp3/ogg 链接" style="width: 100%;" /><br/> B 音 URL:<input id="bUrl" type="text" placeholder="mp3/ogg 链接" style="width: 100%;" /> </div> <div style="margin-top: 10px;"> 下次 A 音:<span id="nextA">--</span><br/> 下次 B 音:<span id="nextB">--</span> </div> <hr/> <div> <button id="aPreviewBtn">试听 A 音</button> <button id="bPreviewBtn">试听 B 音</button> </div> `; document.body.appendChild(dom.panel); // 获取DOM元素引用 dom.nextADisplay = document.getElementById("nextA"); dom.nextBDisplay = document.getElementById("nextB"); // 输入元素 dom.inputs = { aMin: document.getElementById("aMin"), aMax: document.getElementById("aMax"), bInt: document.getElementById("bInt"), bPause: document.getElementById("bPause"), aUrl: document.getElementById("aUrl"), bUrl: document.getElementById("bUrl"), aPause: document.getElementById("aPause"), }; // 设置输入框初始值 dom.inputs.aMin.value = state.settings.A_MIN; dom.inputs.aMax.value = state.settings.A_MAX; dom.inputs.bInt.value = state.settings.B_INTERVAL; dom.inputs.bPause.value = state.settings.B_PAUSE; dom.inputs.aUrl.value = state.settings.A_URL || ""; dom.inputs.bUrl.value = state.settings.B_URL || ""; dom.inputs.aPause.value = state.settings.A_PAUSE; } // 设置事件监听器 function setupEventListeners() { // 拖拽功能 dom.panel.addEventListener("mousedown", startDrag); dom.panel.addEventListener("touchstart", startDrag, { passive: false }); // 按钮事件 document.getElementById("startBtn").addEventListener("click", start); document.getElementById("stopBtn").addEventListener("click", stop); document.getElementById("muteBtn").addEventListener("click", toggleMute); document.getElementById("pauseBtn").addEventListener("click", togglePause); document .getElementById("notifyBtn") .addEventListener("click", toggleNotify); document .getElementById("resetBtn") .addEventListener("click", resetSettings); document .getElementById("minimizeBtn") .addEventListener("click", toggleMinimize); document .getElementById("aPreviewBtn") .addEventListener("click", () => previewSound("A")); document .getElementById("bPreviewBtn") .addEventListener("click", () => previewSound("B")); // 添加关闭按钮事件 document.getElementById("closeBtn").addEventListener("click", closePanel); // 输入框事件 Object.keys(dom.inputs).forEach((key) => { dom.inputs[key].addEventListener("change", updateSettingsFromInputs); if (key === "aUrl" || key === "bUrl") { dom.inputs[key].addEventListener("input", updateSettingsFromInputs); } }); } // 关闭面板 function closePanel() { // 停止所有定时器 stop(); // 保存当前设置 saveSettings(); // 清除缓存配置 localStorage.removeItem("sound_timer_settings"); // 移除面板 if (dom.panel && dom.panel.parentNode) { dom.panel.parentNode.removeChild(dom.panel); } // 重置注入标记,允许重新注入 window.__soundTimerInjected = false; log("面板已关闭"); } // 拖拽开始 function startDrag(e) { // 如果点击的是按钮或输入框,则不拖拽 if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT") { return; } e.preventDefault(); state.isDragging = true; const rect = dom.panel.getBoundingClientRect(); if (e.type === "mousedown") { state.dragOffsetX = e.clientX - rect.left; state.dragOffsetY = e.clientY - rect.top; } else { state.dragOffsetX = e.touches[0].clientX - rect.left; state.dragOffsetY = e.touches[0].clientY - rect.top; } // 移除可能存在的bottom/right定位 dom.panel.style.bottom = "auto"; dom.panel.style.right = "auto"; document.addEventListener("mousemove", drag); document.addEventListener("touchmove", drag, { passive: false }); document.addEventListener("mouseup", endDrag); document.addEventListener("touchend", endDrag); } // 拖拽过程 function drag(e) { if (!state.isDragging) return; e.preventDefault(); const clientX = e.type === "mousemove" ? e.clientX : e.touches[0].clientX; const clientY = e.type === "mousemove" ? e.clientY : e.touches[0].clientY; // 计算新位置 let newX = clientX - state.dragOffsetX; let newY = clientY - state.dragOffsetY; // 限制在视窗范围内 const panelWidth = dom.panel.offsetWidth; const panelHeight = dom.panel.offsetHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; newX = Math.max(0, Math.min(newX, windowWidth - panelWidth)); newY = Math.max(0, Math.min(newY, windowHeight - panelHeight)); dom.panel.style.left = `${newX}px`; dom.panel.style.top = `${newY}px`; } // 拖拽结束 function endDrag() { state.isDragging = false; // 保存位置 const rect = dom.panel.getBoundingClientRect(); state.settings.PANEL_X = rect.left; state.settings.PANEL_Y = rect.top; saveSettings(); document.removeEventListener("mousemove", drag); document.removeEventListener("touchmove", drag); document.removeEventListener("mouseup", endDrag); document.removeEventListener("touchend", endDrag); } // 最小化/最大化面板 function toggleMinimize() { state.isMinimized = !state.isMinimized; const minimizeBtn = document.getElementById("minimizeBtn"); minimizeBtn.textContent = state.isMinimized ? "🔼" : "🔽"; const elements = dom.panel.querySelectorAll("button, div, hr"); elements.forEach((el) => { if (el !== minimizeBtn && el.id !== "closeBtn") { el.style.display = state.isMinimized ? "none" : "block"; } }); // 确保关闭按钮在最小化状态下仍然可见 const closeBtn = document.getElementById("closeBtn"); if (closeBtn) { closeBtn.style.display = "block"; } if (!state.isMinimized) { // 恢复面板样式 dom.panel.style.cssText = ` position: fixed; background: white; border: 1px solid #ccc; padding: 10px; z-index: 999999; font-size: 14px; font-family: sans-serif; box-shadow: 0 0 10px rgba(0,0,0,0.2); max-width: 280px; cursor: move; user-select: none; touch-action: none; `; // 恢复位置 if (state.settings.PANEL_X !== null && state.settings.PANEL_Y !== null) { dom.panel.style.left = `${state.settings.PANEL_X}px`; dom.panel.style.top = `${state.settings.PANEL_Y}px`; } else { dom.panel.style.bottom = "20px"; dom.panel.style.right = "20px"; } const buttons = dom.panel.querySelectorAll("button"); buttons.forEach((btn) => { btn.style.display = "inline-block"; btn.style.margin = "2px"; }); } else { // 在最小化状态下,确保标题和按钮在同一行 const headerDiv = dom.panel.querySelector("div"); if (headerDiv) { headerDiv.style.display = "flex"; } } } // 从输入框更新设置 function updateSettingsFromInputs() { state.settings.A_MIN = Math.max( 1, parseInt(dom.inputs.aMin.value) || DEFAULT_SETTINGS.A_MIN ); state.settings.A_MAX = Math.max( state.settings.A_MIN, parseInt(dom.inputs.aMax.value) || DEFAULT_SETTINGS.A_MAX ); dom.inputs.aMax.value = state.settings.A_MAX; state.settings.B_INTERVAL = Math.max( 1, parseInt(dom.inputs.bInt.value) || DEFAULT_SETTINGS.B_INTERVAL ); state.settings.B_PAUSE = Math.max( 1, parseInt(dom.inputs.bPause.value) || DEFAULT_SETTINGS.B_PAUSE ); state.settings.A_PAUSE = Math.max( 1, parseInt(dom.inputs.aPause.value) || DEFAULT_SETTINGS.A_PAUSE ); state.settings.A_URL = dom.inputs.aUrl.value.trim() || DEFAULT_SETTINGS.A_URL; state.settings.B_URL = dom.inputs.bUrl.value.trim() || DEFAULT_SETTINGS.B_URL; saveSettings(); } // 格式化时间显示 function formatTime(ms) { if (!ms || ms <= 0) return "--"; const totalSec = Math.floor(ms / 1000); const min = Math.floor(totalSec / 60); const sec = totalSec % 60; return `${min}分${sec.toString().padStart(2, "0")}秒`; } // 更新倒计时显示 function updateCountdownDisplay() { const now = Date.now(); dom.nextADisplay.textContent = formatTime( state.nextATime ? Math.max(0, state.nextATime - now) : null ); dom.nextBDisplay.textContent = formatTime( state.nextBTime ? Math.max(0, state.nextBTime - now) : null ); } // 开始倒计时更新器 function startCountdownUpdater() { clearInterval(state.countdownInterval); state.countdownInterval = setInterval(updateCountdownDisplay, 1000); } // 停止倒计时更新器 function stopCountdownUpdater() { clearInterval(state.countdownInterval); dom.nextADisplay.textContent = "--"; dom.nextBDisplay.textContent = "--"; } // 发送通知 function notify(title, body) { if (!state.isNotifyEnabled) return; // 如果通知权限已经授予 if (Notification.permission === "granted") { try { new Notification(title, { body }); } catch (e) { console.warn("通知发送失败:", e); } } // 如果通知权限还未请求 else if (Notification.permission !== "denied") { Notification.requestPermission().then((permission) => { if (permission === "granted") { new Notification(title, { body }); } }); } } // 切换静音状态 function toggleMute() { state.isMuted = !state.isMuted; const muteBtn = document.getElementById("muteBtn"); muteBtn.textContent = state.isMuted ? "🔇 已静音" : "🔈 静音"; log(state.isMuted ? "已静音" : "取消静音"); if (state.isMuted && state.currentAudio) { state.currentAudio.pause(); log("当前音频已暂停"); } else if ( !state.isMuted && state.currentAudio && !state.currentAudio.ended ) { state.currentAudio.play().catch((err) => { log("音频继续播放失败: " + err.message); }); } } // 试听声音 function previewSound(type) { const url = type === "A" ? state.settings.A_URL : state.settings.B_URL; const label = `${type} 提示音`; playSound(url, label, () => {}); } // 播放声音 function playSound(url, label, onComplete) { if (state.isPaused) { log(`${label} 已暂停,延后播放`); setTimeout(() => playSound(url, label, onComplete), 5000); return; } if (state.currentAudio && !state.currentAudio.ended) { log(`当前正在播放音频,跳过新的播放请求`); return; } notify(label, `即将播放 ${label}`); if (state.isMuted) { log(`${label} 静音中,跳过播放`); setTimeout(onComplete, 100); return; } const audio = new Audio(url); state.currentAudio = audio; audio .play() .then(() => { log(`${label} 播放中...`); audio.onended = () => { log(`${label} 播放完成`); onComplete(); }; }) .catch((err) => { log(`${label} 播放失败: ${err.message}`); onComplete(); }); } // 随机安排A音 function scheduleRandomA() { if (!state.isRunning) return; updateSettingsFromInputs(); const delay = getRandomDelay( state.settings.A_MIN * 60 * 1000, state.settings.A_MAX * 60 * 1000 ); state.nextATime = Date.now() + delay; updateCountdownDisplay(); log(`A 音将在 ${Math.round(delay / 1000)} 秒后播放`); state.aTimer = setTimeout(() => { playSound(state.settings.A_URL, "A 提示音", () => { log(`A 音播放后暂停 ${state.settings.A_PAUSE} 秒...`); state.nextATime = Date.now() + state.settings.A_PAUSE * 1000; updateCountdownDisplay(); setTimeout(scheduleRandomA, state.settings.A_PAUSE * 1000); }); }, delay); } // 安排B音 function scheduleLoopB() { if (!state.isRunning) return; updateSettingsFromInputs(); const delay = state.settings.B_INTERVAL * 60 * 1000; state.nextBTime = Date.now() + delay; updateCountdownDisplay(); log(`B 音将在 ${state.settings.B_INTERVAL} 分钟后播放`); state.bTimer = setTimeout(() => { playSound(state.settings.B_URL, "B 提示音", () => { log(`B 音播放后暂停 ${state.settings.B_PAUSE} 分钟...`); state.nextBTime = Date.now() + state.settings.B_PAUSE * 60 * 1000; updateCountdownDisplay(); setTimeout(scheduleLoopB, state.settings.B_PAUSE * 60 * 1000); }); }, delay); } // 获取随机延迟时间 function getRandomDelay(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // 开始定时器 function start() { if (state.isRunning) return log("已经启动"); // 尝试播放音频以解锁权限 const unlock = new Audio(state.settings.A_URL); unlock .play() .then(() => { unlock.pause(); unlock.currentTime = 0; log("已解锁音频播放权限,开始定时"); state.isRunning = true; state.isPaused = false; document.getElementById("pauseBtn").textContent = "⏸ 暂停"; Notification.requestPermission().catch(() => {}); startCountdownUpdater(); scheduleRandomA(); scheduleLoopB(); }) .catch((err) => { alert("请先点击页面并允许播放音频,然后再点击开始按钮"); log("音频权限未授予:" + err.message); }); } // 停止定时器 function stop() { if (!state.isRunning) return log("已停止"); state.isRunning = false; clearTimeout(state.aTimer); clearTimeout(state.bTimer); stopCountdownUpdater(); log("已停止所有定时任务"); } // 切换暂停状态 function togglePause() { state.isPaused = !state.isPaused; const pauseBtn = document.getElementById("pauseBtn"); pauseBtn.textContent = state.isPaused ? "▶️ 恢复" : "⏸ 暂停"; log(state.isPaused ? "已暂停播放" : "已恢复播放"); if (state.isPaused) { // 保存剩余时间并清除定时器 if (state.nextATime) { window.__pausedATimeRemaining = Math.max( 0, state.nextATime - Date.now() ); clearTimeout(state.aTimer); } if (state.nextBTime) { window.__pausedBTimeRemaining = Math.max( 0, state.nextBTime - Date.now() ); clearTimeout(state.bTimer); } clearInterval(state.countdownInterval); } else { // 恢复定时器 if (window.__pausedATimeRemaining) { state.nextATime = Date.now() + window.__pausedATimeRemaining; state.aTimer = setTimeout(() => { playSound(state.settings.A_URL, "A 提示音", () => { log(`A 音播放后暂停 ${state.settings.A_PAUSE} 秒...`); state.nextATime = Date.now() + state.settings.A_PAUSE * 1000; updateCountdownDisplay(); setTimeout(scheduleRandomA, state.settings.A_PAUSE * 1000); }); }, window.__pausedATimeRemaining); window.__pausedATimeRemaining = null; } if (window.__pausedBTimeRemaining) { state.nextBTime = Date.now() + window.__pausedBTimeRemaining; state.bTimer = setTimeout(() => { playSound(state.settings.B_URL, "B 提示音", () => { log(`B 音播放后暂停 ${state.settings.B_PAUSE} 分钟...`); state.nextBTime = Date.now() + state.settings.B_PAUSE * 60 * 1000; updateCountdownDisplay(); setTimeout(scheduleLoopB, state.settings.B_PAUSE * 60 * 1000); }); }, window.__pausedBTimeRemaining); window.__pausedBTimeRemaining = null; } startCountdownUpdater(); } updateCountdownDisplay(); } // 切换通知状态 function toggleNotify() { state.isNotifyEnabled = !state.isNotifyEnabled; const notifyBtn = document.getElementById("notifyBtn"); notifyBtn.textContent = state.isNotifyEnabled ? "🔔 通知" : "🔕 静默"; log(state.isNotifyEnabled ? "已开启通知" : "已关闭通知"); } // 重置设置 function resetSettings() { if (!confirm("确定要重置所有设置为默认值吗?")) return; state.settings = { ...DEFAULT_SETTINGS }; // 更新UI dom.inputs.aMin.value = state.settings.A_MIN; dom.inputs.aMax.value = state.settings.A_MAX; dom.inputs.bInt.value = state.settings.B_INTERVAL; dom.inputs.bPause.value = state.settings.B_PAUSE; dom.inputs.aUrl.value = state.settings.A_URL; dom.inputs.bUrl.value = state.settings.B_URL; dom.inputs.aPause.value = state.settings.A_PAUSE; // 重置面板位置 dom.panel.style.bottom = "20px"; dom.panel.style.right = "20px"; dom.panel.style.left = "auto"; dom.panel.style.top = "auto"; state.settings.PANEL_X = null; state.settings.PANEL_Y = null; localStorage.removeItem("sound_timer_settings"); log("设置已重置为默认值"); } // 日志函数 function log(msg) { console.log(`[提示音定时器] ${msg}`); } // 初始化应用 init(); })();