Unlock Unlimited Medium

Unlock all Medium-based articles via freedium.cfd with enhanced detection and performance optimizations

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Unlock Unlimited Medium
// @namespace    https://github.com/htrnguyen/User-Scripts/tree/main/Unlock-Unlimited-Medium
// @version      2.0
// @description  Unlock all Medium-based articles via freedium.cfd with enhanced detection and performance optimizations
// @author       Hà Trọng Nguyễn
// @license      MIT
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @supportURL   https://github.com/htrnguyen/User-Scripts/tree/main/Unlock-Unlimited-Medium/issues
// @homepage     https://freedium.cfd/
// @icon         https://github.com/htrnguyen/User-Scripts/raw/main/Unlock-Unlimited-Medium/Unlock%20Unlimited%20Medium%20Logo.png
// ==/UserScript==

;(function () {
    'use strict'

    // Configuration
    const CONFIG = {
        FREEDIUM_BASE: 'https://freedium.cfd/',
        BUTTON_POSITION: GM_getValue('buttonPosition', 'bottom-right'),
        SHOW_NOTIFICATIONS: GM_getValue('showNotifications', true),
        DETECTION_INTERVAL: 1000, // ms
        CACHE_DURATION: 300000 // 5 minutes
    }

    // Cache để tránh kiểm tra lại nhiều lần
    const cache = new Map()
    let isProcessing = false
    let unlockButton = null

    // Utility functions
    const debounce = (func, delay) => {
        let timeoutId
        return (...args) => {
            clearTimeout(timeoutId)
            timeoutId = setTimeout(() => func.apply(null, args), delay)
        }
    }

    const throttle = (func, limit) => {
        let inThrottle
        return (...args) => {
            if (!inThrottle) {
                func.apply(null, args)
                inThrottle = true
                setTimeout(() => inThrottle = false, limit)
            }
        }
    }

    // Enhanced URL generation with fallback options
    function generateFreediumURL(originalUrl) {
        try {
            const cleanUrl = originalUrl.split('?')[0].split('#')[0]
            return `${CONFIG.FREEDIUM_BASE}${cleanUrl}`
        } catch (error) {
            console.warn('Failed to generate Freedium URL:', error)
            return `${CONFIG.FREEDIUM_BASE}${originalUrl}`
        }
    }

    // Enhanced Medium detection with caching
    function isMediumSite() {
        const hostname = window.location.hostname.toLowerCase()
        const cacheKey = `medium-site-${hostname}`
        
        if (cache.has(cacheKey)) {
            return cache.get(cacheKey)
        }

        const mediumDomains = [
            'medium.com',
            'osintteam.blog',
            'towardsdatascience.com',
            'hackernoon.com',
            'levelup.gitconnected.com',
            'javascript.plainenglish.io',
            'betterprogramming.pub',
            'infosecwriteups.com'
        ]

        const isMedium = mediumDomains.some(domain => hostname.includes(domain)) ||
                        detectMediumByContent()

        cache.set(cacheKey, isMedium)
        setTimeout(() => cache.delete(cacheKey), CONFIG.CACHE_DURATION)
        
        return isMedium
    }

    // Enhanced content-based Medium detection
    function detectMediumByContent() {
        const indicators = [
            // Logo selectors
            'a[data-testid="headerMediumLogo"]',
            'a[aria-label="Homepage"]',
            'svg[data-testid="mediumLogo"]',
            
            // Meta tags
            'meta[property="al:web:url"][content*="medium.com"]',
            'meta[name="twitter:app:name:iphone"][content="Medium"]',
            'meta[property="og:site_name"][content="Medium"]',
            
            // Class patterns
            '.meteredContent',
            '[data-module-result="stream"]',
            '.js-postListHandle'
        ]

        return indicators.some(selector => document.querySelector(selector))
    }

    // Enhanced link detection
    function isMediumArticleLink(link) {
        try {
            const url = new URL(link.href)
            const hostname = url.hostname.toLowerCase()
            
            // Direct domain check
            const mediumDomains = ['medium.com', 'osintteam.blog']
            if (mediumDomains.some(domain => hostname.includes(domain))) {
                return true
            }

            // Check for article patterns
            const articlePatterns = [
                /\/[a-f0-9-]{36}$/,  // Medium article ID pattern
                /\/@[\w-]+\/[\w-]+/,  // Author/article pattern
                /\/p\/[a-f0-9-]+$/    // Publication pattern
            ]

            if (articlePatterns.some(pattern => pattern.test(url.pathname))) {
                return true
            }

            // Check parent elements for Medium indicators
            const parent = link.closest('article, .postArticle, [data-testid="story-container"]')
            if (parent && parent.querySelector('svg[data-testid="mediumLogo"], .clap-count, [data-testid="applauseButton"]')) {
                return true
            }

        } catch (error) {
            console.debug('Error checking Medium link:', error)
        }
        
        return false
    }

    // Open article in Freedium
    function openFreediumArticle(mediumUrl) {
        const freediumUrl = generateFreediumURL(mediumUrl)
        
        if (CONFIG.SHOW_NOTIFICATIONS) {
            showNotification('Opening in Freedium...', 'info')
        }
        
        GM_openInTab(freediumUrl, { active: true })
    }

    // Create and manage unlock button
    function createUnlockButton() {
        if (unlockButton) return

        const btn = document.createElement('button')
        btn.innerHTML = '🔓 <span>Unlock</span>'
        btn.title = 'Unlock this Medium article'
        
        // Enhanced styling
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: CONFIG.BUTTON_POSITION.includes('bottom') ? '20px' : 'auto',
            top: CONFIG.BUTTON_POSITION.includes('top') ? '20px' : 'auto',
            right: CONFIG.BUTTON_POSITION.includes('right') ? '20px' : 'auto',
            left: CONFIG.BUTTON_POSITION.includes('left') ? '20px' : 'auto',
            zIndex: '10000',
            padding: '12px 16px',
            cursor: 'pointer',
            fontSize: '14px',
            fontWeight: '500',
            color: '#fff',
            backgroundColor: '#1a8917',
            border: 'none',
            borderRadius: '8px',
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
            transition: 'all 0.3s ease',
            fontFamily: 'system-ui, -apple-system, sans-serif',
            display: 'flex',
            alignItems: 'center',
            gap: '6px'
        })

        // Hover effects
        btn.addEventListener('mouseenter', () => {
            btn.style.backgroundColor = '#156f12'
            btn.style.transform = 'translateY(-2px)'
            btn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.3)'
        })

        btn.addEventListener('mouseleave', () => {
            btn.style.backgroundColor = '#1a8917'
            btn.style.transform = 'translateY(0)'
            btn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'
        })

        btn.addEventListener('click', throttle(() => {
            openFreediumArticle(window.location.href)
        }, 1000))

        document.body.appendChild(btn)
        unlockButton = btn

        // Auto-hide button after inactivity
        let hideTimeout
        const resetHideTimeout = () => {
            clearTimeout(hideTimeout)
            btn.style.opacity = '1'
            hideTimeout = setTimeout(() => {
                btn.style.opacity = '0.7'
            }, 5000)
        }

        document.addEventListener('mousemove', resetHideTimeout)
        resetHideTimeout()
    }

    // Show notifications
    function showNotification(message, type = 'info') {
        if (!CONFIG.SHOW_NOTIFICATIONS) return

        const notification = document.createElement('div')
        notification.textContent = message
        
        Object.assign(notification.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            zIndex: '10001',
            padding: '12px 16px',
            borderRadius: '6px',
            color: '#fff',
            backgroundColor: type === 'error' ? '#dc3545' : '#28a745',
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
            fontFamily: 'system-ui, -apple-system, sans-serif',
            fontSize: '14px',
            transition: 'all 0.3s ease'
        })

        document.body.appendChild(notification)
        
        setTimeout(() => {
            notification.style.opacity = '0'
            notification.style.transform = 'translateY(-20px)'
            setTimeout(() => notification.remove(), 300)
        }, 3000)
    }

    // Process all links on the page - chỉ để nhận diện, không thêm visual indicator
    function processLinks() {
        if (isProcessing) return
        isProcessing = true

        try {
            const links = document.querySelectorAll('a[href]:not([data-medium-processed])')
            let processedCount = 0

            links.forEach(link => {
                if (isMediumArticleLink(link)) {
                    link.setAttribute('data-medium-processed', 'true')
                    processedCount++
                }
            })

            console.log(`Identified ${processedCount} Medium links`)
        } catch (error) {
            console.error('Error processing links:', error)
        } finally {
            isProcessing = false
        }
    }

    // Debounced link processing for dynamic content
    const debouncedProcessLinks = debounce(processLinks, 500)

    // Initialize script
    function initialize() {
        console.log('Unlock Unlimited Medium: Initializing...')

        // Register menu commands
        GM_registerMenuCommand('🔓 Unlock Current Page', () => {
            openFreediumArticle(location.href)
        })

        GM_registerMenuCommand('🔔 Toggle Notifications', () => {
            CONFIG.SHOW_NOTIFICATIONS = !CONFIG.SHOW_NOTIFICATIONS
            GM_setValue('showNotifications', CONFIG.SHOW_NOTIFICATIONS)
            showNotification(`Notifications ${CONFIG.SHOW_NOTIFICATIONS ? 'enabled' : 'disabled'}`)
        })

        // Check if we're on a Medium site
        if (isMediumSite()) {
            console.log('Medium site detected')
            
            // Tạo nút unlock (không auto-redirect)
            createUnlockButton()
        }

        // Process existing links (chỉ để nhận diện, không can thiệp)
        processLinks()

        // Set up observers for dynamic content
        const observer = new MutationObserver((mutations) => {
            let shouldProcess = false
            
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    const hasNewLinks = Array.from(mutation.addedNodes).some(node => 
                        node.nodeType === Node.ELEMENT_NODE && 
                        (node.tagName === 'A' || node.querySelector('a'))
                    )
                    if (hasNewLinks) shouldProcess = true
                }
            })

            if (shouldProcess) {
                debouncedProcessLinks()
            }
        })

        observer.observe(document.body, {
            childList: true,
            subtree: true
        })

        console.log('Unlock Unlimited Medium: Initialized successfully')
    }

    // Wait for DOM to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize)
    } else {
        initialize()
    }

})();