Legal Acts Finder — Polished Slide Sidebar

Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.

// ==UserScript==
// @name         Legal Acts Finder — Polished Slide Sidebar
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.
// @author       iamnobody
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @icon         https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTc1LCJwdXIiOiJibG9iX2lkIn19--c218824699773e9e6d58fe11cc76cdbb165a2e65/1000031087.jpg?locale=en
// @banner       https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTczLCJwdXIiOiJibG9iX2lkIn19--77a89502797ffc05cd152a04c877a3b3de4c24be/1000031086.jpg?locale=en
// ==/UserScript==

(function () {
  'use strict';

  /* ---------- Config ---------- */
  const MIN_WIDTH_PX = 250;
  const MAX_WIDTH_PCT = 30; // max width as % of viewport width
  const TRANS_MS = 320;
  const FETCH_CONCURRENCY = 4;
  const OPEN_DELAY_MS = 350; // delay between opening tabs
  const TOGGLE_SHORTCUT = { altKey: true, shiftKey: true, key: 'L' }; // Alt+Shift+L

  /* ---------- Regexes ---------- */
  const actRegex = /(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act,\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+year\s+\d{4}\b)/gi;
  const ruleRegex = /\bsection\s+\w+\s+of\s+\w+\s+act,\s+\d{4}\b/gi;

  /* ---------- Helpers ---------- */
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const escapeHtml = s => String(s).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));

  /* ---------- Extract & Dedupe ---------- */
  function extractUniqueMatches() {
    const text = (document.body.innerText || document.body.textContent || '').replace(/\s+/g, ' ');
    const acts = Array.from(text.matchAll(actRegex)).map(m => (m[0] || '').trim());
    const rules = Array.from(text.matchAll(ruleRegex)).map(m => (m[0] || '').trim());
    const merged = acts.concat(rules);
    const seen = new Map();
    for (const raw of merged) {
      const key = raw.toLowerCase();
      if (!seen.has(key)) seen.set(key, raw);
    }
    return Array.from(seen.values());
  }

  /* ---------- Fetch queue (limited concurrency) ---------- */
  function createQueue(concurrency = FETCH_CONCURRENCY) {
    const q = [];
    let running = 0;
    const runNext = () => {
      if (running >= concurrency || q.length === 0) return;
      const { query, resolve } = q.shift();
      running++;
      fetchPdfForQuery(query).then(res => {
        running--;
        resolve(res);
        runNext();
      }).catch(() => {
        running--;
        resolve({ url: `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`, type: 'search' });
        runNext();
      });
    };
    return query => new Promise(res => { q.push({ query, resolve: res }); runNext(); });
  }

  async function fetchPdfForQuery(query) {
    return new Promise(resolve => {
      const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`;
      try {
        GM_xmlhttpRequest({
          method: 'GET',
          url: googleUrl,
          headers: { 'User-Agent': navigator.userAgent },
          onload(response) {
            const html = response && response.responseText ? response.responseText : '';
            const pdfMatch = html.match(/https?:\/\/[^"'>\s]+?\.pdf\b/gi);
            if (pdfMatch && pdfMatch.length) {
              const pdfUrl = pdfMatch[0].replace(/\\u0026/g, '&');
              resolve({ url: pdfUrl, type: 'pdf' });
            } else {
              resolve({ url: googleUrl, type: 'search' });
            }
          },
          onerror() { resolve({ url: googleUrl, type: 'search' }); },
          timeout: 15000
        });
      } catch (err) {
        resolve({ url: googleUrl, type: 'search' });
      }
    });
  }

  const queuedFetch = createQueue();

  /* ---------- Build UI ---------- */
  function createSidebar(matches) {
    // container
    const container = document.createElement('div');
    container.id = 'la-container';
    container.style.position = 'fixed';
    container.style.top = '50%';
    container.style.right = '0';
    container.style.transform = 'translateY(-50%)';
    container.style.zIndex = '2147483647';
    document.body.appendChild(container);

    // style
    const css = document.createElement('style');
    css.textContent = `
:root{--la-bg:#ffffff;--la-fg:#0b1220;--la-accent:#ff8a00;--la-muted:rgba(11,18,32,0.6);--la-shadow:rgba(12,16,20,0.12)}
@media(prefers-color-scheme:dark){:root{--la-bg:#07101a;--la-fg:#e6eef8;--la-accent:#ffb86b;--la-muted:rgba(230,238,248,0.7);--la-shadow:rgba(0,0,0,0.6)}}
#la-sidebar{position:fixed;right:0;top:50%;transform:translate(100%,-50%);transition:transform ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1),opacity ${TRANS_MS}ms;opacity:0;width:min(${MAX_WIDTH_PCT}vw,100%);max-width:calc(${MAX_WIDTH_PCT}vw);min-width:${MIN_WIDTH_PX}px;border-radius:12px 0 0 12px;box-shadow:0 12px 30px var(--la-shadow);background:linear-gradient(180deg,var(--la-bg),var(--la-bg));color:var(--la-fg);backdrop-filter:blur(6px);display:flex;flex-direction:column;max-height:80vh;overflow:hidden}
#la-sidebar.open{transform:translate(0,-50%);opacity:1}
#la-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid rgba(0,0,0,0.06);cursor:grab}
#la-title{display:flex;gap:8px;align-items:center;font-weight:600;font-size:15px}
.la-dot{width:10px;height:10px;border-radius:50%;background:var(--la-accent);display:inline-block}
#la-controls{display:flex;gap:8px;align-items:center}
#la-openall{background:transparent;border:1px solid rgba(0,0,0,0.08);padding:6px 10px;border-radius:8px;color:var(--la-fg);cursor:pointer;transition:transform 120ms}
#la-openall[disabled]{opacity:0.45;cursor:not-allowed}
#la-close{background:transparent;border:0;font-size:18px;cursor:pointer;padding:6px 8px;border-radius:6px;color:var(--la-fg)}
#la-list{padding:10px;overflow-y:auto;flex:1 1 auto}
.la-item{padding:8px 10px;border-radius:8px;margin-bottom:8px;transition:background 160ms}
.la-item:hover{background:rgba(0,0,0,0.03)}
.la-item a{color:var(--la-accent);text-decoration:none;font-weight:600;display:block}
.la-meta{font-size:12px;color:var(--la-muted);margin-top:6px}
#la-footer{padding:8px 12px;border-top:1px solid rgba(0,0,0,0.05);font-size:12px;color:var(--la-muted);display:flex;justify-content:space-between;align-items:center}
#la-tab{position:absolute;right:0;top:50%;transform:translateY(-50%);width:36px;height:76px;border-radius:8px 0 0 8px;background:var(--la-accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;cursor:pointer;box-shadow:0 8px 20px var(--la-shadow)}
#la-tab:hover{filter:brightness(1.03)}
#la-accordion{padding:10px;border-top:1px solid rgba(0,0,0,0.04)}
#la-accordion-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
#la-accordion-arrow{transition:transform ${TRANS_MS}ms}
#la-accordion-content{overflow:hidden;max-height:0;transition:max-height ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1);padding-top:8px}
.la-pdf-item{padding:8px;border-radius:8px;margin-bottom:8px;background:transparent}
@media(max-width:520px){#la-sidebar{min-width:220px;max-height:70vh}#la-tab{height:64px;width:34px}}`;
    document.head.appendChild(css);

    // sidebar
    const panel = document.createElement('aside');
    panel.id = 'la-sidebar';
    container.appendChild(panel);

    // header
    const header = document.createElement('div');
    header.id = 'la-header';
    panel.appendChild(header);

    const title = document.createElement('div');
    title.id = 'la-title';
    title.innerHTML = `<span class="la-dot" aria-hidden="true"></span><span>Acts & Rules</span>`;
    header.appendChild(title);

    const controls = document.createElement('div');
    controls.id = 'la-controls';
    header.appendChild(controls);

    const openAllBtn = document.createElement('button');
    openAllBtn.id = 'la-openall';
    openAllBtn.textContent = 'Open All PDFs (0)';
    openAllBtn.disabled = true;
    controls.appendChild(openAllBtn);

    const closeBtn = document.createElement('button');
    closeBtn.id = 'la-close';
    closeBtn.title = 'Close';
    closeBtn.textContent = '✕';
    controls.appendChild(closeBtn);

    // list area (all matches)
    const listWrap = document.createElement('div');
    listWrap.id = 'la-list';
    panel.appendChild(listWrap);

    // accordion for individual PDFs
    const accordionWrap = document.createElement('div');
    accordionWrap.id = 'la-accordion';
    panel.appendChild(accordionWrap);

    const accToggle = document.createElement('div');
    accToggle.id = 'la-accordion-toggle';
    accToggle.innerHTML = `<span id="la-accordion-arrow">►</span><span>View individual PDFs</span>`;
    accordionWrap.appendChild(accToggle);

    const accContent = document.createElement('div');
    accContent.id = 'la-accordion-content';
    accordionWrap.appendChild(accContent);

    // footer
    const footer = document.createElement('div');
    footer.id = 'la-footer';
    footer.innerHTML = `<div style="opacity:0.85">Tip: Press Alt+Shift+L to toggle</div><div style="opacity:0.6;font-size:11px">Polite fetch queue</div>`;
    panel.appendChild(footer);

    // collapsed tab
    const tab = document.createElement('div');
    tab.id = 'la-tab';
    tab.textContent = '‹';
    container.appendChild(tab);

    /* ---------- Populate & fetch ---------- */
    const pdfEntries = []; // { query, url }
    const allItems = []; // DOM mapping

    function showNoMatches() {
      listWrap.innerHTML = '';
      const none = document.createElement('div');
      none.className = 'la-item';
      none.innerHTML = `<div class="title">No legal acts or rules detected on this page.</div><div class="la-meta">Try a web search for relevant PDFs: <a href="${`https://www.google.com/search?q=${encodeURIComponent((document.title||location.hostname)+' law act pdf')}`}" target="_blank" rel="noopener noreferrer">Search</a></div>`;
      listWrap.appendChild(none);
    }

    async function processMatches(matches) {
      listWrap.innerHTML = '';
      if (!matches || matches.length === 0) {
        showNoMatches();
        return;
      }

      // placeholder items (loading)
      for (const m of matches) {
        const item = document.createElement('div');
        item.className = 'la-item';
        item.innerHTML = `<div class="title">${escapeHtml(m)}</div><div class="la-meta">Looking up PDF…</div>`;
        listWrap.appendChild(item);
        allItems.push({ query: m, el: item });
      }

      // fetch each via queue
      for (const it of allItems) {
        try {
          const res = await queuedFetch(it.query);
          it.el.innerHTML = '';
          const a = document.createElement('a');
          a.href = res.url;
          a.target = '_blank';
          a.rel = 'noopener noreferrer';
          a.textContent = it.query;
          it.el.appendChild(a);

          const meta = document.createElement('div');
          meta.className = 'la-meta';
          if (res.type === 'pdf') {
            meta.textContent = 'Direct PDF found — opens in new tab';
            // save pdf entry for batch and accordion
            pdfEntries.push({ query: it.query, url: res.url });
          } else {
            meta.innerHTML = `No direct PDF found — opens Google search. Click to refine search.`;
          }
          it.el.appendChild(meta);
        } catch (err) {
          it.el.innerHTML = `<div class="title">${escapeHtml(it.query)}</div><div class="la-meta">Lookup failed — <a href="${`https://www.google.com/search?q=${encodeURIComponent(it.query+' pdf')}`}" target="_blank" rel="noopener noreferrer">Search</a></div>`;
        }
        updatePdfControls();
      }
      buildAccordion();
    }

    function updatePdfControls() {
      const n = pdfEntries.length;
      openAllBtn.textContent = `Open All PDFs (${n})`;
      openAllBtn.disabled = n === 0;
    }

    function buildAccordion() {
      accContent.innerHTML = '';
      if (pdfEntries.length === 0) {
        const hint = document.createElement('div');
        hint.className = 'la-meta';
        hint.style.padding = '6px 0';
        hint.textContent = 'No direct PDFs found. Use the Act links above to search.';
        accContent.appendChild(hint);
        return;
      }
      for (const p of pdfEntries) {
        const row = document.createElement('div');
        row.className = 'la-pdf-item';
        const a = document.createElement('a');
        a.href = p.url;
        a.target = '_blank';
        a.rel = 'noopener noreferrer';
        a.textContent = p.query;
        row.appendChild(a);
        accContent.appendChild(row);
      }
    }

    /* ---------- Toggle, Drag, Accordion, Keyboard ---------- */
    let isOpen = false;
    let dragging = false;
    let dragStartY = 0;
    let currentCenterY = null; // px

    function computeWidth() {
      const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
      const px = Math.max(MIN_WIDTH_PX, Math.floor((MAX_WIDTH_PCT / 100) * vw));
      panel.style.width = Math.min(px + 'px', '100%');
    }
    computeWidth();
    window.addEventListener('resize', computeWidth);

    function openPanel() {
      isOpen = true;
      panel.classList.add('open');
      tab.style.display = 'none';
      // position panel; if we have a custom center use it, else center
      if (currentCenterY !== null) {
        container.style.top = currentCenterY + 'px';
        container.style.transform = 'none';
        panel.style.transform = 'translate(0, -50%)';
      } else {
        container.style.top = '50%';
        container.style.transform = 'translateY(-50%)';
        panel.style.transform = 'translate(0, -50%)';
      }
    }
    function closePanel() {
      isOpen = false;
      panel.classList.remove('open');
      tab.style.display = 'flex';
      currentCenterY = null;
      container.style.top = '50%';
      container.style.transform = 'translateY(-50%)';
      panel.style.transform = 'translate(100%,-50%)';
    }

    tab.addEventListener('click', e => { e.stopPropagation(); openPanel(); });
    closeBtn.addEventListener('click', e => { e.stopPropagation(); closePanel(); });

    // drag only when open
    header.addEventListener('mousedown', e => {
      if (!isOpen) return;
      dragging = true;
      dragStartY = e.clientY;
      const rect = container.getBoundingClientRect();
      currentCenterY = rect.top + rect.height / 2;
      container.style.transform = 'none';
      container.style.top = currentCenterY + 'px';
      header.style.cursor = 'grabbing';
      document.body.style.userSelect = 'none';
    });
    window.addEventListener('mousemove', e => {
      if (!dragging) return;
      const dy = e.clientY - dragStartY;
      const newCenter = currentCenterY + dy;
      const panelRect = panel.getBoundingClientRect();
      const half = panelRect.height / 2;
      const min = half + 8;
      const max = window.innerHeight - half - 8;
      const clamped = clamp(newCenter, min, max);
      currentCenterY = clamped;
      container.style.top = clamped + 'px';
      dragStartY = e.clientY;
    });
    window.addEventListener('mouseup', () => {
      if (!dragging) return;
      dragging = false;
      header.style.cursor = 'grab';
      document.body.style.userSelect = '';
    });

    // accordion toggle
    let accOpen = false;
    const accArrow = document.getElementById ? null : null; // placeholder to avoid lint warnings
    accToggle.addEventListener('click', () => {
      accOpen = !accOpen;
      const arrow = accToggle.querySelector('#la-accordion-arrow');
      if (accOpen) {
        arrow.style.transform = 'rotate(90deg)';
        accContent.style.maxHeight = Math.min(accContent.scrollHeight + 40, window.innerHeight * 0.4) + 'px';
      } else {
        arrow.style.transform = 'rotate(0deg)';
        accContent.style.maxHeight = '0';
      }
    });

    // keyboard toggle Alt+Shift+L (ignore when typing in inputs/textareas or contentEditable)
    window.addEventListener('keydown', e => {
      if (!(e.altKey && e.shiftKey && (e.key.toUpperCase() === TOGGLE_SHORTCUT.key))) return;
      const active = document.activeElement;
      const editable = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
      if (editable) return;
      e.preventDefault();
      if (isOpen) closePanel(); else openPanel();
    });

    /* ---------- Batch open and per-PDF open ---------- */
    openAllBtn.addEventListener('click', async () => {
      const n = pdfEntries.length;
      if (!n) return;
      const confirmMsg = `Open ${n} PDF(s) in new tabs? (This will open ${n} tabs)`;
      if (!confirm(confirmMsg)) return;
      for (let i = 0; i < pdfEntries.length; i++) {
        const url = pdfEntries[i].url;
        window.open(url, '_blank', 'noopener');
        // delay to reduce popup-blocker risk
        await new Promise(res => setTimeout(res, OPEN_DELAY_MS));
      }
    });

    // per-PDF items already link to PDFs in accContent; no extra handlers needed.

    return { panel, container, tab, processMatches, openPanel, closePanel, updatePdfControls };
  }

  /* ---------- Init ---------- */
  function init() {
    try {
      const matches = extractUniqueMatches();
      const sidebar = createSidebar(matches);
      sidebar.processMatches(matches);
      // nothing else; user interacts
    } catch (err) {
      console.error('Legal Acts Finder error:', err);
    }
  }

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    setTimeout(init, 400);
  } else {
    window.addEventListener('load', () => setTimeout(init, 300));
  }
  // Instead of firing all requests at once, use a queue with delays
function processActsWithDelay(acts, delayMs = 5000) { // default 5 sec delay
    let i = 0;

    function next() {
        if (i >= acts.length) return;
        let act = acts[i];

        // Call your function that handles one act at a time
        fetchActReference(act, function () {
            // Once finished, move to next with delay
            i++;
            setTimeout(next, delayMs);
        });
    }

    next();
}

// Example fetch function wrapper
function fetchActReference(act, callback) {
    let query = encodeURIComponent(`${act} site:indiacode.nic.in`);
    let url = `https://www.google.com/search?q=${query}`;

    GM_xmlhttpRequest({
        method: "GET",
        url: url,
        onload: function (response) {
            // parse response, add links to sidebar...
            console.log("Processed:", act);

            if (typeof callback === "function") callback();
        },
        onerror: function () {
            console.warn("Failed:", act);
            if (typeof callback === "function") callback();
        }
    });
}
(function() {
    'use strict';

    // Function to create a random delay (300–800 ms)
    function randomDelay() {
        return new Promise(resolve => {
            const delay = Math.floor(Math.random() * 500) + 300; // 300–800 ms
            setTimeout(resolve, delay);
        });
    }

    async function runWithDelay() {
        console.log("Script started...");

        // Example action 1: click a button
        await randomDelay();
        let button = document.querySelector("button");
        if (button) {
            button.click();
            console.log("Clicked button after delay");
        }

        // Example action 2: fill input
        await randomDelay();
        let input = document.querySelector("input[type='text']");
        if (input) {
            input.value = "Hello World";
            console.log("Filled input after delay");
        }

        // Example action 3: press Enter
        await randomDelay();
        let evt = new KeyboardEvent("keydown", { key: "Enter", bubbles: true });
        input?.dispatchEvent(evt);
        console.log("Pressed Enter after delay");
    }

    runWithDelay();
})();
})();