// ==UserScript== // @name 网页定时刷新器 // @namespace https://via.app/web-refresher // @version 4.6.0 // @description 可拖动定时刷新面板。间隔刷新(单选)、每日定时(多选)、关键词监控(多选,永不自动关闭)。多词命中时同时显示多条红条。 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-end // @noframes // ==/UserScript== (function () { 'use strict'; /* ================================================================== * 一、常量与存储键 * ================================================================== */ const KEY_INTERVALS = 'wpr_intervals'; const KEY_TIMERS = 'wpr_timers'; const KEY_MONITORS = 'wpr_monitors'; const KEY_TIMER_FIRED = 'wpr_timer_fired'; const KEY_UI_MODE = 'wpr_ui_mode'; const KEY_PANEL_POS = 'wpr_panel_pos'; const KEY_LAST_REFRESH = 'wpr_last_refresh'; const KEY_SOUND = 'wpr_sound'; const KEY_SOUND_COUNT = 'wpr_sound_count'; const PAGE_ALIVE_KEY = 'wpr_page_alive'; const SS_COUNT_KEY = 'wpr_refresh_count'; const DEFAULT_INTERVALS = [ { sec: 10, enabled: false, remain: 10 }, { sec: 30, enabled: false, remain: 30 }, { sec: 60, enabled: false, remain: 60 } ]; const LONG_PRESS_MS = 400; const MOVE_CANCEL_PX = 8; const TIMER_DEDUP_MS = 60000; const MONITOR_CHECK_DELAYS = [200, 500, 1000, 1500, 2000, 3000, 5000, 8000]; const HIGHLIGHT_MS = 2500; const SAME_SESSION_WINDOW_MS = 30000; const CLICK_DELAY_MS = 150; const TIMER_STOP_WINDOW_SEC = 10; const TIMER_MISS_WINDOW_MS = 5 * 60 * 1000; const SOUND_COUNT_OPTIONS = [ { val: 3, label: '3次' }, { val: 5, label: '5次' }, { val: 8, label: '8次' }, { val: 25, label: '25次' } ]; const BEEP_INTERVAL_MS = 1200; /* ================================================================== * 二、数据持久化 * ================================================================== */ function loadValue(key, def) { let v; try { if (typeof GM_getValue === 'function') v = GM_getValue(key, def); } catch (e) { v = undefined; } if (v === undefined || v === null) { try { const raw = window.localStorage.getItem('wpr_' + key); v = (raw === null) ? def : raw; } catch (e) { v = def; } } return String(v); } function saveValue(key, val) { const str = String(val); try { if (typeof GM_setValue === 'function') { GM_setValue(key, str); return; } } catch (e) {} try { window.localStorage.setItem('wpr_' + key, str); } catch (e) {} } function loadJSON(key, def) { try { const v = JSON.parse(loadValue(key, JSON.stringify(def))); return (v === null || v === undefined) ? def : v; } catch (e) { return def; } } function saveJSON(key, obj) { saveValue(key, JSON.stringify(obj)); } /* ================================================================== * 三、会话级状态 * ================================================================== */ function getRefreshCount() { try { return parseInt(sessionStorage.getItem(SS_COUNT_KEY) || '0', 10) || 0; } catch (e) { return 0; } } function incRefreshCount() { const n = getRefreshCount() + 1; try { sessionStorage.setItem(SS_COUNT_KEY, String(n)); } catch (e) {} return n; } function resetRefreshCount() { try { sessionStorage.setItem(SS_COUNT_KEY, '0'); } catch (e) {} } /* ================================================================== * 四、运行时状态 * ================================================================== */ let panelEl = null; let dotEl = null; let chipListEl = null; let statusAreaEl = null; let countEl = null; let soundBtnEl = null; let helpBtnEl = null; let helpOverlayEl = null; let inputRow = null; let inputEl = null; let timerInputRow = null; let timerHH = null, timerMM = null, timerSS = null; let monitorInputRow = null; let monitorInputEl = null; let intervals = []; let timers = []; let monitors = []; let tickTimer = null; let refreshPending = false; let soundEnabled = false; let soundCount = 3; let soundPlayedCount = 0; let isRinging = false; let audioCtx = null; let soundTimer = null; let soundMenuEl = null; let soundLongPressed = false; let soundSessionId = 0; let activeOscillators = []; let selectedChip = null; /* ★ 命中状态:每个词一条独立红条,各自维护定位状态 * wordStates = { * "抖音": { action: "stop", elements: [...], matchIndex: -1 }, * "交锋": { action: "click", elements: [...], matchIndex: 0 } * } */ let wordStates = {}; let lastHitWords = []; let dragging = false; let dragPressTimer = null; let dragStartX = 0, dragStartY = 0; let dragStartLeft = 0, dragStartTop = 0; let dragMoved = false; let suppressClick = false; let highlightTimer = null; /* ================================================================== * 五、样式 * ================================================================== */ const PANEL_CSS = [ '#wpr-panel {', ' position: fixed; top: 60px; right: 10px; z-index: 2147483647;', ' width: 236px; max-width: calc(100vw - 20px);', ' box-sizing: border-box; padding: 8px;', ' font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;', ' font-size: 13px; line-height: 1.4; color: #24292f;', ' background: #ffffff; border: 1px solid #d0d7de; border-radius: 10px;', ' box-shadow: 0 4px 18px rgba(0, 0, 0, .18);', ' -webkit-user-select: none; user-select: none;', ' -webkit-tap-highlight-color: transparent;', ' transition: opacity .15s, box-shadow .15s;', '}', '#wpr-panel.wpr-dragging { opacity: 0.85; box-shadow: 0 8px 28px rgba(0,0,0,.35); }', '#wpr-panel * { box-sizing: border-box; font-family: inherit; }', '#wpr-panel .wpr-row { display: flex; align-items: center; gap: 5px; margin-bottom: 6px; }', '#wpr-panel .wpr-title {', ' flex: 1 1 auto; min-width: 0;', ' font-size: 12px; font-weight: 600; color: #24292f;', ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis;', ' text-align: center;', '}', '#wpr-panel .wpr-btn {', ' flex: 0 0 auto; margin: 0; padding: 5px 8px;', ' font-size: 12px; line-height: 1.2; color: #24292f;', ' background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px;', ' cursor: pointer; outline: none;', ' -webkit-appearance: none; appearance: none;', '}', '#wpr-panel .wpr-btn:active { background: #e6ebf1; }', '#wpr-panel .wpr-hide,', '#wpr-panel .wpr-float,', '#wpr-panel .wpr-stop {', ' width: 26px; height: 26px; padding: 0;', ' display: flex; align-items: center; justify-content: center;', ' font-size: 11px; line-height: 1; color: #57606a;', '}', '#wpr-panel .wpr-close {', ' width: 26px; height: 26px; padding: 0;', ' display: flex; align-items: center; justify-content: center;', ' font-size: 16px; line-height: 1;', ' color: #cf222e; background: #ffffff; border-color: #f2c0c0;', '}', '#wpr-panel .wpr-close:active { background: #ffecec; }', '#wpr-panel .wpr-custom,', '#wpr-panel .wpr-timer,', '#wpr-panel .wpr-monitor,', '#wpr-panel .wpr-del { flex: 1 1 0; text-align: center; padding: 5px 4px; }', '#wpr-panel .wpr-hidden { display: none !important; }', '#wpr-panel .wpr-input {', ' flex: 1 1 auto; min-width: 0; width: 100%;', ' padding: 6px 8px; font-size: 13px; line-height: 1.2;', ' color: #24292f; background: #ffffff;', ' border: 1px solid #d0d7de; border-radius: 6px;', ' outline: none; -webkit-appearance: none; appearance: none;', '}', '#wpr-panel .wpr-input:focus {', ' border-color: #0969da;', ' box-shadow: 0 0 0 2px rgba(9, 105, 218, .15);', '}', '#wpr-panel .wpr-tnum {', ' flex: 0 0 auto; width: 30px; padding: 6px 2px;', ' font-size: 13px; line-height: 1.2; text-align: center;', ' color: #24292f; background: #ffffff;', ' border: 1px solid #d0d7de; border-radius: 6px;', ' outline: none; -webkit-appearance: none; appearance: none;', '}', '#wpr-panel .wpr-tnum:focus { border-color: #0969da; }', '#wpr-panel .wpr-sep { color: #8b949e; font-weight: 600; flex: 0 0 auto; }', '#wpr-panel .wpr-list {', ' display: flex; flex-wrap: wrap; gap: 6px;', ' max-height: 140px; overflow-y: auto;', ' margin-bottom: 6px; padding: 2px 0;', ' -webkit-overflow-scrolling: touch;', '}', '#wpr-panel .wpr-chip {', ' flex: 0 0 auto; padding: 4px 10px;', ' font-size: 12px; line-height: 1.4; color: #24292f;', ' background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 999px;', ' cursor: pointer; outline: none;', ' -webkit-appearance: none; appearance: none;', ' -webkit-user-select: none; user-select: none;', ' -webkit-touch-callout: none;', ' transition: box-shadow .12s;', '}', '#wpr-panel .wpr-chip:active { background: #e6ebf1; }', '#wpr-panel .wpr-chip.wpr-on {', ' background: #2da44e; border-color: #1a7f37; color: #ffffff;', '}', '#wpr-panel .wpr-chip.wpr-on:active { background: #2c974b; }', '#wpr-panel .wpr-chip.wpr-selected {', ' box-shadow: 0 0 0 2px rgba(255, 152, 0, .85);', '}', '#wpr-panel .wpr-empty { font-size: 12px; color: #8b949e; }', /* ★ 状态栏区域(多红条容器) */ '#wpr-panel .wpr-status-area {', ' display: flex; flex-direction: column; gap: 4px;', ' max-height: 120px; overflow-y: auto;', ' -webkit-overflow-scrolling: touch;', '}', /* 普通状态条(蓝色 / 黄色) */ '#wpr-panel .wpr-status {', ' padding: 5px 8px; font-size: 12px; text-align: center;', ' color: #0969da; background: #f0f6ff; border-radius: 6px;', ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis;', '}', '#wpr-panel .wpr-status.wpr-paused {', ' color: #663c00; background: #fff8c5;', '}', /* ★ 红条:每个命中词一条 */ '#wpr-panel .wpr-status-item {', ' padding: 5px 8px; font-size: 12px; text-align: center;', ' color: #ffffff; background: #cf222e;', ' border-radius: 6px;', ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis;', ' cursor: pointer;', ' -webkit-user-select: none; user-select: none;', '}', '#wpr-panel .wpr-status-item:active { background: #a40e26; }', '#wpr-panel .wpr-foot-row {', ' display: flex; align-items: center; gap: 5px;', ' margin-top: 5px; margin-bottom: 0;', '}', '#wpr-panel .wpr-sound {', ' width: 26px; height: 26px; padding: 0;', ' display: flex; align-items: center; justify-content: center;', ' font-size: 14px; line-height: 1;', ' color: #8b949e;', '}', '#wpr-panel .wpr-sound.wpr-sound-on {', ' color: #24292f;', ' background: #fff8c5; border-color: #d4a72c;', '}', '#wpr-panel .wpr-sound.wpr-sound-on:active { background: #f5e6a8; }', '#wpr-panel .wpr-help {', ' width: 26px; height: 26px; padding: 0;', ' display: flex; align-items: center; justify-content: center;', ' font-size: 13px; font-weight: 700; line-height: 1;', ' color: #0969da; background: #f0f6ff;', ' border-color: #b6d9ff;', ' border-radius: 50%;', '}', '#wpr-panel .wpr-help:active { background: #dbeaff; }', '#wpr-panel .wpr-count {', ' flex: 1 1 auto; min-width: 0;', ' padding: 3px 8px;', ' font-size: 11px; text-align: center; color: #57606a;', '}', '#wpr-dot {', ' position: fixed; right: 6px; bottom: 6px;', ' z-index: 2147483646;', ' width: 30px; height: 30px; border-radius: 50%;', ' display: flex; align-items: center; justify-content: center;', ' font-size: 11px; font-weight: 600; color: #ffffff;', ' background: rgba(9, 105, 218, .85);', ' border: 1px solid rgba(255, 255, 255, .85);', ' box-shadow: 0 1px 5px rgba(0, 0, 0, .3);', ' cursor: pointer;', ' -webkit-user-select: none; user-select: none;', ' -webkit-tap-highlight-color: transparent;', ' transition: transform .15s, background .15s;', '}', '#wpr-dot:active { transform: scale(0.88); }', '#wpr-dot.wpr-dot-alert { background: rgba(207, 34, 46, .9); }', '.wpr-sound-menu {', ' position: fixed; z-index: 2147483647;', ' display: flex; flex-direction: column; gap: 4px; padding: 6px;', ' background: #ffffff; border: 1px solid #d0d7de; border-radius: 8px;', ' box-shadow: 0 4px 14px rgba(0,0,0,.22);', ' font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;', ' -webkit-user-select: none; user-select: none;', '}', '.wpr-sound-menu .wpr-sm-title {', ' font-size: 11px; color: #57606a; padding: 0 2px 4px;', ' border-bottom: 1px solid #eaeef2; margin-bottom: 2px;', '}', '.wpr-sound-menu .wpr-sm-opts {', ' display: flex; gap: 4px;', '}', '.wpr-sound-menu .wpr-sm-btn {', ' margin: 0; padding: 6px 10px;', ' font-size: 12px; line-height: 1.2; color: #24292f;', ' background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px;', ' cursor: pointer; outline: none;', ' -webkit-appearance: none; appearance: none;', ' white-space: nowrap;', '}', '.wpr-sound-menu .wpr-sm-btn:active { background: #e6ebf1; }', '.wpr-sound-menu .wpr-sm-btn.wpr-sm-on {', ' background: #2da44e; border-color: #1a7f37; color: #ffffff;', '}', '.wpr-sound-menu .wpr-sm-btn.wpr-sm-on:active { background: #2c974b; }', '.wpr-help-mask {', ' position: fixed; inset: 0; z-index: 2147483647;', ' background: rgba(0, 0, 0, .45);', ' display: flex; align-items: center; justify-content: center;', ' padding: 16px;', ' font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;', ' -webkit-tap-highlight-color: transparent;', '}', '.wpr-help-card {', ' width: 100%; max-width: 340px; max-height: 82vh;', ' overflow-y: auto; -webkit-overflow-scrolling: touch;', ' box-sizing: border-box;', ' background: #ffffff; color: #24292f;', ' border-radius: 12px; padding: 14px 16px 12px;', ' box-shadow: 0 10px 40px rgba(0,0,0,.35);', ' font-size: 13px; line-height: 1.6;', ' -webkit-user-select: text; user-select: text;', '}', '.wpr-help-card h3 {', ' margin: 0 0 8px; font-size: 15px; font-weight: 700;', ' color: #0969da; text-align: center;', '}', '.wpr-help-card h4 {', ' margin: 14px 0 4px; font-size: 13px; font-weight: 700; color: #24292f;', ' border-bottom: 1px solid #eaeef2; padding-bottom: 3px;', '}', '.wpr-help-card p { margin: 4px 0; }', '.wpr-help-card ul { margin: 4px 0 4px 18px; padding: 0; }', '.wpr-help-card li { margin: 3px 0; }', '.wpr-help-card .wpr-hl { color: #cf222e; font-weight: 700; }', '.wpr-help-card .wpr-ok { color: #1a7f37; font-weight: 700; }', '.wpr-help-card .wpr-blue { color: #0969da; font-weight: 600; }', '.wpr-help-card .wpr-tag {', ' display: inline-block; padding: 0 5px; border-radius: 4px;', ' font-size: 11px; font-weight: 600;', ' background: #eaeef2; color: #57606a;', ' vertical-align: middle;', '}', '.wpr-help-card .wpr-close-btn {', ' display: block; width: 100%; margin-top: 14px;', ' padding: 9px 0; text-align: center;', ' font-size: 14px; font-weight: 600; color: #ffffff;', ' background: #0969da; border: none; border-radius: 8px;', ' cursor: pointer; -webkit-appearance: none; appearance: none;', '}', '.wpr-help-card .wpr-close-btn:active { background: #0a5ec4; }' ].join('\n'); const HIGHLIGHT_CSS = [ '.wpr-hit-highlight {', ' outline: 3px solid #cf222e !important;', ' outline-offset: 3px !important;', ' background-color: rgba(255, 235, 59, .35) !important;', ' transition: outline .15s, background-color .15s !important;', ' border-radius: 3px !important;', '}' ].join('\n'); function injectStyle() { if (document.getElementById('wpr-style')) return; const style = document.createElement('style'); style.id = 'wpr-style'; style.textContent = PANEL_CSS + '\n' + HIGHLIGHT_CSS; (document.head || document.documentElement).appendChild(style); } /* ================================================================== * 六、数据加载与保存 * ================================================================== */ function makeMonitorId(action, word) { return (action === 'click' ? 'A_' : 'B_') + word; } function loadState() { intervals = loadJSON(KEY_INTERVALS, DEFAULT_INTERVALS); intervals = intervals .filter(function (x) { return x && isFinite(x.sec) && x.sec > 0; }) .map(function (x) { const sec = parseInt(x.sec, 10); let remain = (x.remain !== undefined && isFinite(x.remain)) ? parseInt(x.remain, 10) : sec; if (!isFinite(remain) || remain <= 0 || remain > sec) remain = sec; return { sec: sec, enabled: !!x.enabled, remain: remain }; }); timers = loadJSON(KEY_TIMERS, []); timers = timers .filter(function (x) { return x && /^\d{2}:\d{2}:\d{2}$/.test(x.hms); }) .map(function (x) { return { hms: x.hms, enabled: !!x.enabled }; }); monitors = loadJSON(KEY_MONITORS, []); monitors = monitors .filter(function (x) { return x && typeof x.word === 'string' && x.word !== ''; }) .map(function (x) { const action = (x.action === 'click') ? 'click' : 'stop'; const id = (typeof x.id === 'string' && x.id !== '') ? x.id : makeMonitorId(action, x.word); return { id: id, word: x.word, action: action, enabled: !!x.enabled }; }); const seenIds = {}; monitors = monitors.filter(function (x) { if (seenIds[x.id]) return false; seenIds[x.id] = true; return true; }); if (intervals.length === 0) { intervals = DEFAULT_INTERVALS.map(function (x) { return { sec: x.sec, enabled: x.enabled, remain: x.remain }; }); } } function saveState() { saveJSON(KEY_INTERVALS, intervals); saveJSON(KEY_TIMERS, timers); saveJSON(KEY_MONITORS, monitors); } /* ================================================================== * 七、声音 * ================================================================== */ function ensureAudioCtx() { if (audioCtx) return audioCtx; try { const Ctx = window.AudioContext || window.webkitAudioContext; if (!Ctx) return null; audioCtx = new Ctx(); } catch (e) { audioCtx = null; } return audioCtx; } function playBeep(ctx, startAt, freq, dur) { try { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, startAt); gain.gain.setValueAtTime(0.0001, startAt); gain.gain.exponentialRampToValueAtTime(0.42, startAt + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, startAt + dur); osc.connect(gain); gain.connect(ctx.destination); osc.start(startAt); osc.stop(startAt + dur + 0.03); const item = { osc: osc, gain: gain }; activeOscillators.push(item); osc.onended = function () { for (let i = 0; i < activeOscillators.length; i++) { if (activeOscillators[i] === item) { activeOscillators.splice(i, 1); break; } } try { osc.disconnect(); } catch (e) {} try { gain.disconnect(); } catch (e) {} }; } catch (e) {} } function beepSequence(ctx) { const now = ctx.currentTime + 0.01; playBeep(ctx, now, 880, 0.18); playBeep(ctx, now + 0.22, 1174, 0.18); playBeep(ctx, now + 0.44, 1568, 0.24); } function stopAlertSound() { if (soundTimer !== null) { window.clearTimeout(soundTimer); soundTimer = null; } const list = activeOscillators; activeOscillators = []; for (let i = 0; i < list.length; i++) { const item = list[i]; try { try { item.gain.gain.cancelScheduledValues(0); } catch (e) {} try { item.gain.gain.setValueAtTime(0.0001, 0); } catch (e) {} try { item.osc.stop(0); } catch (e) {} } catch (e) {} } soundPlayedCount = 0; isRinging = false; soundSessionId++; } function playAlertSound() { if (!soundEnabled) return; stopAlertSound(); const ctx = ensureAudioCtx(); if (!ctx) return; isRinging = true; const mySession = soundSessionId; const doRound = function () { if (mySession !== soundSessionId) return; if (!isRinging) return; if (!soundEnabled) return; beepSequence(ctx); soundPlayedCount++; if (soundPlayedCount < soundCount) { soundTimer = window.setTimeout(function () { if (mySession !== soundSessionId) return; if (!isRinging) return; if (!soundEnabled) return; doRound(); }, BEEP_INTERVAL_MS); } else { soundTimer = null; soundPlayedCount = 0; isRinging = false; } }; if (ctx.state === 'suspended' && typeof ctx.resume === 'function') { try { const p = ctx.resume(); if (p && typeof p.then === 'function') { p.then(function () { if (mySession !== soundSessionId) return; if (!isRinging || !soundEnabled) return; doRound(); }).catch(function () { if (mySession === soundSessionId) isRinging = false; }); return; } } catch (e) {} } doRound(); } function previewBeep() { const ctx = ensureAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended' && typeof ctx.resume === 'function') { try { const p = ctx.resume(); if (p && typeof p.then === 'function') { p.then(function () { beepSequence(ctx); }).catch(function () {}); return; } } catch (e) {} } beepSequence(ctx); } function setSoundEnabled(v) { soundEnabled = !!v; saveValue(KEY_SOUND, soundEnabled ? '1' : '0'); if (!soundEnabled) stopAlertSound(); updateSoundButton(); } function setSoundCount(v) { const n = parseInt(v, 10); if (n > 0) soundCount = n; else soundCount = 3; saveValue(KEY_SOUND_COUNT, String(soundCount)); updateSoundButton(); updateSoundMenuSelection(); } function formatCountLabel() { return soundCount + '次'; } function updateSoundButton() { if (!soundBtnEl) return; if (soundEnabled) { soundBtnEl.textContent = '🔔'; soundBtnEl.classList.add('wpr-sound-on'); } else { soundBtnEl.textContent = '🔕'; soundBtnEl.classList.remove('wpr-sound-on'); } soundBtnEl.title = (soundEnabled ? '铃声已开启' : '铃声已关闭') + '(响 ' + formatCountLabel() + ')' + ' · 点击开关 · 长按设次数'; } /* ================================================================== * 八、响铃次数菜单 * ================================================================== */ function buildSoundMenu() { if (soundMenuEl) return; injectStyle(); soundMenuEl = document.createElement('div'); soundMenuEl.className = 'wpr-sound-menu'; const title = document.createElement('div'); title.className = 'wpr-sm-title'; title.textContent = '响铃次数'; soundMenuEl.appendChild(title); const opts = document.createElement('div'); opts.className = 'wpr-sm-opts'; SOUND_COUNT_OPTIONS.forEach(function (opt) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'wpr-sm-btn'; btn.dataset.count = String(opt.val); btn.textContent = opt.label; btn.addEventListener('click', function (e) { e.stopPropagation(); stopAlertSound(); setSoundCount(opt.val); setStatus('响铃次数:' + opt.label); if (!soundEnabled) setSoundEnabled(true); window.setTimeout(function () { previewBeep(); }, 60); closeSoundMenu(); }); opts.appendChild(btn); }); soundMenuEl.appendChild(opts); soundMenuEl.style.display = 'none'; soundMenuEl.addEventListener('mousedown', function (e) { e.stopPropagation(); }); soundMenuEl.addEventListener('touchstart', function (e) { e.stopPropagation(); }, { passive: true }); (document.body || document.documentElement).appendChild(soundMenuEl); updateSoundMenuSelection(); } function updateSoundMenuSelection() { if (!soundMenuEl) return; const btns = soundMenuEl.querySelectorAll('.wpr-sm-btn'); for (let i = 0; i < btns.length; i++) { const d = parseInt(btns[i].dataset.count, 10); if (d === soundCount) btns[i].classList.add('wpr-sm-on'); else btns[i].classList.remove('wpr-sm-on'); } } function openSoundMenu() { if (!soundBtnEl) return; buildSoundMenu(); updateSoundMenuSelection(); soundMenuEl.style.display = 'flex'; soundMenuEl.style.visibility = 'hidden'; soundMenuEl.style.left = '0px'; soundMenuEl.style.top = '0px'; const rect = soundBtnEl.getBoundingClientRect(); const mw = soundMenuEl.offsetWidth; const mh = soundMenuEl.offsetHeight; let left = rect.left - mw - 6; if (left < 4) left = rect.right + 6; if (left + mw > window.innerWidth - 4) left = window.innerWidth - mw - 4; if (left < 4) left = 4; let top = rect.top - mh - 6; if (top < 4) top = rect.bottom + 6; if (top + mh > window.innerHeight - 4) top = window.innerHeight - mh - 4; if (top < 4) top = 4; soundMenuEl.style.left = left + 'px'; soundMenuEl.style.top = top + 'px'; soundMenuEl.style.visibility = ''; window.setTimeout(function () { document.addEventListener('click', onDocClickCloseMenu, true); document.addEventListener('touchstart', onDocClickCloseMenu, true); }, 0); } function closeSoundMenu() { if (!soundMenuEl) return; soundMenuEl.style.display = 'none'; document.removeEventListener('click', onDocClickCloseMenu, true); document.removeEventListener('touchstart', onDocClickCloseMenu, true); } function onDocClickCloseMenu(e) { if (!soundMenuEl) return; if (soundMenuEl.contains(e.target)) return; if (soundBtnEl && soundBtnEl.contains(e.target)) return; closeSoundMenu(); } /* ================================================================== * 九、帮助弹窗 * ================================================================== */ function buildHelpOverlay() { if (helpOverlayEl) return; injectStyle(); helpOverlayEl = document.createElement('div'); helpOverlayEl.className = 'wpr-help-mask'; helpOverlayEl.style.display = 'none'; helpOverlayEl.innerHTML = [ '
', '

网页定时刷新器 · 使用说明

', '

📌 三种任务类型

', ' ', '

🔴 多词同时命中

', ' ', '

👁 命中规则

', ' ', '

⚡ 间隔刷新给定时任务让路

', ' ', '

🔘 面板按钮说明

', ' ', '

🏷 胶囊操作

', ' ', '

🔊 铃声说明

', ' ', '

🖐 拖动面板

', ' ', '

✅ 可以做到

', ' ', '

⚠️ 不能做到

', ' ', '

💡 建议使用方式

', ' ', ' ', '
' ].join(''); helpOverlayEl.addEventListener('click', function (e) { if (e.target === helpOverlayEl) closeHelp(); }); helpOverlayEl.querySelector('.wpr-close-btn') .addEventListener('click', function () { closeHelp(); }); helpOverlayEl.addEventListener('touchstart', function (e) { if (e.target === helpOverlayEl) e.preventDefault(); }, { passive: false }); (document.body || document.documentElement).appendChild(helpOverlayEl); } function openHelp() { if (isRinging) stopAlertSound(); buildHelpOverlay(); helpOverlayEl.style.display = 'flex'; } function closeHelp() { if (!helpOverlayEl) return; helpOverlayEl.style.display = 'none'; } function isHelpOpen() { return !!(helpOverlayEl && helpOverlayEl.style.display !== 'none'); } /* ================================================================== * 十、面板构建 * ================================================================== */ function buildPanel() { if (panelEl) return; injectStyle(); panelEl = document.createElement('div'); panelEl.id = 'wpr-panel'; panelEl.innerHTML = [ '
', ' ', ' ', ' ', ' 网页定时刷新器', ' ', '
', '
', ' ', ' ', ' ', ' ', '
', '
', ' ', ' ', '
', '
', ' ', ' :', ' ', ' :', ' ', ' ', '
', '
', ' ', ' ', ' ', '
', '
', '
', '
', ' ', ' ', '
已刷新 0 次
', '
' ].join(''); (document.body || document.documentElement).appendChild(panelEl); chipListEl = panelEl.querySelector('.wpr-chip-list'); statusAreaEl = panelEl.querySelector('.wpr-status-area'); countEl = panelEl.querySelector('.wpr-count'); soundBtnEl = panelEl.querySelector('.wpr-sound'); helpBtnEl = panelEl.querySelector('.wpr-help'); inputRow = panelEl.querySelector('.wpr-input-row'); inputEl = panelEl.querySelector('.wpr-input'); timerInputRow = panelEl.querySelector('.wpr-timer-input-row'); timerHH = panelEl.querySelector('.wpr-hh'); timerMM = panelEl.querySelector('.wpr-mm'); timerSS = panelEl.querySelector('.wpr-ss'); monitorInputRow = panelEl.querySelector('.wpr-monitor-input-row'); monitorInputEl = panelEl.querySelector('.wpr-monitor-word'); panelEl.querySelector('.wpr-hide') .addEventListener('click', function () { setUiMode('hidden'); }); panelEl.querySelector('.wpr-float') .addEventListener('click', function () { setUiMode('mini'); }); panelEl.querySelector('.wpr-stop') .addEventListener('click', function () { stopAlertSound(); stopAllTasks('已停止全部任务'); }); panelEl.querySelector('.wpr-close') .addEventListener('click', function () { stopAlertSound(); stopAllTasks(); setUiMode('hidden'); }); panelEl.querySelector('.wpr-custom') .addEventListener('click', onCustomClick); panelEl.querySelector('.wpr-timer') .addEventListener('click', onTimerClick); panelEl.querySelector('.wpr-monitor') .addEventListener('click', onMonitorClick); panelEl.querySelector('.wpr-del') .addEventListener('click', onDeleteClick); panelEl.querySelector('.wpr-ok') .addEventListener('click', onConfirmClick); panelEl.querySelector('.wpr-timer-add') .addEventListener('click', onTimerAddClick); panelEl.querySelector('.wpr-mon-stop') .addEventListener('click', function () { onMonitorAddClick('stop'); }); panelEl.querySelector('.wpr-mon-click') .addEventListener('click', function () { onMonitorAddClick('click'); }); helpBtnEl.addEventListener('click', function (e) { e.stopPropagation(); openHelp(); }); soundBtnEl.addEventListener('click', function (e) { e.stopPropagation(); if (soundLongPressed) { e.preventDefault(); return; } const next = !soundEnabled; setSoundEnabled(next); if (next) { const ctx = ensureAudioCtx(); if (ctx) { try { if (ctx.state === 'suspended' && typeof ctx.resume === 'function') { const p = ctx.resume(); if (p && typeof p.then === 'function') p.catch(function () {}); } } catch (err) {} } previewBeep(); setStatus('铃声已开启(响 ' + formatCountLabel() + ')'); } else { setStatus('铃声已关闭'); } }); bindSoundButtonLongPress(); inputEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); onConfirmClick(); } }); monitorInputEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); onMonitorAddClick('stop'); } }); timerHH.addEventListener('input', function () { distributeTime(timerHH, timerMM, timerSS); }); timerMM.addEventListener('input', function () { distributeTime(timerMM, timerSS, null); }); timerSS.addEventListener('input', function () { let v = timerSS.value.replace(/\D/g, ''); if (v.length > 2) v = v.slice(0, 2); timerSS.value = v; }); panelEl.addEventListener('click', function (e) { if (suppressClick) { e.stopPropagation(); e.preventDefault(); suppressClick = false; } }, true); panelEl.addEventListener('touchstart', onPanelTouchStart, { passive: true }); panelEl.addEventListener('touchmove', onPanelTouchMove, { passive: false }); panelEl.addEventListener('touchend', onPanelTouchEnd); panelEl.addEventListener('touchcancel', onPanelTouchEnd); panelEl.addEventListener('mousedown', function (e) { e.stopPropagation(); }); panelEl.addEventListener('touchstart', function () { if (isRinging) stopAlertSound(); }, { passive: true, capture: true }); panelEl.addEventListener('mousedown', function () { if (isRinging) stopAlertSound(); }, true); } function bindSoundButtonLongPress() { if (!soundBtnEl) return; let pressTimer = null; let sx = 0, sy = 0; function start(clientX, clientY) { sx = clientX; sy = clientY; soundLongPressed = false; if (pressTimer !== null) window.clearTimeout(pressTimer); pressTimer = window.setTimeout(function () { pressTimer = null; soundLongPressed = true; try { if (navigator.vibrate) navigator.vibrate(20); } catch (err) {} openSoundMenu(); }, LONG_PRESS_MS); } function cancel() { if (pressTimer !== null) { window.clearTimeout(pressTimer); pressTimer = null; } } function moveCheck(clientX, clientY) { if (pressTimer === null) return; if (Math.abs(clientX - sx) > MOVE_CANCEL_PX || Math.abs(clientY - sy) > MOVE_CANCEL_PX) { cancel(); } } soundBtnEl.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; const t = e.touches[0]; start(t.clientX, t.clientY); }, { passive: true }); soundBtnEl.addEventListener('touchmove', function (e) { if (e.touches.length !== 1) return; const t = e.touches[0]; moveCheck(t.clientX, t.clientY); }, { passive: true }); soundBtnEl.addEventListener('touchend', function (e) { cancel(); if (soundLongPressed) { e.preventDefault(); e.stopPropagation(); window.setTimeout(function () { soundLongPressed = false; }, 400); } }); soundBtnEl.addEventListener('touchcancel', cancel); soundBtnEl.addEventListener('mousedown', function (e) { if (e.button !== 0) return; start(e.clientX, e.clientY); }); soundBtnEl.addEventListener('mousemove', function (e) { moveCheck(e.clientX, e.clientY); }); soundBtnEl.addEventListener('mouseup', cancel); soundBtnEl.addEventListener('mouseleave', cancel); } function buildDot() { if (dotEl) return; injectStyle(); dotEl = document.createElement('div'); dotEl.id = 'wpr-dot'; dotEl.textContent = '刷'; dotEl.title = '点击展开定时刷新面板'; dotEl.addEventListener('click', function (e) { e.stopPropagation(); setUiMode('full'); }); dotEl.addEventListener('touchstart', function (e) { e.stopPropagation(); }, { passive: true }); dotEl.addEventListener('mousedown', function (e) { e.stopPropagation(); }); (document.body || document.documentElement).appendChild(dotEl); } /* ================================================================== * 十一、时间输入分发 * ================================================================== */ function distributeTime(cur, next, nextNext) { let v = cur.value.replace(/\D/g, ''); if (v.length <= 2) { cur.value = v; if (v.length === 2 && next) { try { next.focus(); } catch (e) {} } return; } const rest = v.slice(2); cur.value = v.slice(0, 2); if (next) { let nv = (next.value + rest).replace(/\D/g, ''); if (nv.length > 2) { const rest2 = nv.slice(2); next.value = nv.slice(0, 2); if (nextNext) { let nv2 = (nextNext.value + rest2).replace(/\D/g, ''); if (nv2.length > 2) nv2 = nv2.slice(0, 2); nextNext.value = nv2; } } else { next.value = nv; } try { next.focus(); } catch (e) {} } } /* ================================================================== * 十二、面板长按拖动 * ================================================================== */ function isInteractiveTarget(el) { if (!el) return false; const tag = el.tagName; return tag === 'BUTTON' || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'; } function onPanelTouchStart(e) { if (e.touches.length !== 1) return; if (isInteractiveTarget(e.target)) return; const t = e.touches[0]; const rect = panelEl.getBoundingClientRect(); dragStartX = t.clientX; dragStartY = t.clientY; dragStartLeft = rect.left; dragStartTop = rect.top; dragging = false; dragMoved = false; dragPressTimer = window.setTimeout(function () { dragging = true; panelEl.classList.add('wpr-dragging'); try { if (navigator.vibrate) navigator.vibrate(25); } catch (err) {} }, LONG_PRESS_MS); } function onPanelTouchMove(e) { if (e.touches.length !== 1) return; const t = e.touches[0]; if (!dragging) { const dx = Math.abs(t.clientX - dragStartX); const dy = Math.abs(t.clientY - dragStartY); if (dx > MOVE_CANCEL_PX || dy > MOVE_CANCEL_PX) { if (dragPressTimer !== null) { window.clearTimeout(dragPressTimer); dragPressTimer = null; } } return; } e.preventDefault(); let newLeft = dragStartLeft + (t.clientX - dragStartX); let newTop = dragStartTop + (t.clientY - dragStartY); const w = panelEl.offsetWidth; const h = panelEl.offsetHeight; const maxLeft = Math.max(0, window.innerWidth - w); const maxTop = Math.max(0, window.innerHeight - h); newLeft = Math.max(0, Math.min(maxLeft, newLeft)); newTop = Math.max(0, Math.min(maxTop, newTop)); panelEl.style.left = newLeft + 'px'; panelEl.style.top = newTop + 'px'; panelEl.style.right = 'auto'; dragMoved = true; } function onPanelTouchEnd() { if (dragPressTimer !== null) { window.clearTimeout(dragPressTimer); dragPressTimer = null; } if (dragging) { dragging = false; panelEl.classList.remove('wpr-dragging'); if (dragMoved) { savePanelPos(); suppressClick = true; window.setTimeout(function () { suppressClick = false; }, 120); } } } function savePanelPos() { if (!panelEl) return; const rect = panelEl.getBoundingClientRect(); saveJSON(KEY_PANEL_POS, { left: Math.round(rect.left), top: Math.round(rect.top) }); } function applyPanelPos() { if (!panelEl) return; const pos = loadJSON(KEY_PANEL_POS, null); if (!pos || !isFinite(pos.left) || !isFinite(pos.top)) return; const w = panelEl.offsetWidth || 236; const h = panelEl.offsetHeight || 150; const maxLeft = Math.max(0, window.innerWidth - w); const maxTop = Math.max(0, window.innerHeight - h); const left = Math.max(0, Math.min(maxLeft, pos.left)); const top = Math.max(0, Math.min(maxTop, pos.top)); panelEl.style.left = left + 'px'; panelEl.style.top = top + 'px'; panelEl.style.right = 'auto'; } function clampPanelPos() { if (!panelEl || panelEl.style.display === 'none') return; const rect = panelEl.getBoundingClientRect(); const maxLeft = Math.max(0, window.innerWidth - rect.width); const maxTop = Math.max(0, window.innerHeight - rect.height); let left = rect.left, top = rect.top, changed = false; if (left < 0) { left = 0; changed = true; } if (top < 0) { top = 0; changed = true; } if (left > maxLeft) { left = maxLeft; changed = true; } if (top > maxTop) { top = maxTop; changed = true; } if (changed) { panelEl.style.left = left + 'px'; panelEl.style.top = top + 'px'; panelEl.style.right = 'auto'; savePanelPos(); } } /* ================================================================== * 十三、界面状态 * ================================================================== */ function setUiMode(mode) { saveValue(KEY_UI_MODE, mode); if (mode === 'full') { if (!panelEl) buildPanel(); panelEl.style.display = 'block'; if (dotEl) dotEl.style.display = 'none'; window.setTimeout(clampPanelPos, 0); updateStatusDisplay(); } else if (mode === 'mini') { if (panelEl) panelEl.style.display = 'none'; if (!dotEl) buildDot(); dotEl.style.display = 'flex'; updateStatusDisplay(); } else { if (panelEl) panelEl.style.display = 'none'; if (dotEl) dotEl.style.display = 'none'; } if (mode !== 'full') closeSoundMenu(); } /* ================================================================== * 十四、按钮逻辑 * ================================================================== */ function onCustomClick() { const isHidden = inputRow.classList.contains('wpr-hidden'); if (isHidden) { timerInputRow.classList.add('wpr-hidden'); monitorInputRow.classList.add('wpr-hidden'); inputRow.classList.remove('wpr-hidden'); window.setTimeout(function () { try { inputEl.focus(); } catch (e) {} }, 60); } else { inputRow.classList.add('wpr-hidden'); } } function onTimerClick() { const isHidden = timerInputRow.classList.contains('wpr-hidden'); if (isHidden) { inputRow.classList.add('wpr-hidden'); monitorInputRow.classList.add('wpr-hidden'); timerInputRow.classList.remove('wpr-hidden'); window.setTimeout(function () { try { timerHH.focus(); } catch (e) {} }, 60); } else { timerInputRow.classList.add('wpr-hidden'); } } function onMonitorClick() { const isHidden = monitorInputRow.classList.contains('wpr-hidden'); if (isHidden) { inputRow.classList.add('wpr-hidden'); timerInputRow.classList.add('wpr-hidden'); monitorInputRow.classList.remove('wpr-hidden'); window.setTimeout(function () { try { monitorInputEl.focus(); } catch (e) {} }, 60); } else { monitorInputRow.classList.add('wpr-hidden'); } } /* ★ 点击某条红条:循环定位该词的元素 */ function onStatusItemClick(word) { if (!word) return; stopAlertSound(); const st = wordStates[word]; if (!st) return; /* 重新验证元素是否还在 */ const els = findAllKeywordElements(word); if (els.length === 0) { delete wordStates[word]; if (Object.keys(wordStates).length === 0) stopAlertSound(); updateStatusDisplay(); return; } st.elements = els; if (st.matchIndex < 0 || st.matchIndex >= els.length) { st.matchIndex = 0; } else { st.matchIndex = (st.matchIndex + 1) % els.length; } const el = els[st.matchIndex]; if (el) scrollAndHighlight(el); updateStatusDisplay(); } function onDeleteClick() { if (!selectedChip) { setStatus('请先长按一个胶囊选中'); return; } if (selectedChip.type === 'interval') { const sec = selectedChip.key; intervals = intervals.filter(function (x) { return x.sec !== sec; }); setStatus('已删除 ' + formatSec(sec)); } else if (selectedChip.type === 'timer') { const hms = selectedChip.key; timers = timers.filter(function (x) { return x.hms !== hms; }); const fired = loadJSON(KEY_TIMER_FIRED, {}); delete fired[hms]; saveJSON(KEY_TIMER_FIRED, fired); setStatus('已删除定时 ' + hms); } else if (selectedChip.type === 'monitor') { const id = selectedChip.key; const item = monitors.find(function (x) { return x.id === id; }); monitors = monitors.filter(function (x) { return x.id !== id; }); if (item) { if (wordStates[item.word]) { delete wordStates[item.word]; lastHitWords = lastHitWords.filter(function (w) { return w !== item.word; }); if (Object.keys(wordStates).length === 0) stopAlertSound(); } setStatus('已删除监控「' + item.word + '」'); } } selectedChip = null; saveState(); renderChipList(); updateStatusDisplay(); } function onConfirmClick() { const raw = (inputEl.value || '').trim(); if (raw === '') { setStatus('请先输入秒数'); return; } const num = parseInt(raw, 10); if (!isFinite(num) || num <= 0) { setStatus('请输入大于 0 的整数'); return; } if (num > 86400) { setStatus('时间不能超过 86400 秒'); return; } const exists = intervals.some(function (x) { return x.sec === num; }); if (!exists) { intervals.push({ sec: num, enabled: true, remain: num }); intervals.sort(function (a, b) { return a.sec - b.sec; }); } else { intervals.forEach(function (x) { if (x.sec === num) { x.enabled = true; x.remain = num; } }); } intervals.forEach(function (x) { if (x.sec !== num) { x.enabled = false; x.remain = x.sec; } }); selectedChip = { type: 'interval', key: num }; inputEl.value = ''; inputRow.classList.add('wpr-hidden'); saveState(); renderChipList(); updateStatusDisplay(); } function onTimerAddClick() { const h = parseInt(timerHH.value || '0', 10); const m = parseInt(timerMM.value || '0', 10); const s = parseInt(timerSS.value || '0', 10); if (isNaN(h) || isNaN(m) || isNaN(s)) { setStatus('请输入有效时间'); return; } if (h > 23 || m > 59 || s > 59) { setStatus('范围 0-23 : 0-59 : 0-59'); return; } if (h === 0 && m === 0 && s === 0) { setStatus('时间不能为 0'); return; } const hms = pad2(h) + ':' + pad2(m) + ':' + pad2(s); const exists = timers.some(function (x) { return x.hms === hms; }); if (!exists) { timers.push({ hms: hms, enabled: true }); timers.sort(function (a, b) { return a.hms.localeCompare(b.hms); }); } else { timers.forEach(function (x) { if (x.hms === hms) x.enabled = true; }); } selectedChip = { type: 'timer', key: hms }; timerHH.value = ''; timerMM.value = ''; timerSS.value = ''; timerInputRow.classList.add('wpr-hidden'); saveState(); renderChipList(); updateStatusDisplay(); } function onMonitorAddClick(action) { const word = (monitorInputEl.value || '').trim(); if (word === '') { setStatus('请输入监控关键词'); return; } const id = makeMonitorId(action, word); const exists = monitors.some(function (x) { return x.id === id; }); if (exists) { setStatus('已存在「' + (action === 'click' ? '点·' : '停·') + word + '」'); return; } monitors.push({ id: id, word: word, action: action, enabled: true }); /* 清除该词的历史命中记录,让下次扫描重新判定 */ if (wordStates[word]) { delete wordStates[word]; } lastHitWords = lastHitWords.filter(function (w) { return w !== word; }); selectedChip = { type: 'monitor', key: id }; monitorInputEl.value = ''; monitorInputRow.classList.add('wpr-hidden'); saveState(); renderChipList(); updateStatusDisplay(); window.setTimeout(function () { const found = scanMonitors(); if (!found) setStatus('已添加监控「' + word + '」,等待出现'); }, 30); } /* ================================================================== * 十五、工具函数 * ================================================================== */ function pad2(n) { return n < 10 ? '0' + n : String(n); } function formatSec(sec) { if (sec < 60) return sec + '秒'; const m = Math.floor(sec / 60); const s = sec % 60; if (s === 0) return m + '分钟'; return m + '分' + s + '秒'; } function secsUntilHMS(hms) { const parts = hms.split(':'); const targetSec = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseInt(parts[2], 10); const now = new Date(); const nowSec = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); let diff = targetSec - nowSec; if (diff <= 0) diff += 86400; return diff; } function secsUntilNearestTimer() { let min = Infinity; timers.forEach(function (x) { if (!x.enabled) return; const d = secsUntilHMS(x.hms); if (d < min) min = d; }); return min === Infinity ? null : min; } /* ================================================================== * 十六、胶囊长按与点击事件绑定 * ================================================================== */ function bindChipEvents(chipEl, onTap, onLongPress) { let pressTimer = null; let longPressed = false; let startX = 0, startY = 0; chipEl.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; const t = e.touches[0]; startX = t.clientX; startY = t.clientY; longPressed = false; pressTimer = window.setTimeout(function () { pressTimer = null; longPressed = true; try { if (navigator.vibrate) navigator.vibrate(20); } catch (err) {} onLongPress(); }, LONG_PRESS_MS); }, { passive: true }); chipEl.addEventListener('touchmove', function (e) { if (pressTimer === null) return; const t = e.touches[0]; const dx = Math.abs(t.clientX - startX); const dy = Math.abs(t.clientY - startY); if (dx > MOVE_CANCEL_PX || dy > MOVE_CANCEL_PX) { window.clearTimeout(pressTimer); pressTimer = null; } }, { passive: true }); chipEl.addEventListener('touchend', function (e) { if (pressTimer !== null) { window.clearTimeout(pressTimer); pressTimer = null; } if (longPressed) { e.preventDefault(); e.stopPropagation(); window.setTimeout(function () { longPressed = false; }, 300); } }); chipEl.addEventListener('click', function (e) { if (longPressed) { e.preventDefault(); e.stopPropagation(); return; } onTap(); }); } /* ================================================================== * 十七、胶囊列表渲染 * ================================================================== */ function renderChipList() { if (!chipListEl) return; if (intervals.length === 0 && timers.length === 0 && monitors.length === 0) { chipListEl.innerHTML = '暂无项目'; return; } chipListEl.innerHTML = ''; /* ---- 间隔胶囊(互斥单选)---- */ intervals.forEach(function (item) { const isOn = item.enabled; const isSel = selectedChip && selectedChip.type === 'interval' && selectedChip.key === item.sec; const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'wpr-chip' + (isOn ? ' wpr-on' : '') + (isSel ? ' wpr-selected' : ''); chip.textContent = formatSec(item.sec); chip.dataset.type = 'interval'; chip.dataset.key = String(item.sec); bindChipEvents(chip, function () { const wasOn = item.enabled; intervals.forEach(function (x) { x.enabled = false; x.remain = x.sec; }); if (!wasOn) { item.enabled = true; item.remain = item.sec; } selectedChip = { type: 'interval', key: item.sec }; saveState(); renderChipList(); updateStatusDisplay(); }, function () { selectedChip = { type: 'interval', key: item.sec }; updateChipSelectionClass(); setStatus('已选中 ' + formatSec(item.sec) + ',点删除移除'); } ); chipListEl.appendChild(chip); }); /* ---- 定时胶囊(可多选)---- */ timers.forEach(function (item) { const isOn = item.enabled; const isSel = selectedChip && selectedChip.type === 'timer' && selectedChip.key === item.hms; const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'wpr-chip' + (isOn ? ' wpr-on' : '') + (isSel ? ' wpr-selected' : ''); chip.textContent = item.hms; chip.dataset.type = 'timer'; chip.dataset.key = item.hms; bindChipEvents(chip, function () { item.enabled = !item.enabled; selectedChip = { type: 'timer', key: item.hms }; saveState(); renderChipList(); updateStatusDisplay(); }, function () { selectedChip = { type: 'timer', key: item.hms }; updateChipSelectionClass(); setStatus('已选中定时 ' + item.hms + ',点删除移除'); } ); chipListEl.appendChild(chip); }); /* ---- 监控胶囊(可多选)---- */ monitors.forEach(function (item) { const isOn = item.enabled; const isSel = selectedChip && selectedChip.type === 'monitor' && selectedChip.key === item.id; const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'wpr-chip' + (isOn ? ' wpr-on' : '') + (isSel ? ' wpr-selected' : ''); chip.textContent = (item.action === 'click' ? '点·' : '停·') + item.word; chip.title = item.action === 'click' ? '命中后停止刷新并点击' : '命中后停止全部刷新'; chip.dataset.type = 'monitor'; chip.dataset.key = item.id; bindChipEvents(chip, function () { if (isRinging) stopAlertSound(); item.enabled = !item.enabled; /* 清该词的历史命中记录 */ delete wordStates[item.word]; lastHitWords = lastHitWords.filter(function (w) { return w !== item.word; }); if (Object.keys(wordStates).length === 0) stopAlertSound(); selectedChip = { type: 'monitor', key: item.id }; saveState(); renderChipList(); updateStatusDisplay(); if (item.enabled) { window.setTimeout(function () { const found = scanMonitors(); if (!found) setStatus('已启用监控「' + item.word + '」,等待出现'); }, 30); } }, function () { selectedChip = { type: 'monitor', key: item.id }; updateChipSelectionClass(); setStatus('已选中监控「' + item.word + '」,点删除移除'); } ); chipListEl.appendChild(chip); }); } function updateChipSelectionClass() { if (!chipListEl) return; const chips = chipListEl.querySelectorAll('.wpr-chip'); for (let i = 0; i < chips.length; i++) { const chip = chips[i]; const type = chip.dataset.type; const key = chip.dataset.key; const isSel = selectedChip && selectedChip.type === type && String(selectedChip.key) === key; if (isSel) chip.classList.add('wpr-selected'); else chip.classList.remove('wpr-selected'); } } /* ================================================================== * 十八、状态显示 * ================================================================== */ function calcNearestSec() { let min = Infinity; intervals.forEach(function (x) { if (x.enabled) min = Math.min(min, x.remain); }); timers.forEach(function (x) { if (x.enabled) min = Math.min(min, secsUntilHMS(x.hms)); }); return min === Infinity ? null : min; } /* ★ 更新状态栏:如果 wordStates 非空,就渲染多条红条 * 否则显示普通蓝 / 黄状态 */ function updateStatusDisplay() { if (!statusAreaEl) return; const words = Object.keys(wordStates); /* 有命中 → 渲染多条红条 */ if (words.length > 0) { statusAreaEl.innerHTML = ''; words.forEach(function (word) { const st = wordStates[word]; const item = document.createElement('div'); item.className = 'wpr-status-item'; item.dataset.word = word; let label = '命中「' + word + '」×' + st.elements.length; if (st.matchIndex >= 0 && st.elements.length > 0) { label += ' · ' + (st.matchIndex + 1) + '/' + st.elements.length + ' · 再点切换'; } else { label += ' · 点此定位'; } item.textContent = label; item.addEventListener('click', function () { onStatusItemClick(word); }); statusAreaEl.appendChild(item); }); if (dotEl) { dotEl.classList.add('wpr-dot-alert'); dotEl.textContent = '!'; } return; } /* 无命中 → 显示普通状态 */ statusAreaEl.innerHTML = ''; const nearTimer = secsUntilNearestTimer(); const hasInterval = intervals.some(function (x) { return x.enabled; }); if (nearTimer !== null && nearTimer <= TIMER_STOP_WINDOW_SEC && !hasInterval) { const el = document.createElement('div'); el.className = 'wpr-status wpr-paused'; el.textContent = '定时 ' + nearTimer + ' 秒后触发 · 间隔已停'; statusAreaEl.appendChild(el); if (dotEl) { dotEl.classList.remove('wpr-dot-alert'); dotEl.textContent = nearTimer > 99 ? '99+' : String(nearTimer); } return; } const sec = calcNearestSec(); const hasMonitor = monitors.some(function (x) { return x.enabled; }); const el = document.createElement('div'); el.className = 'wpr-status'; if (sec === null) { el.textContent = hasMonitor ? '监控中 · 等待命中' : '未启用刷新 · 长按拖动'; if (dotEl) { dotEl.classList.remove('wpr-dot-alert'); dotEl.textContent = '刷'; } } else { el.textContent = '将在 ' + sec + ' 秒后刷新'; if (dotEl) dotEl.textContent = sec > 99 ? '99+' : String(sec); } statusAreaEl.appendChild(el); } /* 短时提示(会被下次 tick 覆盖) */ function setStatus(text) { if (!statusAreaEl) return; statusAreaEl.innerHTML = ''; const el = document.createElement('div'); el.className = 'wpr-status'; el.textContent = text; statusAreaEl.appendChild(el); } function clearAlert() { wordStates = {}; lastHitWords = []; stopAlertSound(); updateStatusDisplay(); } /* ================================================================== * 十九、停止全部任务 * ================================================================== */ function stopAllTasks(msg) { stopAlertSound(); intervals.forEach(function (x) { x.enabled = false; x.remain = x.sec; }); timers.forEach(function (x) { x.enabled = false; }); monitors.forEach(function (x) { x.enabled = false; }); selectedChip = null; wordStates = {}; lastHitWords = []; saveState(); renderChipList(); updateStatusDisplay(); if (msg) setStatus(msg); } /* ================================================================== * 二十、关键词监控核心 * ================================================================== */ /* ★ 判断元素是否真正可见 */ function isElementVisible(el) { if (!el || !el.isConnected) return false; let cur = el; let depth = 0; while (cur && cur !== document.body && depth < 30) { try { const style = window.getComputedStyle(cur); if (style.display === 'none') return false; if (style.visibility === 'hidden') return false; if (style.opacity === '0') return false; } catch (e) {} cur = cur.parentElement; depth++; } try { const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; } catch (e) { return false; } return true; } /* ★ 扫描关键词元素:用 textContent 匹配,支持被拆分的文字 */ function findAllKeywordElements(word) { const result = []; if (!word || !document.body) return result; try { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_ELEMENT, { acceptNode: function (el) { const tag = el.tagName; if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') { return NodeFilter.FILTER_REJECT; } if (tag === 'HTML' || tag === 'BODY') { return NodeFilter.FILTER_SKIP; } if (panelEl && panelEl.contains(el)) { return NodeFilter.FILTER_REJECT; } if (helpOverlayEl && helpOverlayEl.contains(el)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false ); let el; while ((el = walker.nextNode())) { const txt = el.textContent || ''; if (txt.indexOf(word) === -1) continue; if (!isElementVisible(el)) continue; /* 只保留最深的匹配元素 */ let hasDeeperMatch = false; for (let j = 0; j < el.children.length; j++) { const child = el.children[j]; const ct = child.tagName; if (ct === 'SCRIPT' || ct === 'STYLE' || ct === 'NOSCRIPT') continue; if (child.textContent && child.textContent.indexOf(word) !== -1) { hasDeeperMatch = true; break; } } if (!hasDeeperMatch) { result.push(el); } } } catch (e) {} return result; } /* ★ 核心:扫描当前页面并更新多词状态 * - 每个命中的词在 wordStates 里维护独立状态 * - 新出现的词 → 触发响铃 + 停间隔刷新 * - 消失的词 → 从 wordStates 移除 * - 一直存在的词 → 只更新元素列表,不重复响铃 */ function scanMonitors() { if (refreshPending) return false; const enabled = monitors.filter(function (x) { return x.enabled; }); /* 没有启用的监控 → 清空所有状态 */ if (enabled.length === 0) { if (Object.keys(wordStates).length > 0) { wordStates = {}; lastHitWords = []; stopAlertSound(); updateStatusDisplay(); } return false; } /* 扫描当前页面 */ const currentHits = []; for (let i = 0; i < enabled.length; i++) { const m = enabled[i]; const arr = findAllKeywordElements(m.word); if (arr.length > 0) { currentHits.push({ word: m.word, action: m.action, elements: arr }); } } const currentWords = currentHits.map(function (h) { return h.word; }); /* 更新 wordStates:保留已有词的 matchIndex,移除消失的词 */ const newStates = {}; for (let i = 0; i < currentHits.length; i++) { const hit = currentHits[i]; const old = wordStates[hit.word]; let idx = old ? old.matchIndex : -1; if (idx >= hit.elements.length) idx = -1; newStates[hit.word] = { action: hit.action, elements: hit.elements, matchIndex: idx }; } wordStates = newStates; /* 新出现的词 */ const newWords = currentWords.filter(function (w) { return lastHitWords.indexOf(w) === -1; }); lastHitWords = currentWords; /* 有新词 → 停间隔刷新 + 响铃(只响一次) */ if (newWords.length > 0) { intervals.forEach(function (x) { x.enabled = false; x.remain = x.sec; }); saveState(); renderChipList(); try { if (navigator.vibrate) navigator.vibrate([50, 50, 150]); } catch (e) {} playAlertSound(); /* 第一个新词如果是"点击"类型 → 自动点击 */ const firstWord = newWords[0]; const firstState = wordStates[firstWord]; if (firstState && firstState.action === 'click') { const first = firstState.elements[0]; const clickable = first ? findClickableAncestor(first) : null; if (clickable) { window.setTimeout(function () { try { try { clickable.scrollIntoView({ block: 'center', inline: 'nearest' }); } catch (e) {} doClick(clickable); } catch (e) {} updateStatusDisplay(); }, CLICK_DELAY_MS); } } updateStatusDisplay(); return true; } /* 没有新词,但可能有词消失了 → 更新显示 */ updateStatusDisplay(); return false; } function findClickableAncestor(el) { if (!el) return null; let cur = el; let depth = 0; let fallbackByPointer = null; let fallbackByClass = null; while (cur && cur !== document.body && depth < 20) { const tag = cur.tagName; if (tag === 'A' || tag === 'BUTTON') return cur; if (tag === 'INPUT') { const t = (cur.getAttribute('type') || '').toLowerCase(); if (t === 'button' || t === 'submit' || t === 'image' || t === 'checkbox' || t === 'radio') return cur; } try { const role = cur.getAttribute && cur.getAttribute('role'); if (role) { const r = role.toLowerCase(); if (r === 'button' || r === 'link' || r === 'tab' || r === 'option' || r === 'menuitem' || r === 'menuitemcheckbox' || r === 'menuitemradio' || r === 'checkbox' || r === 'radio' || r === 'switch' || r === 'treeitem') { return cur; } } } catch (e) {} try { if (cur.hasAttribute && cur.hasAttribute('onclick')) return cur; } catch (e) {} try { if (cur.hasAttribute && cur.hasAttribute('href')) { const href = cur.getAttribute('href'); if (href && href !== '#' && href.indexOf('javascript:') !== 0) return cur; } } catch (e) {} try { if (cur.hasAttribute && cur.hasAttribute('tabindex')) { const ti = parseInt(cur.getAttribute('tabindex'), 10); if (isFinite(ti) && ti >= 0) return cur; } } catch (e) {} if (!fallbackByPointer) { try { const cursor = window.getComputedStyle(cur).cursor; if (cursor === 'pointer') fallbackByPointer = cur; } catch (e) {} } if (!fallbackByClass) { try { const cls = (cur.className && typeof cur.className === 'string') ? cur.className.toLowerCase() : ''; const id = (cur.id || '').toLowerCase(); const joined = cls + ' ' + id; if (/\b(btn|button|click|item|tab|option|menu|link|entry|row|cell|list-item|select)\b/.test(joined)) { fallbackByClass = cur; } } catch (e) {} } cur = cur.parentElement; depth++; } if (fallbackByPointer) return fallbackByPointer; if (fallbackByClass) return fallbackByClass; return el; } function doClick(el) { if (!el) return false; let anySuccess = false; try { if (typeof PointerEvent === 'function') { const pOpts = { bubbles: true, cancelable: true, view: window, button: 0, buttons: 1, pointerType: 'mouse' }; try { el.dispatchEvent(new PointerEvent('pointerdown', pOpts)); el.dispatchEvent(new PointerEvent('pointerup', pOpts)); anySuccess = true; } catch (e) {} } } catch (e) {} try { const mOpts = { bubbles: true, cancelable: true, view: window, button: 0, buttons: 1 }; el.dispatchEvent(new MouseEvent('mousedown', mOpts)); el.dispatchEvent(new MouseEvent('mouseup', mOpts)); try { if (typeof el.focus === 'function') el.focus(); } catch (e) {} el.dispatchEvent(new MouseEvent('click', mOpts)); anySuccess = true; } catch (e) {} try { if (typeof el.click === 'function') { el.click(); anySuccess = true; } } catch (e) {} if (!anySuccess) { try { const ev = document.createEvent('MouseEvents'); ev.initEvent('click', true, true); el.dispatchEvent(ev); anySuccess = true; } catch (e) {} } return anySuccess; } /* ================================================================== * 二十一、滚动定位与高亮 * ================================================================== */ function getScrollParent(el) { let cur = el ? el.parentElement : null; let depth = 0; while (cur && depth < 30) { try { const style = window.getComputedStyle(cur); const overflowY = style.overflowY; if ((overflowY === 'auto' || overflowY === 'scroll') && cur.scrollHeight > cur.clientHeight + 1) { return cur; } } catch (e) {} cur = cur.parentElement; depth++; } return document.scrollingElement || document.documentElement; } function scrollToElement(el) { if (!el) return; const rect = el.getBoundingClientRect(); const elHeight = rect.height; const scroller = getScrollParent(el); const isWindowScroll = (scroller === document.scrollingElement) || (scroller === document.documentElement) || (scroller === document.body); if (isWindowScroll) { const viewportH = window.innerHeight || document.documentElement.clientHeight; const currentScroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; const elContentTop = currentScroll + rect.top; let target = elContentTop - (viewportH - elHeight) / 2; const docHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight ); const maxScroll = Math.max(0, docHeight - viewportH); target = Math.max(0, Math.min(maxScroll, target)); try { window.scrollTo({ top: target, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, target); } } else { const scRect = scroller.getBoundingClientRect(); const scHeight = scroller.clientHeight; const elContentTop = scroller.scrollTop + (rect.top - scRect.top); let target = elContentTop - (scHeight - elHeight) / 2; const maxScroll = Math.max(0, scroller.scrollHeight - scHeight); target = Math.max(0, Math.min(maxScroll, target)); try { scroller.scrollTo({ top: target, behavior: 'smooth' }); } catch (e) { scroller.scrollTop = target; } } } function highlightElement(el) { if (!el || !el.classList) return; try { const olds = document.querySelectorAll('.wpr-hit-highlight'); for (let i = 0; i < olds.length; i++) { olds[i].classList.remove('wpr-hit-highlight'); } } catch (e) {} if (highlightTimer) { window.clearTimeout(highlightTimer); highlightTimer = null; } el.classList.add('wpr-hit-highlight'); highlightTimer = window.setTimeout(function () { try { el.classList.remove('wpr-hit-highlight'); } catch (e) {} highlightTimer = null; }, HIGHLIGHT_MS); } function scrollAndHighlight(el) { if (!el) return; scrollToElement(el); window.setTimeout(function () { highlightElement(el); }, 300); } /* ================================================================== * 二十二、每秒定时任务 * ================================================================== */ function startTick() { if (tickTimer !== null) return; tickTimer = window.setInterval(onTick, 1000); } function onTick() { if (refreshPending) return; const hit = scanMonitors(); if (hit) return; let reason = null; const now = new Date(); const nowTs = now.getTime(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); let nearTimer = null; timers.forEach(function (x) { if (!x.enabled) return; const parts = x.hms.split(':'); const targetSec = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseInt(parts[2], 10); let targetTs = todayStart + targetSec * 1000; if (nowTs >= targetTs) targetTs += 86400000; const diffSec = Math.round((targetTs - nowTs) / 1000); if (diffSec <= TIMER_STOP_WINDOW_SEC) { if (nearTimer === null || diffSec < nearTimer.diffSec) { nearTimer = { hms: x.hms, diffSec: diffSec }; } } }); if (nearTimer !== null) { let hadInterval = false; intervals.forEach(function (x) { if (x.enabled) hadInterval = true; x.enabled = false; x.remain = x.sec; }); if (hadInterval) { saveState(); renderChipList(); } } else { intervals.forEach(function (x) { if (!x.enabled) return; x.remain--; if (x.remain <= 0 && reason === null) reason = 'interval'; }); } const fired = loadJSON(KEY_TIMER_FIRED, {}); let firedChanged = false; timers.forEach(function (x) { if (!x.enabled) return; const parts = x.hms.split(':'); const targetSec = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseInt(parts[2], 10); const targetTs = todayStart + targetSec * 1000; if (nowTs < targetTs) return; if (nowTs - targetTs > TIMER_MISS_WINDOW_MS) return; if (nowTs - (fired[x.hms] || 0) < TIMER_DEDUP_MS) return; fired[x.hms] = nowTs; firedChanged = true; if (reason === null) reason = 'timer'; }); if (firedChanged) saveJSON(KEY_TIMER_FIRED, fired); updateStatusDisplay(); saveState(); if (reason !== null) { refreshPending = true; triggerRefresh(reason); } } function triggerRefresh(reason) { incRefreshCount(); if (countEl) countEl.textContent = '已刷新 ' + getRefreshCount() + ' 次'; if (dotEl) dotEl.textContent = '↻'; saveValue(KEY_LAST_REFRESH, String(Date.now())); try { sessionStorage.setItem(PAGE_ALIVE_KEY, '1'); } catch (e) {} try { window.location.reload(); } catch (e) {} } /* ================================================================== * 二十三、油猴菜单 * ================================================================== */ function registerMenu() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('打开刷新面板', function () { setUiMode('full'); }); GM_registerMenuCommand('停止全部刷新', function () { stopAllTasks('已停止全部任务'); }); } /* ================================================================== * 二十四、初始化 * ================================================================== */ function shouldResumeTasks() { try { if (sessionStorage.getItem(PAGE_ALIVE_KEY) === '1') return true; } catch (e) {} const lastTs = parseInt(loadValue(KEY_LAST_REFRESH, '0'), 10) || 0; if (lastTs > 0) { const diff = Date.now() - lastTs; if (diff > 0 && diff < SAME_SESSION_WINDOW_MS) return true; } return false; } function init() { const resume = shouldResumeTasks(); loadState(); if (!resume) { /* 会话已断 → 只清"会话性"任务(间隔、定时) * ★ 监控是用户明确开启的,永不被自动关闭 */ intervals.forEach(function (x) { x.enabled = false; x.remain = x.sec; }); timers.forEach(function (x) { x.enabled = false; }); /* 不动 monitors */ resetRefreshCount(); saveState(); saveJSON(KEY_TIMER_FIRED, {}); saveValue(KEY_LAST_REFRESH, '0'); } else { try { sessionStorage.setItem(PAGE_ALIVE_KEY, '1'); } catch (e) {} } wordStates = {}; lastHitWords = []; const uiMode = loadValue(KEY_UI_MODE, 'full'); soundEnabled = (loadValue(KEY_SOUND, '0') === '1'); const rawCount = loadValue(KEY_SOUND_COUNT, '3'); const parsedCount = parseInt(rawCount, 10); if (parsedCount > 0) soundCount = parsedCount; else soundCount = 3; buildPanel(); updateSoundButton(); renderChipList(); applyPanelPos(); if (countEl) countEl.textContent = '已刷新 ' + getRefreshCount() + ' 次'; updateStatusDisplay(); startTick(); setUiMode(uiMode); MONITOR_CHECK_DELAYS.forEach(function (delay) { window.setTimeout(function () { scanMonitors(); }, delay); }); window.addEventListener('scroll', function () { if (isRinging) stopAlertSound(); }, { passive: true }); window.addEventListener('keydown', function (e) { if (e.key === 'Escape' && isHelpOpen()) closeHelp(); }); window.addEventListener('resize', function () { clampPanelPos(); closeSoundMenu(); }); window.addEventListener('orientationchange', function () { window.setTimeout(function () { clampPanelPos(); closeSoundMenu(); }, 200); }); registerMenu(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();