Android Anti-App Redirect

Prevents automatic redirection to apps on Android browsers

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Android Anti-App Redirect
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Prevents automatic redirection to apps on Android browsers
// @author       You
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    
    // Configuration
    const DEBUG = false; // Set to true to enable console logging
    const PROMPT_TIMEOUT = 5000; // How long to show the prompt (ms)
    
    // Helper function for logging
    function log(message) {
        if (DEBUG) {
            console.log(`[Anti-App Redirect] ${message}`);
        }
    }
    
    // Check if running on Android
    const isAndroid = /Android/i.test(navigator.userAgent);
    if (!isAndroid) {
        log("Not running on Android, script disabled");
        return;
    }
    
    log("Anti-App Redirect script active on Android");
    
    // Create UI elements for the prompt
    function createPromptUI() {
        const container = document.createElement('div');
        container.id = 'app-redirect-prompt';
        container.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background-color: #333;
            color: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            z-index: 999999;
            font-family: Arial, sans-serif;
            max-width: 90%;
            display: none;
            text-align: center;
        `;
        
        const message = document.createElement('div');
        message.style.cssText = 'margin-bottom: 10px;';
        message.textContent = 'App redirect blocked. What would you like to do?';
        container.appendChild(message);
        
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'display: flex; justify-content: space-around;';
        
        const stayButton = document.createElement('button');
        stayButton.textContent = 'Stay in Browser';
        stayButton.style.cssText = `
            background-color: #4CAF50;
            border: none;
            color: white;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 5px;
        `;
        
        const redirectButton = document.createElement('button');
        redirectButton.textContent = 'Open App';
        redirectButton.style.cssText = `
            background-color: #757575;
            border: none;
            color: white;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
            margin-left: 5px;
        `;
        
        buttonContainer.appendChild(stayButton);
        buttonContainer.appendChild(redirectButton);
        container.appendChild(buttonContainer);
        
        document.body.appendChild(container);
        
        return {
            container,
            stayButton,
            redirectButton
        };
    }
    
    // Store original functions to prevent infinite recursion
    const originalWindowOpen = window.open;
    const originalLocationAssign = window.location.assign;
    const originalLocationReplace = window.location.replace;
    const originalSetAttribute = Element.prototype.setAttribute;
    
    // Blocked schemes that typically trigger app redirects
    const blockedSchemes = [
        'intent:',
        'market:',
        'googlechrome:',
        'reddit:',
        'twitter:',
        'fb:',
        'tg:',
        'whatsapp:',
        'viber:',
        'spotify:',
        'youtube:',
        'vnd.youtube:',
        'instagram:',
        'snapchat:'
    ];
    
    // Variable to store the redirect URL if user chooses to continue
    let pendingRedirect = null;
    let activePrompt = null;
    let promptTimeout = null;
    
    // Function to show the redirect prompt
    function showRedirectPrompt(url) {
        // Don't show multiple prompts
        if (activePrompt) {
            clearTimeout(promptTimeout);
            document.body.removeChild(activePrompt.container);
        }
        
        pendingRedirect = url;
        log(`Blocked redirect to: ${url}`);
        
        // Wait for document body to be available
        if (!document.body) {
            document.addEventListener('DOMContentLoaded', () => showRedirectPrompt(url));
            return;
        }
        
        const ui = createPromptUI();
        activePrompt = ui;
        
        ui.stayButton.addEventListener('click', () => {
            pendingRedirect = null;
            document.body.removeChild(ui.container);
            activePrompt = null;
            clearTimeout(promptTimeout);
        });
        
        ui.redirectButton.addEventListener('click', () => {
            if (pendingRedirect) {
                log(`User allowed redirect to: ${pendingRedirect}`);
                document.body.removeChild(ui.container);
                activePrompt = null;
                clearTimeout(promptTimeout);
                
                // Use the original function to perform the redirect
                originalWindowOpen.call(window, pendingRedirect, '_self');
            }
        });
        
        // Show the prompt
        ui.container.style.display = 'block';
        
        // Auto-hide after timeout
        promptTimeout = setTimeout(() => {
            if (activePrompt === ui && ui.container.parentNode) {
                document.body.removeChild(ui.container);
                activePrompt = null;
                pendingRedirect = null;
            }
        }, PROMPT_TIMEOUT);
    }
    
    // Helper function to check if a URL should be blocked
    function shouldBlockUrl(url) {
        if (!url || typeof url !== 'string') return false;
        
        // Check for app schemes
        return blockedSchemes.some(scheme => url.toLowerCase().startsWith(scheme));
    }
    
    // Override window.open
    window.open = function(url, target, features) {
        if (shouldBlockUrl(url)) {
            showRedirectPrompt(url);
            return null;
        }
        return originalWindowOpen.call(this, url, target, features);
    };
    
    // Override location.assign
    window.location.assign = function(url) {
        if (shouldBlockUrl(url)) {
            showRedirectPrompt(url);
            return;
        }
        return originalLocationAssign.call(this, url);
    };
    
    // Override location.replace
    window.location.replace = function(url) {
        if (shouldBlockUrl(url)) {
            showRedirectPrompt(url);
            return;
        }
        return originalLocationReplace.call(this, url);
    };
    
    // Override direct location changes
    Object.defineProperty(window, 'location', {
        set: function(url) {
            if (shouldBlockUrl(url)) {
                showRedirectPrompt(url);
                return;
            }
            window.location.href = url;
        },
        get: function() {
            return document.location;
        }
    });
    
    // Intercept intent:// links
    Element.prototype.setAttribute = function(name, value) {
        if (name === 'href' && shouldBlockUrl(value)) {
            // Store the original URL
            const originalUrl = value;
            
            // Replace it with a javascript: URL that will trigger our handler
            originalSetAttribute.call(this, 'href', 'javascript:void(0)');
            originalSetAttribute.call(this, 'data-original-url', originalUrl);
            
            // Add click handler
            this.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                showRedirectPrompt(originalUrl);
                return false;
            });
            
            return;
        }
        return originalSetAttribute.call(this, name, value);
    };
    
    // Handle click events on links that might have been added before our script ran
    document.addEventListener('click', function(e) {
        const target = e.target.closest('a');
        if (!target) return;
        
        const href = target.getAttribute('href');
        if (shouldBlockUrl(href)) {
            e.preventDefault();
            e.stopPropagation();
            showRedirectPrompt(href);
            return false;
        }
    }, true); // Use capture phase to intercept events before they reach the link
    
    // MutationObserver to watch for dynamically added links
    const observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.type === 'attributes' && 
                mutation.attributeName === 'href' && 
                mutation.target.tagName === 'A') {
                
                const href = mutation.target.getAttribute('href');
                if (shouldBlockUrl(href)) {
                    log(`Found dynamic app link: ${href}`);
                    
                    // Replace with safe version
                    const originalUrl = href;
                    mutation.target.setAttribute('href', 'javascript:void(0)');
                    mutation.target.setAttribute('data-original-url', originalUrl);
                    
                    mutation.target.addEventListener('click', function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        showRedirectPrompt(originalUrl);
                        return false;
                    });
                }
            }
        });
    });
    
    // Start observing when the DOM is ready
    document.addEventListener('DOMContentLoaded', function() {
        observer.observe(document.body, {
            attributes: true,
            attributeFilter: ['href'],
            subtree: true
        });
        
        log("MutationObserver started");
    });
    
    // Add additional schemes to block
    function addBlockedScheme(scheme) {
        if (!scheme.endsWith(':')) {
            scheme += ':';
        }
        if (!blockedSchemes.includes(scheme)) {
            blockedSchemes.push(scheme);
            log(`Added scheme to blocklist: ${scheme}`);
        }
    }
    
    // Expose function to add schemes (can be called from console)
    window.addAppRedirectScheme = addBlockedScheme;
    
    log("Anti-App Redirect initialized successfully");
})();