Sleazy Fork is available in English.
Replace small images by their full size and add a download button to download zip of all images + cover
// ==UserScript== // @name iStripper downloader // @description Replace small images by their full size and add a download button to download zip of all images + cover // @version 1.0.0 // @author BreatFR // @namespace http://usercssjs.breat.fr/i/istripper // @match *://*.istripper.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js // @copyright 2025, BreatFR (https://breat.fr) // @icon https://www.istripper.com/favicons/istripper/apple-icon-120x120.png // @license AGPL-3.0-or-later; https://www.gnu.org/licenses/agpl-3.0.txt // @grant GM_xmlhttpRequest // @grant GM_download // @run-at document-end // ==/UserScript== (function() { 'use strict'; const style = document.createElement('style'); style.textContent = ` a.box:hover div.img.icard { background-position: initial; } [src*="https://www.istripper.com/free/sets"] { transition: transform .2s ease-in-out; } a.box:hover [src*="https://www.istripper.com/free/sets"] { transform: scale(1.1); transition: transform .2s ease-in-out; } .istripper-download-btn { align-items: center; background-color: rgba(24, 24, 24, .2); border: none; border-radius: .5em; color: #fff; cursor: pointer; display: flex; flex-direction: column; font-family: poppins, cursive; font-size: 1.15rem; gap: 1em; justify-content: center; line-height: 1.5; padding: .5em 1em; position: absolute; transition: background-color .3s ease, box-shadow .3s ease; z-index: 9999; } .istripper-download-btn:hover { background-color: rgba(255, 80, 80, .85); box-shadow: 0 0 2em rgba(255, 80, 80, .85); } @keyframes spinLoop { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .istripper-btn-icon.spin { animation: spinLoop 1s linear infinite; } @keyframes pulseLoop { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } } .istripper-btn-icon.pulse { animation: pulseLoop 1.8s ease-in-out infinite; } .istripper-btn-icon { font-size: 3em; line-height: 1em; } #top { aspect-ratio: 1 / 1; background: transparent; border: none; bottom: 1em; cursor: pointer; font-size: 1.2em; left: 1em; position: fixed; z-index: 9999; } `; document.head.appendChild(style); function updateImagesAndOverlays() { const images = document.querySelectorAll('img.illustration'); images.forEach(img => { if (!img.src.includes("_card_full")) img.src = img.src.replace(".jpg", "_card_full.jpg"); }); const divCollection = document.querySelectorAll('.collection-label'); divCollection.forEach(div => { div.style.pointerEvents = 'none'; }); const divsWithOverlay = document.querySelectorAll('div[data-overlay]'); divsWithOverlay.forEach(div => { div.removeAttribute('data-overlay'); }); const overlayImages = document.querySelectorAll('img.overlay'); overlayImages.forEach(img => { img.parentNode && img.parentNode.removeChild(img); }); const icardDivs = document.querySelectorAll('div.img.icard'); icardDivs.forEach(div => { const backgroundImageURL = div.style.backgroundImage; if (backgroundImageURL && !backgroundImageURL.includes("_card_full")) { div.style.alignItems = 'center'; div.style.backgroundPosition = 'center center'; div.style.backgroundSize = '0'; div.style.display = 'flex'; div.style.justifyContent = 'center'; } }); const icardImg = document.querySelectorAll('div.img.icard > img'); icardImg.forEach(img => { if (!img.src.includes("_card_full") && img.classList.contains("hidden")) { if (img.src.endsWith(".jpg")) { img.src = img.src.replace(".jpg", "_card_full.jpg"); img.removeAttribute('class'); img.style.height = "242px"; img.style.opacity = '1'; img.style.pointerEvents = 'auto'; img.style.width = "auto"; } } }); const icardOverlayDivs = document.querySelectorAll('div.icard-overlay'); icardOverlayDivs.forEach(div => { div.style.pointerEvents = 'none'; div.style.position = 'absolute'; div.style.top = '0'; div.style.left = '0'; div.removeAttribute('onmouseover'); }); } updateImagesAndOverlays(); setInterval(updateImagesAndOverlays, 500); // ===== Helpers UI ===== function setButtonContent(btn, icon, label) { let iconEl = btn.querySelector('.istripper-btn-icon'); let labelEl = btn.querySelector('.istripper-btn-label'); if (!iconEl) { iconEl = document.createElement('div'); iconEl.className = 'istripper-btn-icon'; btn.appendChild(iconEl); } if (!labelEl) { labelEl = document.createElement('div'); labelEl.className = 'istripper-btn-label'; btn.appendChild(labelEl); } iconEl.textContent = icon; labelEl.textContent = label; } function setIconAnimation(btn, type) { const icon = btn.querySelector('.istripper-btn-icon'); if (!icon) return; icon.classList.remove('spin', 'pulse'); void icon.offsetWidth; if (type) icon.classList.add(type); } function updateIconWithAnimation(btn, icon, label, animationClass) { setButtonContent(btn, icon, label); requestAnimationFrame(() => setIconAnimation(btn, animationClass)); } function resetDownloadButton(btn) { btn.disabled = false; updateIconWithAnimation(btn, '📦', 'Download pack + cover', null); } function sanitizeZipName(name) { return (name || "istripper_pack").trim().replace(/[\\/:*?"<>|]/g, "_"); } // ===== Binary download via GM (avoids CORS issues) ===== function gmGetArrayBuffer(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", onload: (res) => { if (res.status >= 200 && res.status < 300 && res.response) resolve(res.response); else reject(new Error(`HTTP ${res.status} for ${url}`)); }, onerror: () => reject(new Error(`Network error for ${url}`)), }); }); } async function gmGetUint8(url) { const ab = await gmGetArrayBuffer(url); return new Uint8Array(ab); } // ===== ZIP logic: unzip -> flatten photos/ -> add cover -> rezip ===== function stripPhotosFolder(entries) { const out = {}; for (const [path, data] of Object.entries(entries)) { if (!data || !data.length) continue; out[path.replace(/^photos\//i, "")] = data; } return out; } function ensureUniqueName(map, desired) { if (!map[desired]) return desired; const dot = desired.lastIndexOf("."); const base = dot >= 0 ? desired.slice(0, dot) : desired; const ext = dot >= 0 ? desired.slice(dot) : ""; let i = 2; while (map[`${base}_${i}${ext}`]) i++; return `${base}_${i}${ext}`; } function getOfficialZipUrl() { const a = document.querySelector('a > .bt-download')?.closest('a'); if (a?.href && /\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(a.href)) return a.href; for (const link of document.querySelectorAll('a[href*="/fileaccess/zip/"]')) { if (/\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(link.href)) return link.href; } return null; } async function rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn }) { updateIconWithAnimation(btn, '🌀', 'Downloading official ZIP...', 'spin'); const zipUint8 = await gmGetUint8(zipUrl); updateIconWithAnimation(btn, '📦', 'Unzipping...', 'pulse'); const entries = await new Promise((resolve, reject) => { fflate.unzip(zipUint8, (err, data) => (err ? reject(err) : resolve(data))); }); const flat = stripPhotosFolder(entries); updateIconWithAnimation(btn, '🖼️', 'Downloading cover...', 'pulse'); const coverUint8 = await gmGetUint8(coverUrl); const coverName = ensureUniqueName(flat, 'cover.jpg'); flat[coverName] = coverUint8; updateIconWithAnimation(btn, '📦', `Creating ZIP (${Object.keys(flat).length} files)...`, null); const newZip = await new Promise((resolve, reject) => { fflate.zip(flat, { level: 0 }, (err, data) => (err ? reject(err) : resolve(data))); }); const zipName = sanitizeZipName(zipNameBase) + ".zip"; const blobUrl = URL.createObjectURL(new Blob([newZip], { type: "application/zip" })); updateIconWithAnimation(btn, '📥', 'Saving ZIP...', 'pulse'); GM_download({ url: blobUrl, name: zipName, saveAs: true, onload: () => setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000), onerror: (err) => { URL.revokeObjectURL(blobUrl); console.error('[iStripper Collector] ❌ GM_download failed:', err); alert('ZIP download failed.'); } }); } // ===== Download button injection ===== function addDownloadButton() { const urlMatch = location.href.match(/^https:\/\/www\.istripper\.com\/[^\/]*\/?models\/[^\/]+\/[^\/]+$/); const isPackPage = urlMatch && document.querySelector('img.illustration') && document.querySelectorAll('a.picture').length > 0; const hasDownloadLink = !!document.querySelector('a > .bt-download'); if (!isPackPage || !hasDownloadLink) return; if (document.querySelector('.istripper-download-btn')) return; console.log('[iStripper Collector] Injecting download button'); const btn = document.createElement('button'); btn.className = 'istripper-download-btn'; btn.innerHTML = ` <div class="istripper-btn-icon">📦</div> <div class="istripper-btn-label">Download pack + cover</div> `; const blackBox = document.querySelector('div.cta.showDetail'); const wrapper = document.querySelector('div[style="background: #f6f6f6;"]'); if (blackBox && wrapper) { wrapper.style.position = 'relative'; wrapper.appendChild(btn); requestAnimationFrame(() => { const boxRect = blackBox.getBoundingClientRect(); const wrapperRect = wrapper.getBoundingClientRect(); const btnRect = btn.getBoundingClientRect(); const top = boxRect.top - wrapperRect.top - btnRect.height - 32; const left = boxRect.left - wrapperRect.left + (boxRect.width / 2) - (btnRect.width / 2); btn.style.top = `${top}px`; btn.style.left = `${left}px`; }); } else { console.warn('[iStripper Collector] Positioning fallback'); document.body.appendChild(btn); } btn.addEventListener('click', async () => { btn.disabled = true; try { const zipUrl = getOfficialZipUrl(); if (!zipUrl) throw new Error('Official ZIP link not found.'); const coverImg = document.querySelector('img.illustration'); if (!coverImg?.src) throw new Error('Cover image not found.'); const coverUrl = coverImg.src.includes('_card_full') ? coverImg.src.replace(/\.jpg(\?.*)?$/i, '.jpg$1') : coverImg.src.replace(/\.jpg(\?.*)?$/i, '_card_full.jpg$1'); const h2 = document.querySelector('h2.mdlnav'); const zipNameBase = h2 ? h2.textContent : 'istripper_pack'; await rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn }); updateIconWithAnimation(btn, '✅', 'ZIP ready', null); } catch (e) { console.error('[iStripper Collector] ❌ Failed:', e); alert(`Failed: ${e?.message || e}`); // (plus besoin de remettre "📦 Download..." ici, le finally reset tout) updateIconWithAnimation(btn, '❌', 'Failed', null); } finally { // Laisse le user voir "ZIP ready" (ou "Failed") 2s, puis reset état initial setTimeout(() => resetDownloadButton(btn), 2000); } }); } addDownloadButton(); })();