Scribd Enhancer All-in-One (v3.1.0)

Scribd Enhancer with OCR, TXT/HTML export, Snapshot PDF (pixel-perfect), Rich HTML (images inlined), page-range + quality controls. Draggable/collapsible panel + floating gear with position memory. Rich HTML de-duplicates layered text/image. + External Downloader button (scribd.vdownloaders.com) with URL templating. Preview now has a quick-hide toggle.

  1. // ==UserScript==
  2. // @name Scribd Enhancer All-in-One (v3.1.0)
  3. // @namespace https://greatest.deepsurf.us/users/Eliminater74
  4. // @version 3.1.0
  5. // @description Scribd Enhancer with OCR, TXT/HTML export, Snapshot PDF (pixel-perfect), Rich HTML (images inlined), page-range + quality controls. Draggable/collapsible panel + floating gear with position memory. Rich HTML de-duplicates layered text/image. + External Downloader button (scribd.vdownloaders.com) with URL templating. Preview now has a quick-hide toggle.
  6. // @author Eliminater74
  7. // @license MIT
  8. // @match *://*.scribd.com/*
  9. // @match *://scribd.vdownloaders.com/*
  10. // @grant none
  11. // @icon https://s-f.scribdassets.com/favicon.ico
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // ---------- KEYS ----------
  18. const SETTINGS_KEY = 'scribdEnhancerSettings';
  19. const UI_MENU_KEY = 'scribdEnhancer_ui_menu';
  20. const UI_GEAR_KEY = 'scribdEnhancer_ui_gear';
  21. const UI_PREVIEW_POS = 'scribdEnhancer_ui_preview';
  22.  
  23. // ---------- SETTINGS ----------
  24. const defaultSettings = {
  25. unblur: true,
  26. autoScrape: false,
  27. darkMode: false,
  28. showPreview: true, // panel toggle (still available)
  29. previewCollapsed: false, // NEW: remembers quick-hide state
  30. enableOCR: true,
  31. ocrLang: 'auto',
  32. splitEvery: 0,
  33.  
  34. // Snapshot controls
  35. pageRange: 'all', // 'all' | '1-25' | '5,7,10-12'
  36. snapshotScale: 2, // 1..4
  37. snapshotQuality: 0.92, // 0.8 | 0.92 | 1.0
  38.  
  39. // Rich HTML layer preference: 'auto' | 'preferText' | 'preferImage'
  40. richPref: 'auto',
  41.  
  42. // NEW: External downloader
  43. // Supports {url} template. If not present, appends ?url=<encoded>.
  44. downloaderUrl: 'https://scribd.vdownloaders.com/?url={url}',
  45. };
  46. const settings = { ...defaultSettings, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') };
  47. const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  48.  
  49. // ---------- LIBS ----------
  50. const loadScript = (src) => { const s = document.createElement('script'); s.src = src; document.head.appendChild(s); return s; };
  51. loadScript('https://cdn.jsdelivr.net/npm/tesseract.js@4.0.2/dist/tesseract.min.js');
  52. loadScript('https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js');
  53. loadScript('https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js');
  54.  
  55. // ---------- STYLES ----------
  56. const style = document.createElement('style');
  57. style.textContent = `
  58. #se-gear {
  59. position: fixed; width: 40px; height: 40px; line-height: 40px; text-align: center;
  60. background:#2b2b2b; color:#fff; border-radius: 50%; cursor: pointer;
  61. box-shadow: 0 2px 10px rgba(0,0,0,.45); z-index: 2147483647; user-select:none;
  62. font-size: 20px;
  63. }
  64. #se-panel {
  65. position: fixed; background:#1e1f22; color:#f1f1f1; width: 340px; border-radius: 12px;
  66. box-shadow: 0 10px 30px rgba(0,0,0,.6); z-index: 2147483646; font-family: system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
  67. display: none;
  68. }
  69. #se-header {
  70. display:flex; align-items:center; justify-content:space-between; padding:8px 10px; cursor:move;
  71. background:#2a2b2f; border-top-left-radius:12px; border-top-right-radius:12px;
  72. font-weight:600;
  73. }
  74. #se-header .controls { display:flex; gap:6px; }
  75. #se-header .btn {
  76. width:24px; height:24px; line-height:24px; text-align:center; border-radius:6px; background:#3a3b41; cursor:pointer;
  77. user-select:none;
  78. }
  79. #se-body { padding:8px 10px 10px; max-height: 70vh; overflow:auto; }
  80. #se-body label { display:flex; align-items:center; gap:6px; font-size:13px; margin:4px 0; }
  81. #se-body .row { display:flex; gap:8px; }
  82. #se-body .row > * { flex:1; }
  83. #se-body input[type="text"], #se-body select {
  84. width:100%; padding:6px; border-radius:6px; border:1px solid #444; background:#121316; color:#eee; font-size:13px;
  85. }
  86. #se-body button {
  87. width:100%; padding:8px; margin-top:6px; border:none; border-radius:8px; background:#3b3d45; color:#fff;
  88. cursor:pointer; font-size:13px;
  89. }
  90. #se-body button:hover { filter:brightness(1.08); }
  91. #se-preview {
  92. position: fixed; right: 20px; bottom: 80px; width: 380px; top: 12px;
  93. background:#111; color:#eee; border:1px solid #444; border-radius:10px;
  94. padding:0; font-family: ui-monospace,Menlo,Consolas,monospace; font-size:12px; white-space:pre-wrap;
  95. overflow:auto; z-index: 2147483645;
  96. }
  97. #se-preview.collapsed { display:none !important; }
  98. #se-preview .bar {
  99. display:flex; align-items:center; justify-content:space-between; gap:6px;
  100. padding:6px 8px; background:#202225; border-bottom:1px solid #333; border-top-left-radius:10px; border-top-right-radius:10px;
  101. user-select:none;
  102. }
  103. #se-preview .bar .title { font-size:12px; opacity:.9 }
  104. #se-preview .bar .btns { display:flex; gap:6px; }
  105. #se-preview .bar .btn {
  106. width:20px; height:20px; line-height:20px; text-align:center; border-radius:5px; background:#2f3136; cursor:pointer;
  107. }
  108. #se-preview .content { padding:8px; }
  109. .se-dark #se-preview { background:#222; color:#eee; border-color:#555; }
  110. `;
  111. document.head.appendChild(style);
  112.  
  113. // ---------- HELPERS ----------
  114. const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  115. const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
  116. const safe = (s) => (s || '').toString();
  117.  
  118. function applyDarkMode() {
  119. document.documentElement.classList.toggle('se-dark', settings.darkMode);
  120. document.body.classList.toggle('se-dark', settings.darkMode);
  121. }
  122.  
  123. function unblurContent() {
  124. if (!settings.unblur) return;
  125. const cleanup = () => {
  126. document.querySelectorAll('.blurred_page, .promo_div, [unselectable="on"]').forEach(el => el.remove());
  127. document.querySelectorAll('*').forEach(el => {
  128. const cs = getComputedStyle(el);
  129. if (cs.color === 'transparent') el.style.color = '#111';
  130. if (cs.textShadow && cs.textShadow.includes('white')) el.style.textShadow = 'none';
  131. });
  132. };
  133. cleanup();
  134. new MutationObserver(cleanup).observe(document.body, { childList: true, subtree: true });
  135. }
  136.  
  137. function cleanOCRText(text) {
  138. return text.split('\n').map(t => t.trim())
  139. .filter(line => line.length >= 3 && /[a-zA-Z]/.test(line) && !/^[^a-zA-Z0-9]{3,}$/.test(line))
  140. .join('\n');
  141. }
  142.  
  143. function detectLanguage(text) {
  144. const map = { spa:/[ñáéíóúü]/i, fra:/[éèêëàâôûùç]/i, deu:/[äöüß]/i, ron:/[șțăîâ]/i };
  145. for (const [k,re] of Object.entries(map)) if (re.test(text)) return k;
  146. return 'eng';
  147. }
  148.  
  149. async function preprocessImage(src) {
  150. return new Promise(resolve => {
  151. const img = new Image(); img.crossOrigin = 'anonymous';
  152. img.onload = () => {
  153. if (img.naturalWidth < 100 || img.naturalHeight < 100 || /logo|icon|watermark/i.test(src)) return resolve(null);
  154. const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
  155. const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0);
  156. const d = ctx.getImageData(0,0,c.width,c.height);
  157. for (let i=0; i<d.data.length; i+=4) {
  158. const avg = (d.data[i]+d.data[i+1]+d.data[i+2])/3;
  159. d.data[i]=d.data[i+1]=d.data[i+2]=avg;
  160. }
  161. ctx.putImageData(d,0,0);
  162. resolve(c.toDataURL('image/png'));
  163. };
  164. img.onerror = () => resolve(null);
  165. img.src = src;
  166. });
  167. }
  168.  
  169. function getScribdPages() {
  170. return [...document.querySelectorAll(
  171. '.page, .reader_column, [id^="page_container"], .outer_page, .abs_page, .scribd_page, .text_layer'
  172. )];
  173. }
  174.  
  175. function parsePageRange(rangeText, totalPages) {
  176. const txt = safe(rangeText).trim().toLowerCase();
  177. if (!txt || txt === 'all') return Array.from({length: totalPages}, (_,i)=>i);
  178. const set = new Set();
  179. for (const part of txt.split(/[,;]\s*/)) {
  180. const m = part.match(/^(\d+)\s*-\s*(\d+)$/);
  181. if (m) {
  182. let a = clamp(+m[1],1,totalPages), b = clamp(+m[2],1,totalPages);
  183. if (a>b) [a,b]=[b,a];
  184. for (let p=a; p<=b; p++) set.add(p-1);
  185. } else {
  186. const n = clamp(parseInt(part,10),1,totalPages);
  187. if (!isNaN(n)) set.add(n-1);
  188. }
  189. }
  190. return [...set].sort((x,y)=>x-y);
  191. }
  192.  
  193. // ---------- EXPORTS ----------
  194. function exportOutput(content, ext) {
  195. const split = settings.splitEvery | 0;
  196. const parts = content.split(/(?=\[Page \d+])/);
  197. if (!split || split < 1) {
  198. const blob = new Blob([content], { type: ext==='html' ? 'text/html' : 'text/plain' });
  199. const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_output.${ext}`; a.click();
  200. return;
  201. }
  202. for (let i=0; i<parts.length; i+=split) {
  203. const chunk = parts.slice(i,i+split).join('\n');
  204. const blob = new Blob([chunk], { type: ext==='html' ? 'text/html' : 'text/plain' });
  205. const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_part${Math.floor(i/split)+1}.${ext}`; a.click();
  206. }
  207. }
  208.  
  209. function printToPDF(content) {
  210. const win = window.open('', 'PrintView');
  211. win.document.write(`<html><head><title>Scribd Print</title></head><body><pre>${content}</pre></body></html>`);
  212. win.document.close(); win.focus(); setTimeout(() => win.print(), 600);
  213. }
  214.  
  215. async function exportSnapshotPDF(allPages) {
  216. await new Promise(r => { const chk = () => (window.html2canvas && window.jspdf) ? r() : setTimeout(chk,100); chk(); });
  217. const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');
  218.  
  219. const scale = clamp(+settings.snapshotScale || 2, 1, 4);
  220. const quality = +settings.snapshotQuality || 0.92;
  221.  
  222. const { jsPDF } = window.jspdf;
  223. const pdf = new jsPDF({ unit:'pt', format:'a4', compress:true });
  224. const pageW = pdf.internal.pageSize.getWidth();
  225. const pageH = pdf.internal.pageSize.getHeight();
  226.  
  227. for (let i=0; i<pages.length; i++) {
  228. const node = pages[i];
  229. node.scrollIntoView({block:'center'}); await sleep(220);
  230. const canvas = await window.html2canvas(node, { useCORS:true, allowTaint:true, backgroundColor:'#ffffff', scale });
  231. const imgData = canvas.toDataURL('image/jpeg', quality);
  232. const imgW = pageW, imgH = (canvas.height/canvas.width) * imgW;
  233. if (i>0) pdf.addPage();
  234. const finalH = imgH > pageH ? pageH : imgH;
  235. const finalW = imgH > pageH ? (pageH/imgH)*imgW : imgW;
  236. pdf.addImage(imgData, 'JPEG', 0, 0, finalW, finalH);
  237. if (i % 10 === 0) await sleep(40);
  238. }
  239. pdf.save('scribd_snapshot.pdf');
  240. }
  241. function getPagesInRange(allPages) {
  242. const idxs = parsePageRange(settings.pageRange, allPages.length);
  243. return idxs.map(i => allPages[i]).filter(Boolean);
  244. }
  245.  
  246. // --- Rich HTML (DOM clone + images inlined) with layer de-dup ---
  247. async function exportRichHTML(allPages) {
  248. const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');
  249. const sections = [];
  250.  
  251. for (let i=0; i<pages.length; i++) {
  252. const clone = pages[i].cloneNode(true);
  253.  
  254. // Remove hidden bits that can become visible offline
  255. clone.querySelectorAll('[aria-hidden="true"], [style*="opacity:0"], [style*="opacity: 0"], [style*="visibility:hidden"]').forEach(n => n.remove());
  256.  
  257. // Decide which layer to keep
  258. const hasTextLayer = !!clone.querySelector('.text_layer, [class*="textLayer"]');
  259. const preferText = settings.richPref === 'preferText' || (settings.richPref === 'auto' && hasTextLayer);
  260.  
  261. if (preferText) {
  262. clone.querySelectorAll('canvas').forEach(n => n.remove());
  263. clone.querySelectorAll('img').forEach(img => {
  264. const cls = img.className || '';
  265. const w = (img.getAttribute('width') || '') + (img.style?.width || '');
  266. const h = (img.getAttribute('height') || '') + (img.style?.height || '');
  267. if (/page|render|canvas|background/i.test(cls) || /100%/.test(w+h)) img.remove();
  268. });
  269. } else {
  270. clone.querySelectorAll('.text_layer, [class*="textLayer"]').forEach(n => n.remove());
  271. }
  272.  
  273. // Inline images (best effort)
  274. const imgs = [...clone.querySelectorAll('img')];
  275. await Promise.all(imgs.map(async (img) => {
  276. try {
  277. const src = img.getAttribute('src') || img.src;
  278. if (!src) return;
  279. img.setAttribute('src', await imageToDataURL(src));
  280. } catch { /* keep original src */ }
  281. }));
  282.  
  283. clone.querySelectorAll('script, link[rel="stylesheet"]').forEach(n => n.remove());
  284. sections.push(`<section style="page-break-after:always">${clone.outerHTML}</section>`);
  285. if (i % 20 === 0) await sleep(15);
  286. }
  287.  
  288. const html = `<!doctype html>
  289. <html>
  290. <head>
  291. <meta charset="utf-8">
  292. <title>Scribd Rich Export</title>
  293. <meta name="viewport" content="width=device-width, initial-scale=1">
  294. <style>
  295. *{transform:none !important}
  296. body{margin:16px;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;}
  297. section{margin:0 auto; max-width:900px;}
  298. img{max-width:100%; height:auto;}
  299. </style>
  300. </head>
  301. <body>
  302. ${sections.join('\n')}
  303. </body>
  304. </html>`;
  305.  
  306. const blob = new Blob([html], { type: 'text/html' });
  307. const a = document.createElement('a');
  308. a.href = URL.createObjectURL(blob);
  309. a.download = 'scribd_rich.html';
  310. a.click();
  311. }
  312.  
  313. function imageToDataURL(src) {
  314. return new Promise(resolve => {
  315. const img = new Image(); img.crossOrigin = 'anonymous';
  316. img.onload = () => {
  317. try {
  318. const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight;
  319. const ctx = c.getContext('2d'); ctx.drawImage(img,0,0); resolve(c.toDataURL('image/png'));
  320. } catch { resolve(src); }
  321. };
  322. img.onerror = () => resolve(src);
  323. const bust = src.includes('?') ? '&' : '?'; img.src = src + bust + 'x=' + Date.now();
  324. });
  325. }
  326.  
  327. // ---------- SCRAPER ----------
  328. async function scrapePages(pages, previewEl) {
  329. const contentEl = previewEl.querySelector('.content');
  330. const concurrency = 4; let index = 0; const firstText = [];
  331. async function scrape(page, i) {
  332. page.scrollIntoView(); await sleep(300);
  333. let found = false;
  334. const text = page.innerText.trim();
  335. if (text) { contentEl.textContent += `[Page ${i+1}] \n${text}\n\n`; firstText.push(text); found = true; }
  336. if (settings.enableOCR && window.Tesseract) {
  337. const imgs = page.querySelectorAll('img');
  338. for (let img of imgs) {
  339. const src = img.src || ''; const processed = await preprocessImage(src);
  340. if (!processed) continue;
  341. const lang = settings.ocrLang === 'auto' ? detectLanguage(firstText.join(' ')) : settings.ocrLang;
  342. try {
  343. const res = await window.Tesseract.recognize(processed, lang);
  344. const ocrText = cleanOCRText(res.data.text || '');
  345. if (ocrText) { contentEl.textContent += `[OCR] ${ocrText}\n\n`; found = true; }
  346. } catch {}
  347. }
  348. }
  349. if (!found) contentEl.textContent += `[Page ${i+1}] No content\n\n`;
  350. }
  351. const workers = Array(concurrency).fill().map(async ()=>{ while (index < pages.length) { const i = index++; await scrape(pages[i], i); }});
  352. await Promise.all(workers);
  353. alert(`✅ Scraped ${pages.length} pages.`);
  354. }
  355.  
  356. // ---------- DRAGGABLE + UI ----------
  357. function makeDraggable(el, storageKey, fallbackPos) {
  358. el.style.position = 'fixed'; el.style.touchAction = 'none';
  359. try {
  360. const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
  361. if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) {
  362. el.style.left = saved.x + 'px'; el.style.top = saved.y + 'px';
  363. } else if (fallbackPos) {
  364. const {x,y} = fallbackPos(); el.style.left = x + 'px'; el.style.top = y + 'px';
  365. }
  366. } catch {}
  367. let startX, startY, startL, startT, moved=false;
  368. const onDown = (e) => {
  369. moved = false;
  370. const p = e.touches ? e.touches[0] : e;
  371. startX=p.clientX; startY=p.clientY;
  372. const r = el.getBoundingClientRect(); startL=r.left; startT=r.top;
  373. document.addEventListener('mousemove', onMove);
  374. document.addEventListener('mouseup', onUp);
  375. document.addEventListener('touchmove', onMove, {passive:false});
  376. document.addEventListener('touchend', onUp);
  377. };
  378. const onMove = (e) => {
  379. const p = e.touches ? e.touches[0] : e;
  380. if (e.cancelable) e.preventDefault();
  381. moved = true;
  382. const nx = clamp(startL + (p.clientX-startX), 0, window.innerWidth - el.offsetWidth);
  383. const ny = clamp(startT + (p.clientY-startY), 0, window.innerHeight - el.offsetHeight);
  384. el.style.left = nx + 'px'; el.style.top = ny + 'px';
  385. };
  386. const onUp = () => {
  387. document.removeEventListener('mousemove', onMove);
  388. document.removeEventListener('mouseup', onUp);
  389. document.removeEventListener('touchmove', onMove);
  390. document.removeEventListener('touchend', onUp);
  391. const r = el.getBoundingClientRect();
  392. localStorage.setItem(storageKey, JSON.stringify({x:r.left, y:r.top}));
  393. if (moved) { el.dataset.justDragged = '1'; setTimeout(()=>delete el.dataset.justDragged,150); }
  394. };
  395. el.addEventListener('mousedown', onDown);
  396. el.addEventListener('touchstart', onDown, {passive:false});
  397. }
  398.  
  399. function buildUI(previewEl) {
  400. // Gear
  401. const gear = document.createElement('div');
  402. gear.id = 'se-gear'; gear.textContent = '⚙️';
  403. document.body.appendChild(gear);
  404. makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));
  405.  
  406. // Panel
  407. const panel = document.createElement('div'); panel.id = 'se-panel';
  408. panel.innerHTML = `
  409. <div id="se-header">
  410. <div>📚 Scribd Enhancer</div>
  411. <div class="controls">
  412. <div id="se-min" class="btn" title="Collapse">–</div>
  413. <div id="se-close" class="btn" title="Close">✕</div>
  414. </div>
  415. </div>
  416. <div id="se-body">
  417. <label><input type="checkbox" id="opt-unblur"> Unblur</label>
  418. <label><input type="checkbox" id="opt-autoscrape"> Auto Scrape</label>
  419. <label><input type="checkbox" id="opt-dark"> Dark Mode</label>
  420.  
  421. <div class="row">
  422. <label style="flex:1"><input type="checkbox" id="opt-preview"> Show Preview</label>
  423. <button id="btn-toggle-preview" title="Quick hide/show (hotkey: P)">👁️ Toggle Preview</button>
  424. </div>
  425.  
  426. <div class="row">
  427. <label style="flex:1">OCR
  428. <select id="opt-lang">
  429. <option value="auto">Auto</option>
  430. <option value="eng">English</option>
  431. <option value="spa">Spanish</option>
  432. <option value="fra">French</option>
  433. <option value="deu">German</option>
  434. </select>
  435. </label>
  436. <label style="flex:1">Split
  437. <select id="opt-split">
  438. <option value="0">Off</option>
  439. <option value="100">100</option>
  440. <option value="250">250</option>
  441. <option value="500">500</option>
  442. </select>
  443. </label>
  444. </div>
  445.  
  446. <label>Export Page Range
  447. <input id="opt-range" type="text" placeholder="all | 1-25 | 5,7,10-12">
  448. </label>
  449.  
  450. <div class="row">
  451. <label>Scale
  452. <select id="opt-scale">
  453. <option value="1">1x</option>
  454. <option value="2">2x</option>
  455. <option value="3">3x</option>
  456. <option value="4">4x</option>
  457. </select>
  458. </label>
  459. <label>JPEG
  460. <select id="opt-quality">
  461. <option value="0.8">0.80</option>
  462. <option value="0.92">0.92</option>
  463. <option value="1.0">1.00</option>
  464. </select>
  465. </label>
  466. </div>
  467.  
  468. <label>Rich Export Preference
  469. <select id="opt-richpref">
  470. <option value="auto">Auto (prefer text layer if present)</option>
  471. <option value="preferText">Keep Text (remove page images)</option>
  472. <option value="preferImage">Keep Images (remove text layer)</option>
  473. </select>
  474. </label>
  475.  
  476. <hr style="border-color:#333">
  477.  
  478. <label>External Downloader URL
  479. <input id="opt-downloader" type="text" placeholder="https://scribd.vdownloaders.com/?url={url}">
  480. </label>
  481. <div class="row">
  482. <button id="btn-open-downloader">⬇️ Open Downloader</button>
  483. <button id="btn-copy-url" title="Copy current page URL">📋 Copy URL</button>
  484. </div>
  485.  
  486. <button id="btn-scrape">📖 Scrape Pages (Text/OCR)</button>
  487. <button id="btn-export">💾 Export TXT</button>
  488. <button id="btn-html">🧾 Export Plain HTML</button>
  489. <button id="btn-print">🖨️ Print (Text)</button>
  490. <button id="btn-snapshot-pdf">📸 Export Snapshot PDF</button>
  491. <button id="btn-rich-html">🖼️ Export Rich HTML</button>
  492. </div>
  493. `;
  494. document.body.appendChild(panel);
  495. makeDraggable(panel, UI_MENU_KEY, () => ({ x: window.innerWidth - 360, y: window.innerHeight - 360 }));
  496.  
  497. // Open/Close & collapse
  498. const togglePanel = () => {
  499. if (gear.dataset.justDragged) return;
  500. panel.style.display = (panel.style.display === 'none' || !panel.style.display) ? 'block' : 'none';
  501. };
  502. gear.addEventListener('click', togglePanel);
  503. panel.querySelector('#se-close').addEventListener('click', () => panel.style.display = 'none');
  504.  
  505. const body = panel.querySelector('#se-body');
  506. let collapsed = false;
  507. panel.querySelector('#se-min').addEventListener('click', () => {
  508. collapsed = !collapsed;
  509. body.style.display = collapsed ? 'none' : 'block';
  510. panel.querySelector('#se-min').textContent = collapsed ? '+' : '–';
  511. });
  512.  
  513. // Keyboard shortcuts
  514. document.addEventListener('keydown', (e) => {
  515. if (e.key.toLowerCase() === 'g') togglePanel();
  516. if (e.key.toLowerCase() === 'p') togglePreview(previewEl); // NEW hotkey
  517. if (e.key === 'Escape') panel.style.display = 'none';
  518. });
  519.  
  520. // Bind controls
  521. const bind = (sel, prop, parser = v=>v) => {
  522. const el = panel.querySelector(sel);
  523. el.value = (prop in settings) ? settings[prop] : el.value;
  524. if (el.type === 'checkbox') el.checked = !!settings[prop];
  525. el.addEventListener('change', () => {
  526. settings[prop] = el.type === 'checkbox' ? el.checked : parser(el.value);
  527. saveSettings();
  528. applyDarkMode();
  529. if (prop === 'showPreview') {
  530. // If turned on, ensure preview exists and respects collapsed state
  531. if (settings.showPreview && !document.getElementById('se-preview')) {
  532. document.body.appendChild(previewEl);
  533. }
  534. previewEl.classList.toggle('collapsed', !settings.showPreview || settings.previewCollapsed);
  535. }
  536. });
  537. return el;
  538. };
  539.  
  540. bind('#opt-unblur', 'unblur');
  541. bind('#opt-autoscrape','autoScrape');
  542. bind('#opt-dark', 'darkMode');
  543. bind('#opt-preview', 'showPreview');
  544. bind('#opt-lang', 'ocrLang');
  545. bind('#opt-split', 'splitEvery', v=>parseInt(v,10)||0);
  546. bind('#opt-range', 'pageRange', v=>safe(v)||'all');
  547. bind('#opt-scale', 'snapshotScale', v=>clamp(parseInt(v,10)||2,1,4));
  548. bind('#opt-quality', 'snapshotQuality', v=>Number(v)||0.92);
  549. bind('#opt-richpref', 'richPref');
  550. bind('#opt-downloader','downloaderUrl', v=>safe(v).trim() || defaultSettings.downloaderUrl);
  551.  
  552. // Actions
  553. panel.querySelector('#btn-toggle-preview').onclick = () => togglePreview(previewEl);
  554.  
  555. panel.querySelector('#btn-open-downloader').onclick = () => {
  556. const srcUrl = location.href;
  557. const tpl = settings.downloaderUrl || defaultSettings.downloaderUrl;
  558. const target = tpl.includes('{url}')
  559. ? tpl.replace('{url}', encodeURIComponent(srcUrl))
  560. : (tpl.includes('?') ? `${tpl}&url=${encodeURIComponent(srcUrl)}` : `${tpl}?url=${encodeURIComponent(srcUrl)}`);
  561. window.open(target, '_blank', 'noopener');
  562. // best-effort clipboard for convenience
  563. if (navigator.clipboard?.writeText) navigator.clipboard.writeText(srcUrl).catch(()=>{});
  564. };
  565. panel.querySelector('#btn-copy-url').onclick = async () => {
  566. try { await navigator.clipboard.writeText(location.href); alert('✅ URL copied.'); }
  567. catch { prompt('Copy URL:', location.href); }
  568. };
  569.  
  570. panel.querySelector('#btn-scrape').onclick = () => {
  571. const pages = getScribdPages();
  572. if (!pages.length) return alert('❌ No pages found.');
  573. if (settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(previewEl);
  574. previewEl.classList.remove('collapsed'); settings.previewCollapsed = false; saveSettings();
  575. scrapePages(pages, previewEl);
  576. };
  577. panel.querySelector('#btn-export').onclick = () => exportOutput(previewEl.querySelector('.content').textContent, 'txt');
  578. panel.querySelector('#btn-html').onclick = () => exportOutput(`<html><body><pre>${previewEl.querySelector('.content').textContent}</pre></body></html>`, 'html');
  579. panel.querySelector('#btn-print').onclick = () => printToPDF(previewEl.querySelector('.content').textContent);
  580. panel.querySelector('#btn-snapshot-pdf').onclick = async () => {
  581. const pages = getScribdPages();
  582. if (!pages.length) return alert('❌ No pages found.');
  583. try { await exportSnapshotPDF(pages); } catch (e) { console.error(e); alert('Snapshot export failed. Try Rich HTML.'); }
  584. };
  585. panel.querySelector('#btn-rich-html').onclick = async () => {
  586. const pages = getScribdPages();
  587. if (!pages.length) return alert('❌ No pages found.');
  588. try { await exportRichHTML(pages); } catch (e) { console.error(e); alert('Rich HTML export failed.'); }
  589. };
  590.  
  591. return { gear, panel };
  592. }
  593.  
  594. // Preview box (with quick-hide and drag memory)
  595. function createPreview() {
  596. const preview = document.createElement('div');
  597. preview.id = 'se-preview';
  598. preview.innerHTML = `
  599. <div class="bar">
  600. <div class="title">Preview</div>
  601. <div class="btns">
  602. <div class="btn" id="se-prev-clear" title="Clear">🧹</div>
  603. <div class="btn" id="se-prev-hide" title="Hide (hotkey: P)">👁️</div>
  604. </div>
  605. </div>
  606. <div class="content">[Preview Initialized]\n</div>
  607. `;
  608. // drag support (optional): keep it fixed but remember position
  609. makeDraggable(preview, UI_PREVIEW_POS, () => ({ x: window.innerWidth - 420, y: 12 }));
  610.  
  611. if (settings.showPreview) document.body.appendChild(preview);
  612. preview.classList.toggle('collapsed', settings.previewCollapsed || !settings.showPreview);
  613.  
  614. preview.querySelector('#se-prev-clear').addEventListener('click', () => {
  615. preview.querySelector('.content').textContent = '';
  616. });
  617. preview.querySelector('#se-prev-hide').addEventListener('click', () => {
  618. togglePreview(preview);
  619. });
  620.  
  621. return preview;
  622. }
  623.  
  624. function togglePreview(preview) {
  625. const willHide = !preview.classList.contains('collapsed') || !settings.showPreview;
  626. if (willHide) {
  627. preview.classList.add('collapsed');
  628. settings.previewCollapsed = true;
  629. settings.showPreview = false; // reflect in panel checkbox as well
  630. } else {
  631. preview.classList.remove('collapsed');
  632. settings.previewCollapsed = false;
  633. settings.showPreview = true;
  634. if (!document.getElementById('se-preview')) document.body.appendChild(preview);
  635. }
  636. saveSettings();
  637. // Sync panel checkbox if present
  638. const chk = document.querySelector('#opt-preview');
  639. if (chk) chk.checked = settings.showPreview;
  640. }
  641.  
  642. // ---------- BOOT ----------
  643. applyDarkMode();
  644. unblurContent();
  645. const preview = createPreview();
  646. const { gear } = buildUI(preview);
  647. makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));
  648.  
  649. // If we happen to be on scribd.vdownloaders.com and a ?url= param exists, try to assist
  650. (function assistOnDownloaderPage() {
  651. if (!/scribd\.vdownloaders\.com$/i.test(location.hostname)) return;
  652. const params = new URLSearchParams(location.search);
  653. const u = params.get('url');
  654. if (!u) return;
  655. // best effort: try to fill first URL-like input
  656. const candidate = document.querySelector('input[type="url"], input[name*="url" i], input[placeholder*="link" i], input[placeholder*="url" i]');
  657. if (candidate && !candidate.value) candidate.value = u;
  658. })();
  659.  
  660. // Auto-scrape if desired
  661. if (settings.autoScrape) {
  662. const pages = getScribdPages();
  663. if (pages.length && settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
  664. if (pages.length) scrapePages(pages, preview);
  665. }
  666. })();