您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Detect and show reCAPTCHA badge. Always assume Google-owned sites use it. Continuously check for late-injected elements. User can choose fade-out behavior on first use.
// ==UserScript== // @name reCAPTCHA Badge Visibility Notifier // @version 1.8.1 // @description Detect and show reCAPTCHA badge. Always assume Google-owned sites use it. Continuously check for late-injected elements. User can choose fade-out behavior on first use. // @author EthanJoyce // @namespace https://github.com/Ethanjoyce2010/Recaptcha-notifier // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // ====== CONFIGURATION ====== const FADE_OUT_TIME = 2000; // milliseconds (only used if fade out is enabled) // ============================ // Tracks the last alert type shown: 'present', 'absent', or null let lastAlertType = null; let badgeFound = false; let fadeOutEnabled = null; // Will be set based on user choice or saved preference // Add styles for the alert box and choice dialog GM_addStyle(` .recaptcha-alert { position: fixed; bottom: 20px; right: 20px; background: #333; color: #fff; padding: 10px 15px; border-radius: 6px; font-size: 14px; z-index: 999999; opacity: 0.95; cursor: pointer; transition: opacity 0.5s ease-out; } .recaptcha-alert.fade-out { opacity: 0; } .recaptcha-choice-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; color: #333; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 1000000; font-family: Arial, sans-serif; max-width: 400px; text-align: center; } .recaptcha-choice-dialog h3 { margin: 0 0 15px 0; font-size: 18px; } .recaptcha-choice-dialog p { margin: 0 0 20px 0; font-size: 14px; line-height: 1.4; } .recaptcha-choice-buttons { display: flex; gap: 10px; justify-content: center; } .recaptcha-choice-btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } .recaptcha-choice-btn.auto { background: #4CAF50; color: white; } .recaptcha-choice-btn.auto:hover { background: #45a049; } .recaptcha-choice-btn.manual { background: #2196F3; color: white; } .recaptcha-choice-btn.manual:hover { background: #1976D2; } .recaptcha-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999999; } `); // Preference helpers: try GM_* APIs (sync or promise), GM.* APIs, then localStorage fallback async function getPref(key, defaultValue) { try { // Greasemonkey/Tampermonkey legacy functions if (typeof GM_getValue === 'function') { const value = GM_getValue(key, defaultValue); // If it returned a Promise (modern GM), await it if (value != null && typeof value.then === 'function') return await value; return (typeof value === 'undefined') ? defaultValue : value; } // Newer GM.* API if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') { const v = await GM.getValue(key, defaultValue); return (typeof v === 'undefined') ? defaultValue : v; } } catch (e) { // fallthrough to localStorage } try { const raw = localStorage.getItem('recaptcha_' + key); if (raw === null) return defaultValue; return JSON.parse(raw); } catch (e) { return defaultValue; } } async function setPref(key, value) { try { if (typeof GM_setValue === 'function') { const res = GM_setValue(key, value); if (res && typeof res.then === 'function') await res; return; } if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') { await GM.setValue(key, value); return; } } catch (e) { // fallthrough to localStorage } try { localStorage.setItem('recaptcha_' + key, JSON.stringify(value)); } catch (e) { // ignore } } function showChoiceDialog() { return new Promise((resolve) => { // Create overlay const overlay = document.createElement("div"); overlay.className = "recaptcha-overlay"; // Create dialog const dialog = document.createElement("div"); dialog.className = "recaptcha-choice-dialog"; dialog.innerHTML = ` <h3>reCAPTCHA Notification Settings</h3> <p>How would you like reCAPTCHA notifications to behave?</p> <p>(You can change this later by clicking the gear icon in notifications)</p> <p>(Will apply when reloaded)</p> <div class="recaptcha-choice-buttons"> <button class="recaptcha-choice-btn auto">Auto-fade (2s)</button> <button class="recaptcha-choice-btn manual">Stay until clicked</button> </div> `; // Add event listeners const autoBtn = dialog.querySelector('.auto'); const manualBtn = dialog.querySelector('.manual'); autoBtn.addEventListener('click', async () => { await setPref('fadeOutEnabled', true); fadeOutEnabled = true; document.body.removeChild(overlay); resolve(true); }); manualBtn.addEventListener('click', async () => { await setPref('fadeOutEnabled', false); fadeOutEnabled = false; document.body.removeChild(overlay); resolve(false); }); overlay.appendChild(dialog); document.body.appendChild(overlay); }); } async function initializeFadeOutSetting() { const savedSetting = await getPref('fadeOutEnabled', null); if (savedSetting === null) { // First time use - show choice dialog fadeOutEnabled = await showChoiceDialog(); } else { // Use saved setting fadeOutEnabled = savedSetting; } } function showAlert(message, type) { // type is 'present' or 'absent'. Only suppress if it's identical to lastAlertType if (type && lastAlertType === type) return; lastAlertType = type || null; const alertBox = document.createElement("div"); alertBox.className = "recaptcha-alert"; // Message container const msg = document.createElement('span'); msg.textContent = message; alertBox.appendChild(msg); // Settings gear to reopen choice dialog const gear = document.createElement('button'); gear.title = 'Notification settings'; gear.style.marginLeft = '10px'; gear.style.background = 'transparent'; gear.style.border = 'none'; gear.style.color = 'inherit'; gear.style.cursor = 'pointer'; gear.textContent = '⚙️'; gear.addEventListener('click', async (e) => { e.stopPropagation(); await showChoiceDialog(); }); alertBox.appendChild(gear); // Remove on click alertBox.addEventListener("click", () => { alertBox.remove(); }); document.body.appendChild(alertBox); if (fadeOutEnabled) { setTimeout(() => { alertBox.classList.add("fade-out"); setTimeout(() => { if (alertBox.parentNode) { alertBox.remove(); } }, 500); }, FADE_OUT_TIME); } } function checkReCaptcha() { const badge = document.querySelector(".grecaptcha-badge"); if (badge) { badge.style.visibility = "visible"; // force visible if (!badgeFound) { showAlert("This site uses reCAPTCHA"); badgeFound = true; } return true; } return false; } // Special case: Google-owned sites -> always assume yes function isGoogleSite() { return /\.google\./.test(window.location.hostname) || /\.youtube\./.test(window.location.hostname) || /\.blogger\./.test(window.location.hostname) || /\.gmail\./.test(window.location.hostname); } window.addEventListener("load", async () => { // Initialize fade-out setting first await initializeFadeOutSetting(); setTimeout(() => { if (isGoogleSite()) { showAlert("This Google site uses reCAPTCHA"); } else if (!checkReCaptcha()) { showAlert("This site does NOT use reCAPTCHA"); } // Watch for late injection const observer = new MutationObserver(() => checkReCaptcha()); observer.observe(document.body, { childList: true, subtree: true }); // Poll every 1s until found const interval = setInterval(() => { if (checkReCaptcha()) { clearInterval(interval); } }, 1000); }, 1000); }); })();