GreasyFork/SleazyFork Install Tracker

Track your own Gf/Sf install counts and get desktop notifications via API.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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.0 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 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: () => {
                alert(`Full Installation Update:\n\n${fullText}`);
            }
        });
    };

    const addCoffeeButton = () => {
        const contributionURL = GM_info.script.header.match(/@contributionURL\s+(.+)/)?.[1] || "";
        if (contributionURL && !document.querySelector('#coffee-btn')) {
            const btn = document.createElement('a');
            btn.id = 'coffee-btn';
            btn.href = contributionURL.trim();
            btn.target = '_blank';
            btn.innerText = '☕ Buy me a coffee';
            btn.className = 'notranslate';
            btn.style = 'margin-left: 20px; padding: 5px 12px; background: #FFDD00; color: #000; border-radius: 20px; text-decoration: none; font-weight: bold; font-size: 13px; border: 1px solid #e6c600; display: inline-block; vertical-align: middle; transition: 0.2s;';

            const target = document.querySelector('#main-header h2') || document.querySelector('.width-constraint h2');
            if (target) target.appendChild(btn);
        }
    };

    window.addEventListener('load', () => {
        addCoffeeButton();
        setTimeout(checkStats, 2000);
    });
})();