GreasyFork User Dashboard

It redesigns user pages.

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址