IGG Games Infinite Scroll

Infinite scroll for igg-games

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         IGG Games Infinite Scroll
// @namespace    github.com/GreenMan36
// @version      1.0.2
// @description  Infinite scroll for igg-games
// @author       GreenMan36
// @license      MIT
// @match        https://igg-games.com/
// @icon         https://igg-games.com/wp-content/uploads/2023/08/i96x96.png
// @require      https://cdn.jsdelivr.net/npm/idb@7/build/umd.js
// @grant        GM_registerMenuCommand
// ==/UserScript==

(async function() {
    'use strict';

    // --- CONFIGURATION ---
    const BUFFER_PIXELS = 1000;
    const DB_NAME = 'IGG_Scroll_Cache';
    const STORE_NAME = 'articles';
    const SETTINGS_STORE = 'settings';
    const ITEMS_PER_PAGE = 10;

    // --- THROTTLING CONFIG ---
    let minRequestGap = 1000;
    const BURST_THRESHOLD = 10;
    const BURST_WINDOW = 10000;
    const requestTimestamps = [];

    // --- STATE ---
    let isLoading = false;
    let mode = 'INITIAL_CHECK';
    let currentPage = 1;
    let lastRequestTime = 0;

    // --- DATABASE SETUP ---
    const dbPromise = idb.openDB(DB_NAME, 1, {
        upgrade(db) {
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
                store.createIndex('date', 'date');
            }
            if (!db.objectStoreNames.contains(SETTINGS_STORE)) {
                db.createObjectStore(SETTINGS_STORE);
            }
        },
    });

    // --- MENU COMMANDS ---
    GM_registerMenuCommand("🗑️ Clear Cache & Reset", async () => {
        if (confirm("Delete all cached games and reload the page?")) {
            try {
                const db = await dbPromise;
                db.close();
                await idb.deleteDB(DB_NAME);
                location.reload();
            } catch (e) {
                alert("Error: " + e);
            }
        }
    });

    const TARGET_SELECTOR = '.container-main-post > div:nth-child(2)';

    // --- HELPER: TOAST ---
    const showToast = (msg, isError = false) => {
        let toast = document.getElementById('igg-scroll-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'igg-scroll-toast';
            toast.style = "position:fixed; bottom:20px; right:20px; background:#333; color:#fff; padding:10px 20px; border-radius:5px; z-index:9999; font-family:sans-serif; transition: opacity 0.3s; opacity:0; pointer-events:none;";
            document.body.appendChild(toast);
        }
        toast.textContent = msg;
        toast.style.borderLeft = isError ? "5px solid #ff4444" : "5px solid #44ff44";
        toast.style.opacity = "1";
        setTimeout(() => { toast.style.opacity = "0"; }, 3000);
    };

    const sleep = (ms) => new Promise(res => setTimeout(res, ms));

    async function adaptiveThrottle() {
        const now = Date.now();
        while (requestTimestamps.length > 0 && now - requestTimestamps[0] > BURST_WINDOW) {
            requestTimestamps.shift();
        }
        if (requestTimestamps.length >= BURST_THRESHOLD) {
            if (minRequestGap !== 2000) {
                minRequestGap = 2000;
                showToast("Burst detected! Throttling increased to 2s.", true);
            }
        }
        const timeSinceLast = now - lastRequestTime;
        if (timeSinceLast < minRequestGap) {
            await sleep(minRequestGap - timeSinceLast);
        }
        lastRequestTime = Date.now();
        requestTimestamps.push(lastRequestTime);
    }

    // --- INIT ---
    await initialize();

    window.addEventListener('scroll', () => {
        if (isLoading) return;
        const dist = document.documentElement.scrollHeight - (window.innerHeight + window.scrollY);
        if (dist < BUFFER_PIXELS) loadNextBatch();
    });

    async function initialize() {
        const currentGrid = document.querySelector(TARGET_SELECTOR);
        if (!currentGrid) return;

        const items = Array.from(currentGrid.children);
        const parsedItems = items.map(parseArticleElement).filter(i => i);
        if (parsedItems.length === 0) return;

        // Save current page items to cache
        await saveItemsToDB(parsedItems);

        const db = await dbPromise;
        const lastKnownTopId = await db.get(SETTINGS_STORE, 'latest_seen_id');
        const currentTopId = parsedItems[0].id;

        // Decide Mode
        if (lastKnownTopId === currentTopId) {
            mode = 'CACHE_STREAM';
        } else {
            console.log(`[Cache] Update detected (Old: ${lastKnownTopId} vs New: ${currentTopId}). GAP FILL.`);
            mode = 'NETWORK_GAP_FILL';
            await db.put(SETTINGS_STORE, currentTopId, 'latest_seen_id');
        }
    }

    async function loadNextBatch() {
        isLoading = true;
        try {
            if (mode === 'CACHE_STREAM') await streamFromCache();
            else await fetchFromNetwork();
        } catch (err) {
            console.error(err);
        } finally {
            isLoading = false;
        }
    }

    async function streamFromCache() {
        const articles = document.querySelectorAll('article');
        const lastArticle = articles[articles.length - 1];
        if (!lastArticle) return;

        const lastDateStr = extractDate(lastArticle);
        const db = await dbPromise;
        const index = db.transaction(STORE_NAME).store.index('date');

        // Get older items from DB
        let cursor = await index.openCursor(IDBKeyRange.upperBound(lastDateStr, true), 'prev');

        const nextItems = [];
        while (cursor && nextItems.length < ITEMS_PER_PAGE) {
            // DEDUPLICATION: Check if ID exists in DOM
            if (!document.getElementById(cursor.value.id)) {
                nextItems.push(cursor.value);
            }
            cursor = await cursor.continue();
        }

        if (nextItems.length > 0) {
            appendItemsToDom(nextItems);
        } else {
            mode = 'NETWORK_FALLBACK';
            await fetchFromNetwork();
        }
    }

    async function fetchFromNetwork() {
        await adaptiveThrottle();

        // Calculate next page based on current DOM count if we just switched from Cache
        if (mode === 'NETWORK_FALLBACK') {
             const total = document.querySelectorAll('article').length;
             // Ensure we don't jump backwards. If we have 55 items, we are roughly at end of page 5 or 6.
             // (55 / 10) + 1 = 6.
             currentPage = Math.floor(total / ITEMS_PER_PAGE) + 1;
             mode = 'NETWORK_GAP_FILL'; // Switch state so we don't recalc every time
        } else {
             currentPage++;
        }

        console.log(`[Network] Fetching Page ${currentPage}...`);

        try {
            const response = await fetch(`https://igg-games.com/page/${currentPage}`);
            if (!response.ok) return;

            const text = await response.text();
            const doc = new DOMParser().parseFromString(text, 'text/html');
            const fetchedGrid = doc.querySelector(TARGET_SELECTOR);

            if (fetchedGrid) {
                const rawElements = Array.from(fetchedGrid.children);
                const parsedData = rawElements.map(parseArticleElement).filter(i => i);

                if (parsedData.length > 0) {
                    await saveItemsToDB(parsedData);

                    const currentGrid = document.querySelector(TARGET_SELECTOR);

                    // STRICT DEDUPLICATION LOOP
                    let appendedCount = 0;
                    rawElements.forEach(el => {
                        const article = el.querySelector('article');
                        if (article && !document.getElementById(article.id)) {
                            currentGrid.appendChild(el);
                            appendedCount++;
                        }
                    });

                    // Cleanup
                    const pag = document.querySelector('.uk-pagination');
                    if (pag) pag.remove();

                    if (appendedCount === 0) {
                        console.log("[Network] All items on this page were duplicates. Fetching next...");
                        // Optional: Recursively fetch next page immediately if we got a page of pure dupes
                        // fetchFromNetwork();
                    }
                }
            }
        } catch (err) {
            showToast("Network Error", true);
        }
    }

    // --- HELPERS ---
    function parseArticleElement(div) {
        const article = div.querySelector('article');
        if (!article) return null;
        return { id: article.id, date: extractDate(article), html: div.outerHTML };
    }

    async function saveItemsToDB(items) {
        const db = await dbPromise;
        const tx = db.transaction(STORE_NAME, 'readwrite');
        await Promise.all([...items.map(item => tx.store.put(item)), tx.done]);
    }

    function appendItemsToDom(dbItems) {
        const container = document.querySelector(TARGET_SELECTOR);
        const temp = document.createElement('div');
        dbItems.forEach(item => {
            temp.innerHTML = item.html;
            container.appendChild(temp.firstElementChild);
        });
        const pag = document.querySelector('.uk-pagination');
        if (pag) pag.remove();
    }

    function extractDate(article) {
        const timeTag = article.querySelector('time');
        return timeTag ? timeTag.getAttribute('datetime') : new Date().toISOString();
    }
})();