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

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 now de-duplicates layered text/image to avoid doubled content. By Eliminater74.

当前为 2025-08-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Scribd Enhancer All-in-One (v3.0.1)
// @namespace    https://greasyfork.org/users/Eliminater74
// @version      3.0.1
// @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 now de-duplicates layered text/image to avoid doubled content. By Eliminater74.
// @author       Eliminater74
// @license      MIT
// @match        *://*.scribd.com/*
// @grant        none
// @icon         https://s-f.scribdassets.com/favicon.ico
// ==/UserScript==

(function () {
  'use strict';

  // ---------- KEYS ----------
  const SETTINGS_KEY   = 'scribdEnhancerSettings';
  const UI_MENU_KEY    = 'scribdEnhancer_ui_menu';
  const UI_GEAR_KEY    = 'scribdEnhancer_ui_gear';

  // ---------- SETTINGS ----------
  const defaultSettings = {
    unblur: true,
    autoScrape: false,
    darkMode: false,
    showPreview: true,
    enableOCR: true,
    ocrLang: 'auto',
    splitEvery: 0,

    // Snapshot controls
    pageRange: 'all',     // 'all' | '1-25' | '5,7,10-12'
    snapshotScale: 2,     // 1..4
    snapshotQuality: 0.92, // 0.8 | 0.92 | 1.0

    // NEW: Rich HTML layer preference: 'auto' | 'preferText' | 'preferImage'
    richPref: 'auto'
  };
  const settings = { ...defaultSettings, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') };
  const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));

  // ---------- LIBS ----------
  const loadScript = (src) => { const s = document.createElement('script'); s.src = src; document.head.appendChild(s); return s; };
  loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/tesseract.min.js');
  loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js');
  loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js');

  // ---------- STYLES ----------
  const style = document.createElement('style');
  style.textContent = `
    #se-gear {
      position: fixed; width: 40px; height: 40px; line-height: 40px; text-align: center;
      background:#2b2b2b; color:#fff; border-radius: 50%; cursor: pointer;
      box-shadow: 0 2px 10px rgba(0,0,0,.45); z-index: 2147483647; user-select:none;
      font-size: 20px;
    }
    #se-panel {
      position: fixed; background:#1e1f22; color:#f1f1f1; width: 320px; border-radius: 12px;
      box-shadow: 0 10px 30px rgba(0,0,0,.6); z-index: 2147483646; font-family: system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
      display: none;
    }
    #se-header {
      display:flex; align-items:center; justify-content:space-between; padding:8px 10px; cursor:move;
      background:#2a2b2f; border-top-left-radius:12px; border-top-right-radius:12px;
      font-weight:600;
    }
    #se-header .controls { display:flex; gap:6px; }
    #se-header .btn {
      width:24px; height:24px; line-height:24px; text-align:center; border-radius:6px; background:#3a3b41; cursor:pointer;
      user-select:none;
    }
    #se-body { padding:8px 10px 10px; max-height: 70vh; overflow:auto; }
    #se-body label { display:flex; align-items:center; gap:6px; font-size:13px; margin:4px 0; }
    #se-body .row { display:flex; gap:8px; }
    #se-body .row > * { flex:1; }
    #se-body input[type="text"], #se-body select {
      width:100%; padding:6px; border-radius:6px; border:1px solid #444; background:#121316; color:#eee; font-size:13px;
    }
    #se-body button {
      width:100%; padding:8px; margin-top:6px; border:none; border-radius:8px; background:#3b3d45; color:#fff;
      cursor:pointer; font-size:13px;
    }
    #se-body button:hover { filter:brightness(1.08); }
    #se-preview {
      position: fixed; right: 20px; bottom: 80px; width: 380px; top: 12px;
      background:#111; color:#eee; border:1px solid #444; border-radius:10px;
      padding:10px; font-family: ui-monospace,Menlo,Consolas,monospace; font-size:12px; white-space:pre-wrap;
      overflow:auto; z-index: 2147483645;
    }
    .se-dark #se-preview { background:#222; color:#eee; border-color:#555; }
  `;
  document.head.appendChild(style);

  // ---------- HELPERS ----------
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
  const safe  = (s) => (s || '').toString();

  function applyDarkMode() {
    document.documentElement.classList.toggle('se-dark', settings.darkMode);
    document.body.classList.toggle('se-dark', settings.darkMode);
  }

  function unblurContent() {
    if (!settings.unblur) return;
    const cleanup = () => {
      document.querySelectorAll('.blurred_page, .promo_div, [unselectable="on"]').forEach(el => el.remove());
      document.querySelectorAll('*').forEach(el => {
        const cs = getComputedStyle(el);
        if (cs.color === 'transparent') el.style.color = '#111';
        if (cs.textShadow && cs.textShadow.includes('white')) el.style.textShadow = 'none';
      });
    };
    cleanup();
    new MutationObserver(cleanup).observe(document.body, { childList: true, subtree: true });
  }

  function cleanOCRText(text) {
    return text.split('\n').map(t => t.trim())
      .filter(line => line.length >= 3 && /[a-zA-Z]/.test(line) && !/^[^a-zA-Z0-9]{3,}$/.test(line))
      .join('\n');
  }

  function detectLanguage(text) {
    const map = { spa:/[ñáéíóúü]/i, fra:/[éèêëàâôûùç]/i, deu:/[äöüß]/i, ron:/[șțăîâ]/i };
    for (const [k,re] of Object.entries(map)) if (re.test(text)) return k;
    return 'eng';
  }

  async function preprocessImage(src) {
    return new Promise(resolve => {
      const img = new Image(); img.crossOrigin = 'anonymous';
      img.onload = () => {
        if (img.naturalWidth < 100 || img.naturalHeight < 100 || /logo|icon|watermark/i.test(src)) return resolve(null);
        const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
        const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0);
        const d = ctx.getImageData(0,0,c.width,c.height);
        for (let i=0; i<d.data.length; i+=4) {
          const avg = (d.data[i]+d.data[i+1]+d.data[i+2])/3;
          d.data[i]=d.data[i+1]=d.data[i+2]=avg;
        }
        ctx.putImageData(d,0,0);
        resolve(c.toDataURL('image/png'));
      };
      img.onerror = () => resolve(null);
      img.src = src;
    });
  }

  function getScribdPages() {
    return [...document.querySelectorAll(
      '.page, .reader_column, [id^="page_container"], .outer_page, .abs_page, .scribd_page, .text_layer'
    )];
  }

  function parsePageRange(rangeText, totalPages) {
    const txt = safe(rangeText).trim().toLowerCase();
    if (!txt || txt === 'all') return Array.from({length: totalPages}, (_,i)=>i);
    const set = new Set();
    for (const part of txt.split(/[,;]\s*/)) {
      const m = part.match(/^(\d+)\s*-\s*(\d+)$/);
      if (m) {
        let a = clamp(+m[1],1,totalPages), b = clamp(+m[2],1,totalPages);
        if (a>b) [a,b]=[b,a];
        for (let p=a; p<=b; p++) set.add(p-1);
      } else {
        const n = clamp(parseInt(part,10),1,totalPages);
        if (!isNaN(n)) set.add(n-1);
      }
    }
    return [...set].sort((x,y)=>x-y);
  }

  // ---------- EXPORTS ----------
  function exportOutput(content, ext) {
    const split = settings.splitEvery | 0;
    const parts = content.split(/(?=\[Page \d+])/);
    if (!split || split < 1) {
      const blob = new Blob([content], { type: ext==='html' ? 'text/html' : 'text/plain' });
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_output.${ext}`; a.click();
      return;
    }
    for (let i=0; i<parts.length; i+=split) {
      const chunk = parts.slice(i,i+split).join('\n');
      const blob = new Blob([chunk], { type: ext==='html' ? 'text/html' : 'text/plain' });
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_part${Math.floor(i/split)+1}.${ext}`; a.click();
    }
  }

  function printToPDF(content) {
    const win = window.open('', 'PrintView');
    win.document.write(`<html><head><title>Scribd Print</title></head><body><pre>${content}</pre></body></html>`);
    win.document.close(); win.focus(); setTimeout(() => win.print(), 600);
  }

  async function exportSnapshotPDF(allPages) {
    await new Promise(r => { const chk = () => (window.html2canvas && window.jspdf) ? r() : setTimeout(chk,100); chk(); });
    const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');

    const scale   = clamp(+settings.snapshotScale || 2, 1, 4);
    const quality = +settings.snapshotQuality || 0.92;

    const { jsPDF } = window.jspdf;
    const pdf = new jsPDF({ unit:'pt', format:'a4', compress:true });
    const pageW = pdf.internal.pageSize.getWidth();
    const pageH = pdf.internal.pageSize.getHeight();

    for (let i=0; i<pages.length; i++) {
      const node = pages[i];
      node.scrollIntoView({block:'center'}); await sleep(220);
      const canvas = await window.html2canvas(node, { useCORS:true, allowTaint:true, backgroundColor:'#ffffff', scale });
      const imgData = canvas.toDataURL('image/jpeg', quality);
      const imgW = pageW, imgH = (canvas.height/canvas.width) * imgW;
      if (i>0) pdf.addPage();
      const finalH = imgH > pageH ? pageH : imgH;
      const finalW = imgH > pageH ? (pageH/imgH)*imgW : imgW;
      pdf.addImage(imgData, 'JPEG', 0, 0, finalW, finalH);
      if (i % 10 === 0) await sleep(40);
    }
    pdf.save('scribd_snapshot.pdf');
  }
  function getPagesInRange(allPages) {
    const idxs = parsePageRange(settings.pageRange, allPages.length);
    return idxs.map(i => allPages[i]).filter(Boolean);
  }

  // --- Rich HTML (DOM clone + images inlined) with layer de-dup ---
  async function exportRichHTML(allPages) {
    const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');
    const sections = [];

    for (let i=0; i<pages.length; i++) {
      const clone = pages[i].cloneNode(true);

      // Remove hidden bits that can become visible offline
      clone.querySelectorAll('[aria-hidden="true"], [style*="opacity:0"], [style*="opacity: 0"], [style*="visibility:hidden"]').forEach(n => n.remove());

      // Decide which layer to keep
      const hasTextLayer = !!clone.querySelector('.text_layer, [class*="textLayer"]');
      const preferText = settings.richPref === 'preferText' || (settings.richPref === 'auto' && hasTextLayer);

      if (preferText) {
        // Keep selectable text: drop canvases and likely page-wide images
        clone.querySelectorAll('canvas').forEach(n => n.remove());
        clone.querySelectorAll('img').forEach(img => {
          const cls = img.className || '';
          const w = (img.getAttribute('width') || '') + (img.style?.width || '');
          const h = (img.getAttribute('height') || '') + (img.style?.height || '');
          if (/page|render|canvas|background/i.test(cls) || /100%/.test(w+h)) img.remove();
        });
      } else {
        // Keep raster layer: drop absolutely-positioned text
        clone.querySelectorAll('.text_layer, [class*="textLayer"]').forEach(n => n.remove());
      }

      // Inline images (best effort)
      const imgs = [...clone.querySelectorAll('img')];
      await Promise.all(imgs.map(async (img) => {
        try {
          const src = img.getAttribute('src') || img.src;
          if (!src) return;
          img.setAttribute('src', await imageToDataURL(src));
        } catch { /* keep original src */ }
      }));

      // Strip scripts/styles inside clone
      clone.querySelectorAll('script, link[rel="stylesheet"]').forEach(n => n.remove());
      sections.push(`<section style="page-break-after:always">${clone.outerHTML}</section>`);
      if (i % 20 === 0) await sleep(15);
    }

    const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Scribd Rich Export</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  /* keep it readable offline */
  *{transform:none !important}
  body{margin:16px;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;}
  section{margin:0 auto; max-width:900px;}
  img{max-width:100%; height:auto;}
</style>
</head>
<body>
${sections.join('\n')}
</body>
</html>`;

    const blob = new Blob([html], { type: 'text/html' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'scribd_rich.html';
    a.click();
  }

  function imageToDataURL(src) {
    return new Promise(resolve => {
      const img = new Image(); img.crossOrigin = 'anonymous';
      img.onload = () => {
        try {
          const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight;
          const ctx = c.getContext('2d'); ctx.drawImage(img,0,0); resolve(c.toDataURL('image/png'));
        } catch { resolve(src); }
      };
      img.onerror = () => resolve(src);
      const bust = src.includes('?') ? '&' : '?'; img.src = src + bust + 'x=' + Date.now();
    });
  }

  // ---------- SCRAPER ----------
  async function scrapePages(pages, preview) {
    const concurrency = 4; let index = 0; const firstText = [];
    async function scrape(page, i) {
      page.scrollIntoView(); await sleep(300);
      let found = false;
      const text = page.innerText.trim();
      if (text) { preview.textContent += `[Page ${i+1}] ✅\n${text}\n\n`; firstText.push(text); found = true; }
      if (settings.enableOCR && window.Tesseract) {
        const imgs = page.querySelectorAll('img');
        for (let img of imgs) {
          const src = img.src || ''; const processed = await preprocessImage(src);
          if (!processed) continue;
          const lang = settings.ocrLang === 'auto' ? detectLanguage(firstText.join(' ')) : settings.ocrLang;
          try {
            const res = await window.Tesseract.recognize(processed, lang);
            const ocrText = cleanOCRText(res.data.text || '');
            if (ocrText) { preview.textContent += `[OCR] ${ocrText}\n\n`; found = true; }
          } catch {}
        }
      }
      if (!found) preview.textContent += `[Page ${i+1}] ❌ No content\n\n`;
    }
    const workers = Array(concurrency).fill().map(async ()=>{ while (index < pages.length) { const i = index++; await scrape(pages[i], i); }});
    await Promise.all(workers);
    alert(`✅ Scraped ${pages.length} pages.`);
  }

  // ---------- DRAGGABLE + UI ----------
  function makeDraggable(el, storageKey, fallbackPos) {
    el.style.position = 'fixed'; el.style.touchAction = 'none';
    try {
      const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
      if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) {
        el.style.left = saved.x + 'px'; el.style.top = saved.y + 'px';
      } else if (fallbackPos) {
        const {x,y} = fallbackPos(); el.style.left = x + 'px'; el.style.top = y + 'px';
      }
    } catch {}
    let startX, startY, startL, startT, moved=false;
    const onDown = (e) => {
      moved = false;
      const p = e.touches ? e.touches[0] : e;
      startX=p.clientX; startY=p.clientY;
      const r = el.getBoundingClientRect(); startL=r.left; startT=r.top;
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
      document.addEventListener('touchmove', onMove, {passive:false});
      document.addEventListener('touchend', onUp);
    };
    const onMove = (e) => {
      const p = e.touches ? e.touches[0] : e;
      if (e.cancelable) e.preventDefault();
      moved = true;
      const nx = clamp(startL + (p.clientX-startX), 0, window.innerWidth - el.offsetWidth);
      const ny = clamp(startT + (p.clientY-startY), 0, window.innerHeight - el.offsetHeight);
      el.style.left = nx + 'px'; el.style.top = ny + 'px';
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onUp);
      const r = el.getBoundingClientRect();
      localStorage.setItem(storageKey, JSON.stringify({x:r.left, y:r.top}));
      if (moved) { el.dataset.justDragged = '1'; setTimeout(()=>delete el.dataset.justDragged,150); }
    };
    el.addEventListener('mousedown', onDown);
    el.addEventListener('touchstart', onDown, {passive:false});
  }

  function buildUI() {
    // Gear
    const gear = document.createElement('div');
    gear.id = 'se-gear'; gear.textContent = '⚙️';
    document.body.appendChild(gear);
    makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));

    // Panel
    const panel = document.createElement('div'); panel.id = 'se-panel';
    panel.innerHTML = `
      <div id="se-header">
        <div>📚 Scribd Enhancer</div>
        <div class="controls">
          <div id="se-min" class="btn" title="Collapse">–</div>
          <div id="se-close" class="btn" title="Close">✕</div>
        </div>
      </div>
      <div id="se-body">
        <label><input type="checkbox" id="opt-unblur"> Unblur</label>
        <label><input type="checkbox" id="opt-autoscrape"> Auto Scrape</label>
        <label><input type="checkbox" id="opt-dark"> Dark Mode</label>
        <label><input type="checkbox" id="opt-preview"> Show Preview</label>

        <div class="row">
          <label style="flex:1">OCR
            <select id="opt-lang">
              <option value="auto">Auto</option>
              <option value="eng">English</option>
              <option value="spa">Spanish</option>
              <option value="fra">French</option>
              <option value="deu">German</option>
            </select>
          </label>
          <label style="flex:1">Split
            <select id="opt-split">
              <option value="0">Off</option>
              <option value="100">100</option>
              <option value="250">250</option>
              <option value="500">500</option>
            </select>
          </label>
        </div>

        <label>Export Page Range
          <input id="opt-range" type="text" placeholder="all | 1-25 | 5,7,10-12">
        </label>

        <div class="row">
          <label>Scale
            <select id="opt-scale">
              <option value="1">1x</option>
              <option value="2">2x</option>
              <option value="3">3x</option>
              <option value="4">4x</option>
            </select>
          </label>
          <label>JPEG
            <select id="opt-quality">
              <option value="0.8">0.80</option>
              <option value="0.92">0.92</option>
              <option value="1.0">1.00</option>
            </select>
          </label>
        </div>

        <label>Rich Export Preference
          <select id="opt-richpref">
            <option value="auto">Auto (prefer text layer if present)</option>
            <option value="preferText">Keep Text (remove page images)</option>
            <option value="preferImage">Keep Images (remove text layer)</option>
          </select>
        </label>

        <button id="btn-scrape">📖 Scrape Pages (Text/OCR)</button>
        <button id="btn-export">💾 Export TXT</button>
        <button id="btn-html">🧾 Export Plain HTML</button>
        <button id="btn-print">🖨️ Print (Text)</button>
        <button id="btn-snapshot-pdf">📸 Export Snapshot PDF</button>
        <button id="btn-rich-html">🖼️ Export Rich HTML</button>
      </div>
    `;
    document.body.appendChild(panel);
    makeDraggable(panel, UI_MENU_KEY, () => ({ x: window.innerWidth - 360, y: window.innerHeight - 360 }));

    // Open/Close & collapse
    const togglePanel = () => {
      if (gear.dataset.justDragged) return;
      panel.style.display = (panel.style.display === 'none' || !panel.style.display) ? 'block' : 'none';
    };
    gear.addEventListener('click', togglePanel);
    panel.querySelector('#se-close').addEventListener('click', () => panel.style.display = 'none');

    const body = panel.querySelector('#se-body');
    let collapsed = false;
    panel.querySelector('#se-min').addEventListener('click', () => {
      collapsed = !collapsed;
      body.style.display = collapsed ? 'none' : 'block';
      panel.querySelector('#se-min').textContent = collapsed ? '+' : '–';
    });

    // Keyboard shortcuts
    document.addEventListener('keydown', (e) => {
      if (e.key.toLowerCase() === 'g') togglePanel();
      if (e.key === 'Escape') panel.style.display = 'none';
    });

    // Bind controls
    const bind = (sel, prop, parser = v=>v) => {
      const el = panel.querySelector(sel);
      el.value = (prop in settings) ? settings[prop] : el.value;
      if (el.type === 'checkbox') el.checked = !!settings[prop];
      el.addEventListener('change', () => {
        settings[prop] = el.type === 'checkbox' ? el.checked : parser(el.value);
        saveSettings();
        applyDarkMode();
        if (prop === 'showPreview') {
          if (settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
          if (!settings.showPreview && document.getElementById('se-preview')) preview.remove();
        }
      });
      return el;
    };

    bind('#opt-unblur',   'unblur');
    bind('#opt-autoscrape','autoScrape');
    bind('#opt-dark',     'darkMode');
    bind('#opt-preview',  'showPreview');
    bind('#opt-lang',     'ocrLang');
    bind('#opt-split',    'splitEvery', v=>parseInt(v,10)||0);
    bind('#opt-range',    'pageRange',  v=>safe(v)||'all');
    bind('#opt-scale',    'snapshotScale', v=>clamp(parseInt(v,10)||2,1,4));
    bind('#opt-quality',  'snapshotQuality', v=>Number(v)||0.92);
    bind('#opt-richpref', 'richPref');

    // Actions
    panel.querySelector('#btn-scrape').onclick = () => {
      const pages = getScribdPages();
      if (!pages.length) return alert('❌ No pages found.');
      if (settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
      scrapePages(pages, preview);
    };
    panel.querySelector('#btn-export').onclick = () => exportOutput(preview.textContent, 'txt');
    panel.querySelector('#btn-html').onclick   = () => exportOutput(`<html><body><pre>${preview.textContent}</pre></body></html>`, 'html');
    panel.querySelector('#btn-print').onclick  = () => printToPDF(preview.textContent);
    panel.querySelector('#btn-snapshot-pdf').onclick = async () => {
      const pages = getScribdPages();
      if (!pages.length) return alert('❌ No pages found.');
      try { await exportSnapshotPDF(pages); } catch (e) { console.error(e); alert('Snapshot export failed. Try Rich HTML.'); }
    };
    panel.querySelector('#btn-rich-html').onclick = async () => {
      const pages = getScribdPages();
      if (!pages.length) return alert('❌ No pages found.');
      try { await exportRichHTML(pages); } catch (e) { console.error(e); alert('Rich HTML export failed.'); }
    };

    return { gear, panel };
  }

  // Preview box
  function createPreview() {
    const preview = document.createElement('div');
    preview.id = 'se-preview';
    if (settings.showPreview) {
      preview.textContent = '[Preview Initialized]\n';
      document.body.appendChild(preview);
    }
    return preview;
  }

  // ---------- BOOT ----------
  applyDarkMode();
  unblurContent();
  const preview = createPreview();
  const { gear } = buildUI();
  makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));

  // Auto-scrape if desired
  if (settings.autoScrape) {
    const pages = getScribdPages();
    if (pages.length && settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
    if (pages.length) scrapePages(pages, preview);
  }
})();