Track your own Gf/Sf install counts and get desktop notifications via API.
// ==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 installation 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.2 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
// @grant GM_info
// @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==
(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) return;
const myId = userLink.getAttribute('href').match(/\/users\/(\d+)/)?.[1];
if (!window.location.href.includes(`/users/${myId}`)) 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;
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' });
fetchStats(scriptId, scriptName, response.ok ? "api.sleazyfork.org" : "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);
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: ${e.message}`);
} finally {
checkCompletion();
}
},
onerror: checkCompletion,
ontimeout: checkCompletion
});
}
function checkCompletion() {
processedCount++;
if (processedCount === scriptElements.length && totalNew > 0) {
showNotification(totalNew, updateDetails);
}
}
};
const showNotification = (total, details) => {
const fullText = details.join('\n');
GM_notification({
title: `🚀 ${total} New Installs Detected`,
text: `Click to view details!`,
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;">×</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(); };
};
window.addEventListener('load', () => {
setTimeout(checkStats, 2000);
});
})();