Greasy Fork 还支持 简体中文。

GreasyFork User Dashboard

It redesigns your own user page.

目前為 2019-02-04 提交的版本,檢視 最新版本

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