// ==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 = [
'
',
'
网页定时刷新器 · 使用说明
',
'
📌 三种任务类型
',
'
',
' - 自定义(间隔):按秒循环刷新,只能同时启用一个。点一个新的会自动关掉旧的。
',
' - 定时:每天 HH:MM:SS 触发一次,可以多选,各自独立计时,互不影响。
',
' - 监控:页面出现关键词就触发,可以多选。一旦开启就不会被自动关闭,只有用户手动点才能关。
',
'
',
'
🔴 多词同时命中
',
'
',
' - 多个关键词同时出现 → 每条命中词各显示一条红条。
',
' - 每条红条可独立点击,循环定位自己那一词的元素。
',
' - 红条上显示「命中「X」×N · i/N · 点此定位」。
',
' - 某个词从页面消失 → 对应的红条自动消失。
',
'
',
'
👁 命中规则
',
'
',
' - 只扫描真正显示在屏幕上的关键词。
',
' - 网页里常见的隐藏元素(display:none、visibility:hidden、尺寸 0、opacity:0)全部忽略。
',
' - 文字被标签拆开(如 <span>抖</span><span>音</span>)也能正常识别。
',
'
',
'
⚡ 间隔刷新给定时任务让路
',
'
',
' - 当定时任务距离触发只剩 10 秒时,正在跑的间隔刷新会自动停止。
',
' - 停止后不会自动恢复,需要手动再开启。
',
'
',
'
🔘 面板按钮说明
',
'
',
' - 隐 完全隐藏面板,任务在后台继续运行。
',
' - 浮 缩成右下角小圆点,点圆点可展开。
',
' - 停 停止所有任务(间隔 / 定时 / 监控 全部关闭)。
',
' - × 停止所有任务并隐藏面板。
',
' - 自定义 输入秒数添加间隔刷新。
',
' - 定时 输入时分秒添加每日定时。
',
' - 监控 输入关键词,选「停止」或「点击」。
',
' - 删除 删除当前选中(橙色边框)的胶囊。
',
'
',
'
🏷 胶囊操作
',
'
',
' - 单击:开 / 关该任务(绿色 = 开)。
',
' - 长按:选中该胶囊(橙色边框),然后点「删除」可移除。
',
' - 间隔类互斥:点一个新的会自动关掉旧的。
',
' - 定时和监控可以多个同时开启。
',
'
',
'
🔊 铃声说明
',
'
',
' - 点击铃铛:开 / 关铃声。
',
' - 长按铃铛:弹出菜单,可设 3次 / 5次 / 8次 / 25次。
',
' - 首次开启需要点一下铃铛解锁浏览器音频权限。
',
' - 关闭铃声时,正在响的铃声会立即停止。
',
' - 滑动页面或操作面板,正在响的铃声会自动停止。
',
'
',
'
🖐 拖动面板
',
'
',
' - 长按面板空白处(约 0.4 秒)可拖动到任意位置。
',
' - 位置会记住,下次打开还在原处。
',
'
',
'
✅ 可以做到
',
'
',
' - 只要 Via 保持前台,多个标签页(含后台标签页)都能各自按设置定时刷新,互不影响。
',
' - 面板隐藏、缩成小圆点、切到其他标签页,任务都会继续。
',
' - 监控一旦开启,无论如何刷新、切换、重启都不会自动关闭。
',
'
',
'
⚠️ 不能做到
',
'
',
' - 不能锁屏:手机息屏后浏览器被系统挂起,所有任务暂停。
',
' - 不能退出 Via:完全关闭浏览器,脚本随之停止。
',
' - 不能切到其他 App:把 Via 切到后台,系统可能冻结定时器。
',
' - 省电模式有影响:部分手机「省电 / 深度睡眠」会限制后台,请把 Via 加入白名单。
',
'
',
'
💡 建议使用方式
',
'
',
' - 屏幕保持常亮(或把息屏时间调长)
',
' - 把 Via 固定在前台,不要切走
',
' - 系统设置里给 Via 开启「自启动 / 后台弹窗 / 无限制」
',
' - 如需长时间挂机,建议接上充电器
',
'
',
'
',
'
'
].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 = [
'',
' ',
' ',
' ',
' 网页定时刷新器',
' ',
'
',
'',
' ',
' ',
' ',
' ',
'
',
'',
' ',
' ',
'
',
'',
' ',
' :',
' ',
' :',
' ',
' ',
'
',
'',
' ',
' ',
' ',
'
',
'',
'',
''
].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();
}
})();