GreasyFork/SleazyFork Install Tracker

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();