Greasyfork Stats

Displays total number of scripts, installs, and version numbers for users on Greasyfork/Sleazyfork.

  1. // ==UserScript==
  2. // @name Greasyfork Stats
  3. // @description Displays total number of scripts, installs, and version numbers for users on Greasyfork/Sleazyfork.
  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/*/users/*
  11. // @match https://greasyfork.org/users/*
  12. // @match https://sleazyfork.org/*/users/*
  13. // @match https://sleazyfork.org/users/*
  14. // @match https://greasyfork.org/*/scripts*
  15. // @match https://greasyfork.org/scripts*
  16. // @match https://sleazyfork.org/*/scripts*
  17. // @match https://sleazyfork.org/scripts*
  18. // @grant none
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict';
  23. const style = document.createElement('style');
  24. style.textContent = `
  25. .badge {
  26. border: 1px solid transparent;
  27. display: inline-block;
  28. font-size: 0.65em;
  29. font-weight: 600;
  30. line-height: 1;
  31. padding: 0.15em 0.3em;
  32. text-align: center;
  33. vertical-align: baseline;
  34. white-space: nowrap;
  35. margin-right: 0.5em;
  36. }
  37. .badge-js {
  38. background-color: #efd81d;
  39. color: #000;
  40. }
  41. .badge-css {
  42. background-color: #254bdd;
  43. color: #fff;
  44. }
  45. .script-version {
  46. color: #000;
  47. }
  48. #user-stats {
  49. color: inherit;
  50. }
  51. body.no-dark-mode-detected #user-stats,
  52. body.no-dark-mode-detected #user-stats .text-content,
  53. body.no-dark-mode-detected #user-stats h3,
  54. body.no-dark-mode-detected #user-stats p,
  55. body.no-dark-mode-detected #user-stats strong {
  56. color: #000 !important;
  57. }
  58. `;
  59. document.head.appendChild(style);
  60. function detectNoDarkModeScript() {
  61. let darkModeRulesRemoved = true;
  62. for (let i = 0; i < document.styleSheets.length; i++) {
  63. try {
  64. const sheet = document.styleSheets[i];
  65. const rules = sheet.cssRules;
  66. if (!rules) continue;
  67. for (let j = 0; j < rules.length; j++) {
  68. const rule = rules[j];
  69. if (rule.type === CSSRule.MEDIA_RULE &&
  70. rule.conditionText.includes('prefers-color-scheme: dark')) {
  71. darkModeRulesRemoved = false;
  72. break;
  73. }
  74. }
  75. if (!darkModeRulesRemoved) break;
  76. } catch (e) {
  77. continue;
  78. }
  79. }
  80. if (darkModeRulesRemoved) {
  81. document.body.classList.add('no-dark-mode-detected');
  82. } else {
  83. document.body.classList.remove('no-dark-mode-detected');
  84. }
  85. }
  86. function isCssScript(h2Element) {
  87. const parentLi = h2Element.closest('li[data-script-language]');
  88. if (parentLi && parentLi.getAttribute('data-script-language') === 'css') {
  89. return true;
  90. }
  91. return h2Element.querySelector('.badge-css') !== null;
  92. }
  93.  
  94. function createBadge(type) {
  95. const badge = document.createElement('span');
  96. badge.className = `badge badge-${type}`;
  97. badge.title = type === 'js' ? 'User script' : 'CSS script';
  98. badge.textContent = type.toUpperCase();
  99. return badge;
  100. }
  101.  
  102. function createVersionSpan(version) {
  103. const versionSpan = document.createElement('span');
  104. versionSpan.textContent = `v${version}`;
  105. versionSpan.classList.add('script-version');
  106. return versionSpan;
  107. }
  108.  
  109. function addBadgeAndVersion(h2Element, version) {
  110. if (!h2Element) return;
  111.  
  112. const scriptLink = h2Element.querySelector('.script-link');
  113. if (!scriptLink) return;
  114.  
  115. const existingVersion = h2Element.querySelector('.script-version');
  116. const existingBadge = h2Element.querySelector('.badge');
  117. if (existingVersion) existingVersion.remove();
  118. if (existingBadge) existingBadge.remove();
  119.  
  120. const badgeType = isCssScript(h2Element) ? 'css' : 'js';
  121. const badge = createBadge(badgeType);
  122. scriptLink.insertAdjacentElement('afterend', badge);
  123. if (version) {
  124. const versionSpan = createVersionSpan(version);
  125. badge.insertAdjacentElement('afterend', versionSpan);
  126. }
  127. }
  128.  
  129. function appendVersionNumbers() {
  130. const listItems = document.querySelectorAll('li[data-script-id]');
  131. if (!listItems || listItems.length === 0) return;
  132. listItems.forEach(listItem => {
  133. const version = listItem.getAttribute('data-script-version');
  134. if (!version) return;
  135. const h2 = listItem.querySelector('h2');
  136. if (h2) {
  137. addBadgeAndVersion(h2, version);
  138. }
  139. });
  140. }
  141.  
  142. function displayVersionNumbers() {
  143. const headings = document.querySelectorAll('h2 a.script-link');
  144. headings.forEach(heading => {
  145. const version = heading.closest('li').getAttribute('data-script-version');
  146. if (version) {
  147. addBadgeAndVersion(heading.parentElement, version);
  148. }
  149. });
  150. }
  151.  
  152. function displayUserStats() {
  153. const parseInstallCount = text => parseInt(text.replace(/,/g, '')) || 0;
  154. const scripts = [...document.querySelectorAll('.script-list-total-installs span')]
  155. .filter(element => !element.textContent.includes('Total installs'));
  156. if (scripts.length === 0) return;
  157. const totalInstalls = scripts.reduce((sum, element) =>
  158. sum + parseInstallCount(element.textContent), 0);
  159. const statsData = {
  160. scriptsCount: scripts.length,
  161. totalInstalls: totalInstalls
  162. };
  163. if (document.getElementById('user-stats')) return;
  164. const statsElement = document.createElement('section');
  165. statsElement.id = 'user-stats';
  166. const userDiscussionsElement = document.getElementById('user-discussions');
  167. if (!userDiscussionsElement) return;
  168. const stylesToCopy = [
  169. 'padding',
  170. 'border',
  171. 'borderRadius',
  172. 'backgroundColor',
  173. 'fontSize',
  174. 'fontFamily',
  175. 'lineHeight'
  176. ];
  177. const computedStyle = window.getComputedStyle(userDiscussionsElement);
  178. stylesToCopy.forEach(property => {
  179. statsElement.style[property] = computedStyle.getPropertyValue(property);
  180. });
  181. const header = document.createElement('header');
  182. const headerStyle = window.getComputedStyle(userDiscussionsElement.querySelector('header'));
  183. header.style.padding = headerStyle.padding;
  184. header.style.borderBottom = headerStyle.borderBottom;
  185. const h3 = document.createElement('h3');
  186. h3.textContent = 'Stats';
  187. const originalH3Style = window.getComputedStyle(userDiscussionsElement.querySelector('h3'));
  188. h3.style.margin = originalH3Style.margin;
  189. h3.style.fontSize = originalH3Style.fontSize;
  190. header.appendChild(h3);
  191. const contentSection = document.createElement('section');
  192. contentSection.className = 'text-content';
  193. const originalContentStyle = window.getComputedStyle(userDiscussionsElement.querySelector('.text-content'));
  194. contentSection.style.padding = originalContentStyle.padding;
  195. const p = document.createElement('p');
  196. p.innerHTML = `This user has <strong>${statsData.scriptsCount}</strong> script${statsData.scriptsCount !== 1 ? 's' : ''} with <strong>${statsData.totalInstalls.toLocaleString()}</strong> total install${statsData.totalInstalls !== 1 ? 's' : ''}.`;
  197. contentSection.appendChild(p);
  198. statsElement.appendChild(header);
  199. statsElement.appendChild(contentSection);
  200. userDiscussionsElement.parentNode.insertBefore(statsElement, userDiscussionsElement.nextSibling);
  201. }
  202.  
  203. function init() {
  204. detectNoDarkModeScript();
  205. const currentPath = window.location.pathname;
  206. if (currentPath.includes('/scripts')) {
  207. appendVersionNumbers();
  208. } else if (currentPath.includes('/users/')) {
  209. displayUserStats();
  210. displayVersionNumbers();
  211. }
  212. }
  213.  
  214. window.addEventListener('load', () => {
  215. init();
  216. setInterval(detectNoDarkModeScript, 1000);
  217. });
  218. let lastUrl = location.href;
  219. const observer = new MutationObserver((mutations) => {
  220. const url = location.href;
  221. if (url !== lastUrl) {
  222. lastUrl = url;
  223. }
  224. const hasRelevantChanges = mutations.some(mutation =>
  225. [...mutation.addedNodes].some(node =>
  226. node.nodeType === 1 &&
  227. (node.matches?.('li[data-script-id]') ||
  228. node.querySelector?.('li[data-script-id]') ||
  229. node.matches?.('h2') ||
  230. node.querySelector?.('h2'))
  231. )
  232. );
  233. if (hasRelevantChanges) {
  234. init();
  235. }
  236. });
  237.  
  238. function startObserver() {
  239. const observeTarget = document.querySelector('#browse-script-list, .script-list');
  240. if (observeTarget) {
  241. observer.observe(observeTarget, {
  242. childList: true,
  243. subtree: true
  244. });
  245. } else {
  246. requestAnimationFrame(startObserver);
  247. }
  248. }
  249.  
  250. startObserver();
  251. init();
  252. })();