您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a small badge to download images; also a floating button to download the largest image on the page.
当前为
// ==UserScript== // @name One-Click Image Downloader // @namespace Tunc // @version 1.0.0 // @description Add a small badge to download images; also a floating button to download the largest image on the page. // @author Me // @license MIT // @match *://*/* // @exclude *://greasyfork.org/* // @grant GM_download // @grant GM_addStyle // ==/UserScript== (function () { "use strict"; const MIN_W = 200; // minimum width for images to consider const MIN_H = 200; // minimum height for images to consider const BADGE_TEXT = "↓"; const BADGE_CLASS = "ocid-badge"; const PANEL_CLASS = "ocid-panel"; // --- Styles --- const css = ` .${BADGE_CLASS}{ position:absolute; top:6px; right:6px; font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; padding:6px 8px; border-radius:10px; background: rgba(0,0,0,.75); color:#fff; cursor:pointer; z-index: 2147483647; user-select:none; text-decoration:none; box-shadow: 0 2px 6px rgba(0,0,0,.25); } .${BADGE_CLASS}:hover{ background: rgba(0,0,0,.9) } .${PANEL_CLASS}{ position: fixed; inset: auto 16px 16px auto; display:flex; gap:8px; align-items:center; background: rgba(0,0,0,.7); color:#fff; border-radius: 12px; padding:10px 12px; z-index: 2147483647; backdrop-filter: blur(4px); font: 600 13px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } .${PANEL_CLASS} button{ border:0; border-radius:10px; padding:8px 10px; cursor:pointer; background: #fff; color:#111; font-weight:700; } .ocid-relative{ position: relative !important; } `; if (typeof GM_addStyle === "function") GM_addStyle(css); else { const s = document.createElement("style"); s.textContent = css; document.documentElement.appendChild(s); } // --- Helpers --- const toFilename = (url, fallbackBase = "image") => { try { const u = new URL(url, location.href); const base = u.pathname.split("/").pop() || fallbackBase; const clean = base.split("?")[0].split("#")[0]; // Ensure an extension exists const hasExt = /\.[a-z0-9]{2,5}$/i.test(clean); return hasExt ? clean : `${clean || fallbackBase}.jpg`; } catch { return `${fallbackBase}.jpg`; } }; const canGMDownload = typeof GM_download === "function"; const download = (url, name) => { if (canGMDownload) { try { GM_download({ url, name, saveAs: true }); return; } catch (e) { // fall through to a-tag } } const a = document.createElement("a"); a.href = url; a.download = name; a.rel = "noopener"; document.body.appendChild(a); a.click(); a.remove(); }; const getImgSrc = (img) => { // Prefer currentSrc (resolves srcset); fall back to src; consider common lazy attributes return img.currentSrc || img.src || img.getAttribute("data-src") || img.getAttribute("data-lazy-src"); }; const qualifies = (img) => { // Use rendered size when possible const w = Math.max(img.naturalWidth || 0, img.clientWidth || 0); const h = Math.max(img.naturalHeight || 0, img.clientHeight || 0); return w >= MIN_W && h >= MIN_H; }; const ensureRelative = (el) => { // So the badge can be absolutely positioned inside the image box const s = getComputedStyle(el); if (!["relative", "absolute", "fixed"].includes(s.position)) { el.classList.add("ocid-relative"); } }; // --- Badge injection on images --- const addBadge = (img) => { if (!qualifies(img) || img.dataset.ocidBadged) return; const src = getImgSrc(img); if (!src) return; const wrapTarget = img.parentElement || img; ensureRelative(wrapTarget); const badge = document.createElement("div"); badge.textContent = BADGE_TEXT; badge.className = BADGE_CLASS; badge.title = "Download image"; badge.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const url = getImgSrc(img); if (!url) return; const name = toFilename(url, "image"); download(url, name); }); // Keep badge within the same stacking context as the image wrapTarget.appendChild(badge); img.dataset.ocidBadged = "1"; }; const scan = () => { const imgs = Array.from(document.images || []); imgs.forEach(addBadge); }; // --- Floating panel (download the largest image) --- const createPanel = () => { if (document.querySelector(`.${PANEL_CLASS}`)) return; const panel = document.createElement("div"); panel.className = PANEL_CLASS; const btnLargest = document.createElement("button"); btnLargest.textContent = "Download largest image"; btnLargest.title = "Find the biggest visible image and download it"; btnLargest.addEventListener("click", () => { const imgs = Array.from(document.images || []).filter(qualifies); if (!imgs.length) { alert("No large images found on this page."); return; } // Score by area; prefer natural over client to get original size when possible const best = imgs.reduce((a, b) => { const area = (x) => Math.max(x.naturalWidth || 0, x.clientWidth || 0) * Math.max(x.naturalHeight || 0, x.clientHeight || 0); return area(b) > area(a) ? b : a; }, imgs[0]); const url = getImgSrc(best); if (!url) { alert("Couldn't resolve image URL."); return; } const name = toFilename(url, "largest"); download(url, name); }); panel.appendChild(btnLargest); document.documentElement.appendChild(panel); }; // Initial run and observers const onReady = () => { createPanel(); scan(); // Watch for dynamically loaded images (SPA, infinite scroll) const obs = new MutationObserver(() => scan()); obs.observe(document.documentElement, { childList: true, subtree: true }); // Re-scan when images load to get proper dimensions window.addEventListener("load", scan, { once: true }); document.addEventListener("load", (e) => { const t = e.target; if (t && t.tagName === "IMG") addBadge(t); }, true); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", onReady); } else { onReady(); } })();