NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name fudium // @namespace https://github.com/ThoriqFathurrozi/ // @version 1.1 // @description Tampermonkey/Greasemonkey script hack for Medium articles – zaps paywalls overlays nags so you can read without the noise. Not affiliated with Medium. Use at your own risk. // @author frrzyriq // @match https://medium.com // @match https://*.medium.com/* // @match https://*/* // @icon https://miro.medium.com/v2/5d8de952517e8160e40ef9841c781cdc14a5db313057fa3c3de41c6f5b494b19 // @grant none // @run-at document-end // @noframes // @homepageURL https://github.com/ThoriqFathurrozi/fudium // @updateURL https://openuserjs.org/meta/frrzyriq/fudium.meta.js // @downloadURL https://openuserjs.org/src/scripts/frrzyriq/fudium.user.js // @license MIT; https://raw.githubusercontent.com/ThoriqFathurrozi/fudium/refs/heads/main/LICENSE // ==/UserScript== (async () => { 'use strict'; const FREEDIUM_URL = 'https://freedium.cfd/'; const BANNER_ID_ARTICLE = 'fudium-article-banner'; const BANNER_ID_PAGE = 'fudium-page-banner'; // Utility function to wait for elements const waitForElement = async (selector, timeout = 5000, multiple = false) => { const queryMethod = multiple ? 'querySelectorAll' : 'querySelector'; const existing = document[queryMethod](selector); if (multiple ? existing.length > 0 : existing) return existing; return new Promise((resolve, reject) => { const observer = new MutationObserver(() => { const element = document[queryMethod](selector); if (multiple ? element.length > 0 : element) { observer.disconnect(); clearTimeout(timeoutId); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); const timeoutId = setTimeout(() => { observer.disconnect(); resolve(multiple ? [] : null); }, timeout); }); }; // Create banner elements const createBanner = (link, isPageBanner = false) => { const banner = document.createElement('a'); banner.href = FREEDIUM_URL + link; banner.id = isPageBanner ? BANNER_ID_PAGE : BANNER_ID_ARTICLE; Object.assign(banner.style, { position: isPageBanner ? 'fixed' : 'absolute', padding: '10px', borderRadius: '5px', color: 'white', zIndex: '498', backgroundColor: 'rgba(0, 0, 0, 0.5)', textDecoration: 'none', fontSize: '14px', fontWeight: 'bold', ...(isPageBanner ? { bottom: '50vh', right: '20px' } : { top: '0', right: '0' } ) }); banner.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="16" fill="white" style="vertical-align: middle; margin-right: 5px;"> <path d="M320 96c-35.3 0-64 28.7-64 64v64h192c35.3 0 64 28.7 64 64v224c0 35.3-28.7 64-64 64H192c-35.3 0-64-28.7-64-64V288c0-35.3 28.7-64 64-64v-64c0-70.7 57.3-128 128-128 63.5 0 116.1 46.1 126.2 106.7 2.9 17.4-8.8 33.9-26.3 36.9s-33.9-8.8-36.9-26.3c-5-30.2-31.3-53.3-63-53.3m40 328c13.3 0 24-10.7 24-24s-10.7-24-24-24h-80c-13.3 0-24 10.7-24 24s10.7 24 24 24z"/> </svg> Open Free `; return banner; }; // Check if article is member-only const isMemberOnlyArticle = async (element) => { if (element) { return element.querySelector('button[aria-label="Member-only story"]') !== null; } // Check for page-level paywall indicators const paywallButton = await waitForElement('#paywallButton-programming', 2000); if (!paywallButton) { return false; } return [...document.querySelectorAll('div>p')] .some(p => p.innerText.includes('Member-only story')); }; // Check if current page is an full article page const isFullArticlePage = () => { const locationPath = window.location.pathname; const exceptPath = ['/@MediumStaff/list']; if (exceptPath.some(except => locationPath.includes(except))) return false; const pathParts = locationPath.split('/').filter(Boolean); if (pathParts.length === 0) return false; const lastPart = pathParts[pathParts.length - 1]; const possibleHash = lastPart.split('-').pop(); return /^[a-f0-9]{12,}$/i.test(possibleHash); }; // Check if we're on Medium const isMediumSite = async () => { try { const logo = await waitForElement('#wordmark-medium-desc', 2000); return logo !== null; } catch { return false; } }; // Add banners to article cards const addArticleBanners = async () => { try { const articles = await waitForElement('article', 4000, true); if (!articles.length) return; for (const article of articles) { const linkElement = article.querySelector('div[role="link"]'); if (!linkElement || linkElement.querySelector(`#${BANNER_ID_ARTICLE}`)) continue; if (await isMemberOnlyArticle(linkElement)) { linkElement.style.position = 'relative'; linkElement.appendChild(createBanner(linkElement.dataset.href)); } } } catch (error) { console.error('Error adding article banners:', error); } }; // Add banner to article page const addPageBanner = async () => { if (document.querySelector(`#${BANNER_ID_PAGE}`) || !await isMemberOnlyArticle()) { return; } try { const heading = await waitForElement('h1', 4000); if (heading?.parentElement) { heading.parentElement.style.position = 'relative'; heading.parentElement.appendChild(createBanner(window.location.href, true)); } } catch (error) { console.error('Error adding page banner:', error); } }; // Remove page banner if not on article page const cleanupPageBanner = () => { const banner = document.querySelector(`#${BANNER_ID_PAGE}`); if (banner) banner.remove(); }; // Throttled scroll handler let scrollTimeout; const handleScroll = () => { if (scrollTimeout) return; scrollTimeout = setTimeout(async () => { if (isFullArticlePage()) { await addPageBanner(); } else { cleanupPageBanner(); await addArticleBanners(); } scrollTimeout = null; }, 150); }; // Initialize the script const initialize = async () => { if (!await isMediumSite()) return; if (isFullArticlePage()) { await addPageBanner(); } else { await addArticleBanners(); } // Set up scroll listener with throttling window.addEventListener('scroll', handleScroll, { passive: true }); }; // Handle navigation changes (SPA) const handleNavigation = async () => { await new Promise(resolve => setTimeout(resolve, 100)); await initialize(); }; // Listen for navigation events ['popstate', 'pushstate', 'locationchange'].forEach(event => { window.addEventListener(event, handleNavigation); }); // Start the script await initialize(); })();