// ==UserScript==
// @name Medium Unlocked
// @namespace https://github.com/ShrekBytes
// @description Adds alternate reading links (ReadMedium and Freedium) to Medium paywalled articles with improved reliability.
// @version 3.0.1
// @author ShrekBytes
// @license MIT
// @match https://medium.com/*
// @match https://*.medium.com/*
// @match https://infosecwriteups.com/*
// @match https://*.infosecwriteups.com/*
// @match https://betterprogramming.pub/*
// @match https://*.betterprogramming.pub/*
// @match https://betterhumans.pub/*
// @match https://*.betterhumans.pub/*
// @match https://uxplanet.org/*
// @match https://*.uxplanet.org/*
// @match https://writingcooperative.com/*
// @match https://*.writingcooperative.com/*
// @match https://entrepreneurshandbook.co/*
// @match https://*.entrepreneurshandbook.co/*
// @match https://medium.muz.li/*
// @match https://*.medium.muz.li/*
// @match https://blog.prototypr.io/*
// @match https://*.blog.prototypr.io/*
// @match https://bettermarketing.pub/*
// @match https://*.bettermarketing.pub/*
// @match https://byrslf.co/*
// @match https://*.byrslf.co/*
// @match https://levelup.gitconnected.com/*
// @match https://*.levelup.gitconnected.com/*
// @match https://javascript.plainenglish.io/*
// @match https://*.javascript.plainenglish.io/*
// @match https://thebelladonnacomedy.com/*
// @match https://*.thebelladonnacomedy.com/*
// @match https://medium.datadriveninvestor.com/*
// @match https://*.medium.datadriveninvestor.com/*
// @match https://itnext.io/*
// @match https://*.itnext.io/*
// @match https://proandroiddev.com/*
// @match https://*.proandroiddev.com/*
// @match https://code.likeagirl.io/*
// @match https://*.code.likeagirl.io/*
// @match https://blog.bitsrc.io/*
// @match https://*.blog.bitsrc.io/*
// @match https://uxdesign.cc/*
// @match https://*.uxdesign.cc/*
// @match https://thebolditalic.com/*
// @match https://*.thebolditalic.com/*
// @match https://towardsdatascience.com/*
// @match https://*.towardsdatascience.com/*
// @match https://medium.freecodecamp.org/*
// @match https://*.medium.freecodecamp.org/*
// @match https://hackernoon.com/*
// @match https://*.hackernoon.com/*
// @match https://codeburst.io/*
// @match https://*.codeburst.io/*
// @match https://blog.usejournal.com/*
// @match https://*.blog.usejournal.com/*
// @match https://chatbotslife.com/*
// @match https://*.chatbotslife.com/*
// @match https://plainenglish.io/*
// @match https://*.plainenglish.io/*
// @match https://blog.devgenius.io/*
// @match https://*.blog.devgenius.io/*
// @match https://aws.plainenglish.io/*
// @match https://*.aws.plainenglish.io/*
// @match https://python.plainenglish.io/*
// @match https://*.python.plainenglish.io/*
// @match https://medium.com/@*
// @match https://link.medium.com/*
// @match https://stories.medium.com/*
// @icon https://raw.githubusercontent.com/ShrekBytes/medium-unlocked/refs/heads/main/freedom.png
// @grant none
// @noframes
// @run-at document-start
// @homepageURL https://github.com/ShrekBytes/medium-unlocked
// @supportURL https://github.com/ShrekBytes/medium-unlocked/issues
// ==/UserScript==
(function() {
'use strict';
// State management
const state = {
buttonsAdded: false,
currentUrl: window.location.href,
isChecking: false,
lastCheck: 0,
observer: null,
checkTimeout: null
};
// Performance optimized selectors - ordered by likelihood and specificity
const PAYWALL_SELECTORS = Object.freeze([
// Primary paywall indicators (most common)
'[data-testid="paywall"]',
'[data-testid="meter-stats"]',
'[data-testid="subscribe-paywall"]',
'.paywall',
// Secondary indicators
'[data-testid="paywall-upsell"]',
'[data-testid="meter-card"]',
'.js-paywall',
'.meteredContent',
'.u-showForMembers',
'.memberPreview',
'.js-memberPreview',
// Content limitation indicators
'.js-truncatedPostBody',
'[data-source="paywall"]',
'[data-post-id][data-source="meter"]',
'.u-lineHeightTighter.u-fontSize18:last-child',
// Subscribe/upgrade prompts
'[data-testid="subscribe-button"]',
'[data-testid="upgrade-button"]',
'.js-upgradeButton',
'.js-subscribeButton'
]);
// Optimized text patterns - case insensitive, ordered by frequency
const PAYWALL_PATTERNS = Object.freeze([
'member-only story',
'subscribe to read',
'become a member',
'sign up to read',
'continue reading with',
'read the full story',
'unlock unlimited',
'upgrade to continue',
'this story is published in',
'get unlimited access'
]);
// Efficient paywall detection with early returns
function isPaywalled() {
// Quick DOM-based detection first (fastest)
for (const selector of PAYWALL_SELECTORS) {
if (document.querySelector(selector)) {
return true;
}
}
// Text-based detection (slower, but thorough)
const bodyText = document.body?.textContent?.toLowerCase();
if (!bodyText) return false;
return PAYWALL_PATTERNS.some(pattern => bodyText.includes(pattern));
}
// Optimized button creation with minimal DOM operations
function createButton(text, url, top) {
const button = document.createElement('a');
// Set properties in batch for better performance
Object.assign(button, {
innerHTML: text,
href: url,
target: '_blank',
rel: 'noopener noreferrer',
className: 'medium-unlock-btn'
});
// Optimized styles as single string
button.style.cssText = `
position:fixed;top:${top}px;right:64px;z-index:9999;
background:rgba(64, 64, 128,.33);backdrop-filter:blur(2px);
color:#000;border:1px solid #000;border-radius:2px;
font:400 14px/-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
cursor:pointer;width:128px;height:36px;
display:flex;align-items:center;justify-content:center;
text-decoration:none;box-sizing:border-box;
`;
return button;
}
// Efficient button management
function addButtons() {
if (state.buttonsAdded || !document.body) return;
const url = encodeURIComponent(window.location.href);
const fragment = document.createDocumentFragment();
// Create buttons in memory first
fragment.appendChild(createButton('ReadMedium', `https://readmedium.com/en/${url}`, 400));
fragment.appendChild(createButton('Freedium', `https://freedium.cfd/${url}`, 440));
// Single DOM append operation
document.body.appendChild(fragment);
state.buttonsAdded = true;
}
function removeButtons() {
if (!state.buttonsAdded) return;
// Use more specific selector to avoid conflicts
const buttons = document.querySelectorAll('.medium-unlock-btn');
if (buttons.length > 0) {
buttons.forEach(btn => btn.remove());
state.buttonsAdded = false;
}
}
// Throttled paywall check to prevent excessive calls
function checkPaywall() {
const now = Date.now();
// Prevent rapid successive checks
if (state.isChecking || (now - state.lastCheck) < 100) {
return;
}
state.isChecking = true;
state.lastCheck = now;
try {
const isPaywalledNow = isPaywalled();
if (isPaywalledNow && !state.buttonsAdded) {
addButtons();
} else if (!isPaywalledNow && state.buttonsAdded) {
removeButtons();
}
} catch (error) {
// Silent error handling - don't break the page
} finally {
state.isChecking = false;
}
}
// Debounced check for performance
function scheduleCheck(delay = 150) {
if (state.checkTimeout) {
clearTimeout(state.checkTimeout);
}
state.checkTimeout = setTimeout(() => {
checkPaywall();
state.checkTimeout = null;
}, delay);
}
// Optimized mutation observer with smart filtering
function createObserver() {
return new MutationObserver((mutations) => {
let shouldCheck = false;
// Efficient mutation analysis
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check only Element nodes for relevant changes
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
// Check if added node or its children contain paywall indicators
if (element.matches?.(PAYWALL_SELECTORS.join(',')) ||
element.querySelector?.(PAYWALL_SELECTORS.join(','))) {
shouldCheck = true;
break;
}
}
}
if (shouldCheck) break;
}
}
if (shouldCheck) {
scheduleCheck();
}
});
}
// Handle URL changes efficiently
function handleUrlChange() {
const newUrl = window.location.href;
if (newUrl !== state.currentUrl) {
state.currentUrl = newUrl;
removeButtons();
scheduleCheck(300); // Slight delay for page transition
}
}
// Optimized history API interception
function interceptHistory() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
const result = originalPushState.apply(this, args);
setTimeout(handleUrlChange, 0);
return result;
};
history.replaceState = function(...args) {
const result = originalReplaceState.apply(this, args);
setTimeout(handleUrlChange, 0);
return result;
};
}
// Initialize with optimal timing
function initialize() {
// Immediate check if DOM is ready
if (document.readyState !== 'loading') {
scheduleCheck(0);
}
// Setup observers and listeners
if (!state.observer) {
state.observer = createObserver();
// Wait for body to be available
const startObserving = () => {
if (document.body) {
state.observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-testid', 'class', 'data-source']
});
} else {
setTimeout(startObserving, 50);
}
};
startObserving();
}
// Event listeners with passive option for performance
window.addEventListener('popstate', handleUrlChange, { passive: true });
// Handle visibility changes
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
scheduleCheck(50);
}
}, { passive: true });
// DOM ready fallback
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => scheduleCheck(0), { once: true });
}
}
// Cleanup function
function cleanup() {
if (state.observer) {
state.observer.disconnect();
state.observer = null;
}
if (state.checkTimeout) {
clearTimeout(state.checkTimeout);
state.checkTimeout = null;
}
removeButtons();
state.isChecking = false;
}
// Handle page unload
window.addEventListener('beforeunload', cleanup, { passive: true });
// Start everything
interceptHistory();
initialize();
})();