Pornolab Preloaded Preview

Preloads and dynamically displays preview images below links. Implements fallback to the next image if the first fails to load. Includes debug mode.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Pornolab Preloaded Preview
// @version      1.7.0
// @description  Preloads and dynamically displays preview images below links. Implements fallback to the next image if the first fails to load. Includes debug mode.
// @author       Ace
// @license      MIT
// @match        *://pornolab.net/forum/tracker*
// @match        *://pornolab.net/forum/viewforum*
// @match        *://pornolab.net/forum/search*
// @icon         https://static.pornolab.net/favicon.ico
// @run-at       document-end
// @grant        none
// @namespace https://greasyfork.org/users/1418199
// ==/UserScript==

(function () {
    "use strict";

    /* ================= CONFIG ================= */
    const DEBUG_MODE = false; // Set to true to see detailed logs in console (F12)
    const MIN_HEIGHT = 51;
    const MAX_CONCURRENT_FETCHES = 4;
    const PREVIEW_HEIGHT = "21rem";
    const LAZY_LOAD_MARGIN = "500px";

    /* ================= LOGGER ================= */
    function log(...args) {
        if (DEBUG_MODE) {
            console.log("[Pornolab Preview]", ...args);
        }
    }

    /* ================= STYLES ================= */
    const style = document.createElement("style");
    style.textContent = `
    .preview-container {
      position: relative;
      width: 100%;
      max-width: 100%;
      height: ${PREVIEW_HEIGHT};
      display: flex; /* Reverted to flex for perfect centering */
      justify-content: center;
      align-items: center;
      overflow: hidden;
      margin-top: 0.5rem;
      background: #111;
      border-radius: 6px;
      font-size: 0.9rem;
      color: #888;
      text-decoration: none !important;
      cursor: pointer;
      isolation: isolate;
      box-sizing: border-box;
      transition: all 0.2s ease;
    }

    .preview-container:hover {
      box-shadow: 0 0 12px rgba(255, 255, 255, 0.15);
    }

    /* --- OWNED TOPIC STYLES --- */
    .preview-container.owned-topic {
        border: 3px solid #4CAF50;
        box-shadow: 0 0 15px rgba(76, 175, 80, 0.25);
    }
    .preview-container.owned-topic:hover {
        box-shadow: 0 0 20px rgba(76, 175, 80, 0.5);
    }

    .owned-badge {
        position: absolute;
        top: 8px;
        right: 8px;
        background: #4CAF50;
        color: #fff;
        padding: 4px 10px;
        border-radius: 4px;
        font-size: 0.8rem;
        font-weight: bold;
        z-index: 10;
        box-shadow: 0 2px 6px rgba(0,0,0,0.6);
        pointer-events: none;
        text-transform: uppercase;
        letter-spacing: 0.5px;
    }

    /* --- CAROUSEL NAVIGATION --- */
    .nav-arrow {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        background: rgba(0, 0, 0, 0.6);
        color: white;
        border: none;
        font-size: 36px;
        cursor: pointer;
        padding: 20px 15px;
        z-index: 10;
        opacity: 0;
        transition: opacity 0.2s, background 0.2s;
        border-radius: 4px;
        user-select: none;
    }
    .preview-container:hover .nav-arrow { opacity: 1; }
    .nav-arrow:hover { background: rgba(0, 0, 0, 0.9); }
    .nav-prev { left: 10px; }
    .nav-next { right: 10px; }

    .img-counter {
        position: absolute;
        bottom: 8px;
        right: 8px;
        background: rgba(0, 0, 0, 0.7);
        color: #fff;
        padding: 3px 8px;
        border-radius: 4px;
        font-size: 0.75rem;
        z-index: 10;
        opacity: 0;
        transition: opacity 0.2s;
        pointer-events: none;
    }
    .preview-container:hover .img-counter { opacity: 1; }

    /* TEXT PLACEHOLDER FOR NO IMAGES */
    .preview-container .no-img-text {
        position: relative;
    }

    /* THE BLURRED BACKGROUND */
    .preview-container img.bg-blur {
      position: absolute;
      top: -10%; left: -10%;
      width: 120%; height: 120%;
      object-fit: cover;
      filter: blur(15px);
      opacity: 0.4;
      z-index: -1;
      pointer-events: none;
    }

    /* THE MAIN FOREGROUND IMAGE */
    .preview-container img.main-img {
      position: relative;
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
      z-index: 1;
      border-radius: 4px;
      box-shadow: 0 4px 10px rgba(0,0,0,0.5);
    }
  `;
    document.head.appendChild(style);

    /* ================= INDEXED DB LOGIC ================= */
    let dbCache = null;

    async function getDb() {
        if (dbCache) return dbCache;
        return new Promise((resolve, reject) => {
            const request = indexedDB.open("pornolab_torrents");
            request.onsuccess = (event) => {
                dbCache = event.target.result;
                resolve(dbCache);
            };
            request.onerror = (event) => reject(event.target.error);
        });
    }

    async function checkTopicInDb(topicId) {
        if (!topicId || isNaN(topicId)) return false;
        try {
            const db = await getDb();
            if (!db.objectStoreNames.contains("torrent_ids")) return false;
            return new Promise((resolve) => {
                const transaction = db.transaction(["torrent_ids"], "readonly");
                const store = transaction.objectStore("torrent_ids");
                const reqNum = store.get(topicId);
                reqNum.onsuccess = (event) => {
                    if (event.target.result !== undefined) {
                        resolve(true);
                    } else {
                        const reqStr = store.get(topicId.toString());
                        reqStr.onsuccess = (e) => resolve(e.target.result !== undefined);
                        reqStr.onerror = () => resolve(false);
                    }
                };
                reqNum.onerror = () => resolve(false);
            });
        } catch (e) {
            return false;
        }
    }

    function extractTopicId(url) {
        try {
            const urlObj = new URL(url);
            const tParam = urlObj.searchParams.get("t");
            return tParam ? parseInt(tParam, 10) : null;
        } catch (e) {
            return null;
        }
    }

    /* ================= CACHE & NETWORK ================= */
    const previewCache = new Map();

    function buildUrl(link) {
        return new URL(link.getAttribute("href"), location.href).href;
    }

    function fetchPreviewUrls(url) {
        if (!previewCache.has(url)) {
            log(`[Network] Fetching HTML for: ${url}`);
            const fetchPromise = (async () => {
                const res = await fetch(url);
                if (!res.ok) throw new Error(`HTTP ${res.status}`);
                const html = await res.text();

                // REGEX PARSING
                const urls = [];
                const tagRegex = /<[^>]+class=(["'])[^>]*\bpostImg\b[^>]*\1[^>]*>/gi;
                let match;

                while ((match = tagRegex.exec(html)) !== null) {
                    const titleMatch = match[0].match(/title=(["'])(.*?)\1/i);
                    if (titleMatch && titleMatch[2]) {
                        const extractedUrl = titleMatch[2].replace(/&amp;/g, '&');

                        // Prevent pushing JS regex templates like "$1" found in <script> blocks
                        if (!extractedUrl.startsWith("$")) {
                            urls.push(extractedUrl);
                        }
                    }
                }

                log(`[Network] Successfully parsed ${urls.length} image URLs for ${url}`);
                return urls;
            })().catch(err => {
                log(`[Network] Fetch failed for ${url}`, err);
                return [];
            });
            previewCache.set(url, fetchPromise);
        }
        return previewCache.get(url);
    }

    /* ================= DOM INJECTION & CAROUSEL ================= */
    function insertPreview(link, urls, isOwned, topicId) {
        const container = document.createElement("a");
        container.className = "preview-container";
        container.href = link.href;
        container.target = "_blank";
        container.rel = "noopener noreferrer";

        if (isOwned) {
            container.classList.add("owned-topic");
            const badge = document.createElement("div");
            badge.className = "owned-badge";
            badge.textContent = "OWNED";
            container.appendChild(badge);
        }

        if (!urls || urls.length === 0) {
            const span = document.createElement("span");
            span.className = "no-img-text";
            span.textContent = "No Eligible preview found";
            container.appendChild(span);
            link.after(container);
            return;
        }

        let validUrls = [...urls];
        let currentIndex = 0;

        const bgImg = document.createElement("img");
        bgImg.className = "bg-blur";
        //bgImg.loading = "lazy";

        const mainImg = document.createElement("img");
        mainImg.className = "main-img";
        //mainImg.loading = "lazy";

        const counter = document.createElement("div");
        counter.className = "img-counter";

        const updateView = (index) => {
            if (validUrls.length === 0) return;
            currentIndex = (index + validUrls.length) % validUrls.length;

            bgImg.src = validUrls[currentIndex];
            mainImg.src = validUrls[currentIndex];

            if (validUrls.length > 1) {
                counter.textContent = `${currentIndex + 1} / ${validUrls.length}`;
                counter.style.display = "block";
            } else {
                counter.style.display = "none";
            }
        };

        const prevBtn = document.createElement("button");
        prevBtn.className = "nav-arrow nav-prev";
        prevBtn.innerHTML = "&#10094;";

        const nextBtn = document.createElement("button");
        nextBtn.className = "nav-arrow nav-next";
        nextBtn.innerHTML = "&#10095;";

        const handleNavClick = (e, direction) => {
            e.preventDefault();
            e.stopPropagation();
            updateView(currentIndex + direction);
        };

        prevBtn.onclick = (e) => handleNavClick(e, -1);
        nextBtn.onclick = (e) => handleNavClick(e, 1);

        const handleImageError = () => {
            validUrls.splice(currentIndex, 1);

            if (validUrls.length > 0) {
                updateView(currentIndex >= validUrls.length ? 0 : currentIndex);
            } else {
                const span = document.createElement("span");
                span.className = "no-img-text";
                span.textContent = "No Eligible preview found";
                mainImg.replaceWith(span);
                bgImg.remove();
                prevBtn.remove();
                nextBtn.remove();
                counter.remove();
            }

            if (validUrls.length <= 1) {
                prevBtn.style.display = 'none';
                nextBtn.style.display = 'none';
            }
        };

        const handleImageLoad = () => {
            if (mainImg.naturalHeight < MIN_HEIGHT) handleImageError();
        };

        mainImg.addEventListener("error", handleImageError);
        mainImg.addEventListener("load", handleImageLoad);

        container.appendChild(bgImg);
        container.appendChild(mainImg);
        container.appendChild(counter);

        if (urls.length > 1) {
            container.appendChild(prevBtn);
            container.appendChild(nextBtn);
        }

        updateView(0);
        link.after(container);
    }

    /* ================= LAZY QUEUE PROCESSING ================= */
    const queue = [];
    let activeFetches = 0;

    async function processQueue() {
        if (activeFetches >= MAX_CONCURRENT_FETCHES || queue.length === 0) return;
        const link = queue.shift();
        activeFetches++;

        const url = buildUrl(link);
        const topicId = extractTopicId(url);

        try {
            const [urls, isOwned] = await Promise.all([
                fetchPreviewUrls(url),
                checkTopicInDb(topicId)
            ]);
            insertPreview(link, urls, isOwned, topicId);
        } catch (err) {
            const isOwned = await checkTopicInDb(topicId);
            insertPreview(link, [], isOwned, topicId);
        } finally {
            activeFetches--;
            processQueue();
        }
    }

    /* ================= INTERSECTION OBSERVER ================= */
    const observer = new IntersectionObserver((entries, obs) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const link = entry.target;
                obs.unobserve(link);
                queue.push(link);
                processQueue();
            }
        });
    }, { rootMargin: LAZY_LOAD_MARGIN });

    const links = document.querySelectorAll(".tLink, .tt-text");
    links.forEach(link => observer.observe(link));

})();