fudium

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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
// @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();
})();