GreasyFork User Dashboard

It redesigns your own user page.

当前为 2019-02-01 提交的版本,查看 最新版本

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