GreasyFork/SleazyFork Tracker di Installazioni

Tieni traccia dei tuoi conteggi di installazione di Gf/Sf tramite API e ricevi notifiche desktop.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name            GreasyFork/SleazyFork Install Tracker
// @name:de         GreasyFork/SleazyFork Installations-Tracker
// @name:fr         GreasyFork/SleazyFork Install Tracker
// @name:es         GreasyFork/SleazyFork Rastreador de Instalaciones
// @name:it         GreasyFork/SleazyFork Tracker di Installazioni
// @name:ru         GreasyFork/SleazyFork Трекер установок
// @name:zh-CN      GreasyFork/SleazyFork 安装 Tracker
//
// @description     Track your own Gf/Sf install counts and get desktop notifications via API.
// @description:de  Behalte deine eigenen Gf/Sf-Installationszahlen via API im Blick und erhalte Desktop-Benachrichtigungen.
// @description:fr  Suivez vos propres compteurs d'installation Gf/Sf via API et recevez des notifications de bureau.
// @description:es  Siga sus propios recuentos de instalación de Gf/Sf a través de la API und reciba notificaciones.
// @description:it  Tieni traccia dei tuoi conteggi di installazione di Gf/Sf tramite API e ricevi notifiche desktop.
// @description:ru  Отслеживайте собственные счетчики установок Gf/Sf через API und получайте уведомления.
// @description:zh-CN 通过 API 跟踪您 eigenen Gf/Sf 安装计数 und 获取桌面通知.
//
// @version         0.1.3 beta
// @author          Wack.3gp (https://greasyfork.org/users/4792)
// @copyright       2026+, Wack.3gp
// @namespace       https://greasyfork.org/users/4792
// @license         CC BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
// @icon            https://greasyfork.org/vite/assets/blacklogo16-DftkYuVe.png
//
// @match           https://greasyfork.org/*users/*
// @match           https://sleazyfork.org/*users/*
//
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_notification
// @grant           GM_xmlhttpRequest
// @connect         api.greasyfork.org
// @connect         api.sleazyfork.org
//
// @supportURL      https://greasyfork.org/scripts/575950/feedback
// @compatible      Chrome tested with Tampermonkey
// @contributionURL https://www.paypal.com/donate?hosted_button_id=BYW9D395KJWZ2
// @contributionAmount €1.00
// ==/UserScript==

/* jshint esversion: 11 */

(function() {
    'use strict';

    const isSleazy = window.location.hostname.includes('sleazyfork.org');
    const contributionURL = "https://www.paypal.com/donate?hosted_button_id=BYW9D395KJWZ2";

    const notificationIcon = isSleazy ?
        'https://sleazyfork.org/vite/assets/blacklogo96-CxYTSM_T.png' :
        'https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png';

    const checkStats = async () => {
        const userLink = document.querySelector('#user-control-links .user-profile-link a, .user-profile-link a');

        if (!userLink) {
            console.warn(`[Tracker] No logged-in user detected. Tracker is idle.`);
            return;
        }

        const myId = userLink.getAttribute('href').match(/\/users\/(\d+)/)?.[1];
        if (!window.location.href.includes(`/users/${myId}`)) {
            console.info(`[Tracker] Current page is not the user's own profile. Skipping scan.`);
            return;
        }

        const scriptElements = Array.from(document.querySelectorAll('#user-script-list li[data-script-id], #user-unlisted-script-list li[data-script-id]'));
        if (scriptElements.length === 0) return;

        console.info(`[Tracker] Starting analysis for ${scriptElements.length} scripts...`);

        let totalNew = 0;
        let updateDetails = [];
        let processedCount = 0;

        for (const s of scriptElements) {
            const scriptId = s.getAttribute('data-script-id');
            const scriptName = s.querySelector('.script-link')?.innerText.trim() || "Unknown";
            const scriptUrl = s.querySelector('.script-link')?.href;

            if (isSleazy) {
                try {
                    const response = await fetch(scriptUrl, { method: 'HEAD' });
                    if (response.ok) {
                        fetchStats(scriptId, scriptName, "api.sleazyfork.org");
                    } else {
                        fetchStats(scriptId, scriptName, "api.greasyfork.org");
                    }
                } catch (e) {
                    fetchStats(scriptId, scriptName, "api.greasyfork.org");
                }
            } else {
                fetchStats(scriptId, scriptName, "api.greasyfork.org");
            }
        }

        function fetchStats(scriptId, scriptName, domain) {
            const apiUrl = `https://${domain}/scripts/${scriptId}/stats.json`;

            GM_xmlhttpRequest({
                method: "GET",
                url: apiUrl,
                timeout: 10000,
                onload: function(response) {
                    if (!isSleazy && response.status === 404 && domain === "api.greasyfork.org") {
                        fetchStats(scriptId, scriptName, "api.sleazyfork.org");
                        return;
                    }

                    try {
                        if (response.status !== 200) throw new Error(`HTTP ${response.status}`);

                        const stats = JSON.parse(response.responseText);
                        let currentTotal = 0;
                        for (let date in stats) {
                            if (stats[date] && typeof stats[date].installs === 'number') {
                                currentTotal += stats[date].installs;
                            }
                        }

                        const lastTotal = GM_getValue("api_sum_id_" + scriptId, -1);
                        console.info(`[Tracker] ID: ${scriptId} | ${scriptName} | Source: ${domain} | Total: ${currentTotal}`);

                        if (lastTotal !== -1 && currentTotal > lastTotal) {
                            const diff = currentTotal - lastTotal;
                            totalNew += diff;
                            updateDetails.push(`${scriptName}: +${diff}`);
                        }
                        GM_setValue("api_sum_id_" + scriptId, currentTotal);

                    } catch (e) {
                        console.error(`[Tracker] Error for ID: ${scriptId} (${scriptName}): ${e.message}`);
                    } finally {
                        checkCompletion();
                    }
                },
                onerror: () => { checkCompletion(); },
                ontimeout: () => { checkCompletion(); }
            });
        }

        function checkCompletion() {
            processedCount++;
            if (processedCount === scriptElements.length) {
                if (totalNew > 0) {
                    console.info(`[Tracker] Scan complete. ${totalNew} new installs found.`);
                    showNotification(totalNew, updateDetails);
                } else {
                    console.info(`[Tracker] Scan complete. No new installs found.`);
                }
            }
        }
    };

    const showNotification = (total, details) => {
        const fullText = details.join('\n');
        GM_notification({
            title: `🚀 ${total} New Installs Detected`,
            text: fullText,
            image: notificationIcon,
            silent: true,
            onclick: () => {
                showCentralModal(total, fullText);
            }
        });
    };

    const showCentralModal = (total, detailsText) => {
        const oldOverlay = document.querySelector('#tracker-modal-overlay');
        if (oldOverlay) oldOverlay.remove();

        const overlay = document.createElement('div');
        overlay.id = 'tracker-modal-overlay';
        overlay.style = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.75); display: flex; align-items: center; justify-content: center; z-index: 999999; font-family: sans-serif; backdrop-filter: blur(4px);';
        
        overlay.innerHTML = `
            <div style="background: #fff; width: 95%; max-width: 480px; border-radius: 16px; overflow: hidden; box-shadow: 0 25px 50px rgba(0,0,0,0.5); animation: tracker-pop 0.3s ease-out; position: relative;">
                <button id="tracker-x-btn" style="position: absolute; top: 15px; right: 15px; background: none; border: none; color: #aaa; cursor: pointer; font-size: 24px; line-height: 1;">&times;</button>
                <div style="padding: 20px; background: #f9f9f9; border-bottom: 1px solid #eee; text-align: center;">
                    <h2 style="margin: 0; color: #111; font-size: 22px;">Installation Update</h2>
                </div>
                <div style="padding: 25px; color: #333; line-height: 1.6;">
                    <p style="margin-top: 0; font-weight: bold; font-size: 17px; color: #2d89ef;">You have ${total} new installs!</p>
                    <div style="background: #f4f4f4; padding: 15px; border-radius: 10px; max-height: 160px; overflow-y: auto; font-family: monospace; font-size: 13px; margin-bottom: 20px; white-space: pre-wrap; border: 1px solid #e0e0e0;">${detailsText}</div>
                    <p style="font-size: 14px; text-align: center; color: #666; margin-bottom: 0;">Support my work with a coffee? ☕</p>
                </div>
                <div style="padding: 20px; display: flex; gap: 12px; background: #fff;">
                    <button id="tracker-ok-btn" style="flex: 1; background: #eee; border: none; padding: 12px; border-radius: 10px; cursor: pointer; font-weight: bold; font-size: 15px; color: #444;">OK</button>
                    <a href="${contributionURL}" target="_blank" id="tracker-coffee-btn" style="flex: 2; text-align: center; background: #FFDD00; color: #000; text-decoration: none; padding: 12px; border-radius: 10px; font-weight: bold; font-size: 15px; border: 1px solid #e6c600;">☕ Buy me a coffee</a>
                </div>
            </div>
            <style>
                @keyframes tracker-pop {
                    from { transform: scale(0.9); opacity: 0; }
                    to { transform: scale(1); opacity: 1; }
                }
                #tracker-x-btn:hover { color: #333 !important; }
                #tracker-ok-btn:hover { background: #e2e2e2 !important; }
                #tracker-coffee-btn:hover { background: #f7d000 !important; }
            </style>
        `;

        document.body.appendChild(overlay);

        const close = () => overlay.remove();
        overlay.querySelector('#tracker-x-btn').onclick = close;
        overlay.querySelector('#tracker-ok-btn').onclick = close;
        overlay.querySelector('#tracker-coffee-btn').onclick = close;
        overlay.onclick = (e) => { if (e.target === overlay) close(); };
    };

    setTimeout(checkStats, 2000);
})();