您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Blocks automatic redirects to apps and shows an overlay with options
当前为
// ==UserScript== // @name Android App Redirect Blocker // @namespace http://tampermonkey.net/ // @version 1.0 // @description Blocks automatic redirects to apps and shows an overlay with options // @author You // @match *://*/* // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; // Configuration const config = { // Time in ms to wait before showing the overlay after detecting a redirect redirectDelay: 300, // Time in ms to keep the overlay visible (0 for infinite) overlayDuration: 5000, // Position of the overlay - 'top', 'bottom' overlayPosition: 'bottom', // Enable logging for debugging debug: false, // Auto-detect and remember new app schemes rememberNewSchemes: true, // Whether to block all non-HTTP schemes by default blockAllAppSchemes: true, // List of known app URL schemes to monitor knownSchemes: [ 'fb://', 'twitter://', 'instagram://', 'reddit://', 'tiktok://', 'youtube://', 'whatsapp://', 'telegram://', 'intent://', 'market://', 'play-audio://', 'zalo://', 'linkedin://', 'snapchat://', 'spotify://', 'netflix://', 'maps://', 'tel://', 'sms://', 'mailto://', 'comgooglemaps://', 'waze://', 'viber://', 'line://', 'patreon://', 'discord://', 'slack://', 'googlepay://', 'upi://' ] }; // Main variables let lastDetectedApp = ''; let overlayElement = null; let redirectTimeout = null; let dismissTimeout = null; // Inject CSS for the overlay const injectStyles = () => { const style = document.createElement('style'); style.textContent = ` .app-redirect-overlay { position: fixed; ${config.overlayPosition}: 0; left: 0; width: 100%; background-color: rgba(33, 33, 33, 0.95); color: white; z-index: 2147483647; font-family: Arial, sans-serif; padding: 15px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); transition: transform 0.3s ease, opacity 0.3s ease; transform: translateY(${config.overlayPosition === 'top' ? '-100%' : '100%'}); opacity: 0; display: flex; flex-direction: column; box-sizing: border-box; } .app-redirect-overlay.visible { transform: translateY(0); opacity: 1; } .app-redirect-message { font-size: 16px; margin-bottom: 15px; text-align: center; } .app-redirect-app-name { font-weight: bold; } .app-redirect-buttons { display: flex; justify-content: space-around; } .app-redirect-button { background-color: #4285f4; color: white; border: none; border-radius: 4px; padding: 10px 15px; font-size: 14px; font-weight: bold; cursor: pointer; flex: 1; margin: 0 5px; max-width: 150px; } .app-redirect-stay { background-color: #757575; } .app-redirect-open { background-color: #4caf50; } .app-redirect-close { position: absolute; right: 10px; top: 10px; width: 24px; height: 24px; opacity: 0.7; cursor: pointer; background: none; border: none; color: white; font-size: 24px; line-height: 24px; padding: 0; } .app-redirect-close:hover { opacity: 1; } `; document.head.appendChild(style); }; // Create the overlay element const createOverlay = () => { if (overlayElement) return; overlayElement = document.createElement('div'); overlayElement.className = 'app-redirect-overlay'; overlayElement.innerHTML = ` <button class="app-redirect-close">×</button> <div class="app-redirect-message"> This page is trying to open the <span class="app-redirect-app-name"></span> </div> <div class="app-redirect-buttons"> <button class="app-redirect-button app-redirect-stay">Stay in Browser</button> <button class="app-redirect-button app-redirect-open">Open App</button> </div> `; // Add event listeners overlayElement.querySelector('.app-redirect-close').addEventListener('click', hideOverlay); overlayElement.querySelector('.app-redirect-stay').addEventListener('click', () => { hideOverlay(); lastDetectedApp = ''; // Reset the last detected app }); overlayElement.querySelector('.app-redirect-open').addEventListener('click', () => { hideOverlay(); if (lastDetectedApp) { // Proceed with the app redirection window.location.href = lastDetectedApp; } }); document.body.appendChild(overlayElement); }; // Show the overlay with app name const showOverlay = (appName, redirectUrl) => { if (!document.body) return; if (!overlayElement) { createOverlay(); } // Clear any existing timeout if (dismissTimeout) { clearTimeout(dismissTimeout); dismissTimeout = null; } // Update the app name in the overlay const appNameElement = overlayElement.querySelector('.app-redirect-app-name'); appNameElement.textContent = appName || 'unknown app'; // Store the redirect URL lastDetectedApp = redirectUrl; // Show the overlay overlayElement.classList.add('visible'); // Auto-hide after the specified duration (if not 0) if (config.overlayDuration > 0) { dismissTimeout = setTimeout(hideOverlay, config.overlayDuration); } }; // Hide the overlay const hideOverlay = () => { if (overlayElement) { overlayElement.classList.remove('visible'); } if (dismissTimeout) { clearTimeout(dismissTimeout); dismissTimeout = null; } }; // Extract app name from URL const getAppNameFromUrl = (url) => { // Try to extract app name from different URL formats let appName = 'app'; try { // For intent:// URLs if (url.startsWith('intent://')) { const packageMatch = url.match(/package=([^;&#]+)/); if (packageMatch && packageMatch[1]) { // Get app name from package name (com.example.app -> app) appName = packageMatch[1].split('.').pop(); // If there's an action, use that as additional info const actionMatch = url.match(/action=([^;&#]+)/); if (actionMatch && actionMatch[1]) { const action = actionMatch[1].split('.').pop(); if (action && action.toLowerCase() !== 'view' && action.toLowerCase() !== 'main') { appName += ' (' + action + ')'; } } } } // For android-app:// URLs else if (url.startsWith('android-app://')) { const parts = url.split('/'); if (parts.length >= 3) { appName = parts[2].split('.').pop(); } } // For direct scheme URLs (fb://, twitter://, etc.) else if (url.includes('://')) { appName = url.split('://')[0]; // Try to get more context if available const urlParts = url.split('://')[1].split('/'); if (urlParts.length > 1 && urlParts[1] && urlParts[1].length > 0) { const context = urlParts[1]; if (context && context.length < 15 && !/^\d+$/.test(context)) { appName += ' ' + context; } } } // Clean up and capitalize appName = appName.replace(/[^a-zA-Z0-9]/g, ' ').trim(); appName = appName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); // Handle common URL schemes const commonApps = { 'fb': 'Facebook', 'twitter': 'Twitter', 'instagram': 'Instagram', 'reddit': 'Reddit', 'tiktok': 'TikTok', 'youtube': 'YouTube', 'whatsapp': 'WhatsApp', 'telegram': 'Telegram', 'market': 'Play Store', 'play': 'Google Play', 'zalo': 'Zalo', 'linkedin': 'LinkedIn', 'snapchat': 'Snapchat', 'spotify': 'Spotify', 'netflix': 'Netflix', 'maps': 'Maps', 'tel': 'Phone Call', 'sms': 'SMS Message', 'mailto': 'Email App', 'intent': 'Android App', 'comgooglemaps': 'Google Maps', 'waze': 'Waze' }; // Check for common apps const lowerAppName = appName.toLowerCase().split(' ')[0]; if (commonApps[lowerAppName]) { appName = commonApps[lowerAppName]; } } catch (e) { console.error('Error parsing app name:', e); } return appName; }; // Function to detect and intercept app redirects const detectRedirect = (url) => { // Check if this is a URL we want to intercept // Handle known schemes from our list const isKnownAppUrl = config.knownSchemes.some(scheme => url.startsWith(scheme)); // Handle intent and android-app specific URLs const isIntentUrl = url.includes('intent://') || url.includes('android-app://'); // Handle any URL scheme that isn't http/https (likely an app) const urlObj = (() => { try { return new URL(url); } catch(e) { return null; } })(); const isCustomScheme = urlObj && !['http:', 'https:', 'ftp:', 'file:', 'data:', 'javascript:'].includes(urlObj.protocol.toLowerCase()) && urlObj.protocol !== ':'; if (isKnownAppUrl || isIntentUrl || isCustomScheme) { // Prevent the default behavior and show our overlay instead const appName = getAppNameFromUrl(url); // Use a small delay before showing the overlay to allow for quick redirects if (redirectTimeout) { clearTimeout(redirectTimeout); } redirectTimeout = setTimeout(() => { showOverlay(appName, url); }, config.redirectDelay); return true; } return false; }; // Intercept location changes const originalAssign = window.location.assign; window.location.assign = function(url) { if (detectRedirect(url)) { return; } return originalAssign.apply(this, arguments); }; const originalReplace = window.location.replace; window.location.replace = function(url) { if (detectRedirect(url)) { return; } return originalReplace.apply(this, arguments); }; // Override the href property let locationHrefDescriptor = Object.getOwnPropertyDescriptor(window.location, 'href'); if (locationHrefDescriptor && locationHrefDescriptor.configurable) { Object.defineProperty(window.location, 'href', { set: function(url) { if (detectRedirect(url)) { return url; } return locationHrefDescriptor.set.call(this, url); }, get: locationHrefDescriptor.get }); } // Intercept window.open const originalWindowOpen = window.open; window.open = function(url, ...args) { if (url && typeof url === 'string' && detectRedirect(url)) { return null; } return originalWindowOpen.call(this, url, ...args); }; // Listen for clicks on links document.addEventListener('click', function(e) { // Check if the click was on a link let element = e.target; while (element && element !== document.body) { if (element.tagName === 'A' && element.href) { const href = element.href; if (config.knownSchemes.some(scheme => href.startsWith(scheme)) || href.includes('intent://') || href.includes('android-app://')) { e.preventDefault(); e.stopPropagation(); // Show the overlay const appName = getAppNameFromUrl(href); showOverlay(appName, href); return; } } element = element.parentElement; } }, true); // Handle DOM ready const onDomReady = () => { injectStyles(); }; // Initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onDomReady); } else { onDomReady(); } // Function to add new schemes to the known list const addNewScheme = (url) => { try { const urlObj = new URL(url); const scheme = urlObj.protocol + '//'; // Check if this scheme is already in our list if (!config.knownSchemes.includes(scheme) && !['http://', 'https://', 'ftp://', 'file://', 'data://', 'javascript://'].includes(scheme)) { // Add this new scheme to our list config.knownSchemes.push(scheme); if (config.debug) { console.log('[App Redirect Blocker] Added new scheme:', scheme); } // Store in localStorage for persistence try { const storedSchemes = JSON.parse(localStorage.getItem('appRedirectBlocker_schemes') || '[]'); if (!storedSchemes.includes(scheme)) { storedSchemes.push(scheme); localStorage.setItem('appRedirectBlocker_schemes', JSON.stringify(storedSchemes)); } } catch (e) { console.error('[App Redirect Blocker] Error storing scheme:', e); } } } catch (e) { if (config.debug) { console.error('[App Redirect Blocker] Error adding scheme:', e); } } }; // Load any previously stored schemes try { const storedSchemes = JSON.parse(localStorage.getItem('appRedirectBlocker_schemes') || '[]'); for (const scheme of storedSchemes) { if (!config.knownSchemes.includes(scheme)) { config.knownSchemes.push(scheme); if (config.debug) { console.log('[App Redirect Blocker] Loaded stored scheme:', scheme); } } } } catch (e) { console.error('[App Redirect Blocker] Error loading stored schemes:', e); } // Expose the API to the window object for debugging and configuration window.AppRedirectBlocker = { showOverlay, hideOverlay, config, addScheme: (scheme) => { if (!scheme.endsWith('://')) { scheme += '://'; } if (!config.knownSchemes.includes(scheme)) { config.knownSchemes.push(scheme); return true; } return false; }, removeScheme: (scheme) => { if (!scheme.endsWith('://')) { scheme += '://'; } const index = config.knownSchemes.indexOf(scheme); if (index !== -1) { config.knownSchemes.splice(index, 1); return true; } return false; }, debug: (enable) => { config.debug = !!enable; } }; })();