Greasyfork Update Checks Display

Display today's script installations and update checks.

当前为 2025-04-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasyfork Update Checks Display
  3. // @description Display today's script installations and update checks.
  4. // @icon https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png
  5. // @version 1.6
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://greasyfork.org/*
  11. // @match https://sleazyfork.org/*
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @connect api.greasyfork.org
  16. // @connect api.sleazyfork.org
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21. const CACHE_DURATION = 10 * 60 * 1000;
  22. function formatNumber(num) {
  23. return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  24. }
  25. function displayScriptStats() {
  26. document.head.appendChild(Object.assign(document.createElement('style'), {
  27. textContent: '.script-list-installs, .script-list-update-checks { opacity: 1; }'
  28. }));
  29. function addStat(element, label, className) {
  30. const list = element.querySelector('.inline-script-stats');
  31. if (!list) return;
  32. const dt = document.createElement('dt');
  33. const dd = document.createElement('dd');
  34. dt.className = className;
  35. dd.className = className;
  36. dt.textContent = label;
  37. dd.textContent = '...';
  38. list.lastElementChild.parentNode.insertBefore(dt, list.lastElementChild.nextSibling);
  39. dt.after(dd);
  40. return dd;
  41. }
  42. document.querySelectorAll('li[data-script-id]').forEach(script => {
  43. const installsElement = addStat(script, 'Installs', 'script-list-installs');
  44. const checksElement = addStat(script, 'Checks', 'script-list-update-checks');
  45. script.dataset.installsElement = installsElement.id = `installs-${script.dataset.scriptId}`;
  46. script.dataset.checksElement = checksElement.id = `checks-${script.dataset.scriptId}`;
  47. });
  48. }
  49. const collectScriptIds = () =>
  50. Array.from(document.querySelectorAll('li[data-script-id]'))
  51. .map(el => ({
  52. scriptId: el.getAttribute('data-script-id'),
  53. element: el
  54. }));
  55. function getCacheKey(scriptId) {
  56. const domain = window.location.hostname.includes('sleazyfork') ? 'sleazyfork' : 'greasyfork';
  57. return `${domain}_stats_${scriptId}`;
  58. }
  59. function getFromCache(scriptId) {
  60. const cacheKey = getCacheKey(scriptId);
  61. const cachedData = GM_getValue(cacheKey);
  62. if (!cachedData) return null;
  63. const now = Date.now();
  64. if (now - cachedData.timestamp > CACHE_DURATION) {
  65. return null;
  66. }
  67. return cachedData.data;
  68. }
  69. function saveToCache(scriptId, data) {
  70. const cacheKey = getCacheKey(scriptId);
  71. GM_setValue(cacheKey, {
  72. timestamp: Date.now(),
  73. data: data
  74. });
  75. }
  76. function getCurrentLanguage() {
  77. const pathMatch = window.location.pathname.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
  78. if (pathMatch) {
  79. return pathMatch[1];
  80. }
  81. return document.documentElement.lang || 'en';
  82. }
  83. function fetchStats(scriptInfo) {
  84. return new Promise((resolve) => {
  85. const cachedStats = getFromCache(scriptInfo.scriptId);
  86. if (cachedStats) {
  87. resolve(cachedStats);
  88. return;
  89. }
  90. const domain = window.location.hostname.includes('sleazyfork') ? 'sleazyfork.org' : 'greasyfork.org';
  91. const language = getCurrentLanguage();
  92. const apiUrl = `https://api.${domain}/${language}/scripts/${scriptInfo.scriptId}/stats.json`;
  93. GM_xmlhttpRequest({
  94. method: 'GET',
  95. url: apiUrl,
  96. responseType: 'json',
  97. onload: function(response) {
  98. try {
  99. const data = response.response;
  100. if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
  101. resolve({ installs: 0, checks: 0 });
  102. return;
  103. }
  104. const dates = Object.keys(data).sort();
  105. const latestDate = dates[dates.length - 1];
  106. if (!data[latestDate] || typeof data[latestDate] !== 'object') {
  107. resolve({ installs: 0, checks: 0 });
  108. return;
  109. }
  110. const stats = {
  111. installs: data[latestDate].installs || 0,
  112. checks: data[latestDate].update_checks || 0
  113. };
  114. saveToCache(scriptInfo.scriptId, stats);
  115. resolve(stats);
  116. } catch (error) {
  117. console.error(error);
  118. resolve({ installs: 0, checks: 0 });
  119. }
  120. },
  121. onerror: function(error) {
  122. console.error(error);
  123. resolve({ installs: 0, checks: 0 });
  124. }
  125. });
  126. });
  127. }
  128. function updateStats(scriptInfo, stats) {
  129. if (!stats) return;
  130. const element = scriptInfo.element;
  131. const installsElement = document.getElementById(element.dataset.installsElement);
  132. const checksElement = document.getElementById(element.dataset.checksElement);
  133. if (installsElement) installsElement.textContent = formatNumber(stats.installs);
  134. if (checksElement) checksElement.textContent = formatNumber(stats.checks);
  135. }
  136. async function init() {
  137. displayScriptStats();
  138. const scriptInfos = collectScriptIds();
  139. const fetchPromises = scriptInfos.map(async (scriptInfo) => {
  140. try {
  141. const stats = await fetchStats(scriptInfo);
  142. updateStats(scriptInfo, stats);
  143. } catch (error) {
  144. console.error(error);
  145. updateStats(scriptInfo, { installs: 0, checks: 0 });
  146. }
  147. });
  148. await Promise.all(fetchPromises);
  149. }
  150. if (document.readyState === 'loading') {
  151. document.addEventListener('DOMContentLoaded', init);
  152. } else {
  153. init();
  154. }
  155. })();