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