// ==UserScript==
// @name 花瓣网助手:自动屏蔽付费素材 + 快速下载原图
// @name:en Huaban Helper: Block Paid Assets + Quick Original Downloader
// @namespace https://github.com/liuyunixs
// @id huaban-helper-combined@workbuddy
// @version 2.2.5
// @description 在花瓣网自动隐藏付费/VIP素材,同时支持 Alt+左键点击任意网页图片下载最高清原图(按网页名称命名、文件头识别真实格式)。花瓣网走官方接口取签名原图与发布内容命名。右键为浏览器原生菜单。
// @description:en Block paid/VIP assets on Huaban, plus Alt+left-click any web image to download the original (highest-resolution) file with web-name naming and magic-byte format detection. Huaban uses the official API for signed originals and raw_text naming. Right-click is the native menu.
// @author liuyunixs / WorkBuddy
// @license MIT
// @icon data:image/png;base64,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
// @homepageURL https://github.com/liuyunixs
// @match https://huaban.com/*
// @match *://*/*
// @match file:///*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect huaban.com
// @connect huabanimg.com
// @connect self
// @connect *
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
/* ============ 全局配置 ============ */
const CONFIG = {
// —— 屏蔽付费素材(仅花瓣网生效)——
showMarker: true, // 被标记/显示时添加「付费」角标
// —— 快速下载原图 ——
MAX_NAME_LEN: 80,
SRCSET_USE_LARGEST: true,
SHOW_TOAST: true,
FETCH_TIMEOUT: 9000,
};
// 付费素材「隐藏 / 显示」开关:true = 隐藏;false = 显示(粉色背景标记)
// 状态持久化,可通过篡改猴菜单随时切换
const HIDE_PAID_KEY = 'hb_helper_hide_paid';
let hidePaid = GM_getValue(HIDE_PAID_KEY, true);
// 自动屏蔽功能「总开关」:true = 开启(扫描并标记/隐藏付费素材);false = 关闭(彻底不处理)
// 状态持久化,可通过篡改猴菜单「花瓣网自动屏蔽付费素材:开/关」切换
const BLOCKER_ENABLED_KEY = 'hb_helper_blocker_enabled';
let blockerEnabled = GM_getValue(BLOCKER_ENABLED_KEY, true);
/* ============================================================
* 第一部分:花瓣网自动屏蔽付费素材(仅花瓣网生效)
* ============================================================ */
let _blockHiddenCount = 0;
// 付费/版权素材的角标文本关键词(含 EPS、版权素材等)
// 注:PAID_TEXT 用于步骤 1 / 6 的特定 class 元素;更通用的「版权素材 / EPS 类型」漏判
// 由 isPaidCard() 第 8 步的兜底扫描(遍历短文本元素)覆盖。
const PAID_TEXT = /版权素材|版权|付费|VIP|购买|商用|授权|EPS|买断|素材库|会员素材|企业素材|商用授权|可商用/i;
function isPaidCard(cardEl) {
// 1. 检查内部付费标识 div(文本可能是「ID:12345」,也可能是「版权素材」「EPS」「VIP 素材」等)
const paidMarker = cardEl.querySelector('.__2p__B98x, .QzLweiwl');
if (paidMarker) {
const text = paidMarker.textContent || '';
if (/ID[::]\s*\d+/i.test(text) || PAID_TEXT.test(text)) return true;
}
// 2. 检查是否有 data-material-id 属性(官方付费素材容器)
if (cardEl.hasAttribute && cardEl.hasAttribute('data-material-id')) {
return true;
}
// 3. 检查卡片内的链接是否包含 buyVip 关键词
const link = cardEl.querySelector('a[href*="buyVip"]');
if (link) return true;
// 4. 检查 KKIUywzb 容器是否存在 data-material-type 且不为空(EPS/版权等素材类型都在此)
const inner = cardEl.querySelector('.KKIUywzb');
if (inner) {
const matType = inner.getAttribute('data-material-type');
if (matType && matType.trim() !== '') return true;
}
// 5. 任意带 vip 关键字的 class 即判定为付费(不依赖文案)
const vipEl = cardEl.querySelector('[class*="vip" i]');
if (vipEl) return true;
// 6. 版权 / EPS 角标:badge/tag/mark/label/flag 等小角标元素,文本命中付费关键词(限制长度避免误伤正文)
const badges = cardEl.querySelectorAll(
'[class*="badge" i], [class*="tag" i], [class*="mark" i], [class*="label" i], [class*="flag" i], [class*="ribbon" i]'
);
for (const el of badges) {
const t = (el.textContent || '').trim();
if (t.length > 0 && t.length <= 40 && PAID_TEXT.test(t)) return true;
}
// 7. data 属性显式标记
if (cardEl.getAttribute) {
const dp = cardEl.getAttribute('data-paid') || cardEl.getAttribute('data-vip') || cardEl.getAttribute('data-is-paid');
if (dp === 'true' || dp === '1') return true;
}
// 8. 兜底扫描(不依赖易变混淆 class):遍历卡片内所有短文本元素,
// 专门解决「版权素材 / EPS 类型」素材未落入上述特定 class 时的漏判。
// —— HIGH_CONF 用子串匹配最无歧义的关键词(EPS / 版权素材 几乎不会误伤普通文案);
// —— 其余关键词仍用边界正则,避免正常描述里出现「授权 / 商用」等词被误判。
const HIGH_CONF = /版权素材|EPS/i;
const STRICT_PAID = /(^|[\s·•\-:|:、])(版权|付费|VIP|购买|买断|会员素材|企业素材|商用授权|可商用素材)([\s·•\-:|:、]|$)/i;
const STRONG_PAID = /^(VIP|付费|版权|购买|买断|会员素材|企业素材|版权素材|EPS)$/i;
const allNodes = cardEl.querySelectorAll('*');
for (const el of allNodes) {
const t = (el.textContent || '').trim();
if (t.length === 0 || t.length > 40) continue;
// 跳过聚合了大段文本的容器,只看「小标签 / 叶子」级别文本,避免误伤长正文
if (el.children.length > 0 && t.length > 20) continue;
if (HIGH_CONF.test(t) || STRONG_PAID.test(t) || STRICT_PAID.test(t)) return true;
}
return false;
}
function applyPaidStyle(cardEl) {
if (hidePaid) {
cardEl.classList.add('hb-paid-hidden');
cardEl.classList.remove('hb-paid-pinkbg');
} else {
cardEl.classList.remove('hb-paid-hidden');
cardEl.classList.add('hb-paid-pinkbg');
}
if (CONFIG.showMarker) cardEl.classList.add('hb-paid-marker');
else cardEl.classList.remove('hb-paid-marker');
}
function processCard(cardEl) {
if (!blockerEnabled) return;
if (!cardEl || cardEl.dataset.huabanPaidProcessed) return;
cardEl.dataset.huabanPaidProcessed = 'true';
if (isPaidCard(cardEl)) {
_blockHiddenCount++;
applyPaidStyle(cardEl);
}
}
function scanAllCards() {
if (!blockerEnabled) return;
const selectors = [
'.__7b58EDIh',
'.BR9Lhzqi.__7b58EDIh',
'[data-pin-id]',
];
selectors.forEach((sel) => {
document.querySelectorAll(sel).forEach(processCard);
});
if (_blockHiddenCount > 0) {
console.log(`[花瓣屏蔽] 已隐藏 ${_blockHiddenCount} 个付费素材`);
}
}
function initBlockerObserver() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.matches && (
node.matches('.__7b58EDIh') ||
node.matches('[data-pin-id]') ||
node.matches('.BR9Lhzqi')
)) {
processCard(node);
}
const innerCards = node.querySelectorAll &&
node.querySelectorAll('.__7b58EDIh, [data-pin-id], .BR9Lhzqi');
if (innerCards) {
innerCards.forEach(processCard);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function injectBlockerStyles() {
GM_addStyle(`
.hb-paid-hidden {
display: none !important;
}
.hb-paid-pinkbg {
position: relative !important;
outline: 2px solid #ff8fb3 !important;
outline-offset: -2px;
}
.hb-paid-pinkbg::after {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 105, 180, 0.35) !important;
pointer-events: none;
z-index: 5;
}
.hb-paid-marker::before {
content: "付费";
position: absolute;
top: 4px;
left: 4px;
z-index: 9999;
padding: 2px 6px;
background: rgba(255, 59, 48, 0.9) !important;
color: #fff !important;
font-size: 11px;
border-radius: 3px;
pointer-events: none;
font-weight: bold;
}
.hb-top-ad-hidden {
display: none !important;
}
`);
}
/* ---------- 顶部活动横幅广告拦截 ---------- */
function isTopAdKeyword(text) {
// 命中图中「清爽入夏宠粉有礼 / 素材下载低至 / 领专属价」等活动 banner 文案
return /清爽入夏|宠粉有礼|素材下载低至|领专属价|0\.06元|会员特惠|VIP.{0,5}专享|下载低至|限时福利|立即领取|抢福利|查看更多优惠|新客专享|首充|开通会员|升级会员/i.test(text);
}
// 判断候选容器是不是真正的站点导航/头部(含搜索框、logo、nav),避免误伤
function isRealSiteHeader(el) {
const cls = (el.className || '').toString();
if (/header|navbar|topnav|toolbar|logo|global-nav|main-nav/i.test(cls)) return true;
if (el.querySelector && el.querySelector('input[type="search"], input[type="text"], nav, [class*="logo" i], [class*="search" i]')) return true;
return false;
}
// 站点头部 / 导航 / 搜索栏:受最高保护,绝不被隐藏或错误位移。
// 仅检查元素自身特征及其是否直接包含搜索输入,不去向上遍历祖先,
// 以免误伤「头部内的促销条」这类本应隐藏的元素。
function isProtectedNav(el) {
if (!el || el.nodeType !== 1) return false;
const cls = (el.className || '').toString().toLowerCase();
const id = (el.id || '').toString().toLowerCase();
if (el.tagName === 'HEADER' || el.tagName === 'NAV') return true;
if (el.getAttribute && el.getAttribute('role') === 'search') return true;
if (/(^|[\s-])(header|head|navbar|nav|topnav|topbar|toolbar|logo|global-nav|main-nav|search|searchbar|search-bar|top-bar|app-bar|appbar|menu)([\s-]|$)/i.test(cls + ' ' + id)) return true;
if (el.querySelector && el.querySelector('input, form, [role="search"]')) return true;
return false;
}
// 向上遍历祖先:若元素处于站点头部 / 导航 / 搜索栏子树内(搜索下拉、联想面板等
// 通常是 header 的后代,部分 SPA 会 portal 到 body),一律视为受保护区,绝不隐藏。
function isInsideProtectedNav(el) {
let cur = el;
let depth = 0;
while (cur && cur !== document.body && depth < 16) {
const cls = (cur.className || '').toString().toLowerCase();
const id = (cur.id || '').toString().toLowerCase();
const tag = cur.tagName;
if (tag === 'HEADER' || tag === 'NAV') return true;
if (cur.getAttribute && cur.getAttribute('role') === 'search') return true;
if (/(header|head|navbar|nav|topnav|topbar|toolbar|logo|global-nav|main-nav|search|searchbar|search-bar|top-bar|app-bar|appbar|menu)/.test(cls + ' ' + id)) return true;
if (cur.querySelector && cur.querySelector('input, form, [role="search"]')) return true;
cur = cur.parentElement;
depth++;
}
return false;
}
// 搜索 / 弹层类 UI:role=dialog、aria-modal,或 class/id 含 search/overlay/modal/popup 等。
// 这类元素即便满足「顶部全宽矮条」几何特征,也绝不是广告,必须跳过。
function isSearchOverlay(el) {
if (!el || el.nodeType !== 1) return false;
const cls = (el.className || '').toString().toLowerCase();
const id = (el.id || '').toString().toLowerCase();
if (el.getAttribute && (el.getAttribute('role') === 'dialog' || el.getAttribute('aria-modal') === 'true')) return true;
if (/(search|overlay|modal|popup|drop|suggest|panel|tooltip|combobox|listbox)/.test(cls + ' ' + id)) return true;
return false;
}
// 从命中文案的元素向上,找到最可能「整条广告横幅」的容器:
// 优先 class 显式 banner/advert 等;否则用几何特征(紧贴视口顶部 + 全宽 + 矮条 + 块级)兜底。
function findTopAdContainer(el) {
if (!el) return null;
let cur = el;
let depth = 0;
while (cur && cur !== document.body && depth < 16) {
const cls = (cur.className || '').toString();
// 1. 显式广告/banner 类(混淆 class 名里偶尔仍会带这些词)
if (/banner|promo|promotion|notice|activity|advert|popup|float|topbar|top-bar|announcement|suspension|notify|activity-bar/i.test(cls)) {
if (!isProtectedNav(cur) && !isInsideProtectedNav(cur)) return cur;
}
// 2. 几何特征兜底:紧贴视口顶部、全宽、矮、块级 → 典型的顶部横幅
try {
const rect = cur.getBoundingClientRect();
const style = window.getComputedStyle(cur);
const inTop = rect.top >= -16 && rect.top < 80;
const fullW = rect.width >= window.innerWidth * 0.55;
const short = rect.height > 0 && rect.height < 180;
const blockLike = style.display !== 'inline' && style.display !== 'inline-block' && style.display !== 'none';
if (inTop && fullW && short && blockLike && !isRealSiteHeader(cur) && !isProtectedNav(cur) && !isInsideProtectedNav(cur)) {
return cur;
}
} catch (e) {}
cur = cur.parentElement;
depth++;
}
return null;
}
// 通用隐藏:从命中文案/元素向上找「整条广告横幅」容器并隐藏(幂等,靠 data-huaban-top-ad 去重)
function markAd(el, reason) {
if (!el) return false;
const target = findTopAdContainer(el);
if (!target || target.dataset.huabanTopAd === 'true') return false;
if (isProtectedNav(target) || isInsideProtectedNav(target)) return false;
target.dataset.huabanTopAd = 'true';
target.classList.add('hb-top-ad-hidden');
console.log('[花瓣屏蔽] 已拦截顶部广告:', (reason || ''), '→', (target.className || target.tagName));
fixLayoutAfterTopAdHidden();
return true;
}
// 可疑「活动/会员/优惠券」入口:href 命中促销类关键词(仍受 markAd 的「顶部几何门控」约束,不会误伤页脚链接)
const SUS_HREF = /(\/|\.)(activity|promo|promotion|vip|member|coupon|welfare|event|act\b|ad\b|banner|topic|marketing|sales|recharge|pay\/|upgrade)/i;
// 可疑广告图片:src / data-src / data-original 文件名含 banner/act/promo 等
const SUS_IMG = /banner|act|promo|topad|\/ad\/|activity|welfare|coupon/i;
function blockTopAds() {
if (!blockerEnabled) return;
let blocked = 0;
const checkText = (text, el) => {
if (!text || !isTopAdKeyword(text)) return;
if (markAd(el, text.trim().slice(0, 50))) blocked++;
};
// 1. 文本节点(按钮/文案文字都算)
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
checkText(node.textContent, node.parentElement);
}
// 2. 图片 alt / 链接 title / aria-label(广告可能为纯图片形式)
document.querySelectorAll('img[alt], a[title], [aria-label]').forEach((el) => {
const t = el.getAttribute('alt') || el.getAttribute('title') || el.getAttribute('aria-label') || '';
checkText(t, el);
});
// 3. 可疑活动链接:href 命中促销类关键词(如 /activity、/vip、/coupon)
document.querySelectorAll('a[href]').forEach((el) => {
const h = el.getAttribute('href') || '';
if (SUS_HREF.test(h) && markAd(el, '可疑活动链接:' + h.slice(0, 40))) blocked++;
});
// 4. 可疑广告图片:src/data-src/data-original 文件名含 banner/act/promo 等
document.querySelectorAll('img[src], img[data-src], img[data-original]').forEach((el) => {
const s = el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-original') || '';
if (SUS_IMG.test(s) && markAd(el, '可疑广告图片:' + s.slice(0, 40))) blocked++;
});
if (blocked > 0) {
console.log(`[花瓣屏蔽] 本次拦截 ${blocked} 个顶部广告`);
} else {
// 关键词没命中(活动文案可能在 iconfont/图片里取不到文字),
// 退而求其次:按几何特征隐藏「页面最顶、全宽、矮条、非导航/搜索」的元素。
blockTopAdsGeometric();
}
}
// 几何兜底:仅针对页面最顶部(top<90)的全宽(≥55%视口)矮条(12~180px),
// 排除含输入框/nav/logo 以及常驻 UI 文案(发现/关注/首页/搜索…)的容器,隐藏剩下的。
function blockTopAdsGeometric() {
if (!blockerEnabled) return;
const vw = window.innerWidth;
const UI_TEXT = /发现|关注|首页|搜索|你可能感兴趣|登录|注册|消息|通知|我的|创作|客服|下载客户端|分类|热门|推荐|最新|投稿|上传|APP|扫码|客户端/i;
// 仅当元素带有明确「广告/促销」信号时才可能隐藏,避免误伤中性 UI(如搜索下拉面板)。
// 注意:搜索联想面板即便含 链接,也已先被 isSearchOverlay / isInsideProtectedNav 排除。
const PROMO_SIGNAL = /(banner|promo|advert|活动|福利|优惠|领|券|会员|VIP|专享|特惠|限时|宠粉|清爽|抽奖|红包|notice|announcement|suspension|welfare|coupon)/i;
let best = null, bestTop = Infinity;
document.querySelectorAll('*').forEach((el) => {
if (el.dataset.huabanTopAd === 'true') return;
if (isInsideProtectedNav(el)) return; // 处于头部/导航/搜索子树内(含搜索下拉)→ 永不隐藏
if (isSearchOverlay(el)) return; // 搜索 / 弹层类 UI → 永不隐藏
const rect = el.getBoundingClientRect();
if (rect.top < -16 || rect.top > 90) return;
if (rect.width < vw * 0.55) return;
if (rect.height < 12 || rect.height > 180) return;
if (el.querySelector && el.querySelector('input, nav, [class*="logo" i]')) return;
if (isProtectedNav(el)) return;
const t = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (UI_TEXT.test(t)) return;
// 必须带有至少一个明确广告信号,否则视为中性 UI(如搜索联想面板)跳过
const hasPromoClass = /(banner|promo|promotion|advert|activity|活动|notice|announcement|suspension|topbar|top-bar|notify|welfare|coupon)/i.test((el.className || '').toString() + ' ' + (el.id || '').toString());
const hasLinkOrBtn = el.querySelector && el.querySelector('a, button');
const hasPromoText = PROMO_SIGNAL.test(t);
if (!hasPromoClass && !hasLinkOrBtn && !hasPromoText) return;
if (rect.top < bestTop) { bestTop = rect.top; best = el; }
});
if (best) {
best.dataset.huabanTopAd = 'true';
best.classList.add('hb-top-ad-hidden');
console.log('[花瓣屏蔽] 几何兜底拦截顶部条:', (best.className || best.tagName), 'top=' + Math.round(bestTop), 'text=', (best.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 40));
fixLayoutAfterTopAdHidden();
}
}
// banner 隐藏后,修正下方 fixed/sticky 顶部元素以及 body/html 的 padding-top/margin-top,
// 避免出现「广告没了,但导航栏还停在原来被挤下来的位置」的断层空白。
function fixLayoutAfterTopAdHidden() {
// 收集当前可见的 fixed/sticky 元素(排除已被隐藏的广告条自身)
const fixedEls = [];
document.querySelectorAll('*').forEach((el) => {
if (el.classList.contains('hb-top-ad-hidden')) return;
const style = window.getComputedStyle(el);
if ((style.position === 'fixed' || style.position === 'sticky') && el.offsetHeight > 0) {
fixedEls.push(el);
}
});
if (!fixedEls.length) return;
// 最小 top 即 banner 把下面元素挤下来的距离
let minTop = Infinity;
fixedEls.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.top >= -2 && rect.top < minTop) minTop = rect.top;
});
if (!isFinite(minTop) || minTop <= 2) return;
const offset = minTop;
// 把所有基于顶部定位的 fixed/sticky 元素整体上移 offset。
// 关键:每个元素「仅首次」保存它真实的原始 top,之后一律基于该原始值计算,
// 这样无论 markAd 被调用多少次(多次扫描 / 多个广告),位移都只生效一次,
// 不会因为重复叠加把头部 / 搜索栏推到屏幕外。
fixedEls.forEach((el) => {
const cs = window.getComputedStyle(el);
if (el.dataset.huabanOriginalTop === undefined) {
const orig = (cs.top === 'auto' || cs.top === '') ? 'auto' : String(parseFloat(cs.top));
el.dataset.huabanOriginalTop = orig;
}
const origTop = parseFloat(el.dataset.huabanOriginalTop);
if (isNaN(origTop)) return; // top:auto 等不处理
el.style.setProperty('top', (origTop - offset) + 'px', 'important');
el.dataset.huabanLayoutFixed = 'true';
console.log('[花瓣屏蔽] 修正顶部布局,上移:', (el.className || el.tagName), 'by', offset);
});
// 同步修正 body / html 的 padding-top / margin-top(同样基于首次保存值,幂等)
[document.body, document.documentElement].forEach((el) => {
if (!el) return;
['paddingTop', 'marginTop'].forEach((prop) => {
const cssProp = prop === 'paddingTop' ? 'padding-top' : 'margin-top';
const val = parseFloat(window.getComputedStyle(el)[prop]) || 0;
if (val > offset - 2 && val < offset + 2) {
const origKey = prop === 'paddingTop' ? 'huabanOriginalPadding' : 'huabanOriginalMargin';
if (el.dataset[origKey] === undefined) el.dataset[origKey] = window.getComputedStyle(el)[prop] || '0px';
el.style.setProperty(cssProp, '0px', 'important');
el.dataset.huabanLayoutFixed = 'true';
}
});
});
}
// 关闭总开关时恢复被修正过的 top / padding / margin
function restoreTopLayout() {
document.querySelectorAll('[data-huaban-layout-fixed]').forEach((el) => {
if (el.dataset.huabanOriginalTop !== undefined) {
if (el.dataset.huabanOriginalTop) el.style.setProperty('top', el.dataset.huabanOriginalTop, 'important');
else el.style.removeProperty('top');
delete el.dataset.huabanOriginalTop;
}
if (el === document.body || el === document.documentElement) {
if (el.dataset.huabanOriginalPadding !== undefined) {
if (el.dataset.huabanOriginalPadding) el.style.setProperty('padding-top', el.dataset.huabanOriginalPadding, 'important');
else el.style.removeProperty('padding-top');
delete el.dataset.huabanOriginalPadding;
}
if (el.dataset.huabanOriginalMargin !== undefined) {
if (el.dataset.huabanOriginalMargin) el.style.setProperty('margin-top', el.dataset.huabanOriginalMargin, 'important');
else el.style.removeProperty('margin-top');
delete el.dataset.huabanOriginalMargin;
}
}
delete el.dataset.huabanLayoutFixed;
});
}
// 诊断:把页面顶部区域所有「全宽条」候选 + 含广告关键词的元素 + iframe 打印到控制台,
// 用于广告没拦住时定位真实 DOM 结构。菜单「🌸 顶部广告诊断」触发。
function diagnoseTopAds() {
const vw = window.innerWidth, vh = window.innerHeight;
console.log('%c[花瓣屏蔽-诊断] 视口 ' + vw + 'x' + vh + ' scrollY=' + Math.round(window.scrollY), 'color:#0a8');
// 1. iframe 列表(广告可能在 iframe 内,普通扫描扫不到)
const iframes = Array.from(document.querySelectorAll('iframe')).map((f) => f.src || f.getAttribute('data-src') || '(无src)');
if (iframes.length) console.log('%c[花瓣屏蔽-诊断] 页面 iframe(' + iframes.length + '):', 'color:#a50', iframes);
else console.log('[花瓣屏蔽-诊断] 页面无 iframe');
// 2. 全宽候选(放宽:top∈[-50,vh], width≥35%vw, height∈[8,400])附带背景色(彩色促销条线索)
const cands = [];
document.querySelectorAll('*').forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.top < -50 || rect.top > vh) return;
if (rect.width < vw * 0.35) return;
if (rect.height < 8 || rect.height > 400) return;
const t = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (t.length < 2) return;
let bg = '';
try { bg = window.getComputedStyle(el).backgroundColor || ''; } catch (e) {}
cands.push({ tag: el.tagName.toLowerCase(), cls: (el.className || '').toString().slice(0, 70), top: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height), text: t.slice(0, 60), bg });
});
cands.sort((a, b) => a.top - b.top || b.w - a.w);
console.log('%c[花瓣屏蔽-诊断] 全宽候选(前60,含背景色):', 'color:#0a8');
cands.slice(0, 60).forEach((c, i) => console.log(`#${i} <${c.tag}> top=${c.top} w=${c.w} h=${c.h} bg=${c.bg} cls="${c.cls}" text="${c.text}"`));
// 4. 可疑活动链接 / 广告图片(促销入口线索,即使无文案也能定位)
document.querySelectorAll('a[href]').forEach((el) => {
const h = el.getAttribute('href') || '';
if (SUS_HREF.test(h)) { const r = el.getBoundingClientRect(); console.log(` 可疑活动链接 top=${Math.round(r.top)} w=${Math.round(r.width)} href="${h.slice(0, 80)}" cls="${(el.className || '').toString().slice(0, 40)}"`); }
});
document.querySelectorAll('img[src], img[data-src], img[data-original]').forEach((el) => {
const s = el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-original') || '';
if (SUS_IMG.test(s)) { const r = el.getBoundingClientRect(); console.log(`
可疑广告图片 top=${Math.round(r.top)} w=${Math.round(r.width)} src="${s.slice(0, 80)}"`); }
});
// 3. 关键词强制打印:含广告文案的元素,无论尺寸(命中即广告线索)
const KW = /清爽|宠粉|领专属价|专属价|会员|福利|0\.06|VIP|下载低至|开通会员|升级会员|限时|优惠券|首充|新客|活动|领取|抢|领券/i;
let kwHit = 0;
document.querySelectorAll('*').forEach((el) => {
const t = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (t.length < 2 || t.length > 200) return;
if (!KW.test(t)) return;
const rect = el.getBoundingClientRect();
const chain = [];
let p = el, n = 0;
while (p && p !== document.body && n < 4) { chain.push((p.tagName || '').toLowerCase() + '.' + (p.className || '').toString().slice(0, 18)); p = p.parentElement; n++; }
console.log(`<${el.tagName.toLowerCase()}> top=${Math.round(rect.top)} w=${Math.round(rect.width)} cls="${(el.className || '').toString().slice(0, 50)}" text="${t.slice(0, 50)}" 父链=[${chain.join(' < ')}]`);
kwHit++;
});
if (!kwHit) console.log('[花瓣屏蔽-诊断] 当前 DOM 未找到任何广告关键词——广告可能:(a)已关闭/未加载;(b)在 iframe 内;(c)用图片/iconfont 无文本。请确认诊断时黄色广告条仍可见,并告诉我当前在哪个页面(首页/发现/详情)。');
return cands;
}
function initTopAdObserver() {
let timer = null;
const observer = new MutationObserver(() => {
// 节流:DOM 变动后最多 300ms 跑一次扫描
if (timer) return;
timer = setTimeout(() => { timer = null; blockTopAds(); }, 300);
});
observer.observe(document.body, { childList: true, subtree: true });
}
/* ---------- 手动点选屏蔽(兜底,应对混淆 class / 文案取不到 / 动态加载) ---------- */
const MANUAL_AD_SIGS_KEY = 'hb_manual_ad_sigs';
let pickMode = false;
function getManualAdSigs() {
try { return GM_getValue(MANUAL_AD_SIGS_KEY, []) || []; } catch (e) { return []; }
}
function saveManualAdSig(sig) {
if (!sig) return;
const arr = getManualAdSigs();
if (!arr.includes(sig)) { arr.push(sig); GM_setValue(MANUAL_AD_SIGS_KEY, arr); }
}
function clearManualAdSigs() {
try { GM_setValue(MANUAL_AD_SIGS_KEY, []); } catch (e) {}
}
// 套用已保存的「文本签名」:顶部区域里文本含该签名的元素直接隐藏(应对混淆 class 换名)
function applyManualAdSignatures() {
if (!blockerEnabled) return;
const sigs = getManualAdSigs();
if (!sigs.length) return;
const vw = window.innerWidth;
document.querySelectorAll('*').forEach((el) => {
if (el.dataset.huabanTopAd === 'true') return;
if (isRealSiteHeader(el)) return;
if (isProtectedNav(el)) return;
if (isInsideProtectedNav(el)) return;
const rect = el.getBoundingClientRect();
if (rect.top < -16 || rect.top > 160) return;
if (rect.width < vw * 0.3) return;
const t = (el.textContent || '').replace(/\s+/g, ' ').trim();
for (const sig of sigs) {
if (sig && t.indexOf(sig) !== -1) {
el.dataset.huabanTopAd = 'true';
el.classList.add('hb-top-ad-hidden');
console.log('[花瓣屏蔽] 手动签名命中并隐藏:', sig, '→', (el.className || el.tagName));
fixLayoutAfterTopAdHidden();
return;
}
}
});
}
function exitPickMode() {
pickMode = false;
if (document.body) document.body.style.cursor = '';
if (window.__hbPickOnClick) { document.removeEventListener('click', window.__hbPickOnClick, true); window.__hbPickOnClick = null; }
if (window.__hbPickOnKey) { document.removeEventListener('keydown', window.__hbPickOnKey, true); window.__hbPickOnKey = null; }
}
// 进入点选模式:用户点击任意元素即隐藏其「够宽的容器」,含稳定文本则记住、下次自动隐藏
function enterPickMode() {
if (!blockerEnabled) { toast('请先开启「花瓣网自动屏蔽付费素材」'); return; }
if (pickMode) { exitPickMode(); toast('已退出点选模式'); return; }
pickMode = true;
if (document.body) document.body.style.cursor = 'crosshair';
toast('点选屏蔽:点击要隐藏的横幅/广告(按 Esc 取消)');
const onKey = (e) => { if (e.key === 'Escape') { exitPickMode(); toast('已取消点选'); } };
const onClick = (e) => {
e.preventDefault();
e.stopPropagation();
exitPickMode();
let el = e.target;
if (!el || !el.getBoundingClientRect) { return; }
// 向上收敛到「够宽」的容器,避免只隐藏 tiny 文字节点
while (el && el.parentElement && el.tagName !== 'BODY'
&& el.getBoundingClientRect().width < window.innerWidth * 0.5) {
el = el.parentElement;
}
if (!el || el === document.body) { toast('取消:所选范围过大'); return; }
if (isRealSiteHeader(el)) { toast('这是站点导航,已跳过'); return; }
const t = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
el.dataset.huabanTopAd = 'true';
el.classList.add('hb-top-ad-hidden');
if (t.length >= 2) saveManualAdSig(t);
fixLayoutAfterTopAdHidden();
toast('已隐藏所选元素' + (t ? '(已记住:' + t.slice(0, 20) + ')' : '(纯图片,仅本次会话)'));
};
document.addEventListener('click', onClick, true);
document.addEventListener('keydown', onKey, true);
window.__hbPickOnClick = onClick;
window.__hbPickOnKey = onKey;
}
function toggleHidePaid() {
if (!blockerEnabled) {
toast('请先开启「花瓣网自动屏蔽付费素材」');
return;
}
hidePaid = !hidePaid;
GM_setValue(HIDE_PAID_KEY, hidePaid);
// 重新套用样式到已处理过的卡片
document.querySelectorAll('[data-huaban-paid-processed]').forEach((el) => {
if (isPaidCard(el)) applyPaidStyle(el);
else el.classList.remove('hb-paid-hidden', 'hb-paid-pinkbg', 'hb-paid-marker');
});
scanAllCards();
toast('付费素材已' + (hidePaid ? '隐藏' : '显示(粉色背景)'));
}
// 自动屏蔽功能「总开关」:开 = 扫描并标记/隐藏;关 = 彻底清除标记并不再处理
function setBlockerEnabled(on) {
blockerEnabled = on;
GM_setValue(BLOCKER_ENABLED_KEY, on);
if (on) {
scanAllCards();
applyManualAdSignatures();
} else {
// 关闭:移除所有标记与隐藏,重置处理标记以便将来重新扫描
document.querySelectorAll('.hb-paid-hidden, .hb-paid-pinkbg, .hb-paid-marker, .hb-top-ad-hidden').forEach((el) => {
el.classList.remove('hb-paid-hidden', 'hb-paid-pinkbg', 'hb-paid-marker', 'hb-top-ad-hidden');
});
document.querySelectorAll('[data-huaban-paid-processed]').forEach((el) => {
delete el.dataset.huabanPaidProcessed;
});
document.querySelectorAll('[data-huaban-top-ad]').forEach((el) => {
delete el.dataset.huabanTopAd;
});
_blockHiddenCount = 0;
}
refreshBlockerMenu();
toast('花瓣网自动屏蔽付费素材已' + (on ? '开启' : '关闭'));
}
// 动态刷新菜单(标签显示当前状态;支持注销时实时更新,旧引擎降级为仅初始化注册一次)
let _cmdSwitch = null, _cmdMode = null, _menuRegistered = false;
function refreshBlockerMenu() {
const switchLabel = '🌸 花瓣网自动屏蔽付费素材:' + (blockerEnabled ? '已开启 ✓' : '已关闭');
const modeLabel = '🌸 ' + (hidePaid ? '显示付费素材(粉色背景)' : '隐藏付费素材');
if (typeof GM_unregisterMenuCommand === 'function') {
if (_cmdSwitch) GM_unregisterMenuCommand(_cmdSwitch);
if (_cmdMode) GM_unregisterMenuCommand(_cmdMode);
_cmdSwitch = GM_registerMenuCommand(switchLabel, () => setBlockerEnabled(!blockerEnabled));
_cmdMode = GM_registerMenuCommand(modeLabel, toggleHidePaid);
} else if (!_menuRegistered) {
// 不支持注销:只在初始化注册一次,状态靠 toast 提示
GM_registerMenuCommand(switchLabel, () => setBlockerEnabled(!blockerEnabled));
GM_registerMenuCommand(modeLabel, toggleHidePaid);
_menuRegistered = true;
}
}
function initBlocker() {
console.log('[花瓣屏蔽] 自动屏蔽付费素材脚本已加载(功能:' + (blockerEnabled ? '开启' : '关闭') +
',付费素材当前:' + (hidePaid ? '隐藏' : '显示(粉色背景)') + ')');
injectBlockerStyles();
refreshBlockerMenu();
if (blockerEnabled) {
const runScans = () => {
setTimeout(scanAllCards, 500);
setTimeout(blockTopAds, 600);
applyManualAdSignatures();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runScans);
} else {
runScans();
}
// 多次延迟重扫:覆盖延迟挂载 / 异步插入的顶部 banner(1.5s / 3s / 5s / 8s)
[1500, 3000, 5000, 8000].forEach((d) => {
setTimeout(() => {
if (!blockerEnabled) return;
blockTopAds();
applyManualAdSignatures();
}, d);
});
}
initBlockerObserver();
initTopAdObserver();
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('🌸 顶部广告诊断(打印到控制台)', () => {
diagnoseTopAds();
toast('已把顶部候选元素打印到控制台(F12 → Console)');
});
GM_registerMenuCommand('🌸 手动点选屏蔽元素(顶部广告兜底)', () => enterPickMode());
GM_registerMenuCommand('🌸 清除手动屏蔽记忆', () => {
clearManualAdSigs();
toast('已清除手动屏蔽记忆(已隐藏元素将在刷新后恢复)');
});
}
}
/* ============================================================
* 第二部分:快速下载原图(Alt + 左键,全站生效)
* ============================================================ */
/* ----------------------- 工具函数 ----------------------- */
function isImageUrl(u) {
return /\.(jpe?g|png|gif|webp|bmp|svg|avif|tiff?|ico)(\?|#|$)/i.test(u);
}
function toAbs(url) {
try { return new URL(url, location.href).href; } catch (e) { return url; }
}
function cleanText(s) {
if (!s) return '';
return s.replace(/\s+/g, ' ').replace(/[\r\n\t]+/g, ' ').trim();
}
function isJunk(s) {
if (!s || s.length > 120) return true;
if (/^(点击|查看|click|view|enlarge|放大|加载中|loading)/i.test(s)) return true;
if (/^https?:\/\//i.test(s)) return true;
return false;
}
function getExt(url) {
try {
const u = new URL(url, location.href);
const path = decodeURIComponent(u.pathname);
const m = path.match(/\.([a-z0-9]{2,5})$/i);
if (m) return '.' + m[1].toLowerCase();
const q = u.search.match(/[?&][^=?]*\.([a-z0-9]{2,5})(?:$|&)/i);
if (q) return '.' + q[1].toLowerCase();
} catch (e) {}
return '';
}
function mimeToExt(mime) {
const m = {
'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png',
'image/gif': '.gif', 'image/webp': '.webp', 'image/bmp': '.bmp',
'image/svg+xml': '.svg', 'image/avif': '.avif', 'image/tiff': '.tif',
};
return m[(mime || '').toLowerCase().split(';')[0].trim()] || '';
}
function sanitize(name) {
let s = (name || '').trim().replace(/\s+/g, ' ');
s = s.replace(/[\\/:*?"<>|\r\n\t]/g, '');
s = s.replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '');
s = s.replace(/\.{2,}/g, '.');
s = s.replace(/\.[a-z0-9]{2,5}$/i, '');
s = s.slice(0, CONFIG.MAX_NAME_LEN).trim();
s = s.replace(/[. ]+$/g, '');
return s;
}
function isMeaninglessName(s) {
if (!s) return true;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) return true;
if (/^[0-9a-f]{16,}$/i.test(s)) return true;
if (/^\d{10,}$/.test(s)) return true;
if (/^[a-z0-9]{20,}$/i.test(s)) return true;
return false;
}
function dedupe(arr) { return Array.from(new Set(arr)); }
/* ----------------------- 元素级原图地址 ----------------------- */
function largestFromSrcset(set) {
const parts = set.split(',').map((p) => p.trim()).filter(Boolean);
let best = '', bestN = -1;
for (const p of parts) {
const m = p.match(/^(\S+)\s+(?:(\d+)w|(\d+(?:\.\d+)?)x)$/);
if (!m) continue;
const u = m[1];
const n = m[2] ? parseInt(m[2], 10) : parseFloat(m[3]) * (window.devicePixelRatio || 1) * 1000;
if (n > bestN) { bestN = n; best = u; }
}
return best;
}
function getOriginalUrl(el) {
let url = '';
const dataKeys = [
'original', 'src', 'real-src', 'realSrc', 'real', 'large', 'big', 'zoom',
'full', 'hires', 'high', 'hd', 'lazy-src', 'ori', 'origin', 'osrc',
'large-src', 'big-src', 'full-src', 'original-src', 'source-src',
'orjURL', 'zoom-src', 'hd-src', 'true-src', 'bdata-src',
];
for (const k of dataKeys) {
const v = el.getAttribute && el.getAttribute('data-' + k);
if (v && /^(https?:|\/\/|data:image\/)/.test(v)) { url = v; break; }
}
if (CONFIG.SRCSET_USE_LARGEST && el.srcset) {
const s = largestFromSrcset(el.srcset);
if (s) url = s;
}
if (!url) url = el.currentSrc || el.src || '';
try {
const a = el.closest && el.closest('a[href]');
if (a) {
const h = a.getAttribute('href');
if (h && isImageUrl(h) && toAbs(h) !== toAbs(url)) url = h;
}
} catch (e) {}
return toAbs(url);
}
function getBgImage(el) {
try {
const cs = getComputedStyle(el);
const bi = cs.backgroundImage || cs.webkitMaskImage || '';
if (bi && bi !== 'none') {
const m = bi.match(/url\(["']?(.*?)["']?\)/);
if (m && m[1]) return m[1];
}
} catch (e) {}
return null;
}
function getImageFromTarget(target) {
if (!target || target.nodeType !== 1) return null;
if (target.tagName === 'IMG') return { el: target, bgUrl: null };
const bg = getBgImage(target);
if (bg) return { el: target, bgUrl: bg };
const found = findNearestImg(target);
if (found) return { el: found, bgUrl: null };
return null;
}
function findNearestImg(node) {
if (!node || !node.querySelector) return null;
const child = node.querySelector('img');
if (child) return child;
let p = node.parentElement, depth = 0;
while (p && depth < 5) {
const im = p.querySelector && p.querySelector('img');
if (im) return im;
p = p.parentElement;
depth++;
}
return null;
}
/* ----------------------- 花瓣网:接口取原图 ----------------------- */
function isHuabanPage() {
return /(^|\.)huaban\.com$/i.test(location.hostname);
}
function isHuabanCdn(url) {
try {
const h = new URL(url).hostname;
return /hbimg/i.test(h) && /\.huaban(img)?\.com$/i.test(h);
} catch (e) { return false; }
}
function huabanCdnOriginal(url) {
if (!isHuabanCdn(url)) return url;
try {
const u = new URL(url);
u.hostname = u.hostname.replace(/^gd-hbimg-edge\./i, 'gd-hbimg.');
u.pathname = u.pathname
.replace(/_(?:sq|fw|sf|sh|fh)\d+.*$/i, '')
.replace(/\/format\/.*$/i, '');
u.search = '';
u.hash = '';
return u.href;
} catch (e) { return url; }
}
function huabanPinIdFromContext(target) {
let node = target;
for (let i = 0; i < 15 && node; i++) {
try {
const a = node.closest && node.closest('a[href]');
if (a) {
const mm = (a.getAttribute('href') || '').match(/\/pins?\/(\d+)/i);
if (mm) return mm[1];
}
const holder = node.closest && node.closest('[data-pin-id], [data-pinid]');
if (holder) {
const did = holder.getAttribute('data-pin-id') || holder.getAttribute('data-pinid');
if (did && /^\d+$/.test(did)) return did;
}
} catch (e) {}
node = node.parentElement;
}
const img = target && target.tagName === 'IMG' ? target : (target && target.querySelector ? target.querySelector('img') : null);
if (img) {
const did = img.getAttribute('data-id') || img.getAttribute('data-pin-id');
if (did && /^\d{5,}$/.test(did)) return did;
const key = huabanKeyFromUrl(img.currentSrc || img.src || '');
if (key) {
const pid = huabanPinIdByImgKey(key);
if (pid) return pid;
}
}
const m = location.pathname.match(/\/pins?\/(\d+)/i);
if (m) return m[1];
return null;
}
function huabanKeyFromUrl(u) {
try {
if (!u || !isHuabanCdn(u)) return '';
const p = new URL(u, location.href).pathname.replace(/^\//, '');
return p.replace(/_(?:sq|fw|sf|sh|fh)\d+.*$/i, '').replace(/\/format\/.*$/i, '');
} catch (e) { return ''; }
}
function huabanPinIdByImgKey(key) {
try {
const links = document.querySelectorAll('a[href*="/pins/"]');
for (const a of links) {
const img = a.querySelector('img');
const src = img ? (img.currentSrc || img.getAttribute('src') || '') : '';
if (src && src.indexOf(key) !== -1) {
const m = (a.getAttribute('href') || '').match(/\/pins?\/(\d+)/i);
if (m) return m[1];
}
}
} catch (e) {}
try {
const st = window.__INITIAL_STATE__ || window.__APOLLO_STATE__ || window.__PRELOADED_STATE__;
if (st) {
const pid = findPinIdInState(st, key, 0, { n: 8000 });
if (pid) return pid;
}
} catch (e) {}
try {
const scripts = document.querySelectorAll('script:not([src])');
for (const s of scripts) {
const t = s.textContent || '';
if (t.length < 20 || t.length > 3000000) continue;
const i = t.indexOf(key);
if (i === -1) continue;
const before = t.slice(Math.max(0, i - 2000), i);
const re = /"pin_id"\s*:\s*(\d+)/g;
let m, last = null;
while ((m = re.exec(before))) last = m[1];
if (last) return last;
}
} catch (e) {}
return null;
}
function findPinIdInState(obj, key, depth, budget) {
if (!obj || typeof obj !== 'object' || depth > 8 || budget.n <= 0) return null;
budget.n--;
const f = obj.file;
if (f && typeof f === 'object' && f.key === key) {
const id = obj.pin_id || obj.pinId || obj.id;
if (id && /^\d+$/.test(String(id))) return String(id);
}
for (const k in obj) {
const r = findPinIdInState(obj[k], key, depth + 1, budget);
if (r) return r;
}
return null;
}
function getHuabanPin(pinId, cb) {
const apiUrl = 'https://huaban.com/v3/pins/' + pinId;
const parse = (json) => {
const pin = json && json.pin;
if (!pin) throw new Error('no pin');
const f = pin.file || {};
const urls = [];
if (f.url) urls.push(f.url);
if (f.bucket && f.key) urls.push('https://' + f.bucket + '.huaban.com/' + f.key);
return {
urls: dedupe(urls),
ext: mimeToExt(f.type),
width: f.width || 0,
height: f.height || 0,
rawText: cleanText(pin.raw_text || ''),
board: (pin.board && pin.board.title) || '',
};
};
if (isHuabanPage()) {
fetch(apiUrl, { headers: { 'Accept': 'application/json' } })
.then((r) => r.json())
.then((j) => cb(parse(j)))
.catch(() => cb(null));
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
headers: { 'User-Agent': navigator.userAgent, 'Referer': location.href, 'Accept': 'application/json' },
timeout: CONFIG.FETCH_TIMEOUT,
onload: (res) => { try { cb(parse(JSON.parse(res.responseText))); } catch (e) { cb(null); } },
onerror: () => cb(null),
ontimeout: () => cb(null),
});
}
/* ----------------------- 通用站:缩略图规则变换 ----------------------- */
function transformThumbUrl(url) {
const out = [];
try {
const u = new URL(url);
const h = u.hostname;
const p = u.pathname;
if (/-\d{2,4}x\d{2,4}(\.[a-z0-9]{2,5})$/i.test(p)) {
out.push(u.origin + p.replace(/-\d{2,4}x\d{2,4}(\.[a-z0-9]{2,5})$/i, '$1'));
}
if (/(?:_thumb|-thumb|_small|-small|_medium|-medium|_s)(\.[a-z0-9]{2,5})$/i.test(p)) {
out.push(u.origin + p.replace(/(?:_thumb|-thumb|_small|-small|_medium|-medium|_s)(\.[a-z0-9]{2,5})$/i, '$1'));
}
if (/sinaimg\.cn$/i.test(h)) {
out.push(url.replace(/\/(orj360|bmiddle|orj1080|thumb150|thumbnail|square|mw690)\//i, '/oslarge/'));
}
if (/twimg\.com$/i.test(h) && u.search) {
const nu = new URL(url);
nu.searchParams.set('name', 'orig');
out.push(nu.href);
}
if (/pinimg\.com$/i.test(h)) {
out.push(url.replace(/\/\d{2,4}x\//, '/originals/'));
}
if (/hdslb\.com$/i.test(h) && /@/.test(p)) {
out.push(u.origin + p.replace(/@.*$/, ''));
}
} catch (e) {}
return dedupe(out).filter((x) => x && x !== url);
}
/* ----------------------- 通用站:进详情页找原图 ----------------------- */
function fetchDetailCandidates(pageUrl, cb) {
GM_xmlhttpRequest({
method: 'GET',
url: pageUrl,
headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml' },
timeout: CONFIG.FETCH_TIMEOUT,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText || '', 'text/html');
const urls = [];
const abs = (x) => { try { return new URL(x, pageUrl).href; } catch (e) { return ''; } };
doc.querySelectorAll('meta[property="og:image"], meta[name="og:image"], meta[property="twitter:image"], meta[name="twitter:image"]')
.forEach((m) => {
const u = abs(m.getAttribute('content') || '');
if (u && isImageUrl(u)) urls.push({ url: u, trust: 'og' });
});
doc.querySelectorAll('a[href]').forEach((a) => {
const h = a.getAttribute('href') || '';
if (isImageUrl(h)) {
const u = abs(h);
if (u) urls.push({ url: u, trust: 'struct' });
}
});
const imgs = Array.from(doc.querySelectorAll('img')).map((im) => {
const w = parseInt(im.getAttribute('width') || '0', 10);
const hh = parseInt(im.getAttribute('height') || '0', 10);
let best = '';
const keys = ['original', 'src', 'real-src', 'large', 'big', 'full', 'hires', 'zoom', 'lazy-src'];
for (const k of keys) {
const v = im.getAttribute('data-' + k);
if (v && /^(https?:|\/\/)/.test(v)) { best = v; break; }
}
if (!best && im.getAttribute('srcset')) best = largestFromSrcset(im.getAttribute('srcset'));
if (!best) best = im.getAttribute('src') || '';
return { u: abs(best), area: w * hh, dataDriven: !!im.getAttribute('data-original') || !!im.getAttribute('data-src') };
}).filter((x) => x.u && isImageUrl(x.u));
imgs.sort((a, b) => (b.dataDriven - a.dataDriven) || (b.area - a.area));
imgs.slice(0, 2).forEach((x) => urls.push({ url: x.u, trust: x.dataDriven ? 'struct' : 'og' }));
const ogTitle = (doc.querySelector('meta[property="og:title"], meta[name="og:title"]') || {}).content || '';
const title = cleanText(ogTitle || (doc.title || ''));
cb({ urls: dedupe(urls.map((x) => x.url)).map((u) => urls.find((x) => x.url === u)), title: title });
} catch (e) {
cb({ urls: [], title: '' });
}
},
onerror: () => cb({ urls: [], title: '' }),
ontimeout: () => cb({ urls: [], title: '' }),
});
}
/* ----------------------- 取网页显示的名称 ----------------------- */
function collectNameCandidates(el) {
const cands = [];
const push = (s, score) => {
s = cleanText(s);
if (s && !isJunk(s)) cands.push({ s, score });
};
push(el.alt, 50);
push(el.title, 45);
push(el.getAttribute && el.getAttribute('aria-label'), 40);
['name', 'caption', 'title', 'alt', 'desc', 'label', 'figcaption'].forEach((k) => {
const v = el.getAttribute && el.getAttribute('data-' + k);
if (v) push(v, 42);
});
let node = el;
while (node && node.parentElement && node.tagName !== 'FIGURE') node = node.parentElement;
if (node && node.tagName === 'FIGURE') {
const fc = node.querySelector('figcaption');
if (fc) push(fc.textContent, 60);
}
try {
const a = el.closest && el.closest('a[href]');
if (a) {
push(a.getAttribute('title'), 72);
push(a.getAttribute('aria-label'), 66);
const aText = Array.from(a.childNodes)
.filter((n) => n.nodeType === 3 && n.textContent.trim())
.map((n) => n.textContent).join(' ');
if (aText.length <= 60) push(aText, 35);
a.querySelectorAll('.description, .pin-desc, .pin-title, [class*="desc" i], [class*="title" i]')
.forEach((c) => push(c.textContent, 58));
}
} catch (e) {}
const captionSel =
'figcaption, .description, .pin-desc, .pin-title, [class*="caption" i], [class*="title" i], ' +
'[class*="desc" i], [class*="name" i], [class*="info" i], [id*="caption" i], ' +
'[class*="图注" i], [class*="说明" i], [class*="图名" i]';
const container = el.closest
? el.closest('figure, li, .item, [class*="item" i], [class*="card" i], a[href], div, td')
: null;
if (container) container.querySelectorAll(captionSel).forEach((c) => push(c.textContent, 55));
if (el.parentElement) {
Array.from(el.parentElement.children).forEach((ch) => {
if (ch !== el && ch.textContent && ch.textContent.trim().length <= 60) push(ch.textContent, 30);
});
const parentText = Array.from(el.parentElement.childNodes)
.filter((n) => n.nodeType === 3 && n.textContent.trim())
.map((n) => n.textContent).join(' ');
if (parentText.length <= 60) push(parentText, 22);
}
return cands;
}
function getImageName(el) {
const cands = collectNameCandidates(el);
if (cands.length === 0) return '';
const seen = new Set();
const uniq = cands.filter((c) => {
const k = c.s.toLowerCase();
if (seen.has(k)) return false;
seen.add(k);
return true;
});
uniq.sort((a, b) => b.score - a.score || a.s.length - b.s.length);
return uniq[0].s;
}
function fallbackName(url) {
try {
const u = new URL(url, location.href);
const seg = u.pathname.split('/').pop();
const m = seg && seg.match(/^[^.?#]+/);
if (m && m[0] && m[0] !== '') return m[0].slice(0, CONFIG.MAX_NAME_LEN);
} catch (e) {}
return 'image_' + Date.now();
}
function getMeta(name) {
const m = document.querySelector('meta[property="' + name + '"], meta[name="' + name + '"]');
return m ? m.getAttribute('content') : '';
}
function resolveName(name, url) {
let base = sanitize(name);
if (!base || isMeaninglessName(base)) {
const pageName = sanitize(getMeta('og:title') || getMeta('twitter:title') || document.title);
if (pageName && !isMeaninglessName(pageName)) base = pageName;
}
if (!base) base = fallbackName(url);
return base;
}
/* ----------------------- 下载(blob + ,文件名 100% 精确)------ */
function sniffExt(buf) {
const b = new Uint8Array(buf);
if (b.length < 4) return '';
if (b[0] === 0x3C) return 'HTML';
if (b[0] === 0xFF && b[1] === 0xD8) return '.jpg';
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47) return '.png';
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return '.gif';
if (b[0] === 0x42 && b[1] === 0x4D) return '.bmp';
if (b.length > 11 && b[0] === 0x52 && b[1] === 0x49 &&
String.fromCharCode(b[8], b[9], b[10], b[11]) === 'WEBP') return '.webp';
return '';
}
function save(blob, fileName) {
const u = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = u; a.download = fileName;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(u), 15000);
}
const _dlUsed = {};
const STALL_TIMEOUT = 15000;
const HARD_TIMEOUT = 120000;
function fmtSize(n) {
return n >= 1048576 ? (n / 1048576).toFixed(1) + 'MB' : Math.round(n / 1024) + 'KB';
}
function finishBlob(blob, url, baseName, extFallback, onOk) {
if (!blob || blob.size < 400) return Promise.resolve('响应过小(' + (blob ? blob.size : 0) + 'B)');
return blob.slice(0, 16).arrayBuffer().then((buf) => {
const sniffed = sniffExt(buf);
if (sniffed === 'HTML') return '返回的是网页(反爬/重定向)';
if (onOk() === false) return null;
let ext = sniffed || mimeToExt(blob.type) || '';
if (ext && !ext.startsWith('.')) ext = '.' + ext;
if (!ext) ext = getExt(url) || '';
if (!ext) ext = extFallback || '.jpg';
const fn = baseName + ext;
save(blob, fn);
toast('已下载:' + fn + '(' + fmtSize(blob.size) + ')');
return null;
}).catch(() => '数据校验异常');
}
function blobViaXhr(url, baseName, extFallback, onOk, onNext, onProgress) {
let lastData = Date.now();
let wd = null, req = null;
const stop = () => { if (wd) { clearInterval(wd); wd = null; } };
const fail = (reason) => { stop(); onNext(reason); };
try {
req = GM_xmlhttpRequest({
method: 'GET', url, responseType: 'arraybuffer',
headers: { Referer: location.href, Accept: '*/*' },
timeout: HARD_TIMEOUT,
onprogress: (e) => {
lastData = Date.now();
if (onProgress && e && e.loaded) onProgress(e.loaded);
},
onload: (res) => {
stop();
if (res.status && res.status >= 400) { fail('HTTP ' + res.status); return; }
const buf = res.response;
if (!buf || !buf.byteLength) { fail('空响应'); return; }
const ctM = (res.responseHeaders || '').match(/content-type:\s*([^\r\n;]+)/i);
const blob = new Blob([buf], { type: ctM ? ctM[1].trim() : '' });
finishBlob(blob, url, baseName, extFallback, onOk)
.then((reason) => { if (reason) fail(reason); });
},
onerror: () => fail('XHR 网络错误/被拦截(检查脚本域名授权)'),
ontimeout: () => fail('XHR 硬超时'),
});
} catch (e) { fail('请求被拦截(检查脚本域名授权)'); return; }
wd = setInterval(() => {
if (Date.now() - lastData > STALL_TIMEOUT) {
stop();
try { req && req.abort && req.abort(); } catch (e) {}
onNext('卡死(' + Math.round(STALL_TIMEOUT / 1000) + 's无数据)');
}
}, 1000);
}
function downloadFiles(urls, name, extHint) {
if (!urls || urls.length === 0) { toast('未找到可下载的图片地址'); return; }
const queue = dedupe(urls).filter((u) => /^https?:/i.test(u));
if (!queue.length) { toast('下载失败:无可用地址'); return; }
queue.sort((a, b) => (/auth_key=/.test(a) ? 1 : 0) - (/auth_key=/.test(b) ? 1 : 0));
const ext = getExt(queue[0]) || extHint || '.jpg';
let base = resolveName(name, queue[0]);
if (_dlUsed[base + ext]) { let n = 1; while (_dlUsed[base + '_' + n + ext]) n++; base = base + '_' + n; }
_dlUsed[base + ext] = true;
const total = queue.length;
let done = false, idx = 0, lastProg = 0;
const reasons = [];
const onOk = () => { if (done) return false; done = true; return true; };
const progress = (loaded) => {
const now = Date.now();
if (now - lastProg < 400) return;
lastProg = now;
toast('下载中… ' + fmtSize(loaded) + (total > 1 ? '(地址 ' + idx + '/' + total + ')' : ''));
};
const next = (reason) => {
if (reason) reasons.push('地址' + idx + ':' + reason);
if (done) return;
if (idx >= total) {
console.log('[快速下载原图] 下载失败详情:', reasons);
toast('下载失败:' + (reasons.slice(0, 2).join(';') || '所有候选地址均不可用'));
return;
}
const u = queue[idx++];
if (total > 1) toast('正在下载原图…(地址 ' + idx + '/' + total + ')');
blobViaXhr(u, base, extHint, onOk, next, progress);
};
next();
}
/* ----------------------- 主流程 ----------------------- */
function handleHuaban(hit, elUrl) {
const cdnOrig = huabanCdnOriginal(elUrl);
const domName = getImageName(hit.el);
toast('正在下载原图…');
const pinId = huabanPinIdFromContext(hit.el);
if (!pinId) {
downloadFiles(dedupe([cdnOrig, elUrl]), domName, '');
return;
}
let started = false;
const startWith = (urls, name, ext) => {
if (started) return;
started = true;
downloadFiles(urls, name, ext);
};
const timer = setTimeout(() => startWith(dedupe([cdnOrig, elUrl]), domName, ''), 2500);
getHuabanPin(pinId, (pin) => {
clearTimeout(timer);
if (pin && pin.urls.length) {
startWith(dedupe(pin.urls.concat([cdnOrig, elUrl])), pin.rawText || domName || pin.board, pin.ext || '');
} else {
startWith(dedupe([cdnOrig, elUrl]), domName, '');
}
});
}
function handleGeneric(hit, elUrl) {
const cands = dedupe([elUrl].concat(transformThumbUrl(elUrl)));
const name = getImageName(hit.el);
toast('正在下载原图…');
downloadFiles(cands, name, '');
const wrapA = hit.el.closest ? hit.el.closest('a[href]') : null;
const aHref = wrapA && wrapA.getAttribute('href');
if (aHref && !isImageUrl(aHref)) {
fetchDetailCandidates(toAbs(aHref), (dc) => {
if (dc && dc.urls.length) {
console.log('[快速下载原图] 详情页候选:', dc.urls.map((x) => x.url));
}
});
}
}
/* ----------------------- Alt + 左键监听 ----------------------- */
document.addEventListener('click', (e) => {
if (!e.altKey) return;
const hit = getImageFromTarget(e.target);
if (!hit) return;
e.preventDefault();
e.stopPropagation();
const elUrl = hit.bgUrl ? toAbs(hit.bgUrl) : getOriginalUrl(hit.el);
if (isHuabanPage() || isHuabanCdn(elUrl)) handleHuaban(hit, elUrl);
else handleGeneric(hit, elUrl);
}, true);
/* ----------------------- 轻提示 ----------------------- */
let toastTimer = null;
function toast(msg) {
if (!CONFIG.SHOW_TOAST) return;
let box = document.getElementById('rcid-toast');
if (!box) {
box = document.createElement('div');
box.id = 'rcid-toast';
box.style.cssText =
'position:fixed;left:50%;bottom:48px;transform:translateX(-50%);' +
'z-index:2147483647;background:rgba(20,20,22,.92);color:#fff;' +
'padding:10px 16px;border-radius:8px;font-size:14px;line-height:1.4;' +
'max-width:80vw;box-shadow:0 4px 16px rgba(0,0,0,.4);' +
'font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;' +
'pointer-events:none;transition:opacity .3s;';
document.body.appendChild(box);
}
box.textContent = msg;
box.style.opacity = '1';
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { if (box) box.style.opacity = '0'; }, 2600);
}
/* ----------------------- 下载功能:使用说明菜单 ----------------------- */
GM_registerMenuCommand('快速下载原图 - 使用说明', () => {
alert(
'Alt + 左键图片 = 下载原图(按网页名称命名)\n' +
'普通右键 = 浏览器原生菜单\n\n' +
'原图判断逻辑:\n' +
'1. 花瓣网:调官方 v3 接口拿签名原图 + 发布内容命名\n' +
'2. 缩略图外链详情页:抓详情页 og:image / 主图 data-original / srcset 最大图\n' +
'3. 常见缩略图规则变换:WordPress -300x200、微博 orj360→oslarge、twimg name=orig、Pinterest /236x/→/originals/、B站 @后缀\n\n' +
'文件名优先级(通用站):\n' +
'包裹图片 的 title/aria-label > figcaption > alt/title/data-* > 相邻短文本 > 详情页标题 > URL 文件名\n' +
'花瓣网:发布内容(raw_text) > 网页文字 > 画板名 > 花瓣_'
);
});
/* ============================================================
* 初始化
* ============================================================ */
// 屏蔽付费素材:仅花瓣网生效
if (isHuabanPage()) {
initBlocker();
}
})();