您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
It redesigns your own user page.
当前为
// ==UserScript== // @name GreasyFork User Dashboard // @name:ja GreasyFork User Dashboard // @namespace knoa.jp // @description It redesigns your own user page. // @description:ja 自分用の新しいユーザーページを提供します。 // @include https://greasyfork.org/*/users/* // @version 1.0.1 // @grant none // ==/UserScript== (function(){ const SCRIPTNAME = 'GreasyForkUserDashboard'; const DEBUG = false;/* 1.0.1 bug fix. */ if(window === top && console.time) console.time(SCRIPTNAME); const INTERVAL = 1000;/* for fetch */ const DEFAULTMAX = 10;/* for chart scale */ const DAYS = 180;/* for chart length */ const STATSUPDATE = 1000*60*60;/* stats update interval of greasyfork.org */ const TRANSLATIONEXPIRE = 1000*60*60*24*30;/* cache time for translations */ let site = { targets: { userSection: () => $('body > header + div > section:nth-of-type(1)'), controlPanel: () => $('#control-panel'), newScriptSetLink: () => $('a[href$="/sets/new"]'), scriptSets: () => $('body > header + div > section:nth-of-type(2)'), scripts: () => $('body > header + div > section:nth-of-type(2) + div'), userScriptSets: () => $('#user-script-sets'), userScriptList: () => $('#user-script-list'), }, get: { language: (d) => d.documentElement.lang, firstScript: (list) => list.querySelector('li h2 > a'), translation: (d) => {return { info: d.querySelector('#script-links > li.current').textContent, code: d.querySelector('#script-links > li > a[href$="/code"]').textContent, history: d.querySelector('#script-links > li > a[href$="/versions"]').textContent, feedback: d.querySelector('#script-links > li > a[href$="/feedback"]').textContent.replace(/\s\(\d+\)/, ''), stats: d.querySelector('#script-links > li > a[href$="/stats"]').textContent, derivatives: d.querySelector('#script-links > li > a[href$="/derivatives"]').textContent, update: d.querySelector('#script-links > li > a[href$="/versions/new"]').textContent, delete: d.querySelector('#script-links > li > a[href$="/delete"]').textContent, admin: d.querySelector('#script-links > li > a[href$="/admin"]').textContent, version: d.querySelector('#script-stats > dt.script-show-version').textContent, }}, props: (li) => {return { name: li.querySelector('h2 > a'), description: li.querySelector('.description'), stats: li.querySelector('dl.inline-script-stats'), dailyInstalls: li.querySelector('dd.script-list-daily-installs'), totalInstalls: li.querySelector('dd.script-list-total-installs'), ratings: li.querySelector('dd.script-list-ratings'), createdDate: li.querySelector('dd.script-list-created-date'), updatedDate: li.querySelector('dd.script-list-updated-date'), scriptVersion: li.dataset.scriptVersion, }}, } }; let translations = { 'en': { info: 'Info', code: 'Code', history: 'History', feedback: 'Feedback', stats: 'Stats', derivatives: 'Derivatives', update: 'Update', delete: 'Delete', admin: 'Admin', version: 'Version', } }, translation = translations['en']; let elements = {}, shown = {}; let core = { initialize: function(){ core.getElements(); if(elements.length < site.targets.length) return log('Not user own page.'); core.addStyle(); core.getTranslations(); core.hideUserSection(); core.hideControlPanel(); core.addTabNavigation(); core.addNewScriptSetLink(); core.rebuildScriptList(); }, getElements: function(){ if(!site.targets.controlPanel()) return;/* not my own page */ for(let i = 0, keys = Object.keys(site.targets); keys[i]; i++){ let element = site.targets[keys[i]](); if(!element) return log(`Not found: ${keys[i]}`); element.dataset.selector = keys[i]; elements[keys[i]] = element; } shown = Storage.read('shown') || shown; }, getTranslations: function(){ let language = site.get.language(document); translations = Storage.read('translations') || translations; translation = translations[language] || translation; if(site.get.language(document) === 'en' || Object.keys(translations).find((lang) => lang === language)) return; let firstScript = site.get.firstScript(elements.userScriptList); fetch(firstScript.href, {credentials: 'include'}) .then(response => response.text()) .then(text => new DOMParser().parseFromString(text, 'text/html')) .then(d => { translation = translations[site.get.language(d)] = site.get.translation(d); Storage.save('translations', translations, Date.now() + TRANSLATIONEXPIRE); }); }, hideUserSection: function(){ let userSection = elements.userSection, more = createElement(core.html.more()); if(!shown.userSection) userSection.classList.add('hidden'); more.addEventListener('click', function(e){ userSection.classList.toggle('hidden'); shown.userSection = !userSection.classList.contains('hidden'); Storage.save('shown', shown); }); userSection.appendChild(more); }, hideControlPanel: function(){ let controlPanel = elements.controlPanel, header = controlPanel.firstElementChild; if(!shown.controlPanel) controlPanel.classList.add('hidden'); elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px'; header.addEventListener('click', function(e){ controlPanel.classList.toggle('hidden'); shown.controlPanel = !controlPanel.classList.contains('hidden'); Storage.save('shown', shown); elements.userSection.style.minHeight = controlPanel.offsetHeight + controlPanel.offsetTop + 'px'; }); }, addTabNavigation: function(){ const tabs = [ {label: elements.scriptSets.querySelector('header').textContent, selector: 'scriptSets', list: elements.userScriptSets}, {label: elements.scripts.querySelector('header').textContent, selector: 'scripts', list: elements.userScriptList, selected: true}, ]; let nav = createElement(core.html.tabNavigation()), scriptSets = elements.scriptSets; let template = nav.querySelector('li.template'); scriptSets.parentNode.insertBefore(nav, scriptSets); for(let i = 0; tabs[i]; i++){ let tab = template.cloneNode(true); tab.classList.remove('template'); tab.textContent = tabs[i].label + ` (${tabs[i].list.children.length})`; tab.dataset.target = tabs[i].selector; tab.addEventListener('click', function(e){ tab.parentNode.querySelector('[data-selected="true"]').dataset.selected = 'false'; $('[data-tabified][data-selected="true"]').dataset.selected = 'false'; tab.dataset.selected = 'true'; $(`[data-selector="${tab.dataset.target}"]`).dataset.selected = 'true'; }); template.parentNode.insertBefore(tab, template); /**/ let target = elements[tabs[i].selector]; target.dataset.tabified = 'true'; if(tabs[i].selected) tab.dataset.selected = target.dataset.selected = 'true'; else tab.dataset.selected = target.dataset.selected = 'false'; } }, addNewScriptSetLink: function(){ let link = elements.newScriptSetLink.cloneNode(true), list = elements.userScriptSets, li = document.createElement('li'); li.appendChild(link); list.appendChild(li); }, rebuildScriptList: function(){ let stats = Storage.read('stats') || {}, promises = []; for(let i = 0, list = elements.userScriptList, li; li = list.children[i]; i++){ let more = createElement(core.html.more()), props = site.get.props(li); if(!shown[li.dataset.scriptName]) li.classList.add('hidden'); more.addEventListener('click', function(e){ li.classList.toggle('hidden'); shown[li.dataset.scriptName] = !li.classList.contains('hidden'); Storage.save('shown', shown); }); li.appendChild(more); /* attatch titles */ props.name.title = props.description.textContent.trim(); props.dailyInstalls.previousElementSibling.title = props.dailyInstalls.previousElementSibling.textContent; props.totalInstalls.previousElementSibling.title = props.totalInstalls.previousElementSibling.textContent; props.ratings.previousElementSibling.title = props.ratings.previousElementSibling.textContent; props.createdDate.previousElementSibling.title = props.createdDate.previousElementSibling.textContent; props.updatedDate.previousElementSibling.title = props.updatedDate.previousElementSibling.textContent; /* wrap the description to make it an inline element */ let span = document.createElement('span'); span.textContent = props.name.title; props.description.replaceChild(span, props.description.firstChild); /* Link to Code from Version */ let versionLabel = createElement(core.html.dt('script-list-version', translation.version)); let versionLink = createElement(core.html.ddLink('script-list-version', props.scriptVersion, props.name.href + '/code', translation.code)); versionLabel.title = versionLabel.textContent; props.stats.insertBefore(versionLabel, props.createdDate.previousElementSibling); props.stats.insertBefore(versionLink, props.createdDate.previousElementSibling); /* Link to Stats from Total installs */ let statsLink = createElement(core.html.ddLink('script-list-total-installs', props.totalInstalls.textContent, props.name.href + '/stats', translation.stats)); props.stats.replaceChild(statsLink, props.totalInstalls); /* Link to History from Updated date */ let historyLink = createElement(core.html.ddLink('script-list-updated-date', props.updatedDate.textContent, props.name.href + '/versions', translation.history)); props.stats.replaceChild(historyLink, props.updatedDate); /* Draw chart of daily update checks */ let chart = createElement(core.html.chart()); if(stats[li.dataset.scriptName]){ core.buildChart(chart, stats[li.dataset.scriptName].slice(-DAYS)); li.appendChild(chart); continue; }else promises.push(new Promise(function(resolve, reject){ setTimeout(function(){ fetch(props.name.href + '/stats.csv')/* less file size than json */ .then(response => response.text()) .then(csv => { let lines = csv.split('\n'); lines = lines.slice(1, -1);/* cut the labels + blank line */ stats[props.name.textContent] = []; for(let i = 0; lines[i]; i++){ let p = lines[i].split(','); stats[props.name.textContent][i] = { date: p[0], installs: parseInt(p[1]), updateChecks: parseInt(p[2]), }; } core.buildChart(chart, stats[li.dataset.scriptName].slice(-DAYS)); li.appendChild(chart); resolve(); }); }, i * INTERVAL);/* server friendly */ })); } Promise.all(promises) .then(() => { let now = Date.now(), past = now % STATSUPDATE, expire = now - past + STATSUPDATE; Storage.save('stats', stats, expire); }); }, buildChart: function(chart, stats){ let max = DEFAULTMAX; for(let i = 0; stats[i]; i++){ if(stats[i].updateChecks > max) max = stats[i].updateChecks; } let dl = chart.querySelector('dl'), dt = dl.querySelector('dt'), dd = dl.querySelector('dd'); for(let i = 0, last = stats.length - 1; stats[i]; i++){ let date = stats[i].date, installs = stats[i].installs, updateChecks = stats[i].updateChecks; let dateDt = dt.cloneNode(), countDd = dd.cloneNode(); dateDt.classList.remove('template'); countDd.classList.remove('template'); dateDt.textContent = date; countDd.title = date + ': ' + updateChecks + (updateChecks === 1 ? ' check' : ' checks'); countDd.style.height = ((updateChecks / max) * 100) + '%'; if(i === last - 1){ countDd.classList.add('last'); let label = document.createElement('span'); label.textContent = toMetric(updateChecks); countDd.appendChild(label); } dl.insertBefore(dateDt, dt); dl.insertBefore(countDd, dt); } }, addStyle: function(name = 'style'){ let style = createElement(core.html[name]()); document.head.appendChild(style); if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]); elements[name] = style; }, html: { more: () => ` <button class="more"></button> `, tabNavigation: () => ` <nav id="tabNavigation"> <ul> <li class="template"></li> </ul> </nav> `, dt: (className, textContent) => ` <dt class="${className}"><span>${textContent}</span></dt> `, ddLink: (className, textContent, href, title) => ` <dd class="${className}"><a href="${href}" title="${title}">${textContent}</a></dd> `, chart: () => ` <div class="chart"> <dl> <dt class="template date"></dt> <dd class="template count"></dd> </dl> </div> `, style: () => ` <style type="text/css"> /* gray scale: 119-153-187-221 */ /* coommon */ h2, h3{ margin: 0; } ul, ol{ margin: 0; padding: 0 0 0 2em; } .template{ display: none; } section.text-content{ position: relative; padding: 0; } section.text-content > *{ margin: 14px; } section.text-content h2{ text-align: left !important; margin-bottom: 0; } section > header + *{ margin: 0 0 14px !important; } button.more{ color: rgb(153,153,153); background: white; padding: 0; cursor: pointer; } button.more::-moz-focus-inner{ border: none; } button.more::after{ font-size: medium; content: "▴"; } .hidden > button.more{ background: rgb(221, 221, 221); position: absolute; } .hidden > button.more::after{ content: "▾"; } /* User panel */ section[data-selector="userSection"].hidden{ max-height: 10em; overflow: hidden; } section[data-selector="userSection"] > button.more{ position: relative; bottom: 0; width: 100%; margin: 0; border: none; border-top: 1px solid rgba(187, 187, 187); } section[data-selector="userSection"].hidden > button.more{ position: absolute; } /* Control panel */ section#control-panel{ font-size: smaller; width: 200px; position: absolute; top: 0; right: 0; z-index: 1; } section#control-panel h3{ font-size: 1em; padding: .25em 1em; border-radius: 5px 5px 0 0; background: rgb(103, 0, 0); color: white; cursor: pointer; } section#control-panel.hidden h3{ border-radius: 5px 5px 5px 5px; } section#control-panel h3::after{ content: " ▴"; margin-left: .25em; } section#control-panel.hidden h3::after{ content: " ▾"; } ul#user-control-panel{ list-style-type: square; color: rgb(187, 187, 187); width: 100%; margin: .5em 0; padding: .5em .5em .5em 1.5em; background: white; border-radius: 0 0 5px 5px; border: 1px solid rgb(187, 187, 187); border-top: none; box-sizing: border-box; } section#control-panel.hidden > ul#user-control-panel{ display: none; } /* Discussions on your scripts */ #user-discussions-on-scripts-written{ margin-top: 0; } /* tabs */ #tabNavigation > ul{ list-style-type: none; padding: 0; display: flex; } #tabNavigation > ul > li{ font-weight: bold; background: white; padding: .25em 1em; border: 1px solid rgb(187, 187, 187); border-bottom: none; border-radius: 5px 5px 0 0; box-shadow: 0 0 5px rgb(221, 221, 221); cursor: pointer; } #tabNavigation > ul > li:first-child{ } #tabNavigation > ul > li[data-selected="false"]{ color: rgb(153,153,153); background: rgb(221, 221, 221); } [data-selector="scriptSets"] > section, [data-tabified] #user-script-list{ border-radius: 0 5px 5px 5px; } [data-tabified] header{ display: none; } [data-tabified][data-selected="false"]{ display: none; } /* Scripts */ #user-script-list li{ padding: .25em 1em; position: relative; } #user-script-list li:last-child{ border-bottom: none;/* missing in greasyfork.org */ } #user-script-list li article{ position: relative; z-index: 1;/* over the .chart */ pointer-events: none; } #user-script-list li article h2{ margin-bottom: .25em; } #user-script-list li article h2 > a, #user-script-list li article h2 > .description/* it's block! */ > span, #user-script-list li article dl > dt > *, #user-script-list li article dl > dd > *{ pointer-events: auto;/* apply on inline elements */ } #user-script-list li button.more{ border: 1px solid rgb(221,221,221); border-radius: 5px; position: absolute; top: 0; right: 0; margin: 5px; width: 2em; z-index: 1;/* over the .chart */ } #user-script-list li .description{ font-size: small; margin: 0 0 0 .1em;/* ajust first letter position */ } #user-script-list li dl.inline-script-stats{ column-count: 3; max-height: 3em; } #user-script-list li dl.inline-script-stats dt{ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 200px;/* mysterious */ } #user-script-list li dl.inline-script-stats .script-list-author{ display: none; } #user-script-list li dl.inline-script-stats dt.script-list-daily-installs, #user-script-list li dl.inline-script-stats dt.script-list-total-installs{ width: 65%; } #user-script-list li dl.inline-script-stats dd.script-list-daily-installs, #user-script-list li dl.inline-script-stats dd.script-list-total-installs{ width: 35%; } #user-script-list li.hidden .description, #user-script-list li.hidden .inline-script-stats{ display: none; } /* chart */ .chart{ position: absolute; top: 0; right: 0; width: 100%; height: 100%; overflow: hidden; mask-image: linear-gradient(to right, rgba(0,0,0,.5), black); -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,.5), black); } .chart > dl{ position: absolute; bottom: 0; right: 2em; height: calc(100% - 5px); display: flex; align-items: flex-end; } .chart > dl > dt.date{ display: none; } .chart > dl > dd.count{ background: rgb(221,221,221); width: 3px; border-left: 1px solid white; margin: 0; } .chart > dl > dd.count.last, .chart > dl > dd.count:hover{ background: rgb(187,187,187); } .chart > dl > dd.count.last:hover{ background: rgb(153,153,153); } .chart > dl > dd.count.last > span{ font-weight: bold; color: rgb(153,153,153); position: absolute; top: 5px; right: 10px; pointer-events: none; } .chart > dl > dd.count.last:hover > span{ color: rgb(119,119,119); } /* sidebar */ .sidebar{ padding-top: 0; } .ad/* excuse me, it disappears only in my own user page :-) */, #script-list-filter{ display: none !important; } </style> `, }, }; class Storage{ static key(key){ return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key; } static save(key, value, expire = null){ key = Storage.key(key); localStorage[key] = JSON.stringify({ value: value, saved: Date.now(), expire: expire, }); } static read(key){ key = Storage.key(key); if(localStorage[key] === undefined) return undefined; let data = JSON.parse(localStorage[key]); if(data.value === undefined) return data; if(data.expire === undefined) return data; if(data.expire === null) return data.value; if(data.expire < Date.now()) return localStorage.removeItem(key); return data.value; } static delete(key){ key = Storage.key(key); delete localStorage.removeItem(key); } static saved(key){ key = Storage.key(key); if(localStorage[key] === undefined) return undefined; let data = JSON.parse(localStorage[key]); if(data.saved) return data.saved; else return undefined; } } const $ = function(s){return document.querySelector(s)}; const $$ = function(s){return document.querySelectorAll(s)}; const createElement = function(html){ let outer = document.createElement('div'); outer.innerHTML = html; return outer.firstElementChild; }; const toMetric = function(number, fixed = 1){ switch(true){ case(number < 1e3): return (number); case(number < 1e6): return (number/ 1e3).toFixed(fixed) + 'K'; case(number < 1e9): return (number/ 1e6).toFixed(fixed) + 'M'; case(number < 1e12): return (number/ 1e9).toFixed(fixed) + 'G'; default: return (number/1e12).toFixed(fixed) + 'T'; } }; const log = function(){ if(!DEBUG) return; let l = log.last = log.now || new Date(), n = log.now = new Date(); let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error); //console.log(error.stack); console.log( SCRIPTNAME + ':', /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3), /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's', /* :00 */ ':' + line, /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') + /* caller */ (callers[1] || '') + '()', ...arguments ); }; log.formats = [{ name: 'Firefox Scratchpad', detector: /MARKER@Scratchpad/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Console', detector: /MARKER@debugger/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Greasemonkey 3', detector: /\/gm_scripts\//, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1], getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Greasemonkey 4+', detector: /MARKER@user-script:/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500, getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Firefox Tampermonkey', detector: /MARKER@moz-extension:/, getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6, getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm), }, { name: 'Chrome Console', detector: /at MARKER \(<anonymous>/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1], getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm), }, { name: 'Chrome Tampermonkey', detector: /at MARKER \((userscript\.html|chrome-extension:)/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+)\)$/)[1] - 6, getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm), }, { name: 'Edge Console', detector: /at MARKER \(eval/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1], getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm), }, { name: 'Edge Tampermonkey', detector: /at MARKER \(Function/, getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4, getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm), }, { name: 'Safari', detector: /^MARKER$/m, getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/ getCallers: (e) => e.stack.split('\n'), }, { name: 'Default', detector: /./, getLine: (e) => 0, getCallers: (e) => [], }]; log.format = log.formats.find(function MARKER(f){ if(!f.detector.test(new Error().stack)) return false; //console.log('//// ' + f.name + '\n' + new Error().stack); return true; }); core.initialize(); if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME); })();