Medium傻瓜式一键解锁(可配置多源)bypass Medium

在Medium白嫖浏览付费文章,支持多个解锁源。Support for viewing paid articles for medium.com

// ==UserScript==
// @name         Medium傻瓜式一键解锁(可配置多源)bypass Medium
// @namespace    https://www.deviantart.com/yuumei
// @version      1.3
// @description  在Medium白嫖浏览付费文章,支持多个解锁源。Support for viewing paid  articles for medium.com
// @author       mibboy
// @license      GPLv3
// @icon         https://i.imgur.com/Hs7AiY2.png
// @match        *://medium.com/*
// @match        *://*.medium.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==


(function() {
    'use strict';

    // 默认解锁源
    const DEFAULT_SOURCES = [
        {name: 'Freedium', url: 'freedium.cfd', enabled: true},
        {name: 'ReadMedium', url: 'readmedium.com', enabled: false},
        {name: 'Scribe', url: 'scribe.rip', enabled: false}
    ];

    // 获取保存的按钮位置
    function getButtonPosition() {
        return GM_getValue('buttonPosition', {right: '20px', bottom: '20px'});
    }

    // 保存按钮位置
    function saveButtonPosition(position) {
        GM_setValue('buttonPosition', position);
    }

    // 获取保存的解锁源
    function getSources() {
        return GM_getValue('unlockerSources', DEFAULT_SOURCES);
    }

    // 保存解锁源
    function saveSources(sources) {
        GM_setValue('unlockerSources', sources);
    }

    // 创建设置面板
    function createSettingsPanel() {
        const panel = document.createElement('div');
        panel.id = 'medium-unlock-settings';
        panel.innerHTML = `
            <div id="settings-panel" style="
                display: none;
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: white;
                padding: 20px;
                border-radius: 10px;
                box-shadow: 0 0 20px rgba(0,0,0,0.2);
                z-index: 10000;
                min-width: 300px;
                font-family: -apple-system,BlinkMacSystemFont,sans-serif;
            ">
                <h3 style="margin:0 0 15px 0;color:#333;">解锁源设置</h3>
                <div id="sources-list" style="margin-bottom:15px;max-height:300px;overflow-y:auto;"></div>
                <div style="margin-bottom:15px;">
                    <input type="text" id="new-source-name" placeholder="名称" style="margin-right:5px;padding:5px;">
                    <input type="text" id="new-source-url" placeholder="域名" style="margin-right:5px;padding:5px;">
                    <button id="add-source-btn" style="
                        background:#1a8917;
                        color:white;
                        border:none;
                        padding:5px 10px;
                        border-radius:5px;
                        cursor:pointer;
                    ">添加</button>
                </div>
                <div style="text-align:right;">
                    <button id="close-settings-btn" style="
                        background:#666;
                        color:white;
                        border:none;
                        padding:5px 15px;
                        border-radius:5px;
                        cursor:pointer;
                        margin-right:10px;
                    ">关闭</button>
                    <button id="save-settings-btn" style="
                        background:#1a8917;
                        color:white;
                        border:none;
                        padding:5px 15px;
                        border-radius:5px;
                        cursor:pointer;
                    ">保存</button>
                </div>
            </div>
        `;

        document.body.appendChild(panel);

        // 添加事件监听
        document.getElementById('add-source-btn').addEventListener('click', addNewSource);
        document.getElementById('close-settings-btn').addEventListener('click', closeSettings);
        document.getElementById('save-settings-btn').addEventListener('click', saveSettings);
    }

    // 添加新源
    function addNewSource() {
        const nameInput = document.getElementById('new-source-name');
        const urlInput = document.getElementById('new-source-url');

        if(nameInput.value && urlInput.value) {
            const sources = getSources();
            sources.push({
                name: nameInput.value,
                url: urlInput.value,
                enabled: true
            });
            updateSourcesList(sources);
            nameInput.value = '';
            urlInput.value = '';
        }
    }

    // 关闭设置
    function closeSettings() {
        const panel = document.getElementById('settings-panel');
        if(panel) panel.style.display = 'none';
    }

    // 保存设置
    function saveSettings() {
        const sources = [];
        document.querySelectorAll('.source-item').forEach(item => {
            sources.push({
                name: item.querySelector('.source-name').textContent,
                url: item.querySelector('.source-url').textContent,
                enabled: item.querySelector('.source-enabled').checked
            });
        });
        saveSources(sources);
        closeSettings();
        updateUnlockButton();
    }

    // 删除源
    function deleteSource(index) {
        const sources = getSources();
        sources.splice(index, 1);
        updateSourcesList(sources);
    }

    // 更新源列表显示
    function updateSourcesList(sources) {
        const list = document.getElementById('sources-list');
        list.innerHTML = sources.map((source, index) => `
            <div class="source-item" style="
                display:flex;
                align-items:center;
                margin-bottom:10px;
                padding:5px;
                border:1px solid #eee;
                border-radius:5px;
            ">
                <input type="checkbox" class="source-enabled" ${source.enabled ? 'checked' : ''} style="margin-right:10px;">
                <span class="source-name" style="margin-right:10px;min-width:80px;">${source.name}</span>
                <span class="source-url" style="margin-right:10px;color:#666;">${source.url}</span>
                <button onclick="(${deleteSource.toString()})(${index})" style="
                    margin-left:auto;
                    background:#ff4444;
                    color:white;
                    border:none;
                    padding:3px 8px;
                    border-radius:3px;
                    cursor:pointer;
                ">删除</button>
            </div>
        `).join('');
    }

    // 创建可拖动的解锁按钮
    function createUnlockButton() {
        const sources = getSources().filter(s => s.enabled);
        if(sources.length === 0) return;

        const position = getButtonPosition();
        const button = document.createElement('div');
        button.innerHTML = `
            <div id="unlock-button" style="
                position: fixed;
                bottom: ${position.bottom};
                right: ${position.right};
                z-index: 9999;
                display: flex;
                flex-direction: column;
                align-items: flex-end;
                gap: 10px;
                cursor: move;
            ">
                <div class="settings-trigger" style="
                    background: #666;
                    color: white;
                    padding: 8px;
                    border-radius: 50%;
                    cursor: pointer;
                    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                    transition: all 0.3s ease;
                ">
                    ⚙️
                </div>
                ${sources.map(source => `
                    <div class="unlock-option" style="
                        background: #1a8917;
                        color: white;
                        padding: 10px 15px;
                        border-radius: 20px;
                        cursor: pointer;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                        transition: all 0.3s ease;
                        display: flex;
                        align-items: center;
                        font-family: -apple-system,BlinkMacSystemFont,sans-serif;
                    ">
                        <span>${source.name}</span>
                    </div>
                `).join('')}
            </div>
        `;

        document.body.appendChild(button);

        // 添加拖动功能
        const unlockButton = document.getElementById('unlock-button');
        makeDraggable(unlockButton);

        // 添加设置按钮事件
        unlockButton.querySelector('.settings-trigger').addEventListener('click', (e) => {
            e.stopPropagation(); // 防止触发拖动
            document.getElementById('settings-panel').style.display = 'block';
            updateSourcesList(getSources());
        });

        // 添加解锁按钮事件
        unlockButton.querySelectorAll('.unlock-option').forEach((option, index) => {
            option.addEventListener('click', (e) => {
                e.stopPropagation(); // 防止触发拖动
                const currentUrl = window.location.href;
                const unlockUrl = 'https://' + sources[index].url + '/' + currentUrl;
                window.open(unlockUrl, '_blank');
            });

            option.addEventListener('mouseover', function() {
                this.style.transform = 'scale(1.05)';
                this.style.background = '#147811';
            });

            option.addEventListener('mouseout', function() {
                this.style.transform = 'scale(1)';
                this.style.background = '#1a8917';
            });
        });
    }

    // 使元素可拖动
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

        element.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            const newTop = element.offsetTop - pos2;
            const newLeft = element.offsetLeft - pos1;

            // 确保按钮不会超出屏幕
            if (newTop >= 0 && newTop <= window.innerHeight - element.offsetHeight) {
                element.style.top = newTop + "px";
            }
            if (newLeft >= 0 && newLeft <= window.innerWidth - element.offsetWidth) {
                element.style.left = newLeft + "px";
            }
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;

            // 保存最终位置
            saveButtonPosition({
                right: element.style.right,
                bottom: element.style.bottom
            });
        }
    }

    // 检查是否为Medium文章页面
    function isMediumArticle() {
        // 检查多个 Medium 特征
        const mediumFeatures = [
            // 检查是否存在 article 元素
            () => document.querySelector('article') !== null,

            // 检查页面 meta 信息
            () => {
                const generator = document.querySelector('meta[name="generator"]');
                return generator && generator.content.toLowerCase().includes('medium');
            },

            // 检查特定的 Medium CSS 类名
            () => {
                return document.querySelector('.progressiveMedia, .graf--title, .section-content') !== null;
            },

            // 检查 Medium 的特征性 script
            () => {
                const scripts = Array.from(document.getElementsByTagName('script'));
                return scripts.some(script =>
                    script.src && (
                        script.src.includes('medium.com') ||
                        script.src.includes('cdn-client.medium.com')
                    )
                );
            },

            // 检查 Medium 的 API 端点
            () => {
                const links = Array.from(document.getElementsByTagName('link'));
                return links.some(link =>
                    link.href && (
                        link.href.includes('medium.com') ||
                        link.href.includes('cdn-static-1.medium.com')
                    )
                );
            }
        ];

        // 如果满足任意两个特征,就认为是 Medium 文章
        return mediumFeatures.filter(check => check()).length >= 2;
    }

    // 更新解锁按钮
    function updateUnlockButton() {
        const oldButton = document.getElementById('unlock-button');
        if(oldButton) oldButton.remove();
        createUnlockButton();
    }

    // 初始化函数
    function init() {
        // 延迟检查,确保页面完全加载
        setTimeout(() => {
            if(isMediumArticle()) {
                if(!document.getElementById('medium-unlock-settings')) {
                    createSettingsPanel();
                }
                if(!document.getElementById('unlock-button')) {
                    createUnlockButton();
                }
            }
        }, 1500); // 增加延迟时间以确保页面元素加载完成
    }

    // 页面加载和动态导航处理
    if(document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    // 使用 MutationObserver 监听页面变化
    let lastUrl = location.href;
    const observer = new MutationObserver((mutations) => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            init();
        }

        // 检查DOM变化是否添加了新的Medium特征
        if(mutations.some(mutation => mutation.addedNodes.length > 0)) {
            if(!document.getElementById('unlock-button') && isMediumArticle()) {
                init();
            }
        }
    });

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

})();