Greasy Fork

Greasy Fork is available in English.

Medium Member Bypass Modified

Modern Medium GUI with multiple bypass services.

当前为 2025-02-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         Medium Member Bypass Modified
// @author       UniverseDev
// @license      GPL-3.0-or-later
// @namespace    http://tampermonkey.net/
// @version      13.9.8
// @description  Modern Medium GUI with multiple bypass services.
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(() => {
  'use strict';
  const CLASS_SETTINGS = 'medium-settings';
  const CLASS_NOTIFICATION = 'medium-notification';
  const SELECTOR_MEMBER_WALL_CHECK = 'div.s.u.w.fg.fh.q';
  const SELECTOR_FREEDIUM_CLOSE_BUTTON = '.close-button';
  const SELECTOR_SEARCH_BAR = 'div.ax.h';
  const SELECTOR_MEMBER_INDICATOR = 'div.bm > div > svg + p';
  const getSetting = (key, defaultValue) => GM_getValue(key, defaultValue);
  const setSetting = (key, value) => GM_setValue(key, value);
  const config = {
    bypassUrls: {
      freedium: 'https://freedium.cfd',
      readmedium: 'https://readmedium.com',
      libmedium: 'https://md.vern.cc/',
      archive: 'https://archive.is/newest/',
      archiveLi: 'https://archive.li/newest/',
      archiveVn: 'https://archive.vn/newest/',
      archivePh: 'https://archive.ph/newest/'
    },
    currentBypassIndex: getSetting('currentBypassIndex', 0),
    autoRedirectDelay: getSetting('redirectDelay', 5000),
    autoRedirectEnabled: getSetting('autoRedirect', true),
    darkModeEnabled: getSetting('darkModeEnabled', false),
    isBypassSession: getSetting('isBypassSession', false)
  };
  let bypassServiceKeys = Object.keys(config.bypassUrls);
  if (window.location.hostname !== 'medium.com') {
    bypassServiceKeys = bypassServiceKeys.filter(key => key !== 'libmedium');
  }
  let isRedirecting = false;
  let originalArticleUrl;
  let isSettingsVisible = false;
  const injectStyles = () => {
    const style = document.createElement('style');
    style.textContent = `
      .${CLASS_SETTINGS} {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 360px;
        background-color: var(--background-color, white);
        box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        border-radius: 16px;
        font-family: Arial, sans-serif;
        z-index: 10000;
        padding: 20px;
        display: none;
        color: var(--text-color, #333);
        cursor: grab;
        user-select: none;
      }
      .${CLASS_SETTINGS}.dark {
        --background-color: #121212;
        --text-color: white;
      }
      .medium-settings-header {
        font-size: 22px;
        font-weight: bold;
        margin-bottom: 20px;
        text-align: center;
      }
      .medium-settings-toggle {
        margin: 15px 0;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .medium-settings-toggle > span { flex-grow: 1; }
      .medium-settings-input {
        margin-left: 10px;
        padding: 8px 10px;
        border: 1px solid #ccc;
        border-radius: 8px;
        box-sizing: border-box;
        color: #333;
        background-color: white;
      }
      .medium-settings.dark .medium-settings-input {
        color: white;
        background-color: #333;
        border-color: #666;
      }
      .medium-settings-input#redirectDelay { width: 70px; }
      .medium-settings-input#bypassSelector {
        width: 120px;
        appearance: auto;
        background-repeat: no-repeat;
        background-position: right 10px center;
        font-family: Arial, sans-serif;
        font-size: 16px;
        line-height: 1.4;
      }
      .medium-settings-input#bypassSelector option {
        padding: 8px;
        background-color: white;
        color: #333;
        transition: background-color 0.2s ease;
      }
      .medium-settings-input#bypassSelector option:hover {
        background-color: #1a8917;
        color: white;
      }
      .medium-settings.dark .medium-settings-input#bypassSelector option {
        background-color: #444;
        color: white;
      }
      .medium-settings.dark .medium-settings-input#bypassSelector option:hover {
        background-color: #555;
      }
      .medium-settings-button {
        background-color: var(--button-bg-color, #1a8917);
        color: var(--button-text-color, white);
        border: none;
        padding: 8px 14px;
        border-radius: 20px;
        cursor: pointer;
        font-weight: bold;
        transition: background-color 0.3s;
      }
      .medium-settings-button:hover { background-color: #155c11; }
      .${CLASS_NOTIFICATION} {
        position: fixed;
        bottom: 20px;
        right: 20px;
        background-color: #1a8917;
        color: white;
        padding: 15px;
        border-radius: 20px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        font-family: Arial, sans-serif;
        z-index: 10000;
        opacity: 0;
        transform: translateY(20px);
        transition: all 0.3s ease;
      }
      .${CLASS_NOTIFICATION}.show {
        opacity: 1;
        transform: translateY(0);
      }
      .medium-settings-input:focus {
        outline: none;
        border-color: #1a8917;
        box-shadow: 0 0 5px rgba(26,137,23,0.3);
      }
      .switch {
        position: relative;
        display: inline-block;
        width: 40px;
        height: 24px;
      }
      .switch input { opacity: 0; width: 0; height: 0; }
      .slider {
        position: absolute;
        cursor: pointer;
        top: 0; left: 0; right: 0; bottom: 0;
        background-color: #ccc;
        transition: .4s;
      }
      .slider:before {
        position: absolute;
        content: "";
        height: 16px;
        width: 16px;
        left: 4px;
        bottom: 4px;
        background-color: white;
        transition: .4s;
      }
      input:checked + .slider { background-color: #1a8917; }
      input:focus + .slider { box-shadow: 0 0 1px #1a8917; }
      input:checked + .slider:before { transform: translateX(16px); }
      .slider.round { border-radius: 34px; }
      .slider.round:before { border-radius: 50%; }
      .settings-icon-button {
        background: none;
        border: none;
        cursor: pointer;
        padding: 4px;
        margin: 0 4px 0 8px;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .settings-icon {
        width: 32px;
        height: 32px;
        fill: #757575;
        opacity: 0.7;
        transition: opacity 0.3s ease;
      }
      .settings-icon-button:hover .settings-icon {
        fill: #333;
        opacity: 1;
      }
    `;
    document.head.appendChild(style);
  };
  const stealthNotification = message => {
    const notification = document.createElement('div');
    notification.className = CLASS_NOTIFICATION;
    notification.textContent = message;
    document.body.appendChild(notification);
    setTimeout(() => notification.classList.add('show'), 50);
    setTimeout(() => {
      notification.classList.remove('show');
      setTimeout(() => notification.remove(), 300);
    }, 3000);
  };
  const getCurrentBypassService = () => bypassServiceKeys[config.currentBypassIndex % bypassServiceKeys.length];
  const switchToNextBypassService = () => {
    config.currentBypassIndex++;
    setSetting('currentBypassIndex', config.currentBypassIndex);
    stealthNotification(`Trying next bypass service: ${getCurrentBypassService()}`);
  };
  const tryNextBypass = async (articleUrl, attemptNumber) => {
    switchToNextBypassService();
    const nextBypassService = getCurrentBypassService();
    if (nextBypassService) {
      autoBypass(articleUrl, nextBypassService, attemptNumber + 1);
    } else {
      isRedirecting = false;
      stealthNotification("All bypass attempts failed.");
    }
  };
  const autoBypass = async (articleUrl, bypassKey, attemptNumber = 1) => {
    if (sessionStorage.getItem('mediumAntiLoop')) return;
    try {
      let bypassUrl;
      const mediumURL = new URL(decodeURIComponent(originalArticleUrl));
      let pathname = mediumURL.pathname;
      if (bypassKey === 'libmedium') {
        if (pathname.startsWith('/')) pathname = pathname.substring(1);
        bypassUrl = `${config.bypassUrls[bypassKey]}${pathname}`;
      } else if (bypassKey.startsWith('archive')) {
        bypassUrl = config.bypassUrls[bypassKey] + originalArticleUrl;
      } else {
        const bypassBaseURL = new URL(config.bypassUrls[bypassKey]);
        bypassUrl = new URL(mediumURL.pathname, bypassBaseURL).href;
      }
      sessionStorage.setItem('mediumAntiLoop', 'true');
      if (bypassKey.startsWith('archive')) {
        window.open(bypassUrl, '_self');
      } else {
        window.location.href = bypassUrl;
      }
      isRedirecting = true;
    } catch (error) {
      tryNextBypass(articleUrl, attemptNumber);
    }
  };
  const attachSettingsListeners = settingsContainer => {
    settingsContainer.querySelector('#bypassSelector').addEventListener('change', e => {
      const selectedKey = e.target.value;
      config.currentBypassIndex = bypassServiceKeys.indexOf(selectedKey);
      setSetting('currentBypassIndex', config.currentBypassIndex);
      stealthNotification(`Bypass service set to ${selectedKey}`);
    });
    settingsContainer.querySelector('#toggleRedirectCheckbox').addEventListener('change', () => {
      config.autoRedirectEnabled = settingsContainer.querySelector('#toggleRedirectCheckbox').checked;
      setSetting('autoRedirect', config.autoRedirectEnabled);
      stealthNotification('Auto-Redirect toggled');
    });
    settingsContainer.querySelector('#toggleDarkModeCheckbox').addEventListener('change', () => {
      config.darkModeEnabled = settingsContainer.querySelector('#toggleDarkModeCheckbox').checked;
      setSetting('darkModeEnabled', config.darkModeEnabled);
      settingsContainer.classList.toggle('dark', config.darkModeEnabled);
      stealthNotification('Dark Mode toggled');
    });
    settingsContainer.querySelector('#bypassNow').addEventListener('click', async () => {
      stealthNotification('Attempting bypass...');
      setSetting('isBypassSession', true);
      await autoBypass(originalArticleUrl, getCurrentBypassService());
    });
    settingsContainer.querySelector('#resetDefaults').addEventListener('click', () => {
      config.autoRedirectDelay = 5000;
      config.autoRedirectEnabled = true;
      config.darkModeEnabled = false;
      config.currentBypassIndex = 0;
      setSetting('redirectDelay', config.autoRedirectDelay);
      setSetting('autoRedirect', config.autoRedirectEnabled);
      setSetting('darkModeEnabled', config.darkModeEnabled);
      setSetting('currentBypassIndex', config.currentBypassIndex);
      settingsContainer.querySelector('#redirectDelay').value = config.autoRedirectDelay;
      settingsContainer.querySelector('#toggleRedirectCheckbox').checked = config.autoRedirectEnabled;
      settingsContainer.querySelector('#toggleDarkModeCheckbox').checked = config.darkModeEnabled;
      settingsContainer.querySelector('#bypassSelector').innerHTML = bypassServiceKeys.map((key, index) => `
        <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
      `).join('');
      settingsContainer.classList.remove('dark');
      stealthNotification('Settings reset to defaults');
    });
    const saveButton = settingsContainer.querySelector('#saveSettings');
    saveButton.addEventListener('click', () => {
      const delayInput = settingsContainer.querySelector('#redirectDelay');
      const newDelay = parseInt(delayInput.value, 10);
      if (!isNaN(newDelay) && newDelay >= 0) {
        config.autoRedirectDelay = newDelay;
        setSetting('redirectDelay', newDelay);
        saveButton.textContent = 'Saved!';
        setTimeout(() => { saveButton.textContent = 'Save'; }, 1500);
      } else {
        stealthNotification('Invalid redirect delay. Please enter a positive number.');
        delayInput.value = config.autoRedirectDelay;
      }
    });
    settingsContainer.querySelector('#closeSettings').addEventListener('click', () => { hideMediumSettings(); });
  };
  const showMediumSettings = () => {
    let existingPanel = document.querySelector(`.${CLASS_SETTINGS}`);
    if (existingPanel) {
      existingPanel.style.display = 'block';
      isSettingsVisible = true;
      return;
    }
    const settingsContainer = document.createElement('div');
    settingsContainer.className = `${CLASS_SETTINGS} ${config.darkModeEnabled ? 'dark' : ''}`;
    settingsContainer.innerHTML = `
      <div class="medium-settings-header">Medium Settings</div>
      <div class="medium-settings-toggle">
        <span>Auto-Redirect</span>
        <label class="switch">
          <input type="checkbox" id="toggleRedirectCheckbox" ${config.autoRedirectEnabled ? 'checked' : ''}>
          <span class="slider round"></span>
        </label>
      </div>
      <div class="medium-settings-toggle">
        <span>Redirect Delay (ms)</span>
        <input type="number" class="medium-settings-input" id="redirectDelay" value="${config.autoRedirectDelay}" />
      </div>
      <div class="medium-settings-toggle">
        <span>Dark Mode</span>
        <label class="switch">
          <input type="checkbox" id="toggleDarkModeCheckbox" ${config.darkModeEnabled ? 'checked' : ''}>
          <span class="slider round"></span>
        </label>
      </div>
      <div class="medium-settings-toggle">
        <span>Bypass Service</span>
        <select id="bypassSelector" class="medium-settings-input">
          ${bypassServiceKeys.map((key, index) => `
            <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
          `).join('')}
        </select>
      </div>
      <div class="medium-settings-toggle">
        <button class="medium-settings-button" id="bypassNow">Bypass Now</button>
      </div>
      <div class="medium-settings-toggle">
        <button class="medium-settings-button" id="resetDefaults">Reset to Default</button>
      </div>
      <div class="medium-settings-toggle">
        <button class="medium-settings-button" id="saveSettings">Save</button>
        <button class="medium-settings-button" id="closeSettings">Close</button>
      </div>
    `;
    attachSettingsListeners(settingsContainer);
    let isDragging = false, startX, startY;
    settingsContainer.addEventListener('mousedown', e => {
      if (e.target.closest('.medium-settings-input, .medium-settings-button, label')) return;
      isDragging = true;
      startX = e.clientX - settingsContainer.offsetLeft;
      startY = e.clientY - settingsContainer.offsetTop;
      settingsContainer.style.cursor = 'grabbing';
    });
    document.addEventListener('mousemove', e => { if (!isDragging) return; settingsContainer.style.left = `${e.clientX - startX}px`; settingsContainer.style.top = `${e.clientY - startY}px`; });
    document.addEventListener('mouseup', () => { isDragging = false; settingsContainer.style.cursor = 'grab'; });
    document.body.appendChild(settingsContainer);
    settingsContainer.style.display = 'block';
    isSettingsVisible = true;
  };
  const hideMediumSettings = () => {
    const settingsPanel = document.querySelector(`.${CLASS_SETTINGS}`);
    if (settingsPanel) { settingsPanel.style.display = 'none'; isSettingsVisible = false; }
  };
  const toggleMediumSettings = () => { isSettingsVisible ? hideMediumSettings() : showMediumSettings(); };
  const createSettingsIconButton = () => {
    const button = document.createElement('button');
    button.className = 'settings-icon-button';
    button.innerHTML = `<svg class="settings-icon" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.6-.94l-.37-2.54c-.05-.25-.28-.43-.53-.43h-3.82c-.25 0-.48.17-.53.43l-.37 2.54c-.56.25-1.09.56-1.6.94l-2.39-.96c-.22-.07-.47 0-.59.22L2.74 8.87c-.11.2-.06.47.12.61l2.03 1.58c-.05.3-.07.61-.07.94s.02.64.07.94L2.86 14.51c-.18.14-.24.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.6.94l.37 2.54c.05.25.28.43.53.43h3.82c.25 0 .48-.17.53-.43l.37-2.54c.56-.25 1.09-.56 1.6-.94l2.39.96c.22.07.47 0 .59-.22l1.92-3.32c.12-.21.07-.47-.12-.61l-2.01-1.56zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg>`;
    button.addEventListener('click', toggleMediumSettings);
    return button;
  };
  const autoCloseMemberBanner = () => {
    const closeButtons = document.querySelectorAll('button[data-testid="close-button"]');
    closeButtons.forEach(button => button.click());
  };
  const performAutoRedirect = async () => {
    if (sessionStorage.getItem('mediumAntiLoop')) return;
    const wallElement = document.querySelector(SELECTOR_MEMBER_WALL_CHECK);
    const indicator = document.querySelector(SELECTOR_MEMBER_INDICATOR);
    const isMemberArticle = wallElement || (indicator && indicator.textContent.includes('Member-only story'));
    if (config.autoRedirectEnabled && isMemberArticle && !isRedirecting) {
      isRedirecting = true;
      let currentBypass = getCurrentBypassService();
      if (currentBypass) {
        stealthNotification(`Attempting bypass with ${currentBypass}...`);
        setTimeout(async () => {
          setSetting('isBypassSession', true);
          await autoBypass(originalArticleUrl, currentBypass);
        }, config.autoRedirectDelay);
      } else {
        stealthNotification("No available bypass services to try.");
        isRedirecting = false;
      }
    }
  };
  const autoCloseFreediumBanner = () => {
    if (window.location.hostname === 'freedium.cfd') {
      window.addEventListener('load', () => {
        const closeButton = document.querySelector(SELECTOR_FREEDIUM_CLOSE_BUTTON);
        if (closeButton) closeButton.click();
      });
    }
  };
  const insertSettingsButton = () => {
    const settingsIconButton = createSettingsIconButton();
    const searchBar = document.querySelector(SELECTOR_SEARCH_BAR);
    if (searchBar && searchBar.parentNode) {
      searchBar.parentNode.insertBefore(settingsIconButton, searchBar.nextSibling);
    }
  };
  const memberCheck = () => {
    const wallElement = document.querySelector(SELECTOR_MEMBER_WALL_CHECK);
    const indicator = document.querySelector(SELECTOR_MEMBER_INDICATOR);
    return wallElement || (indicator && indicator.textContent.includes('Member-only story'));
  };
  const initializeScript = () => {
    originalArticleUrl = window.location.href;
    injectStyles();
    autoCloseFreediumBanner();
    if (memberCheck()) {
      GM_registerMenuCommand('Open Medium Settings', showMediumSettings);
      insertSettingsButton();
    }
    performAutoRedirect();
    autoCloseMemberBanner();
  };
  if (Object.values(config.bypassUrls).some((url) => window.location.href.startsWith(url) ||
      bypassServiceKeys.some(key => key.startsWith('archive') && window.location.href.startsWith(config.bypassUrls[key])))) {
    sessionStorage.setItem('mediumAntiLoop', 'true');
    isRedirecting = false;
  } else if (document.head?.querySelector('meta[property="al:android:url"]')?.content?.includes('medium://p/')) {
    initializeScript();
  }
})();