Script Universal de CSS pro Ultimate Scrapper
Цей скрипт не слід встановлювати безпосередньо. Це - бібліотека для інших скриптів для включення в мета директиву // @require https://update.greasyfork.org/scripts/585205/1865576/LayoutDownloaderAtt.js
/* * Dependências: * * GM_info(optional) * Docs: https://violentmonkey.github.io/api/gm/#gm_info * * GM_xmlhttpRequest(optional) * Docs: https://violentmonkey.github.io/api/gm/#gm_xmlhttprequest * * JSZIP * Github: https://github.com/Stuk/jszip * CDN: https://unpkg.com/[email protected]/dist/jszip.min.js * * FileSaver * Github: https://github.com/eligrey/FileSaver.js * CDN: https://unpkg.com/[email protected]/dist/FileSaver.min.js */ ; const LayoutDownloaderAtt = (({ JSZip, saveAs }) => { let maxNum = 0; let promiseCount = 0; let fulfillCount = 0; let isErrorOccurred = false; // Elementos let startNumInputElement = null; let endNumInputElement = null; let downloadButtonElement = null; let statusElement = null; let panelElement = null; let guiHost = null; let qualitySliderElement = null; let qualityValElement = null; // Estado let selectedExt = 'jpg'; // svg icons const externalLinkSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16" style="margin-left: 5px;"><path fill-rule="evenodd" d="M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z"></path></svg>`; const reloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16" style="margin-left: 5px;"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>`; // Inicialização function init({ maxImageAmount, getImagePromises, title = `package_${Date.now()}`, imageSuffix = 'jpg', zipOptions = {}, positionOptions = {} }) { maxNum = maxImageAmount; selectedExt = imageSuffix; setupUI(positionOptions); setupUpdateNotification(); if (downloadButtonElement) { downloadButtonElement.onclick = function () { if (!isOKToDownload()) return; this.disabled = true; this.textContent = "Processing..."; this.style.backgroundColor = '#9ca3af'; this.style.cursor = 'not-allowed'; this.style.boxShadow = 'none'; statusElement.innerHTML = "<span style='color:#3b82f6; font-weight:bold;'>Starting download...</span>"; const qualityValue = Number(qualitySliderElement.value); download(getImagePromises, title, selectedExt, zipOptions, qualityValue); } } } function setupUI(positionOptions) { if (document.getElementById('cd-gui-container')) return; guiHost = document.createElement('div'); guiHost.id = 'cd-gui-container'; document.body.appendChild(guiHost); const shadow = guiHost.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.innerHTML = ` :host { all: initial; } *, *::before, *::after { box-sizing: border-box; } #cd-panel { position: fixed; top: 20px; right: 20px; width: 340px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; color: #333; box-sizing: border-box; user-select: none; } .cd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .cd-title { font-weight: 700; color: #3b82f6; font-size: 16px; display: flex; align-items: center; gap: 8px; } .cd-close { border: none; background: none; font-size: 20px; color: #9ca3af; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s; font-weight: bold; } .cd-close:hover { color: #ef4444; } .cd-status { font-size: 14px; margin-bottom: 16px; color: #10b981; font-weight:bold; min-height: 40px; text-align: center; display: flex; align-items: center; justify-content: center; } .cd-box { border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; margin-bottom: 20px; } .cd-box-title { font-size: 11px; font-weight: 700; color: #3b82f6; text-transform: uppercase; text-align: center; margin-bottom: 12px; letter-spacing: 0.5px; } .cd-res-inputs { display: flex; justify-content: center; align-items: center; gap: 12px; } .cd-res-input { width: 80px; padding: 8px; border: 1px solid #e2e8f0; border-radius: 6px; background: #f8fafc; text-align: center; color: #64748b; font-weight: 600; font-size: 14px; outline: none; } .cd-res-input:focus { border-color: #3b82f6; } .cd-format-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; } .cd-fmt-btn { border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 8px; background: #fff; cursor: pointer; text-align: center; transition: all 0.2s; color: #1f2937; font-weight: 600; font-size: 14px; display: flex; flex-direction: column; align-items: center; gap: 4px; } .cd-fmt-btn span { font-size: 11px; font-weight: 400; color: #6b7280; } .cd-fmt-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .cd-fmt-btn.active span { color: #dbeafe; } .cd-fmt-btn:disabled { opacity: 0.5; cursor: not-allowed; background: #f9fafb; } .cd-slider-container { margin-bottom: 20px; } .cd-slider-label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; display: flex; justify-content: space-between; } .cd-slider { width: 100%; cursor: pointer; } .cd-main-btn { width: 100%; padding: 14px; border: none; border-radius: 8px; background: #2563eb; color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; transition: background 0.2s; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); margin-bottom: 16px; } .cd-main-btn:hover { background: #1d4ed8; } .cd-version { text-align: center; font-size: 11px; color: #9ca3af; margin-top: 16px; } .cd-warning { background: #fef9c3; border: 1px solid #fde047; border-radius: 8px; padding: 12px; color: #854d0e; font-size: 12px; text-align: center; line-height: 1.5; font-weight: 500; } `; shadow.appendChild(style); panelElement = document.createElement('div'); panelElement.id = 'cd-panel'; for (const [key, value] of Object.entries(positionOptions)) { if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') { panelElement.style[key] = value; } } panelElement.innerHTML = ` <div class="cd-header"> <div class="cd-title">🔥 Ultimate Scrapper</div> <button id="cd-close" class="cd-close">×</button> </div> <div id="cd-status" class="cd-status">Ready to download!</div> <div class="cd-box"> <div class="cd-box-title">Page Range</div> <div class="cd-res-inputs"> <input type="number" id="cd-start-num" class="cd-res-input" value="1" min="1" max="${maxNum}"> <span style="color: #cbd5e1; font-size: 14px; font-weight: bold;">until</span> <input type="number" id="cd-end-num" class="cd-res-input" value="${maxNum}" min="1" max="${maxNum}"> </div> </div> <div style="font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 12px;">Image's Output</div> <div class="cd-format-grid"> <button class="cd-fmt-btn ${selectedExt === 'webp' ? 'active' : ''}" data-ext="webp">WebP</button> <button class="cd-fmt-btn ${selectedExt === 'jpg' ? 'active' : ''}" data-ext="jpg">JPEG</button> <button class="cd-fmt-btn ${selectedExt === 'png' ? 'active' : ''}" data-ext="png">PNG</button> <button class="cd-fmt-btn ${selectedExt === 'jxl' ? 'active' : ''}" data-ext="jxl">JXL</button> </div> <div class="cd-slider-container"> <div class="cd-slider-label"> <span>Quality:</span> <span style="color: #3b82f6;"><span id="cd-quality-val">100</span>%</span> </div> <input type="range" id="cd-quality" min="0" max="100" value="100" class="cd-slider"> </div> <button id="cd-start" class="cd-main-btn">Download</button> <div class="cd-warning" style="margin-top:10px;">⚠️ When you download a chapter and, if by chance, you want to download another one right after, you <b>NEED</b> to reload the page. Otherwise, it'll download the manga you downloaded last.</div> <div class="cd-version">Powered By: Nkkz</div> `; shadow.appendChild(panelElement); startNumInputElement = shadow.getElementById('cd-start-num'); endNumInputElement = shadow.getElementById('cd-end-num'); downloadButtonElement = shadow.getElementById('cd-start'); statusElement = shadow.getElementById('cd-status'); qualitySliderElement = shadow.getElementById('cd-quality'); qualityValElement = shadow.getElementById('cd-quality-val'); startNumInputElement.onkeydown = (e) => e.stopPropagation(); endNumInputElement.onkeydown = (e) => e.stopPropagation(); endNumInputElement.addEventListener('input', (e) => { if (parseInt(e.target.value, 10) > maxNum) { e.target.value = maxNum; } }); startNumInputElement.addEventListener('input', (e) => { if (parseInt(e.target.value, 10) > maxNum) { e.target.value = maxNum; } }); qualitySliderElement.onkeydown = (e) => e.stopPropagation(); qualitySliderElement.oninput = (e) => { qualityValElement.textContent = e.target.value; }; shadow.getElementById('cd-close').onclick = () => { guiHost.remove(); }; function updateSliderState(ext) { if (ext === 'jxl' || ext === 'png') { qualitySliderElement.disabled = true; qualitySliderElement.style.opacity = '0.4'; qualitySliderElement.style.cursor = 'not-allowed'; qualityValElement.style.opacity = "0.4"; } else { qualitySliderElement.disabled = false; qualitySliderElement.style.opacity = '1'; qualitySliderElement.style.cursor = 'pointer'; qualityValElement.style.opacity = "1"; } } updateSliderState(selectedExt); shadow.querySelectorAll('.cd-fmt-btn').forEach(btn => { btn.onclick = () => { shadow.querySelectorAll('.cd-fmt-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedExt = btn.getAttribute('data-ext'); updateSliderState(selectedExt); }; }); } async function setupUpdateNotification() { if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return; const localVersion = Number(GM_info.script.version); const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id; const scriptURL = `https://update.greasyfork.org/scripts/${scriptID}/raw.js`; const latestVersionString = await new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: scriptURL, responseType: 'text', onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version) }); }); const latestVersion = Number(latestVersionString); if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return; if (latestVersion <= localVersion) return; const updateLinkElement = document.createElement('a'); updateLinkElement.id = 'LayoutDownloaderAtt-UpdateLink'; updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js'); updateLinkElement.innerHTML = `Update v${latestVersionString} is out! ${externalLinkSVG}`; updateLinkElement.style = ` display: flex; justify-content: center; align-items: center; margin-top: 16px; padding: 12px; font-size: 13px; font-weight: bold; text-decoration: none; color: white; background-color: #10b981; border-radius: 8px; cursor: pointer; transition: background 0.2s; `; updateLinkElement.onmouseover = () => updateLinkElement.style.backgroundColor = '#059669'; updateLinkElement.onmouseout = () => updateLinkElement.style.backgroundColor = '#10b981'; updateLinkElement.onclick = () => setTimeout(() => { updateLinkElement.removeAttribute('href'); updateLinkElement.innerHTML = `Please, reload the guide. ${reloadSVG}`; updateLinkElement.style.cursor = 'default'; updateLinkElement.style.backgroundColor = '#f59e0b'; }, 1000); panelElement.appendChild(updateLinkElement); } function isOKToDownload() { const startNum = Number(startNumInputElement.value); const endNum = Number(endNumInputElement.value); if (Number.isNaN(startNum) || Number.isNaN(endNum)) { alert("Please enter page number correctly."); return false; } if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) { alert("Please enter integers correctly."); return false; } if (startNum < 1 || endNum < 1) { alert("Page number shouldn't smaller than 1."); return false; } if (startNum > maxNum || endNum > maxNum) { alert(`Page number shouldn't bigger than ${maxNum}.`); return false; } if (startNum > endNum) { alert("Number of start shouldn't bigger than number of end."); return false; } return true; } async function download(getImagePromises, title, imageSuffix, zipOptions, qualityValue) { const startNum = Number(startNumInputElement.value); const endNum = Number(endNumInputElement.value); promiseCount = endNum - startNum + 1; fulfillCount = 0; // reseta isErrorOccurred = false; let images = []; for (let num = startNum; num <= endNum; num += 4) { const from = num; const to = Math.min(num + 3, endNum); try { const result = await Promise.all(getImagePromises(from, to, imageSuffix, qualityValue)); images = images.concat(result); } catch (error) { return; } } JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000); const zip = new JSZip(); const zipTitle = title.replaceAll(/\/|\\|\:|\*|\?|\"|\<|\>|\|/g, ''); const folder = zip.folder(zipTitle); for (const [index, image] of images.entries()) { const filename = `${String(startNum + index).padStart(maxNum >= 100 ? String(maxNum).length : 2, '0')}.${imageSuffix}`; folder.file(filename, image, zipOptions); } const zipProgressHandler = (metadata) => { downloadButtonElement.textContent = `Zipping...`; statusElement.innerHTML = `Compacting ZIP...ᅠ<b style="color:#3b82f6;">ᅠ${metadata.percent.toFixed()}%</b>`; } const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler); saveAs(content, `${zipTitle}.zip`); downloadButtonElement.textContent = "Download Complete. (Download again?)"; downloadButtonElement.style.backgroundColor = '#10b981'; downloadButtonElement.style.cursor = 'pointer'; downloadButtonElement.disabled = false; statusElement.innerHTML = `<span style="color:#10b981; font-weight:bold;">Sucess! Enjoy it!</span>`; } function fulfillHandler(res) { if (!isErrorOccurred) { fulfillCount++; downloadButtonElement.textContent = `Processing...`; statusElement.innerHTML = `Downloading the page:ᅠ<b>${fulfillCount}/${promiseCount}</b>ᅠ(${Math.round((fulfillCount / promiseCount) * 100)}%)`; } return res; } function rejectHandler(err) { isErrorOccurred = true; console.error(err); downloadButtonElement.textContent = 'Error Occurred(Try again?)'; downloadButtonElement.style.backgroundColor = '#ef4444'; downloadButtonElement.style.cursor = 'pointer'; downloadButtonElement.disabled = false; statusElement.innerHTML = `<span style="color:#ef4444; font-weight:bold;">Error to download the page.</span>`; return Promise.reject(err); } return { init, fulfillHandler, rejectHandler }; })(window);