Greasy Fork is available in English.
Show notable forks of GitHub projects.
// ==UserScript== // @name Lovely Forks // @namespace musically-ut // @version 2.7.0 // @description Show notable forks of GitHub projects. // @homepage https://github.com/musically-ut/lovely-forks // @icon https://github.com/musically-ut/lovely-forks/raw/master/userscript/icon.png // @author musically-ut // @match *://github.com/* // @grant GM_addStyle // @run-at document-end // ==/UserScript== /* WARNING: This usescript was created from a legacy version of Lovely Forks * which does not support user preferences setting and maintains code in pre-ES6 * JavaScript format which ought to be supported by most browsers out there. * If you are using Firefox or Chrome, it is best to use the respective * extension/addon for your browser for a better experience. */ 'use strict'; /*jshint browser: true, es5: true, sub:true */ var script_css = `/* placeholder */ .repohead-details-container .public:after { display: block; visibility: hidden; content: "."; font-size: 12px; line-height: 12px; } /* remove placeholder */ .repohead-details-container .has-lovely-forks.public:after { display: none; } /* smoother integration, show hierarchy */ .lovely-forks-addon { animation: fade-in 0.2s; padding-left: 1em; } .lovely-fork-style { font-size: 12px; } @keyframes fade-in { from { opacity: 0; } } `; GM_addStyle(script_css); var _logName = 'lovely-forks:'; var DEBUG = false; var text; var svgNS = 'http://www.w3.org/2000/svg'; function createIconSVG(type) { var svg = document.createElementNS(svgNS, 'svg'); svg.setAttributeNS(null, 'height', 12); svg.setAttributeNS(null, 'width', 10.5); svg.setAttributeNS(null, 'viewBox', '0 0 14 16'); svg.style['vertical-align'] = 'bottom'; svg.style['fill'] = 'currentColor'; svg.classList.add('opticon', 'opticon-' + type); var title = document.createElementNS(svgNS, 'title'); var iconPath = document.createElementNS(svgNS, 'path'); switch(type) { case 'star': title.appendChild(document.createTextNode('Number of stars')); iconPath.setAttributeNS(null, 'd', 'M14 6l-4.9-0.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14l4.33-2.33 4.33 2.33L10.4 9.26 14 6z'); break; case 'flame': title.appendChild(document.createTextNode('Fork may be more recent than upstream.')); iconPath.setAttributeNS(null, 'd', 'M5.05 0.31c0.81 2.17 0.41 3.38-0.52 4.31-0.98 1.05-2.55 1.83-3.63 3.36-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-0.3-6.61-0.61 2.03 0.53 3.33 1.94 2.86 1.39-0.47 2.3 0.53 2.27 1.67-0.02 0.78-0.31 1.44-1.13 1.81 3.42-0.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52 0.13-2.03 1.13-1.89 2.75 0.09 1.08-1.02 1.8-1.86 1.33-0.67-0.41-0.66-1.19-0.06-1.78 1.25-1.23 1.75-4.09-1.88-6.22l-0.02-0.02z'); iconPath.setAttributeNS(null, 'fill', '#d26911'); break; } iconPath.appendChild(title); svg.appendChild(iconPath); return svg; } function emptyElem(elem) { elem.textContent = ''; // How jQuery does it } function mbStrToMs(dateStr) { return dateStr !== null ? Date.parse(dateStr) : null; } function isExpired(timeMs) { var currentTime = new Date(); // The time of expiry of data is set to be an hour ago var expiryTimeMs = currentTime.valueOf() - 1000 * 60 * 60; return timeMs < expiryTimeMs; } function makeSelfDataKey(user, repo) { return 'lovely-forks@self:' + user + '/' + repo; } function makeRemoteDataKey(user, repo) { return 'lovely-forks@remote:' + user + '/' + repo; } var reDateKey = new RegExp('^lovely-forks@date:(.*)/(.*)$'); function makeTimeKey(user, repo) { return 'lovely-forks@date:' + user + '/' + repo; } function parseTimeKey(key) { var match = reDateKey.exec(key); if (match !== null) { return [match[1], match[2]]; } else { return null; } } function getForksElement() { // Verify that the element exists and it's still valid // otherwise, create it if (document.body.contains(text)) { return text; } // If the layout of the page changes, we'll have to change this location. // We should make sure that we do not accidentally cause errors here. var repoName = document.querySelector('strong[itemprop="name"]').parentNode; if (repoName) { try { text = document.createElement('span'); text.classList.add('lovely-fork-style', 'lovely-forks-addon'); repoName.appendChild(text); return text; } catch (e) { console.error(_logName, 'Error appending data to DOM', e); } } else { console.warn(_logName, 'Looks like the layout of the GitHub page has changed.'); } } function clearLocalStorage() { var keysToUnset = []; /* Remove all items which have expired. */ for(var ii = 0; ii < localStorage.length; ii++) { var key = localStorage.key(ii); var mbUserRepo = parseTimeKey(key); if (mbUserRepo !== null) { var timeMs = mbStrToMs(localStorage.getItem(key)); if (timeMs) { if (isExpired(timeMs)) { var user = mbUserRepo[0], repo = mbUserRepo[1]; keysToUnset.push(makeRemoteDataKey(user, repo)); keysToUnset.push(makeSelfDataKey(user, repo)); keysToUnset.push(makeTimeKey(user, repo)); } } else { console.warn(_logName, 'Unable to parse time: ', localStorage.getItem(key)); } } } keysToUnset.forEach(function (key) { if (DEBUG) { console.log(_logName, 'Removing key: ', key); } localStorage.removeItem(key); }); } function safeUpdateDOM(action, actionName) { // Get the stored version or create it if it doesn't exist var text = getForksElement(); // We should make sure that we do not accidentally cause errors here. if (text) { try { emptyElem(text); action(text); } catch (e) { console.error(_logName, 'Error appending data to DOM', e, 'during action', actionName); } } else { console.warn(_logName, 'Unable to find the lovely-forks loading indicator', 'during action', actionName); } } function showDetails(fullName, url, numStars, remoteIsNewer) { return function (text) { var forkA = document.createElement('a'); forkA.href = url; forkA.appendChild(document.createTextNode(fullName)); text.appendChild(document.createTextNode('also forked to ')); text.appendChild(forkA); text.appendChild(document.createTextNode(' ')); text.appendChild(createIconSVG('star')); text.appendChild(document.createTextNode('' + numStars + ' ')); if (remoteIsNewer) { text.appendChild(createIconSVG('flame')); } text.parentNode.classList.add('has-lovely-forks'); }; } function makeRemoteDataURL(user, repo) { return 'https://api.github.com/repos/' + user + '/' + repo + '/forks?sort=stargazers'; } function makeCommitDiffURL(user, repo, remoteUser, default_branch) { return 'https://api.github.com/repos/' + user + '/' + repo + '/compare/' + user + ':' + default_branch + '...' + remoteUser + ':' + default_branch; } // From: http://crocodillon.com/blog/always-catch-localstorage-security-and-quota-exceeded-errors function isQuotaExceeded(e) { var quotaExceeded = false; if (e) { if (e.code) { switch (e.code) { case 22: quotaExceeded = true; break; case 1014: // Firefox if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { quotaExceeded = true; } break; } } } return quotaExceeded; } function processWithData(user, repo, remoteDataStr, selfDataStr, isFreshData) { try { /* Parse fork data */ /* Can either be just one data element, * or could be the list of all forks. */ var allForksData = JSON.parse(remoteDataStr); var mostStarredFork = allForksData[0]; var forkUrl = mostStarredFork['html_url'], fullName = mostStarredFork['full_name']; /* Parse self data */ /* This could either be the commit-diff data (v2) * or `all_commits` data (v1). */ /* selfData can also be null, if the commit difference API r###lted in * an error. */ var selfData = JSON.parse(selfDataStr), selfDataToSave = selfData, remoteIsNewer = false; if (selfData !== null) { if (selfData.hasOwnProperty('ahead_by')) { // New version var diffData = selfData; remoteIsNewer = (diffData['ahead_by'] - diffData['behind_by']) > 0; } else { // Old version var allCommits = selfData; var remoteUpdateTimeMs = mbStrToMs(mostStarredFork['pushed_at']); if (!allCommits || allCommits.length < 1) { if (DEBUG) { console.log(_logName, 'Repository does not have any commits.'); } return; } var latestCommit = allCommits[0]['commit']; var committer = latestCommit['committer']; if (!committer) { if (DEBUG) { console.error(_logName, 'Could not find the latest committer.'); } return; } var selfUpdateTimeMs = mbStrToMs(committer['date']); remoteIsNewer = remoteUpdateTimeMs > selfUpdateTimeMs; selfDataToSave = [allCommits[0]]; } } else { remoteIsNewer = false; } /* Cache data, if necessary */ if (isFreshData) { var currentTimeMs = (new Date()).toString(); if (DEBUG) { console.log(_logName, 'Saving data'); } try { clearLocalStorage(); localStorage.setItem(makeTimeKey(user, repo), currentTimeMs); // Only the most starred fork is relevant var relevantRemoteDataStr = JSON.stringify([mostStarredFork]); localStorage.setItem(makeRemoteDataKey(user, repo), relevantRemoteDataStr); // Only the latest commit is relevant var relevantSelfDataStr = JSON.stringify(selfDataToSave); localStorage.setItem(makeSelfDataKey(user, repo), relevantSelfDataStr); } catch(e) { if (isQuotaExceeded(e)) { console.warn(_logName, 'localStorage quota full.'); } else { throw e; } } } // Now if the repository doesn't have any notable forks, so not // touch the DOM. var starGazers = mostStarredFork['stargazers_count']; if (!starGazers) { if (DEBUG) { console.log(_logName, 'Repo has only zero starred forks.'); } return; } safeUpdateDOM(showDetails(fullName, forkUrl, starGazers, remoteIsNewer), 'showing details'); } catch (e) { console.warn(_logName, 'Error while handling response: ', e); } } function onreadystatechangeFactory(xhr, successFn) { return function () { if (xhr.readyState === 4) { if (xhr.status === 200) { successFn(); } else if (xhr.status === 403) { console.warn(_logName, 'Looks like the rate-limit was exceeded.'); } else { console.warn(_logName, 'GitHub API returned status:', xhr.status); } } else { // Request is still in progress // Do nothing. } }; } function makeFreshRequest(user, repo) { var xhrFork = new XMLHttpRequest(); xhrFork.onreadystatechange = onreadystatechangeFactory( xhrFork, function () { var forksDataJson = JSON.parse(xhrFork.responseText); if (!forksDataJson || forksDataJson.length === 0) { if (DEBUG) { console.log(_logName, 'Repository does not have any forks.'); } return; } var mostStarredFork = forksDataJson[0], forksDataStr = JSON.stringify([mostStarredFork]); var defaultBranch = mostStarredFork['default_branch'], remoteUser = mostStarredFork['owner']['login']; var xhrDiff = new XMLHttpRequest(); xhrDiff.onreadystatechange = function () { if (xhrDiff.readyState === 4) { if (xhrDiff.status === 200) { var commitDiffJson = JSON.parse(xhrDiff.responseText); // Dropping the list of commits to conserve space. commitDiffJson['commits'] = []; var commitDiffStr = JSON.stringify(commitDiffJson); processWithData(user, repo, forksDataStr, commitDiffStr, true); } else { // In case of any error, ignore recency data. processWithData(user, repo, forksDataStr, null, true); } } }; xhrDiff.open('GET', makeCommitDiffURL(user, repo, remoteUser, defaultBranch)); xhrDiff.send(); } ); xhrFork.open('GET', makeRemoteDataURL(user, repo)); xhrFork.send(); } function getDataFor(user, repo) { var lfTimeKey = makeTimeKey(user, repo), lfRemoteDataKey = makeRemoteDataKey(user, repo), lfSelfDataKey = makeSelfDataKey(user, repo); var ret = { hasData: false }; var savedRemoteDataStr = localStorage.getItem(lfRemoteDataKey); var savedSelfDataStr = localStorage.getItem(lfSelfDataKey); var saveTimeMs = mbStrToMs(localStorage.getItem(lfTimeKey)); if (saveTimeMs === null || savedRemoteDataStr === null || savedSelfDataStr === null) { return ret; } ret.hasData = true; ret.saveTimeMs = saveTimeMs; ret.savedRemoteDataStr = savedRemoteDataStr; ret.savedSelfDataStr = savedSelfDataStr; return ret; } function runFor(user, repo) { try { var cache = getDataFor(user, repo); if (cache.hasData && !isExpired(cache.saveTimeMs)) { if (DEBUG) { console.log(_logName, 'Reusing saved data.'); } processWithData(user, repo, cache.savedRemoteDataStr, cache.savedSelfDataStr, false); } else { if (DEBUG) { console.log(_logName, 'Requesting the data from GitHub API.'); } makeFreshRequest(user, repo); } } catch (e) { console.error(_logName, 'Could not run for ', user + '/' + repo, 'Exception: ', e); } } /* Script execution */ var pathComponents = window.location.pathname.split('/'); if (pathComponents.length >= 3) { var user = pathComponents[1], repo = pathComponents[2]; runFor(user, repo); } else { if (DEBUG) { console.log(_logName, 'The URL did not identify a username/repository pair.'); } }