// ==UserScript==
// @name 无图模式-Icerayer
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 阻止网页加载图片和视频,减少流量消耗,支持自定义配置
// @author Icerayer
// @match *://*/*
// @exclude data:*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 配置项 - 可通过油猴脚本菜单或GM_setValue进行配置
const config = {
blockImages: true, // 阻止图片加载
blockBase64Images: true, // 阻止Base64编码图片
blockSVGFill: true, // 阻止SVG中的图片填充
blockVideos: true, // 阻止视频加载
blockBackgroundImages: true, // 阻止背景图片
blockIframeImages: false, // 阻止iframe中的图片(可能受同源策略限制)
usePlaceholder: false, // 使用占位符替代图片
placeholderType: 'stripes', // 占位符类型: 'stripes', 'solid'
placeholderOpacity: '30', // 占位符透明度
excludedSelectors: ['.ytp-gradient-bottom', '.ytp-gradient-top'], // 排除的元素选择器
excludedDomains: [], // 排除的域名列表
debug: false, // 调试模式
forceBlockExternalStyles: true, // 强制阻止外部样式表中的背景图片
useGlobalStyleOverride: true, // 使用全局样式覆盖作为最后防线
updateDebounceTime: 50 // 更新防抖时间(毫秒)
};
// 尝试从GM存储加载配置
try {
const savedConfig = JSON.parse(GM_getValue('noImageModeConfig', '{}'));
Object.assign(config, savedConfig);
} catch (e) {
console.log('加载配置失败,使用默认配置:', e.message);
}
// 检查当前域名是否在排除列表中
function isExcludedDomain() {
const hostname = window.location.hostname;
return config.excludedDomains.some(domain => hostname.includes(domain));
}
// 如果当前域名在排除列表中,则不执行后续代码
if (isExcludedDomain()) {
console.log('当前域名在排除列表中,无图模式已禁用');
return;
}
// 方法1: 拦截并取消图片请求
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
// 处理单个节点
if (node.nodeType === 1) {
processNode(node);
}
});
});
});
function processNode(node) {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (node.matches(selector) || node.closest(selector)) {
return;
}
}
// 检查当前节点是否为图片
if (node.tagName === 'IMG') {
if (config.blockImages) {
blockImage(node);
}
}
// 检查当前节点是否为视频
else if (node.tagName === 'VIDEO') {
if (config.blockVideos) {
blockVideos(); // 处理所有视频,包括当前节点
}
}
// 检查当前节点是否为SVG
else if (node.tagName === 'SVG' || node.tagName === 'image' && node.namespaceURI === 'http://www.w3.org/2000/svg') {
if (config.blockSVGFill) {
blockSVGFillImages(); // 处理所有SVG中的图片,包括当前节点
}
}
// 检查子节点中的图片
if (config.blockImages) {
const images = node.querySelectorAll('img');
images.forEach(blockImage);
}
// 处理Base64图片
if (config.blockBase64Images) {
const base64Images = node.querySelectorAll('img[src^="data:image/"]');
base64Images.forEach(blockImage);
}
// 处理背景图片 - 包括内联样式和计算样式
if (config.blockBackgroundImages) {
// 1. 检查并清除内联样式中的背景图片
if (node.style && node.style.backgroundImage && node.style.backgroundImage !== 'none') {
node.style.backgroundImage = 'none';
}
// 2. 检查并清除计算样式中的背景图片
const computedStyle = window.getComputedStyle(node);
if (computedStyle.backgroundImage !== 'none') {
node.style.backgroundImage = 'none';
}
// 3. 检查所有可能包含背景图片的样式属性
const backgroundProperties = ['background', 'backgroundImage', 'backgroundUrl'];
backgroundProperties.forEach(prop => {
if (node.style[prop] && node.style[prop] !== 'none' && node.style[prop].includes('url')) {
node.style[prop] = 'none';
}
});
}
}
function blockImage(img) {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (img.matches(selector) || img.closest(selector)) {
return;
}
}
// 保存原始src以备恢复(可选功能)
if (img.src && !img.dataset.originalSrc) {
img.dataset.originalSrc = img.src;
// 标记为被阻止的图片
img.dataset.imageBlocked = 'true';
}
// 清空图片源
img.src = '';
img.srcset = '';
// 根据配置设置显示样式
if (config.usePlaceholder) {
img.style.display = 'block';
img.style.minHeight = '20px';
img.style.backgroundColor = 'transparent';
img.style.backgroundImage = config.placeholderType === 'stripes'
? `linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee), linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee)`
: `linear-gradient(to right, #eee 0%, #eee 100%)`;
img.style.backgroundSize = config.placeholderType === 'stripes' ? '20px 20px' : '100% 100%';
img.style.backgroundPosition = config.placeholderType === 'stripes' ? '0 0, 10px 10px' : '0 0';
img.style.opacity = config.placeholderOpacity / 100;
} else {
img.style.display = 'none';
}
}
// 处理Base64编码图片
function blockBase64Images() {
if (!config.blockBase64Images) return;
// 1. 处理图片元素中的Base64图片
const base64Images = document.querySelectorAll('img[src^="data:image/"]');
base64Images.forEach(img => {
if (!img.dataset.imageBlocked) {
blockImage(img);
}
});
// 2. 处理CSS内联样式中的Base64背景图片
const elementsWithBase64Background = document.querySelectorAll('[style*="url(data:image"]');
elementsWithBase64Background.forEach(element => {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (element.matches(selector) || element.closest(selector)) {
return;
}
}
// 移除背景图片
if (element.style.backgroundImage && element.style.backgroundImage.includes('data:image')) {
element.style.backgroundImage = 'none';
}
// 处理background简写属性
if (element.style.background && element.style.background.includes('data:image')) {
element.style.background = '';
}
});
}
// 处理SVG中的图片填充
function blockSVGFillImages() {
if (!config.blockSVGFill) return;
// 处理SVG中的image元素
const svgImages = document.querySelectorAll('svg image[href^="data:image/"], svg image[xlink\:href^="data:image/"]');
svgImages.forEach(img => {
if (!img.dataset.imageBlocked) {
// 保存原始地址
if (img.href && !img.dataset.originalHref) {
img.dataset.originalHref = img.href;
}
if (img.getAttributeNS('http://www.w3.org/1999/xlink', 'href') && !img.dataset.originalXlinkHref) {
img.dataset.originalXlinkHref = img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
}
// 清空图片地址
if (img.href.baseVal !== undefined) {
img.href.baseVal = '';
} else {
img.removeAttribute('href');
}
img.removeAttributeNS('http://www.w3.org/1999/xlink', 'href');
img.dataset.imageBlocked = 'true';
}
});
// 处理SVG中的pattern和fill属性中的URL
const svgElements = document.querySelectorAll('svg [fill^="url("], svg [stroke^="url("]');
svgElements.forEach(element => {
if (!element.dataset.fillBlocked) {
if (element.getAttribute('fill') && element.getAttribute('fill').includes('url(')) {
element.dataset.originalFill = element.getAttribute('fill');
element.removeAttribute('fill');
}
if (element.getAttribute('stroke') && element.getAttribute('stroke').includes('url(')) {
element.dataset.originalStroke = element.getAttribute('stroke');
element.removeAttribute('stroke');
}
element.dataset.fillBlocked = 'true';
}
});
}
// 处理视频内容
function blockVideos() {
if (!config.blockVideos) return;
const videos = document.querySelectorAll('video');
videos.forEach(video => {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (video.matches(selector) || video.closest(selector)) {
return;
}
}
// 保存原始属性
if (video.src && !video.dataset.originalSrc) {
video.dataset.originalSrc = video.src;
}
if (video.poster && !video.dataset.originalPoster) {
video.dataset.originalPoster = video.poster;
}
// 暂停视频并清空源
video.pause();
video.src = '';
video.poster = '';
// 移除所有source元素
const sources = video.querySelectorAll('source');
sources.forEach(source => {
if (source.src && !source.dataset.originalSrc) {
source.dataset.originalSrc = source.src;
}
source.src = '';
});
// 设置视频样式
if (config.usePlaceholder) {
video.style.backgroundColor = 'transparent';
video.style.backgroundImage = config.placeholderType === 'stripes'
? `linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee), linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee)`
: `linear-gradient(to right, #eee 0%, #eee 100%)`;
video.style.backgroundSize = config.placeholderType === 'stripes' ? '20px 20px' : '100% 100%';
video.style.backgroundPosition = config.placeholderType === 'stripes' ? '0 0, 10px 10px' : '0 0';
video.style.opacity = config.placeholderOpacity / 100;
}
video.dataset.videoBlocked = 'true';
});
}
// 方法2: 重写Image构造函数
if (config.blockImages) {
const originalImage = window.Image;
window.Image = function(width, height) {
const img = new originalImage(width, height);
img.src = '';
return img;
};
window.Image.prototype = originalImage.prototype;
}
// 方法3: 拦截document.createElement
const originalCreateElement = document.createElement;
document.createElement = function(tagName) {
const element = originalCreateElement.call(document, tagName);
if (config.blockImages && tagName.toLowerCase() === 'img') {
// 重写src属性的setter
Object.defineProperty(element, 'src', {
set: function() {
// 忽略设置src的操作
},
get: function() {
return '';
}
});
// 同样处理srcset
Object.defineProperty(element, 'srcset', {
set: function() {
// 忽略设置srcset的操作
},
get: function() {
return '';
}
});
}
// 为所有元素重写setAttribute方法以拦截背景图片设置
const originalSetAttribute = element.setAttribute;
element.setAttribute = function(name, value) {
// 拦截style属性中的背景图片
if (config.blockBackgroundImages && name.toLowerCase() === 'style') {
if (typeof value === 'string' && value.includes('background-image') && value.includes('url')) {
// 移除background-image相关内容
value = value.replace(/background-image\s*:\s*url\([^)]*\);?/gi, '');
value = value.replace(/background\s*:\s*[^;]*url\([^)]*\)[^;]*;?/gi, '');
}
}
// 拦截background和background-image属性
else if (config.blockBackgroundImages && (name.toLowerCase() === 'background' || name.toLowerCase() === 'background-image')) {
if (typeof value === 'string' && value.includes('url')) {
// 忽略设置包含url的背景属性
return;
}
}
// 拦截图片src属性
else if (config.blockImages && (tagName.toLowerCase() === 'img' && (name.toLowerCase() === 'src' || name.toLowerCase() === 'srcset'))) {
if (value && (config.blockBase64Images || !value.startsWith('data:image/'))) {
// 保存原始src
if (name.toLowerCase() === 'src' && !this.dataset.originalSrc) {
this.dataset.originalSrc = value;
}
// 不设置实际的src
return;
}
}
return originalSetAttribute.call(this, name, value);
};
return element;
};
// 方法4: 尝试使用fetch拦截(如果浏览器支持)
if (window.fetch) {
const originalFetch = window.fetch;
window.fetch = function(url, options) {
// 规范化URL
const urlStr = typeof url === 'string' ? url : String(url);
try {
// 使用URL对象获取路径部分,更精确地匹配文件扩展名
const pathname = new URL(urlStr, location.href).pathname;
// 检查是否为图片请求
if (config.blockImages && /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(pathname)) {
// 返回一个空响应
return Promise.resolve(new Response('', { status: 200 }));
}
// 检查是否为视频请求
if (config.blockVideos && /\.(mp4|webm|ogg|avi|mov|mkv)$/i.test(pathname)) {
// 返回一个空响应
return Promise.resolve(new Response('', { status: 200 }));
}
} catch (e) {
// URL解析失败时回退到简单检查
if (config.blockImages && typeof url === 'string' && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(url)) {
return Promise.resolve(new Response('', { status: 200 }));
}
if (config.blockVideos && typeof url === 'string' && /\.(mp4|webm|ogg|avi|mov|mkv)$/i.test(url)) {
return Promise.resolve(new Response('', { status: 200 }));
}
}
return originalFetch(url, options);
};
}
// 方法5: 拦截XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
try {
// 使用URL对象获取路径部分,更精确地匹配文件扩展名
const pathname = new URL(String(url), location.href).pathname;
// 检查是否为图片请求
if (config.blockImages && /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(pathname)) {
// 不执行实际请求
this._blocked = true;
}
// 检查是否为视频请求
if (config.blockVideos && /\.(mp4|webm|ogg|avi|mov|mkv)$/i.test(pathname)) {
// 不执行实际请求
this._blocked = true;
}
} catch (e) {
// URL解析失败时回退到简单检查
if (config.blockImages && typeof url === 'string' && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(url)) {
this._blocked = true;
}
if (config.blockVideos && typeof url === 'string' && /\.(mp4|webm|ogg|avi|mov|mkv)$/i.test(url)) {
this._blocked = true;
}
}
return originalXHROpen.apply(this, arguments);
};
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
if (this._blocked) {
// 模拟完成但不加载内容
setTimeout(() => {
if (this.readyState < 4) {
this.readyState = 4;
this.status = 200;
if (this.onreadystatechange) this.onreadystatechange();
if (this.onload) this.onload();
}
}, 0);
return;
}
return originalXHRSend.apply(this, arguments);
};
// 开始观察DOM变化
observer.observe(document, {
childList: true,
subtree: true,
attributes: true, // 监听属性变化
attributeFilter: ['style', 'background', 'background-image'] // 重点监听这些属性
});
// 增强MutationObserver回调,处理属性变化
const originalObserverCallback = observer.observe.bind(observer);
observer.observe = function(target, options) {
// 确保options包含属性监听
if (!options.attributes) {
options.attributes = true;
}
if (!options.attributeFilter) {
options.attributeFilter = ['style', 'background', 'background-image'];
}
return originalObserverCallback(target, options);
};
// 方法6: 拦截元素的style对象的backgroundImage属性设置
if (config.blockBackgroundImages) {
const originalStylePrototype = Object.getPrototypeOf(HTMLElement.prototype.style);
Object.defineProperty(CSSStyleDeclaration.prototype, 'backgroundImage', {
set: function(value) {
// 忽略包含url的背景图片设置
if (typeof value === 'string' && value.includes('url')) {
return;
}
// 对于不包含url的值,正常设置
originalStylePrototype.setProperty.call(this, 'background-image', value);
},
get: function() {
return originalStylePrototype.getPropertyValue.call(this, 'background-image');
}
});
// 同样拦截background属性
Object.defineProperty(CSSStyleDeclaration.prototype, 'background', {
set: function(value) {
// 忽略包含url的背景设置
if (typeof value === 'string' && value.includes('url')) {
return;
}
originalStylePrototype.setProperty.call(this, 'background', value);
},
get: function() {
return originalStylePrototype.getPropertyValue.call(this, 'background');
}
});
}
// 拦截setProperty方法,防止通过此方法设置背景图片
const originalSetProperty = CSSStyleDeclaration.prototype.setProperty;
CSSStyleDeclaration.prototype.setProperty = function(property, value) {
// 拦截背景相关属性
if (config.blockBackgroundImages) {
const lowerProp = property.toLowerCase();
if ((lowerProp === 'background-image' || lowerProp === 'background') &&
typeof value === 'string' && value.includes('url')) {
return;
}
}
return originalSetProperty.apply(this, arguments);
};
// 方法7: 拦截CSS样式表操作,阻止通过CSS规则添加背景图片
if (config.blockBackgroundImages) {
// 拦截insertRule方法
const originalInsertRule = CSSStyleSheet.prototype.insertRule;
CSSStyleSheet.prototype.insertRule = function(rule, index) {
// 检查规则中是否包含背景图片url
if (rule && typeof rule === 'string' &&
(rule.includes('background-image') || rule.includes('background')) &&
rule.includes('url(')) {
// 修改规则,移除背景图片相关内容
rule = rule.replace(/background-image\s*:\s*url\([^)]*\);?/gi, '');
rule = rule.replace(/background\s*:\s*[^;]*url\([^)]*\)[^;]*;?/gi, '');
}
return originalInsertRule.call(this, rule, index);
};
// 拦截addRule方法(IE兼容)
if (CSSStyleSheet.prototype.addRule) {
const originalAddRule = CSSStyleSheet.prototype.addRule;
CSSStyleSheet.prototype.addRule = function(selector, style, index) {
// 检查样式中是否包含背景图片url
if (style && typeof style === 'string' &&
(style.includes('background-image') || style.includes('background')) &&
style.includes('url(')) {
// 修改样式,移除背景图片相关内容
style = style.replace(/background-image\s*:\s*url\([^)]*\);?/gi, '');
style = style.replace(/background\s*:\s*[^;]*url\([^)]*\)[^;]*;?/gi, '');
}
return originalAddRule.call(this, selector, style, index);
};
}
}
// 方法8: 拦截动态样式标签的创建和插入
// 拦截appendChild方法
const originalHeadAppendChild = document.head.appendChild;
document.head.appendChild = function(child) {
if (config.blockBackgroundImages) {
processStyleElement(child);
}
return originalHeadAppendChild.call(this, child);
};
// 拦截insertBefore方法
const originalHeadInsertBefore = document.head.insertBefore;
document.head.insertBefore = function(newNode, referenceNode) {
if (config.blockBackgroundImages) {
processStyleElement(newNode);
}
return originalHeadInsertBefore.call(this, newNode, referenceNode);
};
// 增强:拦截document.createTextNode,防止通过文本节点注入样式
const originalCreateTextNode = document.createTextNode;
document.createTextNode = function(data) {
const textNode = originalCreateTextNode.call(this, data);
// 标记文本节点,以便后续检查是否为样式内容
if (typeof data === 'string' && (data.includes('background-image') || data.includes('background')) && data.includes('url(')) {
textNode._potentialStyleContent = true;
}
return textNode;
};
// 处理样式元素的函数
function processStyleElement(element) {
if (!config.blockBackgroundImages) return;
const tagName = element.tagName ? element.tagName.toLowerCase() : '';
// 检查是否为style标签
if (tagName === 'style') {
// 立即处理现有内容
if (element.textContent) {
element.textContent = removeBackgroundImages(element.textContent);
}
// 监听内容变化
const styleObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'characterData' || mutation.type === 'childList') {
element.textContent = removeBackgroundImages(element.textContent);
}
});
});
styleObserver.observe(element, {
characterData: true,
childList: true,
subtree: true
});
}
// 检查是否为link标签(外部样式表)
else if (tagName === 'link' &&
element.rel && element.rel.toLowerCase() === 'stylesheet') {
// 记录外部样式表URL以便跟踪
if (element.href) {
if (!window._blockedStyleSheets) window._blockedStyleSheets = new Set();
window._blockedStyleSheets.add(element.href);
}
// 立即尝试处理(可能尚未加载完成)
setTimeout(() => {
// 查找对应的样式表
const styleSheet = Array.from(document.styleSheets).find(sheet =>
sheet.href === element.href
);
if (styleSheet) {
try {
const rules = Array.from(styleSheet.cssRules || []);
rules.forEach(removeBackgroundFromRule);
} catch {}
} else {
// 延迟再尝试一次,确保样式表已加载
setTimeout(() => {
for (const sheet of document.styleSheets) {
try {
const rules = Array.from(sheet.cssRules || []);
rules.forEach(removeBackgroundFromRule);
} catch {}
}
}, 300);
}
}, 100);
}
// 增强:处理可能包含样式的iframe
else if (tagName === 'iframe' && config.blockIframeImages) {
try {
// 尝试访问iframe内容(可能受到同源策略限制)
const iframeDoc = element.contentDocument || element.contentWindow.document;
if (iframeDoc) {
// 递归应用无图模式到iframe
setTimeout(() => {
if (config.blockImages) {
Array.from(iframeDoc.querySelectorAll('img')).forEach(img => {
if (!img.dataset.imageBlocked) {
blockImage(img);
}
});
}
if (config.blockBackgroundImages) {
processExistingStyleSheets(iframeDoc);
}
}, 100);
}
} catch (e) {
if (config.debug) {
console.log('无法访问iframe内容(同源策略限制):', e.message);
}
}
}
}
// 移除文本中的背景图片声明
function removeBackgroundImages(cssText) {
if (!config.blockBackgroundImages || typeof cssText !== 'string') return cssText;
// 增强的正则表达式,处理更复杂的背景图片格式
// 1. 单独的background-image属性
cssText = cssText.replace(/background-image\s*:\s*url\(\s*['"]?([^'")]+)['"]?\s*\)\s*;?/gi, '');
// 2. 处理CSS变量中的背景图片
cssText = cssText.replace(/(--[^:]+)\s*:\s*url\(\s*['"]?([^'")]+)['"]?\s*\)\s*;?/gi, function(match, varName) {
return varName + ': none;'; // 保留变量但将其值设为none
});
// 3. 处理background简写属性中的url部分
// 只移除url部分,保留其他背景属性
cssText = cssText.replace(/(background\s*:\s*[^;]*?)url\(\s*['"]?([^'")]+)['"]?\s*\)([^;]*;?)/gi, function(match, prefix, url, suffix) {
// 如果前后没有其他内容,则返回空
if (!prefix.trim() && !suffix.trim()) return '';
// 否则只返回前后部分(移除url部分)
return prefix + suffix;
});
// 4. 处理其他可能的背景相关属性变体
cssText = cssText.replace(/(background(?:-[^:]+)?\s*:\s*[^;]*?)url\(\s*['"]?([^'")]+)['"]?\s*\)([^;]*;?)/gi, function(match, prefix, url, suffix) {
// 如果前后没有其他内容,则返回空
if (!prefix.trim() && !suffix.trim()) return '';
// 否则只返回前后部分(移除url部分)
return prefix + suffix;
});
return cssText;
}
// 处理单个CSS规则,移除背景图片
function removeBackgroundFromRule(rule) {
// 处理普通CSS规则
if (rule.type === 1) { // CSSStyleRule
// 检查并移除backgroundImage属性
if (rule.style && rule.style.backgroundImage && rule.style.backgroundImage !== 'none') {
rule.style.backgroundImage = 'none';
}
// 更精确地处理background简写属性
if (rule.style && rule.style.background && rule.style.background.includes('url(')) {
// 尝试保留其他背景属性,只移除图片URL
let backgroundValue = rule.style.background;
// 保存非URL部分的背景属性
let preservedBackground = '';
// 处理颜色值
const colorMatch = backgroundValue.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,6}|[a-zA-Z]+)/);
if (colorMatch) preservedBackground += colorMatch[0] + ' ';
// 处理重复性
const repeatMatch = backgroundValue.match(/(repeat|repeat-x|repeat-y|no-repeat)/);
if (repeatMatch) preservedBackground += repeatMatch[0] + ' ';
// 处理定位
const positionMatch = backgroundValue.match(/(left|center|right|top|bottom|\d+(?:px|%|em|rem|vh|vw)?)\s+(left|center|right|top|bottom|\d+(?:px|%|em|rem|vh|vw)?)/);
if (positionMatch) preservedBackground += positionMatch[0] + ' ';
// 设置处理后的背景值
rule.style.background = preservedBackground.trim() || 'none';
}
// 检查并移除可能包含URL的CSS变量
if (rule.style) {
for (let j = 0; j < rule.style.length; j++) {
const propName = rule.style[j];
if (propName.startsWith('--') && rule.style.getPropertyValue(propName).includes('url(')) {
rule.style.setProperty(propName, 'none');
}
}
}
}
// 处理@media规则等嵌套规则
else if (rule.type === 4) { // CSSMediaRule
try {
const rules = Array.from(rule.cssRules || []);
rules.forEach(removeBackgroundFromRule);
} catch {}
}
// 处理其他可能包含规则的规则类型
else if (rule.cssRules || rule.rules) {
try {
const rules = Array.from(rule.cssRules || rule.rules || []);
rules.forEach(removeBackgroundFromRule);
} catch {}
}
}
// 方法9: 处理页面上已有的所有样式表 - 优化版本
function processExistingStyleSheets(doc = document) {
if (!config.blockBackgroundImages) return;
// 批量处理所有样式表,减少try/catch次数,提升性能
for (const sheet of doc.styleSheets) {
try {
const rules = Array.from(sheet.cssRules || []);
rules.forEach(removeBackgroundFromRule);
} catch {}
}
// 同时处理所有内联style标签
try {
doc.querySelectorAll('style').forEach(style => {
if (style.textContent) {
style.textContent = removeBackgroundImages(style.textContent);
}
});
} catch (e) {
if (config.debug) {
console.log('处理内联style标签时出错:', e.message);
}
}
// 增强:添加全局样式覆盖,作为最后防线
if (config.useGlobalStyleOverride) {
addGlobalStyleOverride(doc);
}
}
// 添加全局样式覆盖作为最后防线
function addGlobalStyleOverride(doc = document) {
// 检查是否已存在覆盖样式
if (doc.getElementById('no-image-mode-override')) {
return;
}
const styleElement = doc.createElement('style');
styleElement.id = 'no-image-mode-override';
styleElement.textContent = `
/* 全局背景图片覆盖 */
* {
background-image: none !important;
}
/* 为已设置背景图片的元素保留其他背景属性 */
[style*="background-image"] {
background-image: none !important;
}
/* 处理CSS变量 */
:root {
--background-image: none !important;
--bg-image: none !important;
}
`;
try {
doc.head.appendChild(styleElement);
} catch (e) {
if (config.debug) {
console.log('无法添加全局样式覆盖:', e.message);
}
}
}
// 处理已经存在的图片(如果脚本在页面加载过程中注入)
window.addEventListener('DOMContentLoaded', function() {
// 1. 处理所有图片标签
if (config.blockImages) {
document.querySelectorAll('img').forEach(blockImage);
}
// 2. 处理所有元素的内联背景图片
if (config.blockBackgroundImages) {
document.querySelectorAll('*').forEach(function(element) {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (element.matches(selector) || element.closest(selector)) {
return;
}
}
// 清除内联样式中的背景图片
if (element.style && element.style.backgroundImage && element.style.backgroundImage !== 'none') {
element.style.backgroundImage = 'none';
}
// 清除计算样式中的背景图片
const computedStyle = window.getComputedStyle(element);
if (computedStyle.backgroundImage !== 'none') {
element.style.backgroundImage = 'none';
}
// 检查所有背景相关属性
const backgroundProperties = ['background', 'backgroundImage'];
backgroundProperties.forEach(prop => {
if (element.style[prop] && element.style[prop].includes('url')) {
element.style[prop] = 'none';
}
});
});
}
// 3. 处理页面上所有已加载的样式表(延迟处理以提高性能)
setTimeout(processExistingStyleSheets, 1500);
// 4. 处理Base64编码图片
blockBase64Images();
// 5. 处理SVG中的图片填充
blockSVGFillImages();
// 6. 处理视频内容
blockVideos();
});
// 当所有资源加载完成后再次检查(确保脚本动态添加的图片也被处理)
window.addEventListener('load', function() {
if (config.debug) {
console.log('所有资源加载完成,开始执行最终检查...');
}
// 1. 再次处理所有图片标签(包括动态加载的图片)
if (config.blockImages) {
document.querySelectorAll('img:not([data-image-blocked])').forEach(blockImage);
}
// 2. 再次处理所有元素的内联背景图片
if (config.blockBackgroundImages) {
// 使用更高效的选择器来定位可能有背景图片的元素
document.querySelectorAll('[style*="background-image"], [style*="background"], [style*="url("]').forEach(function(element) {
// 检查是否为排除的元素
for (const selector of config.excludedSelectors) {
if (element.matches(selector) || element.closest(selector)) {
return;
}
}
// 清除内联样式中的背景图片
if (element.style && element.style.backgroundImage && element.style.backgroundImage !== 'none') {
element.style.backgroundImage = 'none';
}
// 处理background简写属性
if (element.style.background && element.style.background.includes('url(')) {
element.style.background = element.style.background.replace(/url\([^)]+\)/gi, 'none');
// 如果替换后background属性无效,则清空
if (!element.style.background.trim()) {
element.style.background = '';
}
}
});
// 再次处理所有样式表
processExistingStyleSheets();
}
// 3. 再次处理Base64编码图片(包括脚本动态添加的)
blockBase64Images();
// 4. 再次处理SVG中的图片填充
blockSVGFillImages();
// 5. 再次处理视频内容
blockVideos();
// 6. 最后添加全局样式覆盖作为保险
if (config.useGlobalStyleOverride) {
addGlobalStyleOverride();
}
if (config.debug) {
console.log('最终检查完成,所有图片和视频应已被阻止。');
}
});
// 方法10: 监听DOM变化,处理新添加的内容
const styleSheetObserver = new MutationObserver(function(mutations) {
// 使用更精确的标志来避免不必要的全局处理
let needsStyleUpdate = false;
let needsImageUpdate = false;
let needsVideoUpdate = false;
let needsSVGUpdate = false;
let newElementProcessed = false;
// 直接处理新增节点,而不是等待定时器
mutations.forEach(function(mutation) {
// 处理新增节点
mutation.addedNodes.forEach(function(node) {
if (!node.tagName) return;
const tagName = node.tagName.toLowerCase();
const isElement = node.nodeType === 1;
// 处理特定标签类型
if (tagName === 'img' && config.blockImages && !node.dataset.imageBlocked) {
blockImage(node);
newElementProcessed = true;
}
else if (tagName === 'video' && config.blockVideos) {
blockVideo(node);
newElementProcessed = true;
}
else if (tagName === 'svg' && config.blockSVGFill) {
blockSVGFillInElement(node);
newElementProcessed = true;
}
else if (config.blockBackgroundImages &&
(tagName === 'style' || (tagName === 'link' && node.rel && node.rel.toLowerCase() === 'stylesheet'))) {
processStyleElement(node);
needsStyleUpdate = true;
}
// 处理可能包含内联样式的元素
else if (isElement && config.blockBackgroundImages &&
node.hasAttribute('style') &&
(node.style.backgroundImage !== 'none' || node.style.background.includes('url('))) {
node.style.backgroundImage = 'none';
if (node.style.background.includes('url(')) {
node.style.background = '';
}
newElementProcessed = true;
}
// 处理包含子元素的节点
if (isElement && node.hasChildNodes()) {
// 性能优化:根据需要选择性处理子节点
if (config.blockImages && node.querySelector('img:not([data-image-blocked])')) {
needsImageUpdate = true;
}
if (config.blockVideos && node.querySelector('video')) {
needsVideoUpdate = true;
}
if (config.blockSVGFill && node.querySelector('svg')) {
needsSVGUpdate = true;
}
if (config.blockBackgroundImages &&
(node.querySelector('style') ||
node.querySelector('[style*="background-image"], [style*="background"]'))) {
needsStyleUpdate = true;
}
}
});
// 处理属性变化
if (mutation.type === 'attributes' && mutation.target.nodeType === 1) {
const target = mutation.target;
const attrName = mutation.attributeName.toLowerCase();
// 检查是否为背景相关属性变化
if (config.blockBackgroundImages &&
(attrName === 'style' || attrName === 'background' || attrName === 'background-image')) {
// 直接处理该元素,而不是触发全局更新
if (target.style && (target.style.backgroundImage !== 'none' || target.style.background.includes('url('))) {
target.style.backgroundImage = 'none';
if (target.style.background.includes('url(')) {
target.style.background = '';
}
}
}
// 检查是否为图片src属性变化
else if (config.blockImages && target.tagName &&
target.tagName.toLowerCase() === 'img' &&
(attrName === 'src' || attrName === 'srcset')) {
// 直接重新阻止该图片
if (!target.dataset.imageBlocked) {
blockImage(target);
}
}
// 检查SVG相关属性变化
else if (config.blockSVGFill && target.tagName &&
(target.tagName.toLowerCase() === 'svg' ||
target.tagName.toLowerCase() === 'path' ||
target.tagName.toLowerCase() === 'image')) {
needsSVGUpdate = true;
}
}
});
// 仅在必要时执行批量更新,避免过度处理
if (needsStyleUpdate || needsImageUpdate || needsVideoUpdate || needsSVGUpdate) {
// 使用防抖,避免频繁更新
if (window._styleUpdateTimer) {
clearTimeout(window._styleUpdateTimer);
}
window._styleUpdateTimer = setTimeout(() => {
// 根据需要处理不同类型的内容
if (needsStyleUpdate && config.blockBackgroundImages) {
processExistingStyleSheets();
}
if (needsImageUpdate && config.blockImages) {
document.querySelectorAll('img:not([data-image-blocked])').forEach(blockImage);
}
if (needsVideoUpdate && config.blockVideos) {
document.querySelectorAll('video').forEach(blockVideo);
}
if (needsSVGUpdate && config.blockSVGFill) {
blockSVGFillImages();
}
if (config.blockBase64Images) {
blockBase64Images();
}
}, config.updateDebounceTime || 50);
}
});
// 单独处理视频的函数
function blockVideo(video) {
if (!config.blockVideos) return;
// 保存原始源
if (!video.dataset.originalSrc) {
video.dataset.originalSrc = video.src;
}
// 清空视频源
video.pause();
video.src = '';
video.srcObject = null;
// 隐藏视频
video.style.visibility = 'hidden';
video.style.height = '0';
video.style.width = '0';
video.style.overflow = 'hidden';
}
// 直接处理单个SVG元素的函数
function blockSVGFillInElement(svg) {
if (!config.blockSVGFill) return;
// 处理SVG中的image元素
svg.querySelectorAll('image').forEach(img => {
if (img.href) {
img.removeAttribute('href');
}
if (img.getAttribute('xlink:href')) {
img.removeAttribute('xlink:href');
}
});
// 处理fill属性中的URL
svg.querySelectorAll('[fill*="url("]').forEach(elem => {
elem.setAttribute('fill', 'none');
});
// 处理pattern中的image
svg.querySelectorAll('pattern image').forEach(img => {
if (img.href) {
img.removeAttribute('href');
}
if (img.getAttribute('xlink:href')) {
img.removeAttribute('xlink:href');
}
});
}
// 开始观察head区域的变化
if (document.head) {
styleSheetObserver.observe(document.head, {
childList: true,
subtree: true
});
}
// 暴露全局配置接口(可选,用于调试或临时修改配置)
window.noImageModeConfig = config;
// 保存配置的函数(可以在控制台手动调用)
window.saveNoImageModeConfig = function(newConfig) {
try {
Object.assign(config, newConfig);
GM_setValue('noImageModeConfig', JSON.stringify(config));
console.log('无图模式配置已保存:', config);
} catch (e) {
console.error('保存配置失败:', e.message);
}
};
// 恢复被阻止图片的函数(用于临时查看图片)
window.restoreImages = function() {
const images = document.querySelectorAll('img[data-image-blocked="true"]');
images.forEach(img => {
if (img.dataset.originalSrc) {
img.src = img.dataset.originalSrc;
img.style.display = '';
img.style.backgroundImage = 'none';
img.style.backgroundColor = 'transparent';
img.style.opacity = '1';
}
});
console.log(`已恢复${images.length}张图片`);
};
// 阻止图片的函数(用于重新阻止图片)
window.blockImages = function() {
document.querySelectorAll('img').forEach(blockImage);
console.log('已重新阻止所有图片');
};
console.log('无图模式已启用,图片和视频加载已被阻止。当前配置:', config);
})();