Greasy Fork 还支持 简体中文。

GreasyFork User Dashboard

It redesigns your own user page.

目前為 2019-01-27 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork User Dashboard
  3. // @name:ja GreasyFork User Dashboard
  4. // @namespace knoa.jp
  5. // @description It redesigns your own user page.
  6. // @description:ja 自分用の新しいユーザーページを提供します。
  7. // @include https://greasyfork.org/*/users/*
  8. // @version 1.1.0
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function(){
  13. const SCRIPTNAME = 'GreasyForkUserDashboard';
  14. const DEBUG = false;/*
  15. 1.1.0
  16. many updates and fixes.
  17.  
  18. [bug]
  19.  
  20. [to do]
  21.  
  22. [possible]
  23. 3カラムのレイアウト崩れる(スクリプト未使用でも発生する)
  24. */
  25. if(window === top && console.time) console.time(SCRIPTNAME);
  26. const INTERVAL = 1000;/* for fetch */
  27. const DRAWINGDELAY = 125;/* for drawing each charts */
  28. const UPDATELINKTEXT = '+';/* for update link text */
  29. const DEFAULTMAX = 10;/* for chart scale */
  30. const DAYS = 180;/* for chart length */
  31. const STATSUPDATE = 1000*60*60;/* stats update interval of greasyfork.org */
  32. const TRANSLATIONEXPIRE = 1000*60*60*24*30;/* cache time for translations */
  33. let site = {
  34. targets: {
  35. userSection: () => $('body > header + div > section:nth-of-type(1)'),
  36. controlPanel: () => $('#control-panel'),
  37. newScriptSetLink: () => $('a[href$="/sets/new"]'),
  38. scriptSets: () => $('body > header + div > section:nth-of-type(2)'),
  39. scripts: () => $('body > header + div > section:nth-of-type(2) + div'),
  40. userScriptSets: () => $('#user-script-sets'),
  41. userScriptList: () => $('#user-script-list'),
  42. },
  43. get: {
  44. language: (d) => d.documentElement.lang,
  45. firstScript: (list) => list.querySelector('li h2 > a'),
  46. translation: (d, t) => {
  47. t.info = d.querySelector('#script-links > li.current').textContent || t.info;
  48. t.code = d.querySelector('#script-links > li > a[href$="/code"]').textContent || t.code;
  49. t.history = d.querySelector('#script-links > li > a[href$="/versions"]').textContent || t.history;
  50. t.feedback = d.querySelector('#script-links > li > a[href$="/feedback"]').textContent.replace(/\s\(\d+\)/, '') || t.feedback;
  51. t.stats = d.querySelector('#script-links > li > a[href$="/stats"]').textContent || t.stats;
  52. t.derivatives = d.querySelector('#script-links > li > a[href$="/derivatives"]').textContent || t.derivatives;
  53. t.update = d.querySelector('#script-links > li > a[href$="/versions/new"]').textContent || t.update;
  54. t.delete = d.querySelector('#script-links > li > a[href$="/delete"]').textContent || t.delete;
  55. t.admin = d.querySelector('#script-links > li > a[href$="/admin"]').textContent || t.admin;
  56. t.version = d.querySelector('#script-stats > dt.script-show-version').textContent || t.version;
  57. return t;
  58. },
  59. translationOnStats: (d, t) => {
  60. t.installs = d.querySelector('table.stats-table > thead > tr > th:nth-child(2)').textContent || t.installs;
  61. t.updateChecks = d.querySelector('table.stats-table > thead > tr > th:nth-child(3)').textContent || t.updateChecks;
  62. return t;
  63. },
  64. props: (li) => {return {
  65. name: li.querySelector('h2 > a'),
  66. description: li.querySelector('.description'),
  67. stats: li.querySelector('dl.inline-script-stats'),
  68. dailyInstalls: li.querySelector('dd.script-list-daily-installs'),
  69. totalInstalls: li.querySelector('dd.script-list-total-installs'),
  70. ratings: li.querySelector('dd.script-list-ratings'),
  71. createdDate: li.querySelector('dd.script-list-created-date'),
  72. updatedDate: li.querySelector('dd.script-list-updated-date'),
  73. scriptVersion: li.dataset.scriptVersion,
  74. }},
  75. scriptUrl: (li) => li.querySelector('h2 > a').href,
  76. }
  77. };
  78. const DEFAULTTRANSLATION = {
  79. info: 'Info',
  80. code: 'Code',
  81. history: 'History',
  82. feedback: 'Feedback',
  83. stats: 'Stats',
  84. derivatives: 'Derivatives',
  85. update: 'Update',
  86. delete: 'Delete',
  87. admin: 'Admin',
  88. version: 'Version',
  89. installs: 'Installs',
  90. updateChecks: 'Update checks',
  91. };
  92. let translations = {}, translation = {};
  93. let elements = {}, shown = {};
  94. let core = {
  95. initialize: function(){
  96. core.getElements();
  97. if(elements.length < site.targets.length) return log('Not user own page.');
  98. core.addStyle();
  99. core.prepareTranslations();
  100. core.hideUserSection();
  101. core.hideControlPanel();
  102. core.addTabNavigation();
  103. core.addNewScriptSetLink();
  104. core.rebuildScriptList();
  105. core.addChartSwitcher();
  106. },
  107. getElements: function(){
  108. if(!site.targets.controlPanel()) return;/* not my own page */
  109. for(let i = 0, keys = Object.keys(site.targets); keys[i]; i++){
  110. let element = site.targets[keys[i]]();
  111. if(!element) return log(`Not found: ${keys[i]}`);
  112. element.dataset.selector = keys[i];
  113. elements[keys[i]] = element;
  114. }
  115. shown = Storage.read('shown') || shown;
  116. },
  117. prepareTranslations: function(){
  118. let language = site.get.language(document);
  119. translations = Storage.read('translations') || {};
  120. translation = translations[language] || DEFAULTTRANSLATION;
  121. if(!Object.keys(DEFAULTTRANSLATION).every((key) => translation[key])){/* some change in translation keys */
  122. Object.keys(DEFAULTTRANSLATION).forEach((key) => translation[key] = translation[key] || DEFAULTTRANSLATION[key]);
  123. core.getTranslations();
  124. }else{
  125. if(site.get.language(document) === 'en') return;
  126. if(Date.now() < (Storage.saved('translations') || 0) + TRANSLATIONEXPIRE) return;
  127. core.getTranslations();
  128. }
  129. },
  130. getTranslations: function(){
  131. let firstScript = site.get.firstScript(elements.userScriptList);
  132. fetch(firstScript.href, {credentials: 'include'})
  133. .then(response => response.text())
  134. .then(text => new DOMParser().parseFromString(text, 'text/html'))
  135. .then(d => translation = translations[site.get.language(d)] = site.get.translation(d, translation))
  136. .then(() => wait(INTERVAL))
  137. .then(() => fetch(firstScript.href + '/stats'))
  138. .then(response => response.text())
  139. .then(text => new DOMParser().parseFromString(text, 'text/html'))
  140. .then(d => {
  141. translation = translations[site.get.language(d)] = site.get.translationOnStats(d, translation);
  142. Storage.save('translations', translations);
  143. });
  144. },
  145. hideUserSection: function(){
  146. let userSection = elements.userSection, more = createElement(core.html.more());
  147. if(!shown.userSection) userSection.classList.add('hidden');
  148. more.addEventListener('click', function(e){
  149. userSection.classList.toggle('hidden');
  150. shown.userSection = !userSection.classList.contains('hidden');
  151. Storage.save('shown', shown);
  152. });
  153. userSection.appendChild(more);
  154. },
  155. hideControlPanel: function(){
  156. let controlPanel = elements.controlPanel, header = controlPanel.firstElementChild;
  157. if(!shown.controlPanel) controlPanel.classList.add('hidden');
  158. setTimeout(function(){elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px'}, 250);/* needs delay */
  159. header.addEventListener('click', function(e){
  160. controlPanel.classList.toggle('hidden');
  161. shown.controlPanel = !controlPanel.classList.contains('hidden');
  162. Storage.save('shown', shown);
  163. elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
  164. });
  165. },
  166. addTabNavigation: function(){
  167. const keys = [
  168. {label: elements.scriptSets.querySelector('header').textContent, selector: 'scriptSets', list: elements.userScriptSets},
  169. {label: elements.scripts.querySelector('header').textContent, selector: 'scripts', list: elements.userScriptList, selected: true},
  170. ];
  171. let nav = createElement(core.html.tabNavigation()), scriptSets = elements.scriptSets;
  172. let template = nav.querySelector('li.template');
  173. scriptSets.parentNode.insertBefore(nav, scriptSets);
  174. for(let i = 0; keys[i]; i++){
  175. let li = template.cloneNode(true);
  176. li.classList.remove('template');
  177. li.textContent = keys[i].label + ` (${keys[i].list.children.length})`;
  178. li.dataset.target = keys[i].selector;
  179. li.addEventListener('click', function(e){
  180. li.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
  181. $('[data-tabified][data-selected="true"]').dataset.selected = 'false';
  182. li.dataset.selected = 'true';
  183. $(`[data-selector="${li.dataset.target}"]`).dataset.selected = 'true';
  184. });
  185. let target = elements[keys[i].selector];
  186. target.dataset.tabified = 'true';
  187. if(keys[i].selected) li.dataset.selected = target.dataset.selected = 'true';
  188. else li.dataset.selected = target.dataset.selected = 'false';
  189. template.parentNode.insertBefore(li, template);
  190. }
  191. },
  192. addNewScriptSetLink: function(){
  193. let link = elements.newScriptSetLink.cloneNode(true), list = elements.userScriptSets, li = document.createElement('li');
  194. li.appendChild(link);
  195. list.appendChild(li);
  196. },
  197. rebuildScriptList: function(){
  198. for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
  199. let more = createElement(core.html.more()), props = site.get.props(li);
  200. if(!shown[li.dataset.scriptName]) li.classList.add('hidden');
  201. more.addEventListener('click', function(e){
  202. li.classList.toggle('hidden');
  203. shown[li.dataset.scriptName] = !li.classList.contains('hidden');
  204. Storage.save('shown', shown);
  205. });
  206. li.dataset.scriptUrl = props.name.href;
  207. li.appendChild(more);
  208. /* attatch titles */
  209. props.dailyInstalls.previousElementSibling.title = props.dailyInstalls.previousElementSibling.textContent;
  210. props.totalInstalls.previousElementSibling.title = props.totalInstalls.previousElementSibling.textContent;
  211. props.ratings.previousElementSibling.title = props.ratings.previousElementSibling.textContent;
  212. props.createdDate.previousElementSibling.title = props.createdDate.previousElementSibling.textContent;
  213. props.updatedDate.previousElementSibling.title = props.updatedDate.previousElementSibling.textContent;
  214. /* wrap the description to make it an inline element */
  215. let span = document.createElement('span');
  216. span.textContent = props.description.textContent.trim();
  217. props.description.replaceChild(span, props.description.firstChild);
  218. /* Link to Code and Update from Version */
  219. let versionLabel = createElement(core.html.dt('script-list-version', translation.version));
  220. let versionDd = createElement(core.html.ddLink('script-list-version', props.scriptVersion, props.name.href + '/code', translation.code));
  221. let updateLink = document.createElement('a');
  222. versionLabel.title = versionLabel.textContent;
  223. updateLink.href = props.name.href + '/versions/new';
  224. updateLink.textContent = UPDATELINKTEXT;
  225. updateLink.title = translation.update;
  226. updateLink.classList.add('update');
  227. versionDd.appendChild(updateLink);
  228. props.stats.insertBefore(versionLabel, props.createdDate.previousElementSibling);
  229. props.stats.insertBefore(versionDd, props.createdDate.previousElementSibling);
  230. /* Link to Stats from Total installs */
  231. let statsDd = createElement(core.html.ddLink('script-list-total-installs', props.totalInstalls.textContent, props.name.href + '/stats', translation.stats));
  232. props.stats.replaceChild(statsDd, props.totalInstalls);
  233. /* Link to History from Updated date */
  234. let historyDd = createElement(core.html.ddLink('script-list-updated-date', props.updatedDate.textContent, props.name.href + '/versions', translation.history));
  235. props.stats.replaceChild(historyDd, props.updatedDate);
  236. }
  237. },
  238. addChartSwitcher: function(){
  239. const keys = [
  240. {label: translation.installs, selector: 'installs'},
  241. {label: translation.updateChecks, selector: 'updateChecks'},
  242. ];
  243. let chartKey = Storage.read('chartKey') || 'updateChecks';
  244. let nav = createElement(core.html.chartSwitcher()), userScriptList = elements.userScriptList;
  245. let template = nav.querySelector('li.template');
  246. userScriptList.parentNode.appendChild(nav);/* less affected on dom */
  247. for(let i = 0; keys[i]; i++){
  248. let li = template.cloneNode(true);
  249. li.classList.remove('template');
  250. li.textContent = keys[i].label;
  251. li.dataset.key = keys[i].selector;
  252. li.addEventListener('click', function(e){
  253. li.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
  254. li.dataset.selected = 'true';
  255. core.drawCharts(li.dataset.key);
  256. Storage.save('chartKey', li.dataset.key);
  257. });
  258. if(keys[i].selector === chartKey) li.dataset.selected = 'true';
  259. else li.dataset.selected = 'false';
  260. template.parentNode.insertBefore(li, template);
  261. }
  262. core.drawCharts(chartKey);
  263. },
  264. drawCharts: function(chartKey = 'updateChecks'){
  265. let stats = Storage.read('stats') || {}, promises = [];
  266. for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
  267. /* Draw chart of daily update checks */
  268. let chart = li.querySelector('.chart') || createElement(core.html.chart());
  269. if(stats[li.dataset.scriptName]){
  270. setTimeout(function(){
  271. core.drawChart(chartKey, chart, stats[li.dataset.scriptName].slice(-DAYS));
  272. if(!chart.isConnected) li.appendChild(chart);
  273. }, i * DRAWINGDELAY);/* CPU friendly */
  274. }
  275. let saved = Storage.saved('stats') || 0, past = saved % STATSUPDATE, expire = saved - past + STATSUPDATE;
  276. if(Date.now() < expire) continue;/* still up-to-date */
  277. promises.push(new Promise(function(resolve, reject){
  278. setTimeout(function(){
  279. fetch(li.dataset.scriptUrl + '/stats.csv')/* less file size than json */
  280. .then(response => response.text())
  281. .then(csv => {
  282. let lines = csv.split('\n');
  283. lines = lines.slice(1, -1);/* cut the labels + blank line */
  284. stats[li.dataset.scriptName] = [];
  285. for(let i = 0; lines[i]; i++){
  286. let p = lines[i].split(',');
  287. stats[li.dataset.scriptName][i] = {
  288. date: p[0],
  289. installs: parseInt(p[1]),
  290. updateChecks: parseInt(p[2]),
  291. };
  292. }
  293. core.drawChart(chartKey, chart, stats[li.dataset.scriptName].slice(-DAYS));
  294. if(!chart.isConnected) li.appendChild(chart);
  295. resolve();
  296. });
  297. }, i * INTERVAL);/* server friendly */
  298. }));
  299. }
  300. if(promises.length) Promise.all(promises).then((values) => Storage.save('stats', stats));
  301. },
  302. drawChart: function(chartKey, chart, stats){
  303. let max = DEFAULTMAX;
  304. for(let i = 0; stats[i]; i++){
  305. if(stats[i][chartKey] > max) max = stats[i][chartKey];
  306. }
  307. let dl = chart.querySelector('dl'), dt = dl.querySelector('dt'), dd = dl.querySelector('dd');
  308. for(let i = 0, last = stats.length - 1; stats[i]; i++){
  309. let date = stats[i].date, count = stats[i][chartKey];
  310. let dateDt = dl.querySelector(`dt[data-date="${date}"]`) || dt.cloneNode();
  311. let countDd = dateDt.nextElementSibling || dd.cloneNode();
  312. if(!dateDt.isConnected){
  313. dateDt.classList.remove('template');
  314. countDd.classList.remove('template');
  315. dateDt.dataset.date = dateDt.textContent = date;
  316. dl.insertBefore(dateDt, dt);
  317. dl.insertBefore(countDd, dt);
  318. }
  319. countDd.title = date + ': ' + count;
  320. countDd.dataset.count = count;
  321. dd.style.height = '0%';
  322. if(i === last - 1){
  323. let label = countDd.querySelector('span') || document.createElement('span');
  324. label.textContent = toMetric(count);
  325. if(!label.isConnected) countDd.appendChild(label);
  326. }
  327. }
  328. /* for animation */
  329. animate(function(){
  330. for(let i = 0, dds = dl.querySelectorAll('dd.count:not(.template)'), dd; dd = dds[i]; i++){
  331. dd.style.height = ((dd.dataset.count / max) * 100) + '%';
  332. }
  333. });
  334. },
  335. addStyle: function(name = 'style'){
  336. let style = createElement(core.html[name]());
  337. document.head.appendChild(style);
  338. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  339. elements[name] = style;
  340. },
  341. html: {
  342. more: () => `
  343. <button class="more"></button>
  344. `,
  345. tabNavigation: () => `
  346. <nav id="tabNavigation">
  347. <ul>
  348. <li class="template"></li>
  349. </ul>
  350. </nav>
  351. `,
  352. chartSwitcher: () => `
  353. <nav id="chartSwitcher">
  354. <ul>
  355. <li class="template"></li>
  356. </ul>
  357. </nav>
  358. `,
  359. dt: (className, textContent) => `
  360. <dt class="${className}"><span>${textContent}</span></dt>
  361. `,
  362. ddLink: (className, textContent, href, title) => `
  363. <dd class="${className}"><a href="${href}" title="${title}">${textContent}</a></dd>
  364. `,
  365. chart: () => `
  366. <div class="chart">
  367. <dl>
  368. <dt class="template date"></dt>
  369. <dd class="template count"></dd>
  370. </dl>
  371. </div>
  372. `,
  373. style: () => `
  374. <style type="text/css">
  375. /* red scale: 103-206 */
  376. /* gray scale: 119-153-187-221 */
  377. /* coommon */
  378. h2, h3{
  379. margin: 0;
  380. }
  381. ul, ol{
  382. margin: 0;
  383. padding: 0 0 0 2em;
  384. }
  385. a:hover,
  386. a:focus{
  387. color: rgb(206,0,0);
  388. }
  389. .template{
  390. display: none !important;
  391. }
  392. section.text-content{
  393. position: relative;
  394. padding: 0;
  395. }
  396. section.text-content > *{
  397. margin: 14px;
  398. }
  399. section.text-content h2{
  400. text-align: left !important;
  401. margin-bottom: 0;
  402. }
  403. section > header + *{
  404. margin: 0 0 14px !important;
  405. }
  406. button.more{
  407. color: rgb(153,153,153);
  408. border: 1px solid rgb(187,187,187);
  409. background: white;
  410. padding: 0;
  411. cursor: pointer;
  412. }
  413. button.more::-moz-focus-inner{
  414. border: none;
  415. }
  416. button.more::after{
  417. font-size: medium;
  418. content: "▴";
  419. }
  420. .hidden > button.more{
  421. background: rgb(221, 221, 221);
  422. position: absolute;
  423. }
  424. .hidden > button.more::after{
  425. content: "▾";
  426. }
  427. /* User panel */
  428. section[data-selector="userSection"].hidden{
  429. max-height: 10em;
  430. overflow: hidden;
  431. }
  432. section[data-selector="userSection"] > button.more{
  433. position: relative;
  434. bottom: 0;
  435. width: 100%;
  436. margin: 0;
  437. border: none;
  438. border-top: 1px solid rgba(187, 187, 187);
  439. border-radius: 0 0 5px 5px;
  440. }
  441. section[data-selector="userSection"].hidden > button.more{
  442. position: absolute;
  443. }
  444. /* Control panel */
  445. section#control-panel{
  446. font-size: smaller;
  447. width: 200px;
  448. position: absolute;
  449. top: 0;
  450. right: 0;
  451. z-index: 1;
  452. }
  453. section#control-panel h3{
  454. font-size: 1em;
  455. padding: .25em 1em;
  456. border-radius: 5px 5px 0 0;
  457. background: rgb(103, 0, 0);
  458. color: white;
  459. cursor: pointer;
  460. }
  461. section#control-panel.hidden h3{
  462. border-radius: 5px 5px 5px 5px;
  463. }
  464. section#control-panel h3::after{
  465. content: " ▴";
  466. margin-left: .25em;
  467. }
  468. section#control-panel.hidden h3::after{
  469. content: " ▾";
  470. }
  471. ul#user-control-panel{
  472. list-style-type: square;
  473. color: rgb(187, 187, 187);
  474. width: 100%;
  475. margin: .5em 0;
  476. padding: .5em .5em .5em 1.5em;
  477. background: white;
  478. border-radius: 0 0 5px 5px;
  479. border: 1px solid rgb(187, 187, 187);
  480. border-top: none;
  481. box-sizing: border-box;
  482. }
  483. section#control-panel.hidden > ul#user-control-panel{
  484. display: none;
  485. }
  486. /* Discussions on your scripts */
  487. #user-discussions-on-scripts-written{
  488. margin-top: 0;
  489. }
  490. /* tabs */
  491. #tabNavigation{
  492. display: inline-block;
  493. }
  494. #tabNavigation > ul{
  495. list-style-type: none;
  496. padding: 0;
  497. display: flex;
  498. }
  499. #tabNavigation > ul > li{
  500. font-weight: bold;
  501. background: white;
  502. padding: .25em 1em;
  503. border: 1px solid rgb(187, 187, 187);
  504. border-bottom: none;
  505. border-radius: 5px 5px 0 0;
  506. box-shadow: 0 0 5px rgb(221, 221, 221);
  507. cursor: pointer;
  508. }
  509. #tabNavigation > ul > li:first-child{
  510. }
  511. #tabNavigation > ul > li[data-selected="false"]{
  512. color: rgb(153,153,153);
  513. background: rgb(221, 221, 221);
  514. }
  515. [data-selector="scriptSets"] > section,
  516. [data-tabified] #user-script-list{
  517. border-radius: 0 5px 5px 5px;
  518. }
  519. [data-tabified] header{
  520. display: none;
  521. }
  522. [data-tabified][data-selected="false"]{
  523. display: none;
  524. }
  525. /* Scripts */
  526. #user-script-list li{
  527. padding: .25em 1em;
  528. position: relative;
  529. }
  530. #user-script-list li:last-child{
  531. border-bottom: none;/* missing in greasyfork.org */
  532. }
  533. #user-script-list li article{
  534. position: relative;
  535. z-index: 1;/* over the .chart */
  536. pointer-events: none;
  537. }
  538. #user-script-list li article h2 > a,
  539. #user-script-list li article h2 > .description/* it's block! */ > span,
  540. #user-script-list li article dl > dt > *,
  541. #user-script-list li article dl > dd > *{
  542. pointer-events: auto;/* apply on inline elements */
  543. }
  544. #user-script-list li button.more{
  545. border-radius: 5px;
  546. position: absolute;
  547. top: 0;
  548. right: 0;
  549. margin: 5px;
  550. width: 2em;
  551. z-index: 1;/* over the .chart */
  552. }
  553. #user-script-list li .description{
  554. font-size: small;
  555. margin: 0 0 0 .1em;/* ajust first letter position */
  556. }
  557. #user-script-list li dl.inline-script-stats{
  558. margin-top: .25em;
  559. column-count: 3;
  560. max-height: 4em;/* Firefox bug? */
  561. }
  562. #user-script-list li dl.inline-script-stats dt{
  563. overflow: hidden;
  564. white-space: nowrap;
  565. text-overflow: ellipsis;
  566. max-width: 200px;/* stretching column mystery on long-lettered languages such as fr-CA */
  567. }
  568. #user-script-list li dl.inline-script-stats .script-list-author{
  569. display: none;
  570. }
  571. #user-script-list li dl.inline-script-stats dd.script-list-version a.update{
  572. padding: 0 .75em 0 .25em;/* enough space for right side */
  573. margin: 0 .25em;
  574. }
  575. #user-script-list li dl.inline-script-stats dt.script-list-daily-installs,
  576. #user-script-list li dl.inline-script-stats dt.script-list-total-installs{
  577. width: 65%;
  578. }
  579. #user-script-list li dl.inline-script-stats dd.script-list-daily-installs,
  580. #user-script-list li dl.inline-script-stats dd.script-list-total-installs{
  581. width: 35%;
  582. }
  583. #user-script-list li.hidden .description,
  584. #user-script-list li.hidden .inline-script-stats{
  585. display: none;
  586. }
  587. /* chartSwitcher */
  588. [data-selector="scripts"] > div > section{
  589. position: relative;/* position anchor */
  590. }
  591. #chartSwitcher{
  592. display: inline-block;
  593. position: absolute;
  594. top: -1.5em;
  595. right: 0;
  596. line-height: 1.25em;
  597. }
  598. #chartSwitcher > ul{
  599. list-style-type: none;
  600. font-size: small;
  601. padding: 0;
  602. margin: 0;
  603. }
  604. #chartSwitcher > ul > li{
  605. color: rgb(187,187,187);
  606. font-weight: bold;
  607. display: inline-block;
  608. border-right: 1px solid rgb(187,187,187);
  609. padding: 0 1em;
  610. margin: 0;
  611. cursor: pointer;
  612. }
  613. #chartSwitcher > ul > li[data-selected="true"]{
  614. color: black;
  615. cursor: auto;
  616. }
  617. #chartSwitcher > ul > li:nth-last-child(2)/* 2nd including template */{
  618. border-right: none;
  619. }
  620. /* chart */
  621. .chart{
  622. position: absolute;
  623. top: 0;
  624. right: 0;
  625. width: 100%;
  626. height: 100%;
  627. overflow: hidden;
  628. mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
  629. -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,.5), black);
  630. }
  631. .chart > dl{
  632. position: absolute;
  633. bottom: 0;
  634. right: 2em;
  635. margin: 0;
  636. height: calc(100% - 5px);
  637. display: flex;
  638. align-items: flex-end;
  639. }
  640. .chart > dl > dt.date{
  641. display: none;
  642. }
  643. .chart > dl > dd.count{
  644. background: rgb(221,221,221);
  645. width: 3px;
  646. border-left: 1px solid white;
  647. margin: 0;
  648. transition: height 250ms;
  649. }
  650. .chart > dl > dd.count:nth-last-of-type(3)/* 3rd including template */,
  651. .chart > dl > dd.count:hover{
  652. background: rgb(187,187,187);
  653. }
  654. .chart > dl > dd.count:nth-last-of-type(3):hover{
  655. background: rgb(153,153,153);
  656. }
  657. .chart > dl > dd.count > span{
  658. display: none;/* default */
  659. }
  660. .chart > dl > dd.count:nth-last-of-type(3) > span{
  661. display: inline;/* overwrite */
  662. font-weight: bold;
  663. color: rgb(153,153,153);
  664. position: absolute;
  665. top: 5px;
  666. right: 10px;
  667. pointer-events: none;
  668. }
  669. .chart > dl > dd.count:nth-last-of-type(3)[data-count="0"] > span{
  670. color: rgb(221,221,221);
  671. }
  672. .chart > dl > dd.count:nth-last-of-type(3):hover > span{
  673. color: rgb(119,119,119);
  674. }
  675. /* sidebar */
  676. .sidebar{
  677. padding-top: 0;
  678. }
  679. .ad/* excuse me, it disappears only in my own user page :-) */,
  680. #script-list-filter{
  681. display: none !important;
  682. }
  683. </style>
  684. `,
  685. },
  686. };
  687. class Storage{
  688. static key(key){
  689. return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
  690. }
  691. static save(key, value, expire = null){
  692. key = Storage.key(key);
  693. localStorage[key] = JSON.stringify({
  694. value: value,
  695. saved: Date.now(),
  696. expire: expire,
  697. });
  698. }
  699. static read(key){
  700. key = Storage.key(key);
  701. if(localStorage[key] === undefined) return undefined;
  702. let data = JSON.parse(localStorage[key]);
  703. if(data.value === undefined) return data;
  704. if(data.expire === undefined) return data;
  705. if(data.expire === null) return data.value;
  706. if(data.expire < Date.now()) return localStorage.removeItem(key);
  707. return data.value;
  708. }
  709. static delete(key){
  710. key = Storage.key(key);
  711. delete localStorage.removeItem(key);
  712. }
  713. static saved(key){
  714. key = Storage.key(key);
  715. if(localStorage[key] === undefined) return undefined;
  716. let data = JSON.parse(localStorage[key]);
  717. if(data.saved) return data.saved;
  718. else return undefined;
  719. }
  720. }
  721. const $ = function(s){return document.querySelector(s)};
  722. const $$ = function(s){return document.querySelectorAll(s)};
  723. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  724. const wait = function(ms){return new Promise((resolve) => setTimeout(resolve, ms))};
  725. const createElement = function(html){
  726. let outer = document.createElement('div');
  727. outer.innerHTML = html;
  728. return outer.firstElementChild;
  729. };
  730. const toMetric = function(number, fixed = 1){
  731. switch(true){
  732. case(number < 1e3): return (number);
  733. case(number < 1e6): return (number/ 1e3).toFixed(fixed) + 'K';
  734. case(number < 1e9): return (number/ 1e6).toFixed(fixed) + 'M';
  735. case(number < 1e12): return (number/ 1e9).toFixed(fixed) + 'G';
  736. default: return (number/1e12).toFixed(fixed) + 'T';
  737. }
  738. };
  739. const log = function(){
  740. if(!DEBUG) return;
  741. let l = log.last = log.now || new Date(), n = log.now = new Date();
  742. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  743. //console.log(error.stack);
  744. console.log(
  745. SCRIPTNAME + ':',
  746. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  747. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  748. /* :00 */ ':' + line,
  749. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  750. /* caller */ (callers[1] || '') + '()',
  751. ...arguments
  752. );
  753. };
  754. log.formats = [{
  755. name: 'Firefox Scratchpad',
  756. detector: /MARKER@Scratchpad/,
  757. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  758. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  759. }, {
  760. name: 'Firefox Console',
  761. detector: /MARKER@debugger/,
  762. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  763. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  764. }, {
  765. name: 'Firefox Greasemonkey 3',
  766. detector: /\/gm_scripts\//,
  767. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  768. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  769. }, {
  770. name: 'Firefox Greasemonkey 4+',
  771. detector: /MARKER@user-script:/,
  772. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  773. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  774. }, {
  775. name: 'Firefox Tampermonkey',
  776. detector: /MARKER@moz-extension:/,
  777. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  778. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  779. }, {
  780. name: 'Chrome Console',
  781. detector: /at MARKER \(<anonymous>/,
  782. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  783. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  784. }, {
  785. name: 'Chrome Tampermonkey',
  786. detector: /at MARKER \((userscript\.html|chrome-extension:)/,
  787. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+)\)$/)[1] - 6,
  788. getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm),
  789. }, {
  790. name: 'Edge Console',
  791. detector: /at MARKER \(eval/,
  792. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  793. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  794. }, {
  795. name: 'Edge Tampermonkey',
  796. detector: /at MARKER \(Function/,
  797. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  798. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  799. }, {
  800. name: 'Safari',
  801. detector: /^MARKER$/m,
  802. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  803. getCallers: (e) => e.stack.split('\n'),
  804. }, {
  805. name: 'Default',
  806. detector: /./,
  807. getLine: (e) => 0,
  808. getCallers: (e) => [],
  809. }];
  810. log.format = log.formats.find(function MARKER(f){
  811. if(!f.detector.test(new Error().stack)) return false;
  812. //console.log('//// ' + f.name + '\n' + new Error().stack);
  813. return true;
  814. });
  815. core.initialize();
  816. if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
  817. })();