Greasy Fork User Statistics+

shows user statistics as total installs, total scripts etc.

目前为 2022-11-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork User Statistics+
  3. // @namespace -
  4. // @version 1.3.1
  5. // @description shows user statistics as total installs, total scripts etc.
  6. // @author NotYou
  7. // @include *greasyfork.org/*/users/*
  8. // @include *sleazyfork.org/*/users/*
  9. // @license GPL-3.0-or-later
  10. // @run-at document-end
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. let translations = {
  16. 'ar': {
  17. stats: 'إحصائيات المستخدم',
  18. works: 'يعمل المستخدم',
  19. },
  20. 'bg': {
  21. stats: 'Потребителска статистика',
  22. works: 'Потребителят работи',
  23. },
  24. 'cs': {
  25. stats: 'Statistiky uživatelů',
  26. works: 'Uživatel pracuje',
  27. },
  28. 'da': {
  29. stats: 'Brugerstatistik',
  30. works: 'Brugeren fungerer',
  31. },
  32. 'de': {
  33. stats: 'Benutzerstatistiken',
  34. works: 'Benutzer funktioniert',
  35. },
  36. 'el': {
  37. stats: 'Στατιστικά στοιχεία χρηστών',
  38. works: 'Ο χρήστης λειτουργεί',
  39. },
  40. 'en': {
  41. stats: 'User statistics',
  42. works: 'User works',
  43. },
  44. 'eo': {
  45. stats: 'Statistiko de uzantoj',
  46. works: 'Uzanto funkcias',
  47. },
  48. 'es': {
  49. stats: 'Estadísticas de usuario',
  50. works: 'El usuario trabaja',
  51. },
  52. 'fi': {
  53. stats: 'Käyttäjätilastot',
  54. works: 'Käyttäjä toimii',
  55. },
  56. 'fr': {
  57. stats: 'Statistiques d\'utilisateurs',
  58. works: 'L\'utilisateur travaille',
  59. },
  60. 'he': {
  61. stats: 'סטטיסטיקות משתמשים',
  62. works: 'משתמש עובד',
  63. },
  64. 'hu': {
  65. stats: 'Felhasználói statisztikák',
  66. works: 'Felhasználó működik',
  67. },
  68. 'id': {
  69. stats: 'Statistik pengguna',
  70. works: 'Pengguna bekerja',
  71. },
  72. 'it': {
  73. stats: 'Statistiche utente',
  74. works: 'L\'utente lavora',
  75. },
  76. 'ja': {
  77. stats: 'ユーザー統計',
  78. works: 'ユーザーは動作します',
  79. },
  80. 'ko': {
  81. stats: '사용자 통계',
  82. works: '사용자 작품',
  83. },
  84. 'ne': {
  85. stats: 'Gebruikersstatistieken',
  86. works: 'Gebruiker werkt',
  87. },
  88. 'pl': {
  89. stats: 'Statystyki użytkowników',
  90. works: 'Użytkownik pracuje',
  91. },
  92. 'ro': {
  93. stats: 'Statistici utilizatori',
  94. works: 'Utilizatorul lucrează',
  95. },
  96. 'ru': {
  97. stats: 'Статистика пользователей',
  98. works: 'Пользовательские работы',
  99. },
  100. 'tr': {
  101. stats: 'Kullanıcı istatistikleri',
  102. works: 'Kullanıcı işleri',
  103. },
  104. 'uk': {
  105. stats: 'Статистика користувачів',
  106. works: 'Користувач працює',
  107. },
  108. 'vi': {
  109. stats: 'Thống kê người dùng',
  110. works: 'Người dùng hoạt động',
  111. },
  112. 'zh-CN': {
  113. stats: '用户统计',
  114. works: '用户作品',
  115. },
  116. 'zh-TW': {
  117. stats: '用戶統計',
  118. works: '用戶作品',
  119. },
  120. }
  121.  
  122. let currentTranslation = translations[document.querySelector('#language-selector-locale').value] || translations.en
  123.  
  124. let data = new Proxy({
  125. total: 0,
  126. daily: 0,
  127. scripts: 0,
  128. styles: 0,
  129. libraries: 0,
  130.  
  131. stats: 0,
  132. works: 0,
  133. }, {
  134. set(target, prop, value) {
  135. let t = target
  136.  
  137. t[prop] = value
  138.  
  139. t.stats = t.total + t.daily
  140. t.works = t.scripts + t.styles + t.libraries
  141. }
  142. })
  143.  
  144. let stats = document.createElement('div')
  145.  
  146. let isCitrusGF = Boolean(document.querySelector('#script-table'))
  147.  
  148. stats.id = 'user-statistics'
  149.  
  150. addStyle({
  151. '#user-statistics': {
  152. position: 'relative',
  153. },
  154.  
  155. '.statistics-bar': {
  156. width: 'calc(100% - 2.4vw)',
  157. margin: '1em',
  158. marginBottom: '1.5em',
  159. },
  160.  
  161. '.statistics-bar div': {
  162. height: '3px',
  163. borderRadius: '20px',
  164. padding: '3px',
  165. position: 'relative',
  166. },
  167.  
  168. '.statistics-bar div[style*=" 0%"]': {
  169. padding: '0',
  170. },
  171.  
  172. '.statistics-bar div[style*=" 0%"] + span': {
  173. color: 'unset !important',
  174. },
  175.  
  176. '#user-statistics-pin-btn': {
  177. width: '25px',
  178. height: '25px',
  179. backgroundColor: 'rgb(191, 191, 191)',
  180. display: 'block',
  181. position: 'absolute',
  182. right: '10px',
  183. top: '10px',
  184. borderRadius: '50%',
  185. cursor: 'pointer',
  186. backgroundImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACDElEQVR42mNkoDFgJFejk6igUIWGfKrb4QudNLGgWE3Ou0RNbvPlj1+qgZa0U92C+SaaBe7iQn0g9pVPX3FaQpYFDVqKNmmKUtsYGRl5Qfz/QHDp45cajyMX2yi2AGR4upL0NqCZPEALGGEWvPn2g+HE2/fVaZfutZNtAczlQCYPsjjI8Lffv4PZt7/9QLGEaAvQXQ5yNUg/1PD/yL5BtoQoC4CGWwMN3w5k8oIMgFkANJzx3Y8fDDAxmAUg5q2v38uAlvRgtaBdR9nVSpg/XICVRQLozO9i7Kzu+IIFCbzf++Zj5d1vP+9c//Lt1IVP375gtcBBVEB0vonWXg5mJl1k1yFFKCN6sIAMn/f4tevip6/PERXJMEvYmRh1iHH5vMevgIa/OYcugTcO0HzCgByhSHHxDpfhREVynreHXqEo9/nPDx8wIUcovmAh2gI7ByfJ6vrGfc/v3VYTWTX3H8+nDyzEupygBTDDHz17qnb/0YOLV1cujcpi/7OKjRESJy9+/l4Wef52DKEQYCTG8JXz57vcvXXznSk/t2iLhhww4pl0X/z8tTbi3O0Qki3AZThMHmbJw28/D6ZdvpdLkgWEDEe2xFiAR3PGw5eHSLJg3ZbtG99+/OiDz3BSAdwCY1Mz/oyCorvXblzbtWnlyhxqGI5igZyCAg8w/f17fP/+N2oYjDWIaAFobgEA6ol7KD65m7AAAAAASUVORK5CYII=)',
  187. backgroundSize: '80% 80%',
  188. backgroundRepeat: 'no-repeat',
  189. backgroundPosition: '40% 40%',
  190. filter: 'grayscale(1)',
  191. },
  192.  
  193. '#user-statistics.stats-pinned': {
  194. position: 'fixed',
  195. zIndex: '100',
  196. right: '10px',
  197. top: 'calc(50vh - 175px)',
  198. borderRadius: '4px',
  199. width: '400px',
  200. height: '350px',
  201. padding: '4px',
  202. border: '1px solid rgb(0, 0 ,0)',
  203. },
  204.  
  205. '#user-statistics.stats-pinned #user-statistics-pin-btn': {
  206. /* GPL-3.0 @Saki */
  207. backgroundImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABqUlEQVR42u2USUvDUBSF8zIYEzqXlnbR/gYHdCe4VRyg0ILiwuF3OSxEsYWCInUruFMcfkO7KZ2blrRmIJ4XWsnCgVREhDwINDd957vvnJsQ5pcX8QAegIlEIpymaTP9fv/RWff5fAuiKD43m01jYkA8Hud1XT8MhUJZRVFyELseQddQy7fb7SIge9VqVXcNCAaDPMuyR6lUaofneQLAoNPpZC3LYsLhcCEQCEimaVrlcvmMELLbarUMVwBJkuYSicQdOpUgQCBs9Xo9DT+pPVP2ZixAB7VabRkW3ru2CJ2uRqNR2q1MAWPQ+DlEh7BtC91f/iTkFVxFv98vOgEQ15BBrtFoXE0cMl04wQYAeQpw1pHJEPZkALiZGADhTQDOIS59ZBEyGcCeLK6SawBCXkwmk7c0ZIfgK9VHJu+ngU1qvV5fgmVPrgAQ5tHwSTqd3uY4zh5T2i3dM7JsGmPKVCqVU/xv3/WY0hWLxQTDMI4xTRn4nYOI/aLhfh0NXKBWEAThAGP66dv8bcj0JPhUzKqq+uCsy7I8D/GXbrdrfrX/7z92HuD/A94AoxXXGeB8ZfgAAAAASUVORK5CYII=)',
  208. },
  209.  
  210. '#user-statistics.stats-pinned .statistics-bar': {
  211. margin: '.5em',
  212. },
  213.  
  214. '.statistics-bar div::before': {
  215. content: '""',
  216. position: 'absolute',
  217. width: 'calc(100% - 2px)',
  218. height: '7px',
  219. margin: '-5px -5px',
  220. borderRadius: '20px',
  221. boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.3), 0 0 4px 0 rgba(0, 0, 0, 0.3) inset',
  222. border: '1px solid rgb(34, 34, 34)',
  223. padding: '2px',
  224. },
  225.  
  226. '#user-statistics-pin-btn:only-child': {
  227. display: 'none',
  228. }
  229. })
  230.  
  231. let pinBtn = document.createElement('div')
  232. pinBtn.id = 'user-statistics-pin-btn'
  233. pinBtn.addEventListener('click', () => {
  234. let styles = window.getComputedStyle(document.body)
  235.  
  236.  
  237. if(stats.classList.contains('stats-pinned')) {
  238. stats.style.cssText = ''
  239. } else {
  240. stats.style.backgroundColor = styles.backgroundColor
  241. stats.style.color = styles.color
  242. }
  243. stats.classList.toggle('stats-pinned')
  244. })
  245. stats.appendChild(pinBtn)
  246.  
  247. document.querySelectorAll('.script-list > li, #script-table tbody tr').forEach(e => {
  248. let dataset = e.dataset
  249.  
  250. if(isCitrusGF && Object.keys(dataset).length === 0) {
  251. data.daily += +e.querySelector(':nth-child(4)').textContent
  252. data.total += +e.querySelector(':nth-child(5)').textContent
  253.  
  254. data.scripts++
  255. } else {
  256. data.total += +dataset.scriptTotalInstalls
  257. data.daily += +dataset.scriptDailyInstalls
  258.  
  259. if(dataset.scriptType === 'library') {
  260. data.libraries++
  261. } else {
  262. data[dataset.scriptLanguage === 'js' ? 'scripts' : 'styles']++
  263. }
  264. }
  265. })
  266.  
  267. createStat({
  268. title: 'stats',
  269. values: [
  270. 'total',
  271. 'daily',
  272. ]
  273. })
  274.  
  275. createStat({
  276. title: 'works',
  277. values: [
  278. 'scripts',
  279. 'styles',
  280. 'libraries',
  281. ]
  282. })
  283.  
  284. function createStat(input) {
  285. let { values, title } = input
  286. let titleEl = document.createElement('h3')
  287.  
  288. if(data[title] > 0) {
  289. titleEl.textContent = currentTranslation[title]
  290. stats.appendChild(titleEl)
  291.  
  292. values.forEach(e => {
  293. let value = data[e]
  294. let total = data[title]
  295. let width = value / total * 100
  296.  
  297. if(width > 0) {
  298. createBar(width, e, value)
  299. }
  300. })
  301. }
  302. }
  303.  
  304. function createBar(width, name, value) {
  305. let bar = document.createElement('div')
  306. let barActual = document.createElement('div')
  307. let text = document.createElement('span')
  308. let bg = '128, 128, 128'
  309.  
  310. bar.className = 'statistics-bar'
  311.  
  312. switch (name) {
  313. case 'total':
  314. bg = '255, 28, 28'
  315. break
  316. case 'daily':
  317. bg = '255, 58, 58'
  318. break
  319. case 'styles':
  320. bg = '50, 149, 208'
  321. break
  322. case 'scripts':
  323. bg = '236, 203, 27'
  324. break
  325. case 'libraries':
  326. bg = '221, 102, 15'
  327. break
  328. }
  329. barActual.style.width = width + '%'
  330. barActual.style.backgroundColor = 'rgba(' + bg + ', .7)'
  331.  
  332. text.textContent = capitalize(name) + ` (${value.toLocaleString()})`
  333.  
  334. bar.appendChild(text)
  335. bar.appendChild(barActual)
  336. stats.appendChild(bar)
  337. }
  338.  
  339. function addStyle(css) {
  340. let keys = Object.keys(css)
  341. let cssActual = ''
  342. let style = document.createElement('style')
  343.  
  344. keys.forEach(e => {
  345. let _keys = Object.keys(css[e])
  346.  
  347. cssActual += e + '{'
  348.  
  349. _keys.forEach(r => {
  350. cssActual += r.replace(/[A-Z]/, m => `-${m.toLowerCase()}`) + ':' + css[e][r] + ';'
  351. })
  352.  
  353. cssActual += '}'
  354. })
  355.  
  356. style.textContent = cssActual
  357. document.querySelector('head').appendChild(style)
  358. }
  359.  
  360. function capitalize(str) {
  361. return str[0].toUpperCase() + str.slice(1)
  362. }
  363.  
  364. document.querySelector('#about-user').appendChild(stats)
  365. })()
  366.  
  367.  
  368.  
  369.  
  370.  
  371.  
  372.  
  373.  
  374.  
  375.  
  376.  
  377.  
  378.  
  379.  
  380.