您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[Desktop Site | Guest Mode Only] Removes login popups/banners, fixes page jumps, and opens media in a new tab to prevent deadlocks. Disables itself when logged in.
// ==UserScript== // @name Facebook Login Wall Remover // @name:en Facebook Login Wall Remover // @name:zh-TW Facebook 登入牆移除器 // @name:ja Facebook ログインウォールリムーバー // @namespace https://greasyfork.org/en/users/1467948-stonedkhajiit // @version 0.1.1 // @description [Desktop Site | Guest Mode Only] Removes login popups/banners, fixes page jumps, and opens media in a new tab to prevent deadlocks. Disables itself when logged in. // @description:en [Desktop Site | Guest Mode Only] Removes login popups and banners, prevents page jumps, and automatically opens media in a new tab to prevent page deadlocks. Automatically disables itself when a logged-in state is detected. // @description:zh-TW 【桌面版網頁|未登入專用】移除登入提示與橫幅、解決頁面跳轉,並自動在新分頁開啟媒體以從根源上防止頁面死鎖。偵測到登入狀態時將自動停用。 // @description:ja 【デスクトップサイト|未ログイン専用】ログインポップアップとバナーを削除し、ページのジャンプを修正します。デッドロックを防ぐためにメディアを新しいタブで自動的に開き、ログイン状態では自動的に無効になります。 // @author StonedKhajiit // @match *://*.facebook.com/* // @icon https://www.facebook.com/favicon.ico // @grant GM_notification // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const CONFIG = { LOG_PREFIX: `[FB Login Wall Remover]`, THROTTLE_DELAY: 250, PROCESSED_MARKER: 'gm-processed', SCROLL_RESTORER: { CORRECTION_DURATION: 250, // ms: How long to force the scroll position. CORRECTION_FREQUENCY: 16, // ms: How often to force the scroll position (16ms ~ 60fps). WATCHER_FREQUENCY: 150, // ms: How often to check for modal changes. MODAL_GRACE_PERIOD: 300, // ms: Time to wait before considering a modal "closed" to ignore transitional states. }, SELECTORS: { // Main component selectors POST_CONTAINER: 'div[role="article"]', MODAL_CONTAINER: 'div.__fb-light-mode', DIALOG: '[role="dialog"]', LOGIN_FORM: 'form#login_form, form[id="login_popup_cta_form"]', // Media links that trigger modals MEDIA_LINK: ` a[href*="/photo"], a[href*="fbid="], a[href*="/videos/"], a[href*="/watch/"], a[href*="/reel/"] `.trim().replace(/\s+/g, ' '), // A robust, multi-language selector for close buttons CLOSE_BUTTON: ` [aria-label="Close"][role="button"], [aria-label="關閉"][role="button"], [aria-label="閉じる"][role="button"], [aria-label="Cerrar"][role="button"], [aria-label="Fermer"][role="button"], [aria-label="Schließen"][role="button"], [aria-label="Fechar"][role="button"], [aria-label="Chiudi"][role="button"], [aria-label="Sluiten"][role="button"], [aria-label="Закрыть"][role="button"], [aria-label="Kapat"][role="button"], [aria-label="Zamknij"][role="button"], [aria-label="Tutup"][role="button"], [aria-label="Đóng"][role="button"], [aria-label="ปิด"][role="button"], [aria-label="Zatvori"][role="button"], [aria-label="Zavrieť"][role="button"], [aria-label="Zavřít"][role="button"], [aria-label="Bezárás"][role="button"], div[role="button"]:has(i[data-visualcompletion="css-img"]) `.trim().replace(/\s+/g, ' '), }, }; // --- Internationalization --- const STRINGS = { en: { notificationTitle: 'Notice', notificationDeadlock: 'A non-closable login modal was hidden. The page is now locked and won\'t load new content.\n\nPro-Tip: To view media without locking your feed, open it in a new tab (middle-click). Please reload this page.', autoOpenMediaInNewTab: 'Auto-open media in new tab (prevents deadlock)', showDeadlockNotification: 'Show deadlock notification', hideUselessElements: 'Hide useless UI elements (for guest)', }, 'zh-TW': { notificationTitle: '注意', notificationDeadlock: '一個無關閉按鈕的登入提示已被隱藏,頁面現已鎖定且無法載入新內容。\n\n提示:為避免動態牆被鎖定,建議以新分頁(滑鼠中鍵)開啟圖片或影片。請重新整理此頁面以繼續。', autoOpenMediaInNewTab: '在新分頁開啟媒體 (防死鎖)', showDeadlockNotification: '顯示頁面死鎖通知', hideUselessElements: '隱藏訪客模式下的多餘介面', }, ja: { notificationTitle: '通知', notificationDeadlock: '閉じるボタンのないログインモーダルを非表示にしました。ページがロックされ、新しいコンテンツは読み込まれません。\n\nヒント:フィードをロックせずにメディアを表示するには、新しいタブで開く(マウスの中央ボタンでクリック)ことをお勧めします。このページをリロードしてください。', autoOpenMediaInNewTab: 'メディアを新しいタブで開く (デッドロック防止)', showDeadlockNotification: 'デッドロック通知を表示', hideUselessElements: '不要なUI要素を非表示にする(ゲスト用)', }, }; /** * Determines the best language for UI strings based on browser settings. * @returns {object} The string object for the detected language. */ function getStrings() { const lang = navigator.language.toLowerCase(); if (lang.startsWith('ja')) return STRINGS.ja; if (lang.startsWith('zh')) return STRINGS['zh-TW']; return STRINGS.en; } /** * Checks if the user is logged into Facebook. * @returns {boolean} True if logged in, false otherwise. */ function isLoggedIn() { // Presence of user-specific links indicates a logged-in state. if (document.querySelector('a[href="/friends/"]') || document.querySelector('a[href*="/watch/"]')) return true; // Presence of login forms indicates a logged-out state. if (document.querySelector('form#login_form') || document.querySelector('a[href*="/login/"]')) return false; // Default to not logged in if unsure. return false; } // --- Settings Manager --- const settings = {}; const SettingsManager = { registeredCommands: [], definitions: [ { key: 'autoOpenMediaInNewTab', label: 'Auto-open media in new tab (prevents deadlock)', type: 'toggle', defaultValue: true }, { key: 'hideUselessElements', label: 'Hide useless UI elements (for guest)', type: 'toggle', defaultValue: true }, { key: 'showDeadlockNotification', label: 'Show deadlock notification', type: 'toggle', defaultValue: true }, ], init(T) { this.definitions.forEach(def => { settings[def.key] = GM_getValue(def.key, def.defaultValue); }); this.renderMenu(T); }, renderMenu(T) { this.registeredCommands.forEach(id => GM_unregisterMenuCommand(id)); this.registeredCommands = []; this.definitions.forEach(def => { const status = settings[def.key] ? '✅' : '❌'; const label = `${status} ${T[def.key] || def.label}`; this.registeredCommands.push(GM_registerMenuCommand(label, () => this.updateSetting(def.key, !settings[def.key], T))); }); }, updateSetting(key, value, T) { GM_setValue(key, value); settings[key] = value; this.renderMenu(T); // Inform user that a reload is needed for style changes to take effect. if (key === 'hideUselessElements') { alert('Please reload the page for this setting to take full effect.'); } }, }; /** * Intercepts and blocks scroll event listeners to prevent the page from locking. * This aggressive strategy is applied at the earliest possible stage. */ function setupInterceptors() { const originalAddEventListener = EventTarget.prototype.addEventListener; Object.defineProperty(EventTarget.prototype, 'addEventListener', { configurable: true, enumerable: true, get: () => function(type, listener, options) { if (type === 'scroll') return; return originalAddEventListener.call(this, type, listener, options); }, }); } /** * Injects CSS rules to hide static UI elements for better performance. * @param {object} currentSettings - The current user settings object. */ function injectHidingStyles(currentSettings) { const rules = []; // General login/register banner at the bottom. rules.push(`div[data-nosnippet]:has(a[href*="/login/"]):has(a[href*="/reg/"])`); // Top login banner, based on user settings. if (currentSettings.hideUselessElements) { rules.push(`div[role="banner"]:has(${CONFIG.SELECTORS.LOGIN_FORM})`); } if (rules.length === 0) return; const css = `${rules.join(',\n')} { display: none !important; }`; const styleElement = document.createElement('style'); styleElement.textContent = css.trim(); document.head.appendChild(styleElement); console.log(`${CONFIG.LOG_PREFIX} [StyleInjector] Injected ${rules.length} hiding rule(s).`); } /** * Counteracts page position jumps caused by opening media modals. * Facebook forcibly scrolls the background page to the top when a modal is opened. * This module records the scroll position just before the modal opens and restores * it after the modal is closed. * @returns {{init: function}} The scroll restorer instance. */ function createScrollRestorer() { let restoreY = null; let watcherInterval = null; let correctionInterval = null; const stopWatcher = () => { if (!watcherInterval) return; clearInterval(watcherInterval); watcherInterval = null; }; const forceScrollCorrection = () => { if (restoreY === null) return; if (correctionInterval) clearInterval(correctionInterval); const { CORRECTION_DURATION, CORRECTION_FREQUENCY } = CONFIG.SCROLL_RESTORER; const startTime = Date.now(); const initialRestoreY = restoreY; correctionInterval = setInterval(() => { window.scrollTo({ top: initialRestoreY, behavior: 'instant' }); if (Date.now() - startTime > CORRECTION_DURATION) { clearInterval(correctionInterval); correctionInterval = null; restoreY = null; } }, CORRECTION_FREQUENCY); }; const startWatcher = () => { stopWatcher(); let isContentModalDetected = false; let modalFirstSeenTime = null; watcherInterval = setInterval(() => { const modal = document.querySelector(CONFIG.SELECTORS.DIALOG); if (!isContentModalDetected && modal && !modal.querySelector(CONFIG.SELECTORS.LOGIN_FORM)) { isContentModalDetected = true; modalFirstSeenTime = Date.now(); } else if (isContentModalDetected && !modal) { if (Date.now() - modalFirstSeenTime > CONFIG.SCROLL_RESTORER.MODAL_GRACE_PERIOD) { stopWatcher(); forceScrollCorrection(); } } }, CONFIG.SCROLL_RESTORER.WATCHER_FREQUENCY); }; const handleCloseClick = e => { const closeButton = e.target.closest(CONFIG.SELECTORS.CLOSE_BUTTON); if (closeButton && restoreY !== null && watcherInterval) { stopWatcher(); forceScrollCorrection(); } }; const recordClick = e => { if (watcherInterval || correctionInterval) return; const postContainer = e.target.closest(CONFIG.SELECTORS.POST_CONTAINER); const externalLink = e.target.closest('a[target="_blank"]'); if (postContainer && !externalLink) { restoreY = window.scrollY; // Delay watcher start to avoid race conditions with Facebook's event handling. // This ensures it runs after Facebook's own 'click' handlers have finished. setTimeout(startWatcher, 0); } }; return { init: () => { document.body.addEventListener('click', recordClick, true); document.body.addEventListener('click', handleCloseClick, true); console.log(`${CONFIG.LOG_PREFIX} [ScrollRestorer] Activated.`); }, }; } /** * Proactively opens media links in a new tab to prevent page deadlocks * caused by non-closable login modals. */ function createLinkInterceptor() { const handleClick = event => { if (!settings.autoOpenMediaInNewTab || event.button !== 0) return; const linkElement = event.target.closest(CONFIG.SELECTORS.MEDIA_LINK); if (linkElement && linkElement.closest(CONFIG.SELECTORS.POST_CONTAINER)) { console.log(`${CONFIG.LOG_PREFIX} [LinkInterceptor] High-risk media link clicked. Opening in new tab.`); event.preventDefault(); event.stopPropagation(); window.open(linkElement.href, '_blank'); } }; return { init: () => { document.body.addEventListener('click', handleClick, true); console.log(`${CONFIG.LOG_PREFIX} [LinkInterceptor] Activated.`); }, }; } /** * Finds and removes/modifies dynamic UI elements using a MutationObserver. * @param {object} T - The internationalized strings object. * @returns {{init: function}} The DOM cleaner instance. */ function createDOMCleaner(T) { // Multi-language keywords for identifying action buttons. const PRIMARY_KEYWORDS = ["Like", "讚", "いいね!", "Me gusta", "J'aime", "Gefällt mir", "Curtir", "Mi piace", "Vind ik leuk", "Нравится", "Beğen", "Lubię to!", "Suka", "Thích", "ถูกใจ", "Sviđa mi se", "Páči sa mi to", "To se mi líbí", "Tetszik"]; const SECONDARY_KEYWORDS = ["Comment", "留言", "コメントする", "Comentar", "Commenter", "Kommentieren", "Comentar", "Commento", "Reageren", "Комментировать", "Yorum Yap", "Skomentuj", "Komentari", "Bình luận", "แสดงความคิดเห็น", "Komentiraj", "Komentovať", "Okmentovat", "Hozzászólás"]; const FINGERPRINTS = [ // Handles the cookie consent dialog. { selector: `${CONFIG.SELECTORS.DIALOG}:has([href*="/policies/cookies/"]) [role="button"][tabindex="0"]`, action: 'click', }, // Handles all login modals, closable or not. { selector: `${CONFIG.SELECTORS.DIALOG}:has(${CONFIG.SELECTORS.LOGIN_FORM})`, action: 'handle_login_modal', }, // Fixes the sticky header that can sometimes appear. { selector: `div[style*="top:"][style*="z-index"]:has(div[role="tablist"])`, action: 'make_static', setting: 'hideUselessElements', }, ]; /** * Hides the main interaction toolbar ("Like", "Comment", etc.). * It locates the toolbar by finding buttons via their innerText, finds their * smallest common ancestor, and hides that ancestor's parent to avoid leaving * an empty container. This method is resilient to CSS class changes. */ const hideInteractionToolbars = () => { const processedToolbars = new Set(); // Scan only within post containers that haven't been fully processed for performance. const unprocessedPosts = document.querySelectorAll(`${CONFIG.SELECTORS.POST_CONTAINER}:not([data-gm-toolbar-processed])`); unprocessedPosts.forEach(post => { const allSpans = post.querySelectorAll('span'); let foundToolbarForThisPost = false; for (const span of allSpans) { const text = span.innerText.trim(); if (PRIMARY_KEYWORDS.includes(text)) { let currentParent = span.parentElement; let depth = 0; while (currentParent && depth < 10) { // Check if the parent also contains a secondary keyword. const hasSecondary = Array.from(currentParent.querySelectorAll('span')).some(otherSpan => SECONDARY_KEYWORDS.includes(otherSpan.innerText.trim())); // Ensure it's a multi-button container and not the entire post. if (hasSecondary && currentParent.querySelectorAll('div[role="button"]').length > 1 && currentParent.getAttribute('role') !== 'article') { const toolbar = currentParent; // The actual container to hide is the parent of the toolbar. const targetContainer = toolbar.parentElement; if (targetContainer && !processedToolbars.has(targetContainer)) { processedToolbars.add(targetContainer); targetContainer.style.display = 'none'; post.dataset.gmToolbarProcessed = 'true'; foundToolbarForThisPost = true; break; // Exit the while loop for this span. } } currentParent = currentParent.parentElement; depth++; } } if (foundToolbarForThisPost) break; // Exit the for...of loop for this post. } }); }; const showNotification = (title, text) => { if (!settings.showDeadlockNotification) return; GM_notification({ title, text, timeout: 8000, silent: true }); }; const runEngine = () => { for (const fingerprint of FINGERPRINTS) { // Skip rules disabled by user settings. if (fingerprint.setting && !settings[fingerprint.setting]) { continue; } document.querySelectorAll(fingerprint.selector).forEach(element => { if (element.dataset.gmProcessed) return; element.dataset.gmProcessed = 'true'; switch (fingerprint.action) { case 'click': element.click(); break; case 'make_static': element.style.position = 'static'; break; case 'handle_login_modal': { const closeButton = element.querySelector(CONFIG.SELECTORS.CLOSE_BUTTON); if (closeButton) { closeButton.click(); } else { // Handle non-closable modals. const allContainers = document.querySelectorAll(CONFIG.SELECTORS.MODAL_CONTAINER); const parentContainer = Array.from(allContainers).find(c => c.contains(element)); if (parentContainer) { parentContainer.style.display = 'none'; console.warn(`${CONFIG.LOG_PREFIX} Non-closable modal hidden. Page deadlocked.`); showNotification(T.notificationTitle, T.notificationDeadlock); } } break; } } }); } if (settings.hideUselessElements) { hideInteractionToolbars(); } }; const throttle = (func, delay) => { let timeoutId = null; return (...args) => { if (timeoutId === null) { timeoutId = setTimeout(() => { func.apply(this, args); timeoutId = null; }, delay); } }; }; return { init: () => { const throttledEngine = throttle(runEngine, CONFIG.THROTTLE_DELAY); throttledEngine(); const observer = new MutationObserver(throttledEngine); observer.observe(document.documentElement, { childList: true, subtree: true }); }, }; } // --- Script Execution --- if (isLoggedIn()) { console.log(`${CONFIG.LOG_PREFIX} Logged-in state detected. Script terminated.`); return; } console.log(`${CONFIG.LOG_PREFIX} Logged-out state detected. Script active.`); setupInterceptors(); window.addEventListener('DOMContentLoaded', () => { const T = getStrings(); SettingsManager.init(T); // Inject CSS hiding rules based on initial settings. injectHidingStyles(settings); const scrollRestorer = createScrollRestorer(); const domCleaner = createDOMCleaner(T); const linkInterceptor = createLinkInterceptor(); scrollRestorer.init(); domCleaner.init(); linkInterceptor.init(); }); })();