GreasyFork User Dashboard

It redesigns your own user page.

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

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