Greasy Fork is available in English.

GreasyFork/SleazyFork Install Tracker

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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