The most powerful copy protection remover. Works on 99% of websites including Medium, Scribd, Bloomberg, NYTimes, academic papers, recipe sites, and all paywalled content. Smart detection - won't interfere with normal editing.
As of
// ==UserScript==
// @name Universal Copy Enabler/Faster Copy - Unlock Text Selection & Copy Faster
// @namespace https://github.com/aezizhu/universal-copy-enabler
// @version 0.0.7
// @description The most powerful copy protection remover. Works on 99% of websites including Medium, Scribd, Bloomberg, NYTimes, academic papers, recipe sites, and all paywalled content. Smart detection - won't interfere with normal editing.
// @author CopyFreedom
// @icon data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%234CAF50"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
// @match *://*/*
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @license MIT
// @homepageURL https://github.com/aezizhu/universal-copy-enabler
// @supportURL https://github.com/aezizhu/universal-copy-enabler
// ==/UserScript==
(function () {
'use strict';
// ============================================
// CONFIGURATION (with persistence)
// ============================================
const CONFIG = {
enableCopyButton: GM_getValue('enableCopyButton', true),
enableCanvasCapture: true, // Basic setting
enableSmartMode: GM_getValue('enableSmartMode', true),
buttonText: 'Copy',
successText: 'Copied!',
buttonDelay: 350,
debug: GM_getValue('debug', false)
};
const log = (...args) => CONFIG.debug && console.log('[UCE]', ...args);
// ============================================
// SMART CONTEXT DETECTION
// Detect if user is in an editable context
// ============================================
const isEditableElement = (element) => {
if (!element) return false;
// Check if element is an input or textarea
const tagName = element.tagName?.toUpperCase();
if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
return true;
}
// Check if element or any parent is contenteditable
let el = element;
while (el) {
if (el.isContentEditable || el.contentEditable === 'true') {
return true;
}
// Check for common editor classes
const className = el.className?.toString() || '';
if (
// className.includes('editor') || // Too aggressive, matches read-only code viewers
className.includes('ProseMirror') ||
className.includes('CodeMirror') ||
className.includes('ace_editor') ||
className.includes('monaco-editor') ||
className.includes('ql-editor') ||
className.includes('tox-edit-area') ||
className.includes('notion-page-content')
) {
return true;
}
// Check for editor roles
const role = el.getAttribute?.('role');
if (role === 'textbox' || role === 'document') {
return true;
}
el = el.parentElement;
}
return false;
};
const isInEditorApp = () => {
const url = window.location.href;
const editorPatterns = [
/docs\.google\.com\/.*\/edit/,
/docs\.google\.com\/.*\/d\//,
/sheets\.google\.com/,
/slides\.google\.com/,
/notion\.so.*[a-f0-9]{32}/,
/notion\.site/,
/figma\.com\/file/,
/canva\.com\/design/,
/codepen\.io.*\/pen/,
/codesandbox\.io\/s\//,
/replit\.com\/@/,
/github\.com\/.*\/edit/,
/gitlab\.com\/.*\/-\/ide/,
/overleaf\.com\/project/
];
return editorPatterns.some(pattern => pattern.test(url));
};
const shouldIntercept = () => {
if (!CONFIG.enableSmartMode) return true;
// Always allow interception if not in editor app
if (!isInEditorApp()) return true;
// In editor apps, only intercept if not focused on editable element
const activeElement = document.activeElement;
return !isEditableElement(activeElement);
};
// ============================================
// COPY PROTECTION DETECTION
// Check if copy is actually blocked
// ============================================
const isCopyBlocked = () => {
try {
// Check for CSS user-select: none
const body = document.body;
if (body) {
const style = window.getComputedStyle(body);
if (style.userSelect === 'none' || style.webkitUserSelect === 'none') {
return true;
}
}
// Check for oncopy handlers
if (document.oncopy || document.body?.oncopy) {
return true;
}
// Check for elements with copy-blocking attributes
const blocked = document.querySelector('[oncopy], [oncontextmenu], [onselectstart]');
if (blocked) {
return true;
}
return false;
} catch (e) {
return false;
}
};
// ============================================
// STYLES
// ============================================
const STYLES = `
/* Force enable text selection - but respect editable elements */
html:not([data-uce-disabled]) *:not(input):not(textarea):not([contenteditable="true"]):not([contenteditable="true"] *) {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
/* cursor: text !important; -- REMOVED to allow native cursor (pointer on links, etc) */
pointer-events: auto !important;
}
/* Exception for non-text elements to avoid messy UI */
/* Removed cursor:auto exception as the main rule no longer forces text cursor */
html:not([data-uce-disabled]) *:not(input):not(textarea):not([contenteditable="true"]) img,
html:not([data-uce-disabled]) *:not(input):not(textarea):not([contenteditable="true"]) button,
html:not([data-uce-disabled]) *:not(input):not(textarea):not([contenteditable="true"]) video,
html:not([data-uce-disabled]) *:not(input):not(textarea):not([contenteditable="true"]) canvas {
/* cursor: auto !important; */
}
/* Remove common overlay blockers */
.paywall-overlay,
.subscription-wall,
.copy-protection-overlay,
[class*="paywall"]:not([class*="content"]),
[class*="prevent-copy"],
[class*="no-select"]:not(input):not(textarea),
[class*="disable-select"]:not(input):not(textarea),
[id*="copy-protection"] {
display: none !important;
pointer-events: none !important;
z-index: -9999 !important;
}
/* Modern Floating copy button - Glassmorphism */
#uce-copy-button {
position: absolute;
z-index: 2147483647;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #333;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 12px;
padding: 8px 16px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 600;
cursor: pointer;
box-shadow:
0 4px 15px rgba(0, 0, 0, 0.1),
0 1px 2px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.6);
opacity: 0;
pointer-events: none;
display: flex;
align-items: center;
gap: 6px;
}
#uce-copy-button::before {
content: '';
display: block;
width: 14px;
height: 14px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='13' height='13' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'%3E%3C/path%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
#uce-copy-button.visible {
opacity: 1;
pointer-events: auto;
}
#uce-copy-button:hover {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
#uce-copy-button:active {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
#uce-copy-button.success {
background: rgba(220, 252, 231, 0.95);
color: #166534;
border-color: rgba(134, 239, 172, 0.4);
}
#uce-copy-button.success::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23166534' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
}
/* Canvas Overlay Button - Minimal & Discrete */
.uce-canvas-overlay-btn {
position: absolute;
top: 6px;
right: 6px;
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(2px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
cursor: pointer;
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s ease;
opacity: 0; /* Hidden by default for "perfect" look */
pointer-events: auto !important;
transform: scale(0.9);
}
/* Show on hover of the button itself, or if we could target parent hover...
Since we can't easily rely on parent hover in generic sites,
we'll make it visible but very faint, OR show on mouse bubbling?
Actually, let's keep it faint (0.1) so users can find it. */
.uce-canvas-overlay-btn {
opacity: 0.1;
}
.uce-canvas-overlay-btn:hover {
opacity: 1;
transform: scale(1.1);
background: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.uce-canvas-overlay-btn::before {
content: '';
width: 16px;
height: 16px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'%3E%3C/path%3E%3Cpolyline points='14 2 14 8 20 8'%3E%3C/polyline%3E%3Cline x1='16' y1='13' x2='8' y2='13'%3E%3C/line%3E%3Cline x1='16' y1='17' x2='8' y2='17'%3E%3C/line%3E%3Cpolyline points='10 9 9 9 8 9'%3E%3C/polyline%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
/* Toast notification */
#uce-toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
z-index: 2147483647;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
}
#uce-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Canvas text extraction panel */
#uce-canvas-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
}
#uce-canvas-panel .panel-content {
background: #fff;
border-radius: 12px;
width: 80%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
#uce-canvas-panel .panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
#uce-canvas-panel .panel-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
#uce-canvas-panel .panel-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
#uce-canvas-panel .panel-body {
padding: 20px;
overflow: auto;
flex: 1;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
user-select: text !important;
}
#uce-canvas-panel .panel-footer {
padding: 16px 20px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
justify-content: flex-end;
}
#uce-canvas-panel .panel-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
#uce-canvas-panel .panel-btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
#uce-canvas-panel .panel-btn-secondary {
background: #f0f0f0;
color: #333;
}
`;
// ============================================
// CANVAS TEXT CAPTURE & OVERLAY
// ============================================
const canvasTextData = new Map();
const addCanvasOverlay = (canvas) => {
if (canvas.dataset.uceHasOverlay) return;
// Only add overlay if we have data
if (!canvasTextData.has(canvas) || canvasTextData.get(canvas).length === 0) return;
const parent = canvas.parentElement;
if (!parent) return;
// Ensure parent can position children
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.position === 'static') {
// We can't safely change parent position without risking layout breakage
// So we'll try to insert a wrapper or just use inline style if possible
// Riskier choice: parent.style.position = 'relative';
// Safer choice: don't show overlay if layout is strict, but most doc viewers use absolute positioning
}
const btn = document.createElement('div');
btn.className = 'uce-canvas-overlay-btn';
btn.title = 'Copy Page Text (Reconstruct)';
// Position relative to the canvas if possible, or parent
// For Baidu Wenku, parent is usually a wrapper.
// We'll trust the CSS to position it top-right of the parent.
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// Get raw data for reconstruction
const data = canvasTextData.get(canvas);
if (data && data.length > 0) {
// Sort it first
const sortedData = [...data].sort((a, b) => {
const yDiff = a.y - b.y;
return Math.abs(yDiff) < 10 ? a.x - b.x : yDiff;
});
showReconstructionPanel(sortedData, canvas.width, canvas.height);
} else {
showToast('No text captured for this page');
}
};
// Position helper
// Ideally we want it in the corner of the canvas itself
// But canvas is an element, can't have children.
// So we append to parent, and assume parent is a container for said canvas.
// For Baidu Wenku, usually canvas is inside a div.page-layer or something.
parent.appendChild(btn);
canvas.dataset.uceHasOverlay = 'true';
};
// ============================================
// RECONSRUCTION PANEL (Visual)
// ============================================
const showReconstructionPanel = (data, width, height) => {
if (!document.body) return;
const panel = document.createElement('div');
panel.id = 'uce-reconstruct-panel';
// Create the close button
const closeBtn = document.createElement('button');
closeBtn.className = 'close-btn';
closeBtn.textContent = '×';
closeBtn.onclick = () => panel.remove();
// Container for the reconstructed page
const container = document.createElement('div');
container.className = 'page-container';
// Attempt to match aspect ratio or width
// We'll set a fixed width for the container and scale content or scroll
container.style.width = width ? `${width}px` : '100%';
container.style.minHeight = height ? `${height}px` : '80vh';
// Add text elements
data.forEach(item => {
const span = document.createElement('span');
span.textContent = item.text;
span.className = 'text-chunk';
span.style.left = `${item.x}px`;
span.style.top = `${item.y}px`;
container.appendChild(span);
});
panel.appendChild(closeBtn);
panel.appendChild(container);
document.body.appendChild(panel);
};
// Simplified styles for the reconstruction panel
GM_addStyle(`
#uce-reconstruct-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
z-index: 2147483647;
display: flex;
justify-content: center;
overflow: auto;
padding: 40px;
}
#uce-reconstruct-panel .page-container {
background: #fff;
position: relative;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
/* Ensure text is selectable */
user-select: text !important;
-webkit-user-select: text !important;
cursor: text !important;
}
#uce-reconstruct-panel .text-chunk {
position: absolute;
white-space: nowrap;
/* Font styling guess */
font-family: sans-serif;
font-size: 14px; /* Default guess, could improve if we captured font size */
color: #000;
line-height: 1;
transform-origin: left top;
}
#uce-reconstruct-panel .close-btn {
position: fixed;
top: 20px;
right: 20px;
background: #fff;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 24px;
cursor: pointer;
z-index: 2147483648;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
`);
const interceptCanvasText = () => {
if (!CONFIG.enableCanvasCapture) return;
// Strategy Selection:
// 1. Baidu Wenku: Strict Proxy on document.createElement (Aggressive)
// 2. Others: Prototype Patch (Safer, Universal)
const isBaidu = window.location.hostname.includes('baidu.com');
if (isBaidu) {
// --- STRICT BAIDU FIX (Proxy) ---
try {
const originalCreateElement = document.createElement;
const linkCanvasToData = (canvas, dataArray) => {
if (!canvasTextData.has(canvas)) {
canvasTextData.set(canvas, dataArray);
canvas.dataset.uceHasOverlay = 'false';
setTimeout(() => addCanvasOverlay(canvas), 1000);
}
};
document.createElement = new Proxy(originalCreateElement, {
apply: function (target, thisArg, argumentsList) {
// Safety: ensure argument exists and is string
if (argumentsList && argumentsList[0] && typeof argumentsList[0] === 'string' && argumentsList[0].toLowerCase() === "canvas") {
const element = Reflect.apply(target, thisArg, argumentsList);
const dataArray = [];
const originalGetContext = element.getContext;
try {
element.getContext = function (type, ...args) {
const context = originalGetContext.apply(this, [type, ...args]);
if (type === '2d') {
const originalFillText = context.fillText;
context.fillText = function (...textArgs) {
if (textArgs[0] && typeof textArgs[0] === 'string' && textArgs[0].trim().length > 0) {
dataArray.push({
text: textArgs[0],
x: textArgs[1],
y: textArgs[2]
});
}
return originalFillText.apply(this, textArgs);
};
const originalStrokeText = context.strokeText;
context.strokeText = function (...textArgs) {
if (textArgs[0] && typeof textArgs[0] === 'string' && textArgs[0].trim().length > 0) {
dataArray.push({
text: textArgs[0],
x: textArgs[1],
y: textArgs[2]
});
}
return originalStrokeText.apply(this, textArgs);
};
}
return context;
};
} catch (e) { /* ignore if already sealed */ }
linkCanvasToData(element, dataArray);
return element;
}
return Reflect.apply(target, thisArg, argumentsList);
}
});
log('Canvas creation intercepted via Proxy (Baidu Mode)');
} catch (e) {
log('Canvas Proxy interception failed:', e);
}
} else {
// --- UNIVERSAL SAFEMODE (Prototype) ---
try {
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
const context = originalGetContext.call(this, type, ...args);
if (type === '2d' && context && !context._uceIntercepted) {
context._uceIntercepted = true;
const canvas = this;
if (!canvasTextData.has(canvas)) {
canvasTextData.set(canvas, []);
}
const originalFillText = context.fillText;
context.fillText = function (text, x, y, ...rest) {
if (text && typeof text === 'string' && text.trim().length > 0) {
canvasTextData.get(canvas)?.push({ text, x, y });
if (!canvas.dataset.uceHasOverlay) {
setTimeout(() => addCanvasOverlay(canvas), 1000);
}
}
return originalFillText.call(this, text, x, y, ...rest);
};
const originalStrokeText = context.strokeText;
context.strokeText = function (text, x, y, ...rest) {
if (text && typeof text === 'string' && text.trim().length > 0) {
canvasTextData.get(canvas)?.push({ text, x, y });
if (!canvas.dataset.uceHasOverlay) {
setTimeout(() => addCanvasOverlay(canvas), 1000);
}
}
return originalStrokeText.call(this, text, x, y, ...rest);
};
}
return context;
};
log('Canvas text interception enabled (Universal Mode)');
} catch (e) {
log('Canvas interception failed:', e);
}
}
};
const getCanvasText = (specificCanvas = null) => {
const allText = [];
const processCanvas = (canvas, textArray) => {
if (canvas.offsetWidth > 0 && canvas.offsetHeight > 0) {
const sorted = [...textArray].sort((a, b) => {
// Sort by Y first (lines), then X (position in line)
const yDiff = a.y - b.y;
// Tolerance for same line = 12px
return Math.abs(yDiff) < 12 ? a.x - b.x : yDiff;
});
// Smart joining with newlines
let lastY = -999;
let textBlock = [];
sorted.forEach(item => {
if (lastY !== -999 && Math.abs(item.y - lastY) > 20) {
textBlock.push('\n');
}
textBlock.push(item.text);
lastY = item.y;
});
const text = textBlock.join(' ').replace(/\s+/g, ' '); // Simplify spaces
if (text.trim()) {
allText.push(text);
}
}
};
if (specificCanvas) {
if (canvasTextData.has(specificCanvas)) {
processCanvas(specificCanvas, canvasTextData.get(specificCanvas));
}
} else {
canvasTextData.forEach((textArray, canvas) => {
processCanvas(canvas, textArray);
});
}
return allText.join('\n\n');
};
// ============================================
// SMART EVENT INTERCEPTION
// Only intercept when copy is actually blocked
// ============================================
const interceptEvents = () => {
const blockedEvents = [
'copy', 'cut', 'paste',
'contextmenu',
// 'selectstart', // Removed to fix selection issues
// 'dragstart', // Removed to fix drag-selection issues
'beforecopy'
];
const smartHandler = (e) => {
// Don't intercept if in editable element
if (isEditableElement(e.target)) {
log('Allowing event in editable element:', e.type);
return; // Let the event proceed normally
}
// Don't intercept if smart mode is on and we're in an editor app
if (CONFIG.enableSmartMode && isInEditorApp() && isEditableElement(document.activeElement)) {
log('Allowing event in editor app:', e.type);
return;
}
// Stop the blocking event
e.stopPropagation();
e.stopImmediatePropagation();
log('Blocked copy-prevention event:', e.type);
};
// Capture phase interception with smart detection
blockedEvents.forEach(eventName => {
document.addEventListener(eventName, smartHandler, true);
window.addEventListener(eventName, smartHandler, true);
});
// Keyboard shortcuts - smart handling
document.addEventListener('keydown', (e) => {
const key = e.key?.toLowerCase();
if ((e.ctrlKey || e.metaKey) && ['c', 'a', 'x', 'v'].includes(key)) {
// If in editable element, let it through
if (isEditableElement(e.target) || isEditableElement(document.activeElement)) {
return;
}
e.stopPropagation();
e.stopImmediatePropagation();
}
}, true);
// Smart addEventListener override
try {
const originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options) {
if (blockedEvents.includes(type)) {
const wrappedListener = function (e) {
// Allow if in editable context
if (isEditableElement(e.target) || isEditableElement(document.activeElement)) {
return listener.call(this, e);
}
// Support for Baidu Wenku and similar sites that use canvas
// NOTE: Use originalAddEventListener to avoid infinite recursion
if (this instanceof HTMLCanvasElement) {
originalAddEventListener.call(this, 'mousedown', (e) => {
// Only show if we actually caught text for this canvas
if (canvasTextData.has(this) && canvasTextData.get(this).length > 0) {
showToast('💡 Text detected! Use "Copy Page Text" from menu if needed.', 3000);
}
});
}
// Block the copy-prevention listener
return;
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
} catch (e) {
log('addEventListener override failed:', e);
}
log('Smart event interception enabled');
};
// ============================================
// SHADOW DOM SUPPORT
// ============================================
const interceptShadowDOM = () => {
try {
const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (init) {
const shadowRoot = originalAttachShadow.call(this, init);
try {
// Inject our styles into the shadow root
const style = document.createElement('style');
style.textContent = STYLES;
shadowRoot.appendChild(style);
log('Injected styles into new Shadow Root');
} catch (e) {
log('Failed to inject styles into Shadow Root', e);
}
return shadowRoot;
};
} catch (e) {
log('Shadow DOM interception failed', e);
}
};
// ============================================
// BAIDU WENKU TEXT LAYER FIX (Reconstruction)
// ============================================
// We already have the specific fix logic in `addCanvasOverlay` and `showCanvasPanel`
// which now handles text reconstruction globally.
// So this section specifically just handles cleanup of ads/overlays.
const fixBaiduWenkuTextLayer = () => {
// Remove click-blocking overlays on Baidu specifically
if (window.location.hostname.includes('baidu.com')) {
try {
// Mock pageData on unsafeWindow
let pageData = {};
if (unsafeWindow.pageData) {
pageData = unsafeWindow.pageData;
}
Object.defineProperty(unsafeWindow, "pageData", {
set: (v) => pageData = v,
get: function () {
if (!pageData.vipInfo) pageData.vipInfo = {};
pageData.vipInfo.global_svip_status = 1;
pageData.vipInfo.global_vip_status = 1;
pageData.vipInfo.isVip = 1;
pageData.vipInfo.isWenkuVip = 1;
return pageData;
}
});
log('Baidu VIP mocked');
} catch (e) {
log('Baidu VIP mock failed', e);
}
// Remove click-blocking overlays on Baidu specifically
document.querySelectorAll('.vip-pay-pop-v2-wrap, .reader-pop-manager-view-containter, .fc-ad-contain, .pay-page, #html-reader-go-more').forEach(el => {
el.style.display = 'none';
el.style.setProperty('pointer-events', 'none', 'important');
});
// Also ensure body is scrollable
document.body.style.setProperty('overflow', 'auto', 'important');
}
};
// ============================================
// REMOVE INLINE RESTRICTIONS
// ============================================
const removeInlineRestrictions = () => {
const restrictedAttrs = [
'oncopy', 'oncut', 'onpaste',
'oncontextmenu', 'onselectstart',
'ondragstart', 'ondrag', 'ondrop',
'onbeforecopy', 'onmousedown', 'onmouseup'
];
document.querySelectorAll('*').forEach(el => {
// Skip editable elements
if (isEditableElement(el)) return;
restrictedAttrs.forEach(attr => {
if (el.hasAttribute(attr)) {
el.removeAttribute(attr);
}
});
if (el.getAttribute('unselectable') === 'on') {
el.removeAttribute('unselectable');
}
// Remove inline styles that prevent selection (but not on editable elements)
if (el.style && !el.isContentEditable) {
const computedStyle = window.getComputedStyle(el);
if (computedStyle.userSelect === 'none') {
el.style.setProperty('user-select', 'text', 'important');
el.style.setProperty('-webkit-user-select', 'text', 'important');
}
}
});
// Clear document-level handlers (these are almost always for blocking)
document.oncopy = null;
document.oncontextmenu = null;
document.onselectstart = null;
document.ondragstart = null;
// Only clear body handlers if not an editor app
if (!isInEditorApp() && document.body) {
document.body.oncopy = null;
document.body.oncontextmenu = null;
document.body.onselectstart = null;
}
log('Inline restrictions removed');
};
// ============================================
// CLIPBOARD OPERATIONS
// ============================================
const copyToClipboard = async (text) => {
if (!text) return false;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
log('Clipboard API failed, trying fallback');
}
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;pointer-events:none;';
const handler = (e) => {
e.stopImmediatePropagation();
e.preventDefault();
e.clipboardData.setData('text/plain', text);
};
document.body.appendChild(textarea);
textarea.addEventListener('copy', handler, true);
textarea.select();
textarea.setSelectionRange(0, text.length);
const success = document.execCommand('copy');
textarea.removeEventListener('copy', handler, true);
document.body.removeChild(textarea);
return success;
} catch (e) {
log('Fallback copy failed:', e);
return false;
}
};
// ============================================
// GET SELECTED TEXT
// ============================================
const getSelectedText = () => {
let text = '';
// Method 1: Standard selection
try {
const selection = window.getSelection();
if (selection && selection.toString().trim()) {
text = selection.toString();
}
} catch (e) { }
// Method 2: activeElement value
if (!text) {
try {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) {
const start = activeEl.selectionStart;
const end = activeEl.selectionEnd;
if (start !== end) {
text = activeEl.value.substring(start, end);
}
}
} catch (e) { }
}
// Method 3: iframe content
if (!text) {
try {
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc) {
const iframeSelection = iframeDoc.getSelection();
if (iframeSelection && iframeSelection.toString().trim()) {
text = iframeSelection.toString();
break;
}
}
} catch (e) { }
}
} catch (e) { }
}
return text;
};
// ============================================
// TOAST NOTIFICATION
// ============================================
let toastElement = null;
let toastTimeout = null;
const showToast = (message, duration = 1500) => {
if (!document.body) return;
if (!toastElement) {
toastElement = document.createElement('div');
toastElement.id = 'uce-toast';
document.body.appendChild(toastElement);
}
clearTimeout(toastTimeout);
toastElement.textContent = message;
toastElement.classList.add('visible');
toastTimeout = setTimeout(() => {
toastElement.classList.remove('visible');
}, duration);
};
// ============================================
// FLOATING COPY BUTTON
// ============================================
let copyButton = null;
let hideTimeout = null;
const createCopyButton = () => {
if (!document.body) return;
copyButton = document.createElement('button');
copyButton.id = 'uce-copy-button';
copyButton.textContent = CONFIG.buttonText;
copyButton.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
copyButton.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const text = getSelectedText();
if (text) {
const success = await copyToClipboard(text);
if (success) {
copyButton.textContent = CONFIG.successText;
copyButton.classList.add('success');
showToast('✓ Copied to clipboard');
setTimeout(() => hideCopyButton(), 500);
} else {
showToast('✗ Copy failed - try Ctrl+C');
}
}
}, true);
document.body.appendChild(copyButton);
};
const showCopyButton = () => {
// Respect the disable toggle
if (!CONFIG.enableCopyButton) return;
if (!copyButton) createCopyButton();
if (!copyButton) return;
// Don't show copy button in editable elements
const activeEl = document.activeElement;
if (isEditableElement(activeEl)) return;
// Get Selection Rect
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// If selection is empty or invisible, abort
if (rect.width === 0 || rect.height === 0) return;
clearTimeout(hideTimeout);
copyButton.textContent = CONFIG.buttonText;
copyButton.classList.remove('success');
// Button Dimensions (approximate, or measure)
// We'll trust CSS to set width, but we need to center it.
// Let's assume a healthy width for calculation or just set left/top
const buttonHeight = 40;
const margin = 10;
// Center horizontally over the selection
// rect.left + rect.width / 2 is the center
// We need to subtract button width / 2.
// Since we don't know exact width before render, let's use transform: translateX(-50%) in CSS?
// Wait, earlier CSS removed transforms.
// Let's just assume a fixed width of 80px for calc, or measure it?
// Measuring is safer.
copyButton.style.display = 'flex'; // Ensure it's rendered to measure
const btnRect = copyButton.getBoundingClientRect();
const btnWidth = btnRect.width || 80;
let posX = (rect.left + window.scrollX) + (rect.width / 2) - (btnWidth / 2);
let posY = (rect.top + window.scrollY) - buttonHeight - margin;
// Viewport Boundary checks
if (posX < 0) posX = 5;
if (posX + btnWidth > window.innerWidth + window.scrollX) {
posX = (window.innerWidth + window.scrollX) - btnWidth - 5;
}
if (posY < window.scrollY) {
// If selection is at very top, show button BELOW selection
posY = (rect.bottom + window.scrollY) + margin;
}
copyButton.style.left = `${posX}px`;
copyButton.style.top = `${posY}px`;
copyButton.classList.add('visible');
};
const hideCopyButton = (delay = CONFIG.buttonDelay) => {
if (!copyButton) return;
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
copyButton.classList.remove('visible');
}, delay);
};
// ============================================
// CANVAS PANEL
// ============================================
const showCanvasPanel = (text) => {
if (!document.body) return;
const panel = document.createElement('div');
panel.id = 'uce-canvas-panel';
panel.innerHTML = `
<div class="panel-content">
<div class="panel-header">
<span class="panel-title">📋 Extracted Text</span>
<button class="panel-close">×</button>
</div>
<div class="panel-body">${escapeHtml(text)}</div>
<div class="panel-footer">
<button class="panel-btn panel-btn-secondary" id="uce-panel-close">Close</button>
<button class="panel-btn panel-btn-primary" id="uce-panel-copy">Copy All</button>
</div>
</div>
`;
document.body.appendChild(panel);
panel.querySelector('.panel-close').onclick = () => panel.remove();
panel.querySelector('#uce-panel-close').onclick = () => panel.remove();
panel.querySelector('#uce-panel-copy').onclick = async () => {
const success = await copyToClipboard(text);
showToast(success ? '✓ Copied!' : '✗ Failed');
if (success) panel.remove();
};
panel.onclick = (e) => {
if (e.target === panel) panel.remove();
};
};
const escapeHtml = (text) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
// ============================================
// MUTATION OBSERVER
// ============================================
const observeDOM = () => {
const observer = new MutationObserver((mutations) => {
let needsCleanup = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const classes = node.className?.toString() || '';
const id = node.id || '';
if (
classes.includes('paywall') ||
(classes.includes('overlay') && classes.includes('block')) ||
id.includes('paywall') ||
id.includes('copy-protection')
) {
node.style.display = 'none';
node.style.pointerEvents = 'none';
}
needsCleanup = true;
}
}
}
}
if (needsCleanup) {
removeInlineRestrictions();
fixBaiduWenkuTextLayer();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
return observer;
};
// ============================================
// INITIALIZATION
// ============================================
const init = () => {
interceptCanvasText();
interceptEvents();
interceptShadowDOM();
try {
GM_addStyle(STYLES);
} catch (e) {
const style = document.createElement('style');
style.textContent = STYLES;
(document.head || document.documentElement).appendChild(style);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDOMReady);
} else {
onDOMReady();
}
window.addEventListener('load', onLoad);
// Periodic check for stubborn elements (Baidu lazy loads)
setInterval(() => {
fixBaiduWenkuTextLayer();
}, 2000);
};
const onDOMReady = () => {
removeInlineRestrictions();
fixBaiduWenkuTextLayer();
observeDOM();
if (CONFIG.enableCopyButton) {
// Hide on selection change (dragging)
document.addEventListener('selectionchange', () => {
if (copyButton && copyButton.classList.contains('visible')) {
hideCopyButton(0);
}
});
document.addEventListener('mouseup', (e) => {
// Debounce slighty to let selection settle
setTimeout(() => {
const text = getSelectedText();
const activeEl = document.activeElement;
const targetEl = e.target;
// Stronger check: IF active element is editable, NEVER show key button
// irrespective of where the user clicked (handles complex editors)
if (isEditableElement(activeEl)) {
hideCopyButton(0);
return;
}
// Don't show button in editable elements
// ADDED: check for text length > 1 to avoid accidental single-char clicks
if (text && text.trim().length > 1 && !isEditableElement(targetEl)) {
// No longer pass x/y, we calculate from selection
showCopyButton();
} else {
hideCopyButton(0);
}
}, 20);
});
document.addEventListener('mousedown', (e) => {
if (copyButton && !copyButton.contains(e.target)) {
hideCopyButton(0);
}
});
// Hide button on typing (Smart Mode)
document.addEventListener('keydown', (e) => {
if (copyButton && copyButton.classList.contains('visible')) {
hideCopyButton(0);
}
}, true);
}
log('Universal Copy Enabler initialized (Smart Mode: ' + CONFIG.enableSmartMode + ')');
};
const onLoad = () => {
removeInlineRestrictions();
};
// ============================================
// MENU COMMANDS
// ============================================
try {
GM_registerMenuCommand('🔓 Force Remove Restrictions', () => {
removeInlineRestrictions();
showToast('✓ Restrictions removed');
});
GM_registerMenuCommand('📋 Copy Page Text', () => {
const text = getCanvasText();
if (text) {
showCanvasPanel(text);
} else {
showToast('No textual content found on page');
}
});
GM_registerMenuCommand('⚡ Force Allow Selection', () => {
try {
document.designMode = 'on';
document.body.contentEditable = 'true';
document.body.setAttribute('contenteditable', 'true');
const style = document.createElement('style');
style.textContent = `
* {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
pointer-events: auto !important;
}
.paywall-overlay, [class*="paywall"] { display: none !important; }
`;
document.head.appendChild(style);
showToast('⚡ Copying Enabled! You can now select anything.');
} catch (e) {
showToast('Force Select failed: ' + e.message);
}
});
GM_registerMenuCommand('🧠 Toggle Smart Mode', () => {
CONFIG.enableSmartMode = !CONFIG.enableSmartMode;
GM_setValue('enableSmartMode', CONFIG.enableSmartMode);
showToast(CONFIG.enableSmartMode ? '✓ Smart Mode ON' : '✓ Smart Mode OFF');
});
GM_registerMenuCommand(CONFIG.enableCopyButton ? '🔘 Disable Copy Button' : '🔘 Enable Copy Button', () => {
CONFIG.enableCopyButton = !CONFIG.enableCopyButton;
GM_setValue('enableCopyButton', CONFIG.enableCopyButton);
if (copyButton) {
copyButton.style.display = CONFIG.enableCopyButton ? '' : 'none';
}
showToast(CONFIG.enableCopyButton ? '✓ Copy button enabled' : '✓ Copy button disabled');
});
GM_registerMenuCommand('🐛 Toggle Debug Mode', () => {
CONFIG.debug = !CONFIG.debug;
GM_setValue('debug', CONFIG.debug);
showToast(CONFIG.debug ? '✓ Debug mode ON' : '✓ Debug mode OFF');
});
} catch (e) {
log('Menu commands registration failed:', e);
}
// Start
init();
})();