您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a small download icon to images on Coomer post pages.
// ==UserScript== // @name Coomer Image Download Button // @namespace http://tampermonkey.net/ // @version 1.5 // @description Adds a small download icon to images on Coomer post pages. // @match https://coomer.st/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_download // @connect * // @run-at document-end // @author nereids // @icon https://icons.duckduckgo.com/ip3/coomer.st.ico // @license MIT // ==/UserScript== (function () { 'use strict'; let imageCounter = 0; let lastUrl = location.href; // --- Styles --- GM_addStyle(` .coomer-dl-btn { position: absolute; bottom: 6px; left: 6px; width: 30px; height: 30px; border-radius: 50%; background: rgba(0,0,0,0.5); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147483647; border: none; padding: 0; } .coomer-dl-btn svg { width:20px; height:20px; fill: white; pointer-events: none; } .coomer-dl-btn.downloading { opacity: 0.7; transform: scale(0.98); } `); // SVG download icon const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e3e3e3"><path d="M434.5-396.5v-329q0-19 13.25-32.25T480.5-771q19 0 32.25 13.25T526-725.5V-395l119-119q13.5-13.5 32-13.5t32 14q13.5 13.5 13.5 32t-13.5 32L512.5-253q-14 14-33.5 14t-33-14L249.5-449.5Q236-463 235.75-481.25t13.75-32.25q14-13.5 32.75-14t32.75 13l119.5 118Z"/></svg>`; function getUserAndPost() { const userMatch = location.pathname.match(/\/user\/([^/]+)/); const postMatch = location.pathname.match(/\/post\/(\d+)/); return { username: userMatch ? userMatch[1] : "", postId: postMatch ? postMatch[1] : "" }; } function findOriginal(img) { const anc = img.closest('a'); if (anc && anc.href && /\/data\//.test(anc.href)) { return anc.href.split('?')[0]; } if (img.srcset) { const candidates = img.srcset.split(',').map(s => s.trim().split(/\s+/)[0]); for (const c of candidates) { if (/\/data\//.test(c)) return c.split('?')[0]; } } const attrs = ['data-src', 'data-full', 'data-original', 'data-img', 'data-lazy-src']; for (const a of attrs) { const v = img.getAttribute(a); if (v && /\/data\//.test(v)) return v.split('?')[0]; } if (!img.src) return null; try { const u = new URL(img.src, location.href); u.pathname = u.pathname.replace('/thumbnail', ''); if (u.hostname === 'img.coomer.st') u.hostname = 'n4.coomer.st'; u.search = ''; return u.toString(); } catch (e) { return img.src.split('?')[0]; } } function getExtension(url) { const m = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/); return m ? "." + m[1] : ".jpg"; } function buildFilename(url, username, postId) { imageCounter += 1; return `${username}-${postId}_${imageCounter}${getExtension(url)}`; } function downloadBinary(url, filename, btn) { if (btn) btn.classList.add('downloading'); GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', onload(res) { if (btn) btn.classList.remove('downloading'); if (res.status >= 200 && res.status < 300 && res.response) { let ct = 'application/octet-stream'; if (res.responseHeaders) { const m = res.responseHeaders.match(/content-type:\s*([^\r\n;]+)/i); if (m) ct = m[1].trim(); } const blob = new Blob([res.response], { type: ct }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 5000); } else { console.error('Binary download failed, status:', res.status); fallbackDownload(url, filename); } }, onerror(err) { if (btn) btn.classList.remove('downloading'); console.error('GM_xmlhttpRequest error', err); fallbackDownload(url, filename); }, ontimeout() { if (btn) btn.classList.remove('downloading'); console.error('GM_xmlhttpRequest timeout'); fallbackDownload(url, filename); } }); } function fallbackDownload(url, filename) { try { if (typeof GM_download === 'function') { try { GM_download({ url, name: filename }); return; } catch (e) { GM_download(url, filename); return; } } } catch (e) { console.warn('GM_download fallback failed', e); } window.open(url, '_blank'); } function addButtonsOnce() { const { username, postId } = getUserAndPost(); if (!username || !postId) return; // only on post pages const images = Array.from(document.querySelectorAll('img')); images.forEach(img => { if (img.dataset.cdlAdded) return; if (!img.src || (!img.src.includes('/thumbnail/') && !img.src.includes('/data/'))) { img.dataset.cdlAdded = '1'; return; } const orig = findOriginal(img); if (!orig) { img.dataset.cdlAdded = '1'; return; } const parent = img.parentElement || img; const cs = getComputedStyle(parent); if (cs.position === 'static') { parent.style.position = 'relative'; parent.dataset._cdl_posset = '1'; } const btn = document.createElement('button'); btn.className = 'coomer-dl-btn'; btn.type = 'button'; btn.title = 'Download full-size image'; btn.innerHTML = ICON_SVG; btn.addEventListener('click', function (e) { e.stopPropagation(); e.preventDefault(); const filename = buildFilename(orig, username, postId); downloadBinary(orig, filename, btn); }, { capture: true }); parent.appendChild(btn); img.dataset.cdlAdded = '1'; }); } function runForPost() { const { username, postId } = getUserAndPost(); if (username && postId) { imageCounter = 0; // reset per post addButtonsOnce(); } } // Watch for DOM changes const mo = new MutationObserver(addButtonsOnce); mo.observe(document.body, { childList: true, subtree: true }); // Watch for URL changes (SPA navigation) function checkUrlChange() { if (location.href !== lastUrl) { lastUrl = location.href; runForPost(); } } setInterval(checkUrlChange, 500); // Initial run runForPost(); })();