GreasyFork Installs Notifier

在您自己的用户页面上,如果每个脚本的安装数量超过整数或靓号,我们将通过浏览器通知通知您。

目前为 2020-06-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork Installs Notifier
  3. // @name:ja GreasyFork Installs Notifier
  4. // @name:zh-CN GreasyFork Installs Notifier
  5. // @namespace knoa.jp
  6. // @description It shows browser notification when any of the numbers of installs reached round numbers on your own user page.
  7. // @description:ja ご自身のユーザーページで各スクリプトのインストール数がキリのいい数字を超えたらブラウザ通知でお知らせします。
  8. // @description:zh-CN 在您自己的用户页面上,如果每个脚本的安装数量超过整数或靓号,我们将通过浏览器通知通知您。
  9. // @include https://greasyfork.org/*/users/*
  10. // @version 1
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. const SCRIPTID = 'GreasyForkInstallsNotifier';
  16. const SCRIPTNAME = 'GreasyFork Installs Notifier';
  17. const DEBUG = false;/*
  18. [update]
  19.  
  20. <strong>How to use this script:</strong>
  21. This is a script for developers who registered some scripts on GreasyFork.
  22. It records the numbers of installs of each script and show a browser notification when it reached numbers you specify.
  23. You can customize the numbers by rewriting the THRESHOLDS in the code.
  24. <code>const THRESHOLDS = [100, 1000, 10000, 100000, 1000000];</code>
  25.  
  26. <strong>このスクリプトの使い方:</strong>
  27. これはGreasyForkにスクリプトを登録している開発者向けのスクリプトです。
  28. ご自身のユーザーページで各スクリプトのインストール数を記録し、指定した数字を超えたときに通知します。
  29. コードの中の THRESHOLDS を書き換えることで、通知する数字は自由に決めることができます。
  30. <code>const THRESHOLDS = [100, 1000, 10000, 100000, 1000000];</code>
  31.  
  32. <strong>如何使用此脚本:</strong>
  33. 这是在GreasyFork中注册脚本的开发人员的脚本。
  34. 在您的用户页面上,记录每个脚本的安装数量,并在超过指定数字时通知您。
  35. 通过重写代码中的THRESHOLDS,通知的数字可以自由决定。
  36. <code>const THRESHOLDS = [100, 1000, 10000, 100000, 1000000];</code>
  37.  
  38.  
  39. [bug]
  40.  
  41. [todo]
  42.  
  43. [possible]
  44.  
  45. [research]
  46.  
  47. [memo]
  48. */
  49. if(window === top && console.time) console.time(SCRIPTID);
  50. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  51. const THRESHOLDS = [100, 1000, 10000, 100000, 1000000];
  52. const FLAGNAME = SCRIPTID.toLowerCase();
  53. const site = {
  54. targets: {
  55. userScriptListItems: () => $$('#user-script-list > li'),
  56. },
  57. get: {
  58. scriptName: (li) => li.dataset.scriptName,
  59. totalInstalls: (li) => parseInt(li.dataset.scriptTotalInstalls),
  60. },
  61. is: {
  62. owner: () => ($('#control-panel') !== null),
  63. },
  64. };
  65. let elements = {}, installs;
  66. const core = {
  67. initialize: function(){
  68. elements.html = document.documentElement;
  69. elements.html.classList.add(SCRIPTID);
  70. if(site.is.owner()){
  71. core.ready();
  72. core.addStyle();
  73. }
  74. },
  75. ready: function(){
  76. core.getTargets(site.targets).then(() => {
  77. log("I'm ready.");
  78. Notification.requestPermission();
  79. core.getInstalls();
  80. }).catch(e => {
  81. console.error(`${SCRIPTID}:${e.lineNumber} ${e.name}: ${e.message}`);
  82. });
  83. },
  84. getInstalls: function(){
  85. installs = Storage.read('installs') || {};
  86. let items = elements.userScriptListItems;
  87. Array.from(items).forEach(li => {
  88. let name = site.get.scriptName(li);
  89. let totalInstalls = site.get.totalInstalls(li);
  90. if(THRESHOLDS.some(t => installs[name] < t && t <= totalInstalls)){
  91. let notification = new Notification(SCRIPTNAME, {body: `${totalInstalls} installs: ${name}`});
  92. notification.addEventListener('click', function(e){
  93. notification.close();
  94. });
  95. li.dataset[FLAGNAME] = 'true';
  96. }
  97. installs[name] = totalInstalls;
  98. });
  99. Storage.save('installs', installs);
  100. },
  101. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  102. const key = selector.name;
  103. const get = function(resolve, reject){
  104. let selected = selector();
  105. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  106. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  107. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  108. else return reject(new Error(`Not found: ${selector.name}, I give up.`));
  109. elements[key] = selected;
  110. resolve(selected);
  111. };
  112. return new Promise(function(resolve, reject){
  113. get(resolve, reject);
  114. });
  115. },
  116. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  117. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  118. },
  119. addStyle: function(name = 'style'){
  120. if(html[name] === undefined) return;
  121. let style = createElement(html[name]());
  122. document.head.appendChild(style);
  123. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  124. elements[name] = style;
  125. },
  126. };
  127. const html = {
  128. style: () => `
  129. <style type="text/css" id="${SCRIPTID}-style">
  130. li[data-${FLAGNAME}="true"]{
  131. background: #ffc;
  132. }
  133. </style>
  134. `,
  135. };
  136. const setTimeout = window.setTimeout.bind(window), clearTimeout = window.clearTimeout.bind(window), setInterval = window.setInterval.bind(window), clearInterval = window.clearInterval.bind(window), requestAnimationFrame = window.requestAnimationFrame.bind(window);
  137. const alert = window.alert.bind(window), confirm = window.confirm.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  138. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  139. class Storage{
  140. static key(key){
  141. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  142. }
  143. static save(key, value, expire = null){
  144. key = Storage.key(key);
  145. localStorage[key] = JSON.stringify({
  146. value: value,
  147. saved: Date.now(),
  148. expire: expire,
  149. });
  150. }
  151. static read(key){
  152. key = Storage.key(key);
  153. if(localStorage[key] === undefined) return undefined;
  154. let data = JSON.parse(localStorage[key]);
  155. if(data.value === undefined) return data;
  156. if(data.expire === undefined) return data;
  157. if(data.expire === null) return data.value;
  158. if(data.expire < Date.now()) return localStorage.removeItem(key);/*undefined*/
  159. return data.value;
  160. }
  161. static remove(key){
  162. key = Storage.key(key);
  163. delete localStorage.removeItem(key);
  164. }
  165. static delete(key){
  166. Storage.remove(key);
  167. }
  168. static saved(key){
  169. key = Storage.key(key);
  170. if(localStorage[key] === undefined) return undefined;
  171. let data = JSON.parse(localStorage[key]);
  172. if(data.saved) return data.saved;
  173. else return undefined;
  174. }
  175. }
  176. const $ = function(s, f){
  177. let target = document.querySelector(s);
  178. if(target === null) return null;
  179. return f ? f(target) : target;
  180. };
  181. const $$ = function(s, f){
  182. let targets = document.querySelectorAll(s);
  183. return f ? Array.from(targets).map(t => f(t)) : targets;
  184. };
  185. const log = function(){
  186. if(!DEBUG) return;
  187. let l = log.last = log.now || new Date(), n = log.now = new Date();
  188. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  189. //console.log(error.stack);
  190. console.log(
  191. SCRIPTID + ':',
  192. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  193. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  194. /* :00 */ ':' + line,
  195. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  196. /* caller */ (callers[1] || '') + '()',
  197. ...arguments
  198. );
  199. };
  200. log.formats = [{
  201. name: 'Firefox Scratchpad',
  202. detector: /MARKER@Scratchpad/,
  203. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  204. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  205. }, {
  206. name: 'Firefox Console',
  207. detector: /MARKER@debugger/,
  208. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  209. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  210. }, {
  211. name: 'Firefox Greasemonkey 3',
  212. detector: /\/gm_scripts\//,
  213. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  214. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  215. }, {
  216. name: 'Firefox Greasemonkey 4+',
  217. detector: /MARKER@user-script:/,
  218. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  219. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  220. }, {
  221. name: 'Firefox Tampermonkey',
  222. detector: /MARKER@moz-extension:/,
  223. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  224. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  225. }, {
  226. name: 'Chrome Console',
  227. detector: /at MARKER \(<anonymous>/,
  228. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  229. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  230. }, {
  231. name: 'Chrome Tampermonkey',
  232. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  233. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 4,
  234. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  235. }, {
  236. name: 'Chrome Extension',
  237. detector: /at MARKER \(chrome-extension:/,
  238. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  239. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  240. }, {
  241. name: 'Edge Console',
  242. detector: /at MARKER \(eval/,
  243. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  244. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  245. }, {
  246. name: 'Edge Tampermonkey',
  247. detector: /at MARKER \(Function/,
  248. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  249. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  250. }, {
  251. name: 'Safari',
  252. detector: /^MARKER$/m,
  253. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  254. getCallers: (e) => e.stack.split('\n'),
  255. }, {
  256. name: 'Default',
  257. detector: /./,
  258. getLine: (e) => 0,
  259. getCallers: (e) => [],
  260. }];
  261. log.format = log.formats.find(function MARKER(f){
  262. if(!f.detector.test(new Error().stack)) return false;
  263. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  264. return true;
  265. });
  266. core.initialize();
  267. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  268. })();