Randomized auto-refresh
Tính đến
// ==UserScript==
// @name LiveReload
// @namespace https://github.com/RustwuIf
// @version 2.2.8
// @description Randomized auto-refresh
// @author Rustwulf
// @match <all_urls>
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
function LiveReload() {
// ==================== STORAGE KEYS ====================
// Keys for persisting user preferences and statistics to localStorage
const storageKey = 'autoRefreshState';
const themeKey = 'autoRefreshTheme';
const opacityKey = 'autoRefreshOpacity';
const statsKey = 'autoRefreshStats';
const defaults = { min: 5, max: 15, opacity: 1 };
// ==================== STATE VARIABLES ====================
// Core runtime state for the auto-reload functionality
let isRunning = false;
let isPausedBySafety = false;
let isPausedByInactivity = false;
let remainingTime = 0;
let maxRemainingTime = 0;
let timerId = null;
let sessionTimerId = null;
let inactivityTimeout = null;
// ==================== CONFIGURATION ====================
// Thresholds and defaults for inactivity detection
const INACTIVITY_THRESHOLD = 60000; // 60 seconds
const originalFavicon = document.querySelector("link[rel~='icon']")?.href || '/favicon.ico';
// ==================== STATISTICS OBJECT ====================
// Tracks usage metrics and session data
let stats = {
totalReloads: 0,
totalRunningTime: 0,
waitTimes: [],
maxReloadsLimit: 0,
jitterMode: false,
sessionStartTime: null
};
// ==================== STORAGE FUNCTIONS ====================
// Load and save statistics from/to localStorage with error handling
const loadStats = () => {
try {
const stored = localStorage.getItem(statsKey);
if (stored) stats = { ...stats, ...JSON.parse(stored) };
} catch (e) {
console.error('Stats load error:', e);
}
};
const saveStats = () => {
try {
localStorage.setItem(statsKey, JSON.stringify(stats));
} catch (e) {
console.error('Stats save error:', e);
}
};
// ==================== SETTINGS FUNCTIONS ====================
// Retrieve user settings from localStorage with fallback to defaults
const getSettings = () => ({
min: parseInt(localStorage.getItem('minDelay'), 10) || defaults.min,
max: parseInt(localStorage.getItem('maxDelay'), 10) || defaults.max,
opacity: localStorage.getItem(opacityKey) || defaults.opacity
});
// ==================== FAVICON UPDATE ====================
// Updates favicon with countdown timer and status indicator
// Shows red circle when time is critical (≤3s), green when waiting, blue when paused
const updateFavicon = (number) => {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 32;
const ctx = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = "Anonymous";
img.src = originalFavicon;
img.onload = () => {
ctx.drawImage(img, 0, 0, 32, 32);
ctx.beginPath();
ctx.arc(20, 20, 12, 0, 2 * Math.PI);
ctx.fillStyle = isPausedBySafety ? '#3498db' : (number <= 3 ? '#ff5f6d' : '#2ecc71');
ctx.fill();
ctx.fillStyle = "white";
ctx.font = "bold 16px Arial";
ctx.textAlign = "center";
ctx.fillText(isPausedBySafety ? "!!" : number, 20, 26);
let link = document.querySelector("link[rel~='icon']") || (() => {
const l = document.createElement('link');
l.rel = 'icon';
document.head.appendChild(l);
return l;
})();
link.href = canvas.toDataURL('image/png');
};
};
// ==================== NOTIFICATION SYSTEM ====================
// Displays temporary toast notifications for user feedback (jitter applied, limits reached, saved settings)
const showToast = (message) => {
const toast = document.createElement('div');
toast.style.cssText = 'position: fixed; bottom: 100px; left: 20px; background: rgba(52, 152, 219, 0.95); color: white; padding: 16px 24px; border-radius: 12px; font-weight: 600; z-index: 2147483646; box-shadow: 0 8px 25px rgba(0,0,0,0.3); backdrop-filter: blur(10px); animation: slideInUp 0.3s ease-out;';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
};
// ==================== RELOAD COUNTER FUNCTIONS ====================
// Manages reload count in localStorage and stats, prevents exceeding max reload limit
const getReloadCount = () => parseInt(localStorage.getItem('reloadCounter')) || 0;
const incrementReloadCount = () => {
const newCount = getReloadCount() + 1;
localStorage.setItem('reloadCounter', newCount.toString());
stats.totalReloads = newCount;
saveStats();
return newCount;
};
const resetReloadCount = () => {
localStorage.setItem('reloadCounter', '0');
stats.totalReloads = 0;
saveStats();
};
// ==================== JITTER MODE ====================
// Adds ±10% random delay to prevent predictable reload patterns
// Returns object with newValue, jitterAmount, and applied flag
const applyJitter = (value) => {
const jitterEnabled = localStorage.getItem('jitterMode') === 'true';
if (!jitterEnabled) {
return { newValue: value, jitterAmount: 0, applied: false };
}
// Calculate 10% of the value
const jitterRange = Math.floor(value * 0.1);
// Generate random adjustment between -jitterRange and +jitterRange
const adjustment = Math.floor(Math.random() * (jitterRange * 2 + 1)) - jitterRange;
// Ensure the final value is at least 1
const newValue = Math.max(1, value + adjustment);
return { newValue: newValue, jitterAmount: adjustment, applied: true };
};
// ==================== ACTIVITY TRACKING ====================
// Monitors user activity and resumes from inactivity pause
const recordActivity = () => {
if (isPausedByInactivity) {
isPausedByInactivity = false;
updateUIState();
}
resetInactivityTimeout();
};
// ==================== INACTIVITY DETECTION ====================
// Automatically pauses reload when user hasn't interacted with page for 60 seconds
const resetInactivityTimeout = () => {
if (inactivityTimeout) clearTimeout(inactivityTimeout);
if (!isRunning || localStorage.getItem('inactivityPauseEnabled') !== 'true') return;
inactivityTimeout = setTimeout(() => {
if (isRunning && !isPausedBySafety) {
isPausedByInactivity = true;
updateUIState();
}
}, INACTIVITY_THRESHOLD);
};
// ==================== UI ELEMENTS ====================
// References to Shadow DOM elements (populated during createShadowUI)
let host, shadow, mainBtn, settingsBtn, themeBtn, settingsModal, opacitySlider;
// ==================== SHADOW DOM CREATION ====================
// Creates isolated UI with shadow DOM for styling isolation and positioning
function createShadowUI() {
host = document.createElement('div');
host.id = 'live-reload-host';
host.style.cssText = 'position: fixed; bottom: 20px; left: 20px; z-index: 2147483647; font-family: "Inter", sans-serif;';
// ========== DRAG & DROP HANDLER ==========
// Allows user to drag widget to any position on screen and persist location
let isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
const startDrag = (clientX, clientY) => {
isDragging = true;
dragOffsetX = clientX - host.offsetLeft;
dragOffsetY = clientY - host.offsetTop;
host.style.cursor = 'grabbing';
};
const moveDrag = (clientX, clientY) => {
if (!isDragging) return;
host.style.left = (clientX - dragOffsetX) + 'px';
host.style.top = (clientY - dragOffsetY) + 'px';
host.style.bottom = 'auto';
localStorage.setItem('liveReloadPos', JSON.stringify({ x: clientX - dragOffsetX, y: clientY - dragOffsetY }));
};
const endDrag = () => {
isDragging = false;
host.style.cursor = 'grab';
};
const checkIfDraggable = (path) => !path.some(el =>
el.tagName === 'INPUT' || (el.className && (el.className.includes('modal') || el.className.includes('stats-section')))
);
// Mouse and touch event listeners for dragging
host.addEventListener('mousedown', (e) => {
if (e.button !== 0 || !checkIfDraggable(e.composedPath())) return;
startDrag(e.clientX, e.clientY);
});
host.addEventListener('touchstart', (e) => {
if (!checkIfDraggable(e.composedPath())) return;
startDrag(e.touches[0].clientX, e.touches[0].clientY);
});
document.addEventListener('mousemove', (e) => moveDrag(e.clientX, e.clientY));
document.addEventListener('touchmove', (e) => moveDrag(e.touches[0].clientX, e.touches[0].clientY));
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
// Restore saved position on page load
try {
const pos = JSON.parse(localStorage.getItem('liveReloadPos'));
if (pos) {
host.style.left = pos.x + 'px';
host.style.top = pos.y + 'px';
host.style.bottom = 'auto';
}
} catch (e) {}
document.body.appendChild(host);
shadow = host.attachShadow({ mode: 'open' });
// ========== SHADOW DOM STYLES ==========
// Complete styling for all UI elements, themes, animations, and responsive design
const style = document.createElement('style');
style.textContent = `
:host { --accent-red: linear-gradient(135deg, #ff5f6d, #ffc371); --accent-green: linear-gradient(135deg, #11998e, #38ef7d); --accent-blue: #3498db; }
:host([theme="dark"]) { --bg: rgba(20,20,25,0.95); --modal-bg: rgba(15,15,20,0.98); --text: #fff; --border: rgba(255,255,255,0.08); --input-bg: rgba(255,255,255,0.05); }
:host([theme="light"]) { --bg: rgba(240,242,245,0.95); --modal-bg: rgba(255,255,255,0.98); --text: #1a1a1b; --border: rgba(0,0,0,0.08); --input-bg: rgba(0,0,0,0.03); }
.btn { cursor: pointer; color: var(--text); border-radius: 12px; padding: 11px 20px; font-weight: 600; border: 1px solid var(--border); display: flex; align-items: center; gap: 8px; backdrop-filter: blur(20px); background: var(--bg); transition: all 0.25s ease; font-size: 14px; }
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.15); border-color: rgba(255,255,255,0.15); }
.btn:active { transform: translateY(0px); }
.btn:hover .icon { animation: iconRotate 0.6s ease-in-out forwards; }
.icon { display: inline-block; font-size: 16px; }
.btn-main { background: var(--accent-red); border: none; color: white; min-width: 130px; justify-content: center; box-shadow: 0 6px 20px rgba(255, 95, 109, 0.3); position: relative; font-size: 15px; font-weight: 700; }
.btn-main::before { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 160px; height: 160px; border: 3px solid rgba(255, 255, 255, 0.2); border-radius: 50%; animation: rotatePulse 3s linear infinite; pointer-events: none; z-index: -1; }
.btn-main.active::before { border-color: rgba(255, 255, 255, 0.7); animation: rotatePulse 2s linear infinite; }
.btn-main.safety::before { border-color: rgba(52, 152, 219, 0.7); animation: rotatePulse 1.5s linear infinite; }
@keyframes rotatePulse { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } }
.btn-main.active { background: linear-gradient(135deg, #f1c40f, #f39c12); color: #000; box-shadow: 0 6px 20px rgba(241, 196, 15, 0.4); }
.btn-main.safety { background: var(--accent-blue); color: white; box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3); animation: softPulse 2s infinite; }
.modal { position: absolute; bottom: 75px; left: 0; background: var(--modal-bg); padding: 0; border-radius: 18px; border: 1px solid var(--border); box-shadow: 0 25px 60px rgba(0,0,0,0.2); color: var(--text); display: none; flex-direction: row; gap: 0; width: 370px; backdrop-filter: blur(30px); max-height: 85vh; overflow: hidden; align-items: stretch; }
.modal-wrapper { display: flex; flex-direction: column; flex: 1; height: 320px; }
.modal-content { flex: 1; display: flex; flex-direction: column; gap: 5px; overflow-y: auto; padding: 12px; padding-right: 8px; padding-bottom: 20px; scrollbar-width: none; }
.modal-content::-webkit-scrollbar { width: 0px; height: 0px; }
.modal-content::-webkit-scrollbar-track { background: transparent; }
.modal-content::-webkit-scrollbar-thumb { background: transparent; border-radius: 2px; display: none; }
.modal-content::-webkit-scrollbar-thumb:hover { background: transparent; }
.modal-footer { display: flex; flex-direction: column; gap: 8px; padding: 16px 12px 12px 12px; border-top: 1px solid var(--border); flex-shrink: 0; align-items: center; }
.modal-tabs { display: flex; flex-direction: column; gap: 4px; padding: 8px; border-left: 1px solid var(--border); flex-shrink: 0; align-items: center; justify-content: flex-start; position: sticky; top: 0; min-width: 54px; background: var(--modal-bg); z-index: 1; height: 320px; }
.tab-btn { background: transparent; border: none; color: var(--text); width: 38px; height: 38px; border-radius: 8px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); opacity: 0.5; flex-shrink: 0; }
.tab-btn:hover { opacity: 0.75; }
.tab-btn:active { opacity: 0.9; }
.tab-btn.active { background: rgba(17, 153, 142, 0.3); color: #11998e; opacity: 1; box-shadow: inset 0 0 0 1px rgba(17, 153, 142, 0.4), 0 0 12px rgba(17, 153, 142, 0.3); animation: tabGlow 0.4s ease-out; }
@keyframes tabGlow { 0% { box-shadow: inset 0 0 0 1px rgba(17, 153, 142, 0.4), 0 0 6px rgba(17, 153, 142, 0.1); } 50% { box-shadow: inset 0 0 0 1px rgba(17, 153, 142, 0.4), 0 0 18px rgba(17, 153, 142, 0.4); } 100% { box-shadow: inset 0 0 0 1px rgba(17, 153, 142, 0.4), 0 0 12px rgba(17, 153, 142, 0.3); } }
.tab-content { display: none; opacity: 0; transform: translateY(-8px); transition: opacity 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); pointer-events: none; }
.tab-content.active { display: flex; flex-direction: column; gap: 5px; opacity: 1; transform: translateY(0); pointer-events: auto; animation: slideInContent 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes slideInContent { 0% { opacity: 0; transform: translateY(-8px); } 100% { opacity: 1; transform: translateY(0); } }
.modal::-webkit-scrollbar { width: 6px; }
.modal::-webkit-scrollbar-track { background: transparent; }
.modal::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
.modal::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
.modal, .modal * { cursor: auto !important; }
.modal button { cursor: pointer !important; }
.modal input { cursor: pointer !important; }
input[type="range"] { width: 100%; height: 6px; border-radius: 5px; background: var(--input-bg); outline: none; -webkit-appearance: none; appearance: none; accent-color: #11998e; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, #11998e, #38ef7d); cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); transition: all 0.2s ease; }
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); box-shadow: 0 4px 12px rgba(17,153,142,0.4); }
input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, #11998e, #38ef7d); cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); border: none; transition: all 0.2s ease; }
input[type="range"]::-moz-range-thumb:hover { transform: scale(1.15); box-shadow: 0 4px 12px rgba(17,153,142,0.4); }
input[type="number"] { background: var(--input-bg); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 8px 12px; font-size: 13px; transition: all 0.2s ease; font-family: inherit; }
input[type="number"]:focus { border-color: #11998e; background: var(--input-bg); box-shadow: 0 0 0 2px rgba(17,153,142,0.1); }
input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: #11998e; }
.preset-btn { background: rgba(17, 153, 142, 0.15); border: 1px solid rgba(17, 153, 142, 0.3); color: var(--text); padding: 7px 10px; border-radius: 8px; font-weight: 600; font-size: 11px; cursor: pointer; transition: all 0.2s ease; font-family: inherit; }
.preset-btn:hover { background: rgba(17, 153, 142, 0.25); border-color: rgba(17, 153, 142, 0.5); transform: translateY(-1px); }
.preset-btn:active { transform: translateY(0); }
.btn-save { background: var(--accent-green); border: none; color: white; padding: 10px 40px; border-radius: 10px; font-weight: 700; margin: 8px auto 0 auto; display: block; cursor: pointer; font-size: 13px; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(17,153,142,0.3); font-family: inherit; flex-shrink: 0; width: fit-content; }
.btn-save:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(17,153,142,0.4); }
.btn-save:active { transform: translateY(0); }
.section-title { font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px; opacity: 0.7; margin-top: 2px; margin-bottom: 4px; }
.input-group { display: flex; flex-direction: column; gap: 4px; }
.input-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.input-row label { font-size: 13px; font-weight: 500; }
.input-row strong { color: #11998e; font-weight: 700; }
@keyframes softPulse { 0% { opacity: 1; } 50% { opacity: 0.75; } 100% { opacity: 1; } }
@keyframes iconRotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.mode-badge { display: inline-block; background: rgba(17, 153, 142, 0.2); border: 1px solid rgba(17, 153, 142, 0.4); color: #11998e; padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: 600; }
.mode-badge.active { background: rgba(17, 153, 142, 0.35); border-color: rgba(17, 153, 142, 0.6); animation: pulse-badge 1.8s infinite; }
@keyframes pulse-badge { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
.mode-card { padding: 8px; background: var(--input-bg); border-radius: 8px; border: 1px solid var(--border); display: flex; align-items: center; gap: 10px; transition: all 0.2s; }
.mode-card.active { background: rgba(17, 153, 142, 0.1); border-color: rgba(17, 153, 142, 0.3); box-shadow: inset 0 0 0 1px rgba(17, 153, 142, 0.2); }
.mode-label { display: flex; align-items: center; gap: 8px; flex: 1; margin: 0; }
.mode-label label { font-size: 12px; font-weight: 500; margin: 0; cursor: pointer; }
.mode-info { font-size: 9px; opacity: 0.6; margin-top: 2px; font-style: italic; line-height: 1.2; }
@media (max-width: 480px) {
.modal { width: 90vw; max-width: 340px; bottom: 60px; left: 50%; transform: translateX(-50%); }
.btn { padding: 9px 14px; font-size: 13px; min-height: 40px; }
.btn-main { min-width: 100px; }
.preset-btn { min-height: 36px; font-size: 10px; }
.mode-card { flex-wrap: wrap; }
.mode-label label { font-size: 13px; }
.tab-btn { width: 34px; height: 34px; font-size: 16px; }
}
`;
// ========== DOM STRUCTURE ==========
// Build UI elements: wrapper, buttons, and settings modal
const wrapper = document.createElement('div');
wrapper.style.opacity = getSettings().opacity;
const container = document.createElement('div');
container.style.cssText = 'display: flex; gap: 10px;';
// Main play/pause button
mainBtn = document.createElement('button');
mainBtn.className = 'btn btn-main';
mainBtn.style.position = 'relative';
mainBtn.innerHTML = '<span class="icon">▶</span><span>Start</span>';
// Settings button
settingsBtn = document.createElement('button');
settingsBtn.className = 'btn';
settingsBtn.innerHTML = '<span class="icon">⚙</span>';
// Theme toggle button
themeBtn = document.createElement('button');
themeBtn.className = 'btn';
// ========== SETTINGS MODAL STRUCTURE ==========
// Three-tab modal: Timing/Presets, Controls, and Statistics
settingsModal = document.createElement('div');
settingsModal.className = 'modal';
settingsModal.innerHTML = `
<div class="modal-wrapper">
<div class="modal-content">
<!-- Tab 1: Timing & Presets -->
<div class="tab-content active" id="tab-timing">
<div style="font-weight: 700; font-size: 11px; margin-bottom: 4px;">⚡ Presets</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; margin-bottom: 4px;">
<button class="preset-btn" id="preset-1-5">1-5s</button>
<button class="preset-btn" id="preset-5-15">5-15s</button>
<button class="preset-btn" id="preset-5-10">5-10s</button>
<button class="preset-btn" id="preset-10-15">10-15s</button>
<button class="preset-btn" id="preset-15-20">15-20s</button>
<button class="preset-btn" id="preset-20-30">20-30s</button>
<button class="preset-btn" id="preset-1-5m" style="grid-column: 1 / -1;">1-5m</button>
</div>
<div class="section-title">⏱ Timing</div>
<div class="input-group">
<div class="input-row">
<label>Min Wait</label>
<strong id="min-display">5</strong><span style="opacity: 0.6;">s</span>
</div>
<input type="range" id="min-input" min="1" max="300" value="5">
</div>
<div class="input-group">
<div class="input-row">
<label>Max Wait</label>
<strong id="max-display">15</strong><span style="opacity: 0.6;">s</span>
</div>
<input type="range" id="max-input" min="1" max="300" value="15">
</div>
</div>
<!-- Tab 2: Controls -->
<div class="tab-content" id="tab-controls">
<div class="section-title">🎛 Controls</div>
<div class="input-group">
<div class="input-row">
<label>Max Reloads</label>
<strong id="max-reloads-display">0</strong><span style="opacity: 0.6;">(0=∞)</span>
</div>
<input type="range" id="max-reloads-input" min="0" max="100" value="0">
</div>
<div class="input-group">
<div class="input-row">
<label>Opacity</label>
<strong style="color: #11998e;" id="opacity-display">100</strong><span style="opacity: 0.6;">%</span>
</div>
<input type="range" id="opacity-slider" min="0.2" max="1" step="0.1" value="1">
</div>
<div class="mode-card" id="inactivity-card">
<div class="mode-label">
<input type="checkbox" id="inactivity-toggle">
<label for="inactivity-toggle">Inactivity Pause</label>
</div>
<span class="mode-badge" id="inactivity-badge">OFF</span>
</div>
<div class="mode-info">Pauses auto-reload when you stop using the page (60s timeout)</div>
<div class="mode-card" id="jitter-card">
<div class="mode-label">
<input type="checkbox" id="jitter-toggle">
<label for="jitter-toggle">Jitter Mode</label>
</div>
<span class="mode-badge" id="jitter-badge">OFF</span>
</div>
<div class="mode-info">Adds ±10% random delay to prevent predictable reload patterns</div>
</div>
<!-- Tab 3: Statistics -->
<div class="tab-content" id="tab-stats">
<div class="section-title">📊 Statistics</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 11px;">
<div style="padding: 6px; background: var(--input-bg); border-radius: 8px; border: 1px solid var(--border);">
<div style="opacity: 0.7; margin-bottom: 2px; font-size: 10px;">Reloads</div>
<div style="font-size: 14px; font-weight: 700; color: #11998e;" id="stat-reloads">0</div>
</div>
<div style="padding: 6px; background: var(--input-bg); border-radius: 8px; border: 1px solid var(--border);">
<div style="opacity: 0.7; margin-bottom: 2px; font-size: 10px;">Limit</div>
<div style="font-size: 14px; font-weight: 700; color: #11998e;" id="stat-max-limit">0</div>
</div>
<div style="padding: 6px; background: var(--input-bg); border-radius: 8px; border: 1px solid var(--border);">
<div style="opacity: 0.7; margin-bottom: 2px; font-size: 10px;">Session</div>
<div style="font-size: 12px; font-weight: 700; color: #11998e;" id="stat-session">0m 0s</div>
</div>
<div style="padding: 6px; background: var(--input-bg); border-radius: 8px; border: 1px solid var(--border);">
<div style="opacity: 0.7; margin-bottom: 2px; font-size: 10px;">Avg Wait</div>
<div style="font-size: 12px; font-weight: 700; color: #11998e;" id="stat-avg">0s</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="save-btn">SAVE</button>
<button class="preset-btn" id="reset-stats-btn" style="padding: 8px; font-size: 11px;">Reset Stats</button>
</div>
</div>
<div class="modal-tabs">
<button class="tab-btn active" data-tab="timing" title="Timing / Presets">⏱</button>
<button class="tab-btn" data-tab="controls" title="Controls">🎛</button>
<button class="tab-btn" data-tab="stats" title="Stats">📊</button>
</div>
`;
container.append(mainBtn, themeBtn, settingsBtn);
wrapper.append(container, settingsModal);
shadow.append(style, wrapper);
// ========== INPUT ELEMENTS CACHE ==========
// Cache frequently accessed input elements
opacitySlider = settingsModal.querySelector('#opacity-slider');
const minInput = settingsModal.querySelector('#min-input');
const maxInput = settingsModal.querySelector('#max-input');
const minDisplay = settingsModal.querySelector('#min-display');
const maxDisplay = settingsModal.querySelector('#max-display');
const maxReloadsInput = settingsModal.querySelector('#max-reloads-input');
const maxReloadsDisplay = settingsModal.querySelector('#max-reloads-display');
const opacityDisplay = settingsModal.querySelector('#opacity-display');
// ========== INPUT VALIDATION & SYNC ==========
// Validate min/max inputs and update displays in real-time
minInput.oninput = (e) => {
const minVal = parseInt(e.target.value), maxVal = parseInt(maxInput.value);
if (minVal > maxVal) { minInput.value = maxVal; minDisplay.textContent = maxVal; }
else minDisplay.textContent = minVal;
};
maxInput.oninput = (e) => {
const minVal = parseInt(minInput.value), maxVal = parseInt(e.target.value);
if (maxVal < minVal) { maxInput.value = minVal; maxDisplay.textContent = minVal; }
else maxDisplay.textContent = maxVal;
};
maxReloadsInput.oninput = (e) => { maxReloadsDisplay.textContent = e.target.value; };
opacitySlider.oninput = (e) => {
wrapper.style.opacity = e.target.value;
opacityDisplay.textContent = Math.round(e.target.value * 100);
};
// ========== TAB SWITCHING ==========
// Handle tab navigation in settings modal
const tabButtons = settingsModal.querySelectorAll('.tab-btn');
const tabContents = settingsModal.querySelectorAll('.tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const tabName = btn.dataset.tab;
tabButtons.forEach(b => b.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
btn.classList.add('active');
settingsModal.querySelector(`#tab-${tabName}`).classList.add('active');
});
});
}
// ==================== UI STATE MANAGEMENT ====================
// Updates button display and favicon based on current running state
function updateUIState() {
if (isPausedByInactivity) {
mainBtn.className = 'btn btn-main safety';
mainBtn.innerHTML = '<span class="icon">💤</span><span>NO ACTIVITY</span>';
} else if (isPausedBySafety) {
mainBtn.className = 'btn btn-main safety';
mainBtn.innerHTML = '<span class="icon">🛡</span><span>PAUSED</span>';
} else if (isRunning) {
mainBtn.className = 'btn btn-main active';
mainBtn.innerHTML = `<span class="icon">⏸</span><span>${remainingTime}s</span>`;
} else {
mainBtn.className = 'btn btn-main';
mainBtn.innerHTML = '<span class="icon">▶</span><span>Start</span>';
}
updateFavicon(remainingTime);
document.title = (isRunning && !isPausedBySafety && !isPausedByInactivity)
? `[${remainingTime}s] ` + document.title.replace(/\[\d+s\]\s/, '')
: document.title.replace(/\[\d+s\]\s/, '');
}
// ==================== RELOAD CYCLE ====================
// Main loop: calculate wait time, start timers, handle reloads
function startCycle() {
const { min, max } = getSettings();
let waitTime = Math.max(1, Math.floor(Math.random() * (max - min) + min));
const jitterResult = applyJitter(waitTime);
waitTime = jitterResult.newValue;
// ========== FIXED: ALWAYS show jitter notification when jitter mode is enabled ==========
if (jitterResult.applied) {
const sign = jitterResult.jitterAmount > 0 ? '+' : '';
const jitterDisplay = jitterResult.jitterAmount === 0 ? '±0' : sign + jitterResult.jitterAmount;
showToast(`⚡ Jitter: ${jitterDisplay}s (${waitTime}s total)`);
}
remainingTime = maxRemainingTime = waitTime;
stats.waitTimes.push(waitTime);
if (!sessionTimerId) {
sessionTimerId = setInterval(() => {
stats.totalRunningTime += 1;
saveStats();
}, 1000);
}
isPausedByInactivity = false;
resetInactivityTimeout();
updateUIState();
// ========== COUNTDOWN & RELOAD TIMER ==========
// Decrements every second and triggers reload when reaching zero
timerId = setInterval(() => {
if (!isPausedBySafety && !isPausedByInactivity) {
remainingTime--;
updateUIState();
if (remainingTime <= 0) {
const maxReloadsLimit = parseInt(localStorage.getItem('maxReloadsLimit')) || 0;
const currentCount = getReloadCount();
if (maxReloadsLimit > 0 && currentCount >= maxReloadsLimit) {
showToast('🛑 Max reached');
isRunning = false;
clearInterval(timerId);
updateUIState();
return;
}
incrementReloadCount();
window.location.reload();
}
}
}, 1000);
}
// ==================== SAFETY CHECK ====================
// Prevents reload while user is typing in input fields (form protection)
const checkSafety = (e) => {
const active = document.activeElement;
const isTyping = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
isPausedBySafety = isRunning && isTyping;
updateUIState();
};
// ==================== INITIALIZATION ====================
// Creates UI, loads settings, restores state
createShadowUI();
host.setAttribute('theme', localStorage.getItem(themeKey) || 'dark');
themeBtn.innerHTML = host.getAttribute('theme') === 'dark' ? '<span class="icon">☀️</span>' : '<span class="icon">🌙</span>';
// ==================== BUTTON EVENT LISTENERS ====================
// Main button: toggle start/stop
mainBtn.onclick = () => {
isRunning = !isRunning;
isPausedBySafety = isPausedByInactivity = false;
localStorage.setItem(storageKey, isRunning);
updateUIState();
if (isRunning) {
startCycle();
} else {
clearInterval(timerId);
clearTimeout(inactivityTimeout);
if (sessionTimerId) clearInterval(sessionTimerId), sessionTimerId = null;
}
};
// ==================== DOCUMENT EVENT LISTENERS ====================
// Safety: detect typing, activity tracking, keyboard shortcuts
document.addEventListener('focusin', checkSafety);
document.addEventListener('focusout', () => setTimeout(checkSafety, 100));
document.addEventListener('mousemove', recordActivity);
document.addEventListener('keypress', recordActivity);
document.addEventListener('click', recordActivity);
document.addEventListener('keydown', (e) => {
if (e.shiftKey && e.key.toUpperCase() === 'T') {
e.preventDefault();
mainBtn.click();
}
});
// ==================== LOAD PERSISTENT DATA ====================
// Restore stats and reload count on page load
loadStats();
stats.totalReloads = getReloadCount();
// ==================== THEME TOGGLE ==========
// Switch between dark and light themes
themeBtn.onclick = () => {
const next = host.getAttribute('theme') === 'dark' ? 'light' : 'dark';
host.setAttribute('theme', next);
localStorage.setItem(themeKey, next);
themeBtn.innerHTML = next === 'dark' ? '<span class="icon">☀️</span>' : '<span class="icon">🌙</span>';
};
// ==================== SETTINGS MODAL HANDLERS ====================
// Open/close settings and sync UI with stored values
settingsBtn.onclick = () => {
settingsModal.style.display = settingsModal.style.display === 'flex' ? 'none' : 'flex';
const s = getSettings();
shadow.querySelector('#min-input').value = s.min;
shadow.querySelector('#max-input').value = s.max;
shadow.querySelector('#min-display').textContent = s.min;
shadow.querySelector('#max-display').textContent = s.max;
const inactivityToggle = shadow.querySelector('#inactivity-toggle');
const jitterToggle = shadow.querySelector('#jitter-toggle');
const inactivityBadge = shadow.querySelector('#inactivity-badge');
const jitterBadge = shadow.querySelector('#jitter-badge');
const inactivityCard = shadow.querySelector('#inactivity-card');
const jitterCard = shadow.querySelector('#jitter-card');
inactivityToggle.checked = localStorage.getItem('inactivityPauseEnabled') === 'true';
jitterToggle.checked = localStorage.getItem('jitterMode') === 'true';
const updateBadges = () => {
const updateBadge = (toggle, badge, card) => {
if (toggle.checked) {
badge.textContent = 'ON';
badge.classList.add('active');
card.classList.add('active');
} else {
badge.textContent = 'OFF';
badge.classList.remove('active');
card.classList.remove('active');
}
};
updateBadge(inactivityToggle, inactivityBadge, inactivityCard);
updateBadge(jitterToggle, jitterBadge, jitterCard);
};
updateBadges();
inactivityToggle.oninput = jitterToggle.oninput = updateBadges;
const maxReloadsValue = localStorage.getItem('maxReloadsLimit') || '0';
shadow.querySelector('#max-reloads-input').value = maxReloadsValue;
shadow.querySelector('#max-reloads-display').textContent = maxReloadsValue;
opacitySlider.value = s.opacity;
shadow.querySelector('#opacity-display').textContent = Math.round(s.opacity * 100);
// Calculate and display session statistics
const sessionTime = (stats.totalRunningTime || 0) * 1000;
const minutes = Math.floor(sessionTime / 60000);
const seconds = Math.floor((sessionTime % 60000) / 1000);
const avgWait = stats.waitTimes.length > 0
? Math.round(stats.waitTimes.reduce((a, b) => a + b) / stats.waitTimes.length)
: 0;
shadow.querySelector('#stat-reloads').textContent = stats.totalReloads;
shadow.querySelector('#stat-max-limit').textContent = maxReloadsValue;
shadow.querySelector('#stat-session').textContent = `${minutes}m ${seconds}s`;
shadow.querySelector('#stat-avg').textContent = avgWait + 's';
};
// ==================== PRESET BUTTONS ====================
// Quick settings presets for common timing scenarios
const updateSliderDisplay = (minVal, maxVal) => {
shadow.querySelector('#min-input').value = minVal;
shadow.querySelector('#max-input').value = maxVal;
shadow.querySelector('#min-display').textContent = minVal;
shadow.querySelector('#max-display').textContent = maxVal;
};
shadow.querySelector('#preset-1-5').onclick = () => updateSliderDisplay(1, 5);
shadow.querySelector('#preset-5-15').onclick = () => updateSliderDisplay(5, 15);
shadow.querySelector('#preset-5-10').onclick = () => updateSliderDisplay(5, 10);
shadow.querySelector('#preset-10-15').onclick = () => updateSliderDisplay(10, 15);
shadow.querySelector('#preset-15-20').onclick = () => updateSliderDisplay(15, 20);
shadow.querySelector('#preset-20-30').onclick = () => updateSliderDisplay(20, 30);
shadow.querySelector('#preset-1-5m').onclick = () => updateSliderDisplay(60, 300);
// ==================== SAVE SETTINGS ====================
// Persist all user settings to localStorage
shadow.querySelector('#save-btn').onclick = () => {
localStorage.setItem('minDelay', shadow.querySelector('#min-input').value);
localStorage.setItem('maxDelay', shadow.querySelector('#max-input').value);
localStorage.setItem(opacityKey, opacitySlider.value);
localStorage.setItem('inactivityPauseEnabled', shadow.querySelector('#inactivity-toggle').checked);
localStorage.setItem('jitterMode', shadow.querySelector('#jitter-toggle').checked);
const maxReloadsValue = parseInt(shadow.querySelector('#max-reloads-input').value) || 0;
localStorage.setItem('maxReloadsLimit', maxReloadsValue.toString());
stats.maxReloadsLimit = maxReloadsValue;
stats.jitterMode = shadow.querySelector('#jitter-toggle').checked;
saveStats();
showToast('✅ Settings saved!');
settingsModal.style.display = 'none';
};
// ==================== RESET STATISTICS ====================
// Clear all reload counts and timing history
shadow.querySelector('#reset-stats-btn').onclick = () => {
if (confirm('Reset all statistics?')) {
resetReloadCount();
stats = {
totalReloads: 0,
totalRunningTime: 0,
waitTimes: [],
maxReloadsLimit: 0,
jitterMode: false,
sessionStartTime: null
};
saveStats();
shadow.querySelector('#stat-reloads').textContent = '0';
shadow.querySelector('#stat-max-limit').textContent = '0';
shadow.querySelector('#stat-session').textContent = '0m 0s';
shadow.querySelector('#stat-avg').textContent = '0s';
showToast('🔄 Stats reset!');
}
};
// ==================== RESTORE SESSION STATE ====================
// Resume auto-reload if it was running on previous page load
if (localStorage.getItem(storageKey) === 'true') {
isRunning = true;
startCycle();
}
}
LiveReload();
})();