GreasyFork User Dashboard

It redesigns user pages.

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

  1. // ==UserScript==
  2. // @name GreasyFork User Dashboard
  3. // @name:ja GreasyFork User Dashboard
  4. // @namespace knoa.jp
  5. // @description It redesigns user pages.
  6. // @description:ja 新しいユーザーページを提供します。
  7. // @include https://greasyfork.org/*/users/*
  8. // @version 1.2.0
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function(){
  13. const SCRIPTNAME = 'GreasyForkUserDashboard';
  14. const DEBUG = false;/*
  15. [update]
  16. 1.2.0
  17. now available in all user's pages.
  18.  
  19. [bug]
  20. fixed? 日付をまたいで初回の1日分追加処理ができてない?
  21.  
  22. [to do]
  23.  
  24. [possible]
  25. 3カラムのレイアウト崩れる(スクリプト未使用でも発生する)
  26.  
  27. [not to do]
  28. なぜか取得できない
  29. https://greasyfork.org/ja/scripts/16782-doujinfree-for-pc/stats.csv
  30. */
  31. if(window === top && console.time) console.time(SCRIPTNAME);
  32. const INTERVAL = 1000;/* for fetch */
  33. const DRAWINGDELAY = 125;/* for drawing each charts */
  34. const UPDATELINKTEXT = '+';/* for update link text */
  35. const DEFAULTMAX = 10;/* for chart scale */
  36. const DAYS = 180;/* for chart length */
  37. const STATSUPDATE = 1000*60*60;/* stats update interval of greasyfork.org */
  38. const TRANSLATIONEXPIRE = 1000*60*60*24*30;/* cache time for translations */
  39. const EASING = 'cubic-bezier(0,.75,.5,1)';/* quick easing */
  40. let site = {
  41. targets: {
  42. userSection: () => $('body > header + div > section:nth-of-type(1)'),
  43. userProfile: () => $('#user-profile'),
  44. controlPanel: () => $('#control-panel'),
  45. newScriptSetLink: () => $('a[href$="/sets/new"]'),
  46. discussionList: () => $('ul.discussion-list'),
  47. scriptSets: () => $('body > header + div > section:nth-of-type(2)'),
  48. scripts: () => $('body > header + div > div:nth-of-type(1)'),
  49. userScriptSets: () => $('#user-script-sets'),
  50. userScriptList: () => $('#user-script-list'),
  51. },
  52. get: {
  53. language: (d) => d.documentElement.lang,
  54. firstScript: (list) => list.querySelector('li h2 > a'),
  55. translation: (d, t) => {
  56. let es = {
  57. info: d.querySelector('#script-links > li.current'),
  58. code: d.querySelector('#script-links > li > a[href$="/code"]'),
  59. history: d.querySelector('#script-links > li > a[href$="/versions"]'),
  60. feedback: d.querySelector('#script-links > li > a[href$="/feedback"]'),
  61. stats: d.querySelector('#script-links > li > a[href$="/stats"]'),
  62. derivatives: d.querySelector('#script-links > li > a[href$="/derivatives"]'),
  63. update: d.querySelector('#script-links > li > a[href$="/versions/new"]'),
  64. delete: d.querySelector('#script-links > li > a[href$="/delete"]'),
  65. admin: d.querySelector('#script-links > li > a[href$="/admin"]'),
  66. version: d.querySelector('#script-stats > dt.script-show-version'),
  67. }
  68. Object.keys(es).forEach((key) => t[key] = es[key] ? es[key].textContent : t[key]);
  69. return t;
  70. },
  71. translationOnStats: (d, t) => {
  72. t.installs = d.querySelector('table.stats-table > thead > tr > th:nth-child(2)').textContent || t.installs;
  73. t.updateChecks = d.querySelector('table.stats-table > thead > tr > th:nth-child(3)').textContent || t.updateChecks;
  74. return t;
  75. },
  76. props: (li) => {return {
  77. name: li.querySelector('h2 > a'),
  78. description: li.querySelector('.description'),
  79. stats: li.querySelector('dl.inline-script-stats'),
  80. dailyInstalls: li.querySelector('dd.script-list-daily-installs'),
  81. totalInstalls: li.querySelector('dd.script-list-total-installs'),
  82. ratings: li.querySelector('dd.script-list-ratings'),
  83. createdDate: li.querySelector('dd.script-list-created-date'),
  84. updatedDate: li.querySelector('dd.script-list-updated-date'),
  85. scriptVersion: li.dataset.scriptVersion,
  86. }},
  87. scriptUrl: (li) => li.querySelector('h2 > a').href,
  88. },
  89. };
  90. const DEFAULTTRANSLATION = {
  91. info: 'Info',
  92. code: 'Code',
  93. history: 'History',
  94. feedback: 'Feedback',
  95. stats: 'Stats',
  96. derivatives: 'Derivatives',
  97. update: 'Update',
  98. delete: 'Delete',
  99. admin: 'Admin',
  100. version: 'Version',
  101. installs: 'Installs',
  102. updateChecks: 'Update checks',
  103. scriptSets: 'Script Sets',
  104. scripts: 'Scripts',
  105. };
  106. let translation = {};
  107. let elements = {}, storages = {}, timers = {};
  108. let core = {
  109. initialize: function(){
  110. core.getElements();
  111. core.read();
  112. core.addStyle();
  113. core.prepareTranslations();
  114. core.hideUserSection();
  115. core.hideControlPanel();
  116. core.addTabNavigation();
  117. core.addNewScriptSetLink();
  118. core.rebuildScriptList();
  119. core.addChartSwitcher();
  120. },
  121. getElements: function(){
  122. for(let i = 0, keys = Object.keys(site.targets); keys[i]; i++){
  123. let element = site.targets[keys[i]]();
  124. if(!element) log(`Not found: ${keys[i]}`);
  125. else{
  126. element.dataset.selector = keys[i];
  127. elements[keys[i]] = element;
  128. }
  129. }
  130. },
  131. read: function(){
  132. storages.translations = Storage.read('translations') || {};
  133. storages.shown = Storage.read('shown') || {};
  134. storages.stats = Storage.read('stats') || {};
  135. storages.chartKey = Storage.read('chartKey') || 'updateChecks';
  136. },
  137. prepareTranslations: function(){
  138. let language = site.get.language(document);
  139. translation = storages.translations[language] || DEFAULTTRANSLATION;
  140. if(!Object.keys(DEFAULTTRANSLATION).every((key) => translation[key])){/* some change in translation keys */
  141. Object.keys(DEFAULTTRANSLATION).forEach((key) => translation[key] = translation[key] || DEFAULTTRANSLATION[key]);
  142. core.getTranslations();
  143. }else{
  144. if(site.get.language(document) === 'en') return;
  145. if(Date.now() < (Storage.saved('translations') || 0) + TRANSLATIONEXPIRE) return;
  146. core.getTranslations();
  147. }
  148. },
  149. getTranslations: function(){
  150. let firstScript = site.get.firstScript(elements.userScriptList);
  151. fetch(firstScript.href, {credentials: 'include'})
  152. .then(response => response.text())
  153. .then(text => new DOMParser().parseFromString(text, 'text/html'))
  154. .then(d => translation = storages.translations[site.get.language(d)] = site.get.translation(d, translation))
  155. .then(() => wait(INTERVAL))
  156. .then(() => fetch(firstScript.href + '/stats'))
  157. .then(response => response.text())
  158. .then(text => new DOMParser().parseFromString(text, 'text/html'))
  159. .then(d => {
  160. translation = storages.translations[site.get.language(d)] = site.get.translationOnStats(d, translation);
  161. Storage.save('translations', storages.translations);
  162. });
  163. },
  164. hideUserSection: function(){
  165. if(!elements.userProfile && !elements.discussionList && !elements.controlPanel) return;/* thin enough */
  166. let userSection = elements.userSection, more = createElement(core.html.more());
  167. if(!storages.shown.userSection) userSection.classList.add('hidden');
  168. more.addEventListener('click', function(e){
  169. userSection.classList.toggle('hidden');
  170. storages.shown.userSection = !userSection.classList.contains('hidden');
  171. Storage.save('shown', storages.shown);
  172. });
  173. userSection.appendChild(more);
  174. },
  175. hideControlPanel: function(){
  176. let controlPanel = elements.controlPanel;
  177. if(!controlPanel) return;/* may be not own user page */
  178. document.documentElement.dataset.owner = 'true';/* user owner flag */
  179. let header = controlPanel.firstElementChild;
  180. if(!storages.shown.controlPanel) controlPanel.classList.add('hidden');
  181. setTimeout(function(){elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px'}, 250);/* needs delay */
  182. header.addEventListener('click', function(e){
  183. controlPanel.classList.toggle('hidden');
  184. storages.shown.controlPanel = !controlPanel.classList.contains('hidden');
  185. Storage.save('shown', storages.shown);
  186. elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px';
  187. });
  188. },
  189. addTabNavigation: function(){
  190. let scriptSets = elements.scriptSets, scripts = elements.scripts;
  191. let userScriptSets = elements.userScriptSets, userScriptList = elements.userScriptList;
  192. const keys = [{
  193. label: scriptSets ? scriptSets.querySelector('header').textContent : translation.scriptSets,
  194. selector: 'scriptSets',
  195. count: userScriptSets ? userScriptSets.children.length : 0,
  196. }, {
  197. label: scripts ? scripts.querySelector('header').textContent : translation.scripts,
  198. selector: 'scripts',
  199. count: userScriptList ? userScriptList.children.length : 0,
  200. selected: true
  201. }];
  202. let nav = createElement(core.html.tabNavigation()), anchor = (scriptSets || scripts);
  203. let template = nav.querySelector('li.template');
  204. if(anchor) anchor.parentNode.insertBefore(nav, anchor);
  205. for(let i = 0; keys[i]; i++){
  206. let li = template.cloneNode(true);
  207. li.classList.remove('template');
  208. li.textContent = keys[i].label + ` (${keys[i].count})`;
  209. li.dataset.target = keys[i].selector;
  210. li.dataset.count = keys[i].count;
  211. li.addEventListener('click', function(e){
  212. /* close tab */
  213. li.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
  214. let openedTarget = $('[data-tabified][data-selected="true"]');
  215. if(openedTarget) openedTarget.dataset.selected = 'false';
  216. /* open tab */
  217. li.dataset.selected = 'true';
  218. let openingTarget = $(`[data-selector="${li.dataset.target}"]`);
  219. if(openingTarget) openingTarget.dataset.selected = 'true';
  220. });
  221. let target = elements[keys[i].selector];
  222. if(target){
  223. target.dataset.tabified = 'true';
  224. if(keys[i].selected) li.dataset.selected = target.dataset.selected = 'true';
  225. else li.dataset.selected = target.dataset.selected = 'false';
  226. }else{
  227. if(keys[i].selected) li.dataset.selected = 'true';
  228. else li.dataset.selected = 'false';
  229. }
  230. template.parentNode.insertBefore(li, template);
  231. }
  232. },
  233. addNewScriptSetLink: function(){
  234. let newScriptSetLink = elements.newScriptSetLink;
  235. if(!newScriptSetLink) return;/* may be not own user page */
  236. let link = newScriptSetLink.cloneNode(true), list = elements.userScriptSets, li = document.createElement('li');
  237. li.appendChild(link);
  238. list.appendChild(li);
  239. },
  240. rebuildScriptList: function(){
  241. if(!elements.userScriptList) return;
  242. for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
  243. let more = createElement(core.html.more()), props = site.get.props(li), key = li.dataset.scriptName, isLibrary = li.dataset.scriptType === 'library';
  244. if(!storages.shown[key]) li.classList.add('hidden');
  245. more.addEventListener('click', function(e){
  246. li.classList.toggle('hidden');
  247. if(li.classList.contains('hidden')) delete storages.shown[key];/* prevent from getting fat storage */
  248. else storages.shown[key] = true;
  249. Storage.save('shown', storages.shown);
  250. });
  251. li.dataset.scriptUrl = props.name.href;
  252. li.appendChild(more);
  253. if(isLibrary) continue;/* not so critical to skip below by continue */
  254. /* attatch titles */
  255. props.dailyInstalls.previousElementSibling.title = props.dailyInstalls.previousElementSibling.textContent;
  256. props.totalInstalls.previousElementSibling.title = props.totalInstalls.previousElementSibling.textContent;
  257. props.ratings.previousElementSibling.title = props.ratings.previousElementSibling.textContent;
  258. props.createdDate.previousElementSibling.title = props.createdDate.previousElementSibling.textContent;
  259. props.updatedDate.previousElementSibling.title = props.updatedDate.previousElementSibling.textContent;
  260. /* wrap the description to make it an inline element */
  261. let span = document.createElement('span');
  262. span.textContent = props.description.textContent.trim();
  263. props.description.replaceChild(span, props.description.firstChild);
  264. /* Link to Code */
  265. let versionLabel = createElement(core.html.dt('script-list-version', translation.version));
  266. let versionDd = createElement(core.html.ddLink('script-list-version', props.scriptVersion, props.name.href + '/code', translation.code));
  267. versionLabel.title = versionLabel.textContent;
  268. props.stats.insertBefore(versionLabel, props.createdDate.previousElementSibling);
  269. props.stats.insertBefore(versionDd, props.createdDate.previousElementSibling);
  270. /* Link to Version up */
  271. if(elements.controlPanel){
  272. let updateLink = document.createElement('a');
  273. updateLink.href = props.name.href + '/versions/new';
  274. updateLink.textContent = UPDATELINKTEXT;
  275. updateLink.title = translation.update;
  276. updateLink.classList.add('update');
  277. versionDd.appendChild(updateLink);
  278. }
  279. /* Link to Stats from Total installs */
  280. let statsDd = createElement(core.html.ddLink('script-list-total-installs', props.totalInstalls.textContent, props.name.href + '/stats', translation.stats));
  281. props.stats.replaceChild(statsDd, props.totalInstalls);
  282. /* Link to History from Updated date */
  283. let historyDd = createElement(core.html.ddLink('script-list-updated-date', props.updatedDate.textContent, props.name.href + '/versions', translation.history));
  284. props.stats.replaceChild(historyDd, props.updatedDate);
  285. }
  286. },
  287. addChartSwitcher: function(){
  288. let userScriptList = elements.userScriptList;
  289. if(!userScriptList) return;
  290. const keys = [
  291. {label: translation.installs, selector: 'installs'},
  292. {label: translation.updateChecks, selector: 'updateChecks'},
  293. ];
  294. let nav = createElement(core.html.chartSwitcher());
  295. let template = nav.querySelector('li.template');
  296. userScriptList.parentNode.appendChild(nav);/* less affected on dom */
  297. for(let i = 0; keys[i]; i++){
  298. let li = template.cloneNode(true);
  299. li.classList.remove('template');
  300. li.textContent = keys[i].label;
  301. li.dataset.key = keys[i].selector;
  302. li.addEventListener('click', function(e){
  303. li.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false';
  304. li.dataset.selected = 'true';
  305. storages.chartKey = li.dataset.key;
  306. Storage.save('chartKey', storages.chartKey);
  307. core.drawCharts();
  308. });
  309. if(keys[i].selector === storages.chartKey) li.dataset.selected = 'true';
  310. else li.dataset.selected = 'false';
  311. template.parentNode.insertBefore(li, template);
  312. }
  313. core.drawCharts();
  314. },
  315. drawCharts: function(){
  316. let promises = [];
  317. if(timers.charts && timers.charts.length) timers.charts.forEach((id) => clearTimeout(id));/* stop all the former timers */
  318. timers.charts = [];
  319. for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){
  320. if(li.dataset.scriptType === 'library') continue;
  321. /* Draw chart of daily update checks */
  322. let chart = li.querySelector('.chart') || createElement(core.html.chart()), key = li.dataset.scriptName;
  323. if(storages.stats[key] && storages.stats[key].data){
  324. timers.charts[i] = setTimeout(function(){
  325. core.drawChart(chart, storages.stats[key].data.slice(-DAYS));
  326. if(!chart.isConnected) li.appendChild(chart);
  327. }, i * DRAWINGDELAY);/* CPU friendly */
  328. }
  329. let now = Date.now(), updated = (storages.stats[key]) ? storages.stats[key].updated || 0 : 0, past = updated % STATSUPDATE, expire = updated - past + STATSUPDATE;
  330. if(now < expire) continue;/* still up-to-date */
  331. promises.push(new Promise(function(resolve, reject){
  332. timers.charts[i] = setTimeout(function(){
  333. fetch(li.dataset.scriptUrl + '/stats.csv')/* less file size than json */
  334. .then(response => response.text())
  335. .then(csv => {
  336. let lines = csv.split('\n');
  337. lines = lines.slice(1, -1);/* cut the labels + blank line */
  338. storages.stats[key] = {data: [], updated: now};
  339. for(let i = 0; lines[i]; i++){
  340. let p = lines[i].split(',');
  341. storages.stats[key].data[i] = {
  342. date: p[0],
  343. installs: parseInt(p[1]),
  344. updateChecks: parseInt(p[2]),
  345. };
  346. }
  347. core.drawChart(chart, storages.stats[key].data.slice(-DAYS));
  348. if(!chart.isConnected) li.appendChild(chart);
  349. resolve();
  350. });
  351. }, i * INTERVAL);/* server friendly */
  352. }));
  353. }
  354. if(promises.length) Promise.all(promises).then((values) => Storage.save('stats', storages.stats));
  355. },
  356. drawChart: function(chart, stats){
  357. let dl = chart.querySelector('dl'), dt = dl.querySelector('dt'), dd = dl.querySelector('dd');
  358. let chartKey = storages.chartKey, max = Math.max(DEFAULTMAX, ...stats.map(s => s[chartKey]));
  359. for(let i = last = stats.length - 1; stats[i]; i--){
  360. let date = stats[i].date, count = stats[i][chartKey];
  361. let dateDt = dl.querySelector(`dt[data-date="${date}"]`) || dt.cloneNode();
  362. let countDd = dateDt.nextElementSibling || dd.cloneNode();
  363. if(!dateDt.isConnected){
  364. dateDt.classList.remove('template');
  365. countDd.classList.remove('template');
  366. dateDt.dataset.date = dateDt.textContent = date;
  367. dl.insertBefore(countDd, dl.firstElementChild);
  368. dl.insertBefore(dateDt, dl.firstElementChild);
  369. }else{
  370. if(dl.dataset.chartKey === chartKey && dl.dataset.max === max && countDd.dataset.count === count && i < last) break;/* it doesn't need update any more. */
  371. }
  372. countDd.title = date + ': ' + count;
  373. countDd.dataset.count = count;
  374. if(i === last - 1){
  375. let label = countDd.querySelector('span') || document.createElement('span');
  376. label.textContent = toMetric(count);
  377. if(!label.isConnected) countDd.appendChild(label);
  378. }
  379. }
  380. dl.dataset.chartKey = chartKey, dl.dataset.max = max;
  381. /* for animation */
  382. animate(function(){
  383. for(let i = 0, dds = dl.querySelectorAll('dd.count:not(.template)'), dd; dd = dds[i]; i++){
  384. dd.style.height = ((dd.dataset.count / max) * 100) + '%';
  385. }
  386. });
  387. },
  388. addStyle: function(name = 'style'){
  389. let style = createElement(core.html[name]());
  390. document.head.appendChild(style);
  391. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  392. elements[name] = style;
  393. },
  394. html: {
  395. more: () => `
  396. <button class="more"></button>
  397. `,
  398. tabNavigation: () => `
  399. <nav id="tabNavigation">
  400. <ul>
  401. <li class="template"></li>
  402. </ul>
  403. </nav>
  404. `,
  405. chartSwitcher: () => `
  406. <nav id="chartSwitcher">
  407. <ul>
  408. <li class="template"></li>
  409. </ul>
  410. </nav>
  411. `,
  412. dt: (className, textContent) => `
  413. <dt class="${className}"><span>${textContent}</span></dt>
  414. `,
  415. ddLink: (className, textContent, href, title) => `
  416. <dd class="${className}"><a href="${href}" title="${title}">${textContent}</a></dd>
  417. `,
  418. chart: () => `
  419. <div class="chart">
  420. <dl>
  421. <dt class="template date"></dt>
  422. <dd class="template count"></dd>
  423. </dl>
  424. </div>
  425. `,
  426. style: () => `
  427. <style type="text/css">
  428. /* red scale: 103-206 */
  429. /* gray scale: 119-153-187-221 */
  430. /* coommon */
  431. h2, h3{
  432. margin: 0;
  433. }
  434. ul, ol{
  435. margin: 0;
  436. padding: 0 0 0 2em;
  437. }
  438. a:hover,
  439. a:focus{
  440. color: rgb(206,0,0);
  441. }
  442. .template{
  443. display: none !important;
  444. }
  445. section.text-content{
  446. position: relative;
  447. padding: 0;
  448. }
  449. section.text-content > *{
  450. margin: 14px;
  451. }
  452. section.text-content h2{
  453. text-align: left !important;
  454. margin-bottom: 0;
  455. }
  456. section > header + *{
  457. margin: 0 0 14px !important;
  458. }
  459. button.more{
  460. color: rgb(153,153,153);
  461. border: 1px solid rgb(187,187,187);
  462. background: white;
  463. padding: 0;
  464. cursor: pointer;
  465. }
  466. button.more::-moz-focus-inner{
  467. border: none;
  468. }
  469. button.more::after{
  470. font-size: medium;
  471. content: "▴";
  472. }
  473. .hidden > button.more{
  474. background: rgb(221, 221, 221);
  475. position: absolute;
  476. }
  477. .hidden > button.more::after{
  478. content: "▾";
  479. }
  480. /* User panel */
  481. section[data-selector="userSection"] > h2:only-child{
  482. margin-bottom: 14px;/* no content in user panel */
  483. }
  484. section[data-selector="userSection"].hidden{
  485. min-height: 5em;
  486. max-height: 10em;
  487. overflow: hidden;
  488. }
  489. section[data-selector="userSection"] > button.more{
  490. position: relative;
  491. bottom: 0;
  492. width: 100%;
  493. margin: 0;
  494. border: none;
  495. border-top: 1px solid rgba(187, 187, 187);
  496. border-radius: 0 0 5px 5px;
  497. }
  498. section[data-selector="userSection"].hidden > button.more{
  499. position: absolute;
  500. }
  501. /* Control panel */
  502. section#control-panel{
  503. font-size: smaller;
  504. width: 200px;
  505. position: absolute;
  506. top: 0;
  507. right: 0;
  508. z-index: 1;
  509. }
  510. section#control-panel h3{
  511. font-size: 1em;
  512. padding: .25em 1em;
  513. border-radius: 5px 5px 0 0;
  514. background: rgb(103, 0, 0);
  515. color: white;
  516. cursor: pointer;
  517. }
  518. section#control-panel.hidden h3{
  519. border-radius: 5px 5px 5px 5px;
  520. }
  521. section#control-panel h3::after{
  522. content: " ▴";
  523. margin-left: .25em;
  524. }
  525. section#control-panel.hidden h3::after{
  526. content: " ▾";
  527. }
  528. ul#user-control-panel{
  529. list-style-type: square;
  530. color: rgb(187, 187, 187);
  531. width: 100%;
  532. margin: .5em 0;
  533. padding: .5em .5em .5em 1.5em;
  534. -webkit-padding-start: 25px;/* ajustment for Chrome */
  535. background: white;
  536. border-radius: 0 0 5px 5px;
  537. border: 1px solid rgb(187, 187, 187);
  538. border-top: none;
  539. box-sizing: border-box;
  540. }
  541. section#control-panel.hidden > ul#user-control-panel{
  542. display: none;
  543. }
  544. /* Discussions on your scripts */
  545. #user-discussions-on-scripts-written{
  546. margin-top: 0;
  547. }
  548. /* tabs */
  549. #tabNavigation{
  550. display: inline-block;
  551. }
  552. #tabNavigation > ul{
  553. list-style-type: none;
  554. padding: 0;
  555. display: flex;
  556. }
  557. #tabNavigation > ul > li{
  558. font-weight: bold;
  559. background: white;
  560. padding: .25em 1em;
  561. border: 1px solid rgb(187, 187, 187);
  562. border-bottom: none;
  563. border-radius: 5px 5px 0 0;
  564. box-shadow: 0 0 5px rgb(221, 221, 221);
  565. }
  566. #tabNavigation > ul > li[data-selected="false"]{
  567. color: rgb(153,153,153);
  568. background: rgb(221, 221, 221);
  569. cursor: pointer;
  570. }
  571. [data-selector="scriptSets"] > section,
  572. [data-tabified] #user-script-list{
  573. border-radius: 0 5px 5px 5px;
  574. }
  575. [data-tabified] header{
  576. display: none;
  577. }
  578. [data-tabified][data-selected="false"]{
  579. display: none;
  580. }
  581. /* Scripts */
  582. [data-selector="scripts"] > div > section > header + p/* no scripts */{
  583. background: white;
  584. border: 1px solid rgb(187, 187, 187);
  585. border-radius: 0 5px 5px 5px;
  586. box-shadow: 0 0 5px rgb(221, 221, 221);
  587. padding: 14px;
  588. }
  589. #user-script-list li{
  590. padding: .25em 1em;
  591. position: relative;
  592. }
  593. #user-script-list li:last-child{
  594. border-bottom: none;/* missing in greasyfork.org */
  595. }
  596. #user-script-list li article{
  597. position: relative;
  598. z-index: 1;/* over the .chart */
  599. pointer-events: none;
  600. }
  601. #user-script-list li article h2 > a{
  602. padding-right: 4em;/* for chart's count number */
  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. })();