GreasyFork/SleazyFork Install Tracker

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

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

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.

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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            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);
    });
})();