Github User Info

Show inline user information on avatar hover.

Stan na 14-04-2015. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @id          Github_User_Info@https://github.com/jerone/UserScripts
// @name        Github User Info
// @namespace   https://github.com/jerone/UserScripts
// @description Show inline user information on avatar hover.
// @author      jerone
// @copyright   2015+, jerone (http://jeroenvanwarmerdam.nl)
// @license     GNU GPLv3
// @homepage    https://github.com/jerone/UserScripts/tree/master/Github_User_Info
// @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_User_Info
// @supportURL  https://github.com/jerone/UserScripts/issues
// @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCYMHWQ7ZMBKW
// @version     0.3.0
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       unsafeWindow
// @run-at      document-end
// @include     https://github.com/*
// @include     https://gist.github.com/*
// ==/UserScript==

(function() {

	function proxy(fn) {
		return function proxyScope() {
			var that = this;
			return function proxyEvent(e) {
				var args = that.slice(0); // clone;
				args.unshift(e); // prepend event;
				fn.apply(this, args);
			};
		}.call([].slice.call(arguments, 1));
	}

	var _timer;

	var userMenu = document.createElement('div');
	userMenu.style =
		'display: none;' +
		'background-color: #F5F5F5;' +
		'border-radius: 3px;' +
		'border: 1px solid #DDDDDD;' +
		'box-shadow: 0 0 10px rgba(0, 0, 1, 0.1);' +
		'font-size: 11px;' +
		'padding: 10px;' +
		'position: absolute;' +
		'width: 335px;' +
		'z-index: 99;';
	userMenu.classList.add('GithubUserInfo');
	userMenu.addEventListener('mouseleave', function mouseleave() {
		console.log('GithubUserInfo:userMenu', 'mouseleave');
		window.clearTimeout(_timer);
		userMenu.style.display = 'none';
	});
	document.body.appendChild(userMenu);


	var userAvatar = document.createElement('a');
	userAvatar.style =
		'width: 100px;' +
		'height: 100px;' +
		'float: left;' +
		'margin-bottom: 10px;';
	userMenu.appendChild(userAvatar);
	var userAvatarImg = document.createElement('img');
	userAvatarImg.style =
		'border-radius: 3px;' +
		'transition-property: height, width;' +
		'transition-duration: 0.5s;';
	userAvatar.appendChild(userAvatarImg);


	var userInfo = document.createElement('div');
	userInfo.style =
		'width: 100%;' +
		'padding-left: 102px;';
	userMenu.appendChild(userInfo);

	var userName = document.createElement('strong');
	userName.style =
		'padding-left: 24px;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userName);

	var userCompany = document.createElement('div');
	userCompany.style =
		'display: none;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userCompany);
	var userCompanyIcon = document.createElement('span');
	userCompanyIcon.classList.add('octicon', 'octicon-organization');
	userCompanyIcon.style =
		'width: 24px;' +
		'text-align: center;' +
		'color: #CCC;';
	userCompany.appendChild(userCompanyIcon);
	var userCompanyText = document.createElement('span');
	userCompany.appendChild(userCompanyText);
	var userCompanyAdmin = document.createElement('span');
	userCompanyAdmin.style =
		'display: none;' +
		'margin-left: 5px;' +
		'position: relative;' +
		'top: -1px;' +
		'padding: 2px 5px;' +
		'font-size: 10px;' +
		'font-weight: bold;' +
		'color: #FFF;' +
		'text-transform: uppercase;' +
		'background-color: #4183C4;' +
		'border-radius: 3px;';
	userCompanyAdmin.appendChild(document.createTextNode('Staff'));
	userCompany.appendChild(userCompanyAdmin);

	var userLocation = document.createElement('div');
	userLocation.style =
		'display: none;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userLocation);
	var userLocationIcon = document.createElement('span');
	userLocationIcon.classList.add('octicon', 'octicon-location');
	userLocationIcon.style =
		'width: 24px;' +
		'text-align: center;' +
		'color: #CCC;';
	userLocation.appendChild(userLocationIcon);
	var userLocationText = document.createElement('a');
	userLocationText.setAttribute('target', '_blank');
	userLocation.appendChild(userLocationText);

	var userMail = document.createElement('div');
	userMail.style =
		'display: none;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userMail);
	var userMailIcon = document.createElement('span');
	userMailIcon.classList.add('octicon', 'octicon-mail');
	userMailIcon.style =
		'width: 24px;' +
		'text-align: center;' +
		'color: #CCC;';
	userMail.appendChild(userMailIcon);
	var userMailText = document.createElement('a');
	userMail.appendChild(userMailText);

	var userLink = document.createElement('div');
	userLink.style =
		'display: none;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userLink);
	var userLinkIcon = document.createElement('span');
	userLinkIcon.classList.add('octicon', 'octicon-link');
	userLinkIcon.style =
		'width: 24px;' +
		'text-align: center;' +
		'color: #CCC;';
	userLink.appendChild(userLinkIcon);
	var userLinkText = document.createElement('a');
	userLinkText.setAttribute('target', '_blank');
	userLink.appendChild(userLinkText);

	var userJoined = document.createElement('div');
	userJoined.style =
		'display: none;' +
		'white-space: nowrap;' +
		'overflow: hidden;' +
		'text-overflow: ellipsis;';
	userInfo.appendChild(userJoined);
	var userJoinedIcon = document.createElement('span');
	userJoinedIcon.classList.add('octicon', 'octicon-clock');
	userJoinedIcon.style =
		'width: 24px;' +
		'text-align: center;' +
		'color: #CCC;';
	userJoined.appendChild(userJoinedIcon);
	userJoined.appendChild(document.createTextNode('Joined on '));
	var userJoinedText = unsafeWindow.document.createElement('time', 'local-time'); // https://github.com/github/time-elements
	userJoinedText.setAttribute('day', 'numeric');
	userJoinedText.setAttribute('month', 'short');
	userJoinedText.setAttribute('year', 'numeric');
	userJoined.appendChild(userJoinedText);


	var userCounts = document.createElement('div');
	userCounts.style =
		'text-align: center;' +
		'border-top: 1px solid #EEE;' +
		'padding-top: 5px;' +
		'margin-top: 10px;' +
		'clear: left;';
	userMenu.appendChild(userCounts);

	var userFollowers = document.createElement('a');
	userFollowers.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userFollowers.classList.add('vcard-stat');
	userFollowers.setAttribute('target', '_blank');
	userFollowers.setAttribute('title', 'Followers');
	userCounts.appendChild(userFollowers);
	var userFollowersCount = document.createElement('strong');
	userFollowersCount.style =
		'display: block;' +
		'font-size: 28px;';
	userFollowers.appendChild(userFollowersCount);
	var userFollowersText = document.createElement('span');
	userFollowersText.appendChild(document.createTextNode('Followers'));
	userFollowersText.classList.add('text-muted');
	userFollowers.appendChild(userFollowersText);

	var userFollowing = document.createElement('a');
	userFollowing.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userFollowing.classList.add('vcard-stat');
	userFollowing.setAttribute('target', '_blank');
	userFollowing.setAttribute('title', 'Following');
	userCounts.appendChild(userFollowing);
	var userFollowingCount = document.createElement('strong');
	userFollowingCount.style =
		'display: block;' +
		'font-size: 28px;';
	userFollowing.appendChild(userFollowingCount);
	var userFollowingText = document.createElement('span');
	userFollowingText.appendChild(document.createTextNode('Following'));
	userFollowingText.classList.add('text-muted');
	userFollowing.appendChild(userFollowingText);

	var userRepos = document.createElement('a');
	userRepos.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userRepos.classList.add('vcard-stat');
	userRepos.setAttribute('target', '_blank');
	userRepos.setAttribute('title', 'Public repositories');
	userCounts.appendChild(userRepos);
	var userReposCount = document.createElement('strong');
	userReposCount.style =
		'display: block;' +
		'font-size: 28px;';
	userRepos.appendChild(userReposCount);
	var userReposText = document.createElement('span');
	userReposText.appendChild(document.createTextNode('Repos'));
	userReposText.classList.add('text-muted');
	userRepos.appendChild(userReposText);

	var userOrgs = document.createElement('a');
	userOrgs.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userOrgs.classList.add('vcard-stat');
	userOrgs.setAttribute('target', '_blank');
	userOrgs.setAttribute('title', 'Public organisations');
	userCounts.appendChild(userOrgs);
	var userOrgsCount = document.createElement('strong');
	userOrgsCount.style =
		'display: block;' +
		'font-size: 28px;';
	userOrgs.appendChild(userOrgsCount);
	var userOrgsText = document.createElement('span');
	userOrgsText.appendChild(document.createTextNode('Orgs'));
	userOrgsText.classList.add('text-muted');
	userOrgs.appendChild(userOrgsText);

	var userMembers = document.createElement('a');
	userMembers.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userMembers.classList.add('vcard-stat');
	userMembers.setAttribute('target', '_blank');
	userMembers.setAttribute('title', 'Public members');
	userCounts.appendChild(userMembers);
	var userMembersCount = document.createElement('strong');
	userMembersCount.style =
		'display: block;' +
		'font-size: 28px;';
	userMembers.appendChild(userMembersCount);
	var userMembersText = document.createElement('span');
	userMembersText.appendChild(document.createTextNode('Members'));
	userMembersText.classList.add('text-muted');
	userMembers.appendChild(userMembersText);

	var userGists = document.createElement('a');
	userGists.style =
		'display: none;' +
		'float: left;' +
		'width: 20%;' +
		'text-decoration: none;';
	userGists.classList.add('vcard-stat');
	userGists.setAttribute('target', '_blank');
	userGists.setAttribute('title', 'Public gists');
	userCounts.appendChild(userGists);
	var userGistsCount = document.createElement('strong');
	userGistsCount.style =
		'display: block;' +
		'font-size: 28px;';
	userGists.appendChild(userGistsCount);
	var userGistsText = document.createElement('span');
	userGistsText.appendChild(document.createTextNode('Gists'));
	userGistsText.classList.add('text-muted');
	userGists.appendChild(userGistsText);


	var UPDATE_INTERVAL_DAYS = 7;

	function getData(elm) {
		var username;
		if (elm.getAttribute('alt')) {
			username = elm.getAttribute('alt').replace('@', '');
		} else if (elm.parentNode.parentNode.querySelector('.author')) {
			username = elm.parentNode.parentNode.querySelector('.author').textContent.trim();
		} else {
			return;
		}

		var rect = elm.getBoundingClientRect();
		var position = {
			top: rect.top + window.scrollY,
			left: rect.left + window.scrollX
		};
		var avatarSize = {
			height: elm.height,
			width: elm.width
		};

		var usersString = GM_getValue('users', '{}');
		var users = JSON.parse(usersString);
		if (users[username]) {
			var date = new Date(users[username].checked_at),
				now = new Date();
			if (date > now.setDate(now.getDate() - UPDATE_INTERVAL_DAYS)) {
				var data = users[username].data;
				console.log('GithubUserInfo:getData', 'CACHED', data);
				fillData(defaultData(data), position, avatarSize);
			} else {
				console.log('GithubUserInfo:getData', 'AJAX - OUTDATED', username);
				fetchData(username, position, avatarSize);
			}
		} else {
			console.log('GithubUserInfo:getData', 'AJAX - NON-EXISTING', username);
			fetchData(username, position, avatarSize);
		}
	}

	function fetchData(username, position, avatarSize) {
		console.log('GithubUserInfo:fetchData', username);
		GM_xmlhttpRequest({
			method: 'GET',
			url: 'https://api.github.com/users/' + username,
			onload: proxy(parseUserData, position, avatarSize)
		});
	}

	function parseUserData(response, position, avatarSize) {
		var dataParsed = parseRawData(response.responseText);
		if (!dataParsed) {
			return;
		}
		var data = defaultData(normalizeData(dataParsed));
		console.log('GithubUserInfo:parseUserData', data.username);

		GM_xmlhttpRequest({
			method: 'GET',
			url: 'https://api.github.com/users/' + data.username + '/orgs',
			onload: proxy(parseOrgsData, position, avatarSize, data)
		});
	}

	function parseOrgsData(response, position, avatarSize, data) {
		var dataParsed = parseRawData(response.responseText);
		if (!dataParsed) {
			return;
		}
		data.orgs = dataParsed.length;
		console.log('GithubUserInfo:parseOrgsData', data.username, data.orgs);

		switch (data.type) {
			case 'Organization':
				{
					GM_xmlhttpRequest({
						method: 'GET',
						url: 'https://api.github.com/orgs/' + data.username + '/members',
						onload: proxy(parseMembersData, position, avatarSize, data)
					});
					break;
				}
			default:
				{
					fillData(data, position, avatarSize);
					setData(data, data.username);
					break;
				}
		}
	}

	function parseMembersData(response, position, avatarSize, data) {
		var dataParsed = parseRawData(response.responseText);
		if (!dataParsed) {
			return;
		}
		data.members = dataParsed.length;
		console.log('GithubUserInfo:parseMembersData', data.username, data.members);

		fillData(data, position, avatarSize);
		setData(data, data.username);
	}

	function parseRawData(data) {
		data = JSON.parse(data);
		if (data.message && data.message.startsWith('API rate limit exceeded')) {
			console.log('GithubUserInfo:parseRawData', 'API RATE LIMIT EXCEEDED');
			return;
		}
		return data;
	}

	function normalizeData(data) {
		return {
			'username': data.login,
			'avatar': data.avatar_url,
			'type': data.type,
			'name': data.name,
			'company': data.company,
			'blog': data.blog,
			'location': data.location,
			'mail': data.email,
			'repos': data.public_repos,
			'gists': data.public_gists,
			'followers': data.followers,
			'following': data.following,
			'created_at': data.created_at,
			'admin': !!data.site_admin
		};
	}

	function defaultData(data) {
		return {
			'username': data.username,
			'avatar': data.avatar,
			'type': data.type,
			'name': data.name || data.username,
			'company': data.admin ? 'GitHub' : data.company || '',
			'blog': data.blog || '',
			'location': data.location || '',
			'mail': data.mail || '',
			'repos': data.repos || 0,
			'gists': data.gists || 0,
			'followers': data.followers || 0,
			'following': data.following || 0,
			'created_at': data.created_at,
			'admin': data.admin || false,
			'orgs': data.orgs || 0,
			'members': data.members || 0
		};
	}

	function setData(data, username) {
		console.log('GithubUserInfo:setData', username, data);
		var usersString = GM_getValue('users', '{}');
		var users = JSON.parse(usersString);
			users[username] = {
				checked_at: (new Date()).toJSON(),
				data: data
			};
		GM_setValue('users', JSON.stringify(users));
	}

	function fillData(data, position, avatarSize) {
		console.log('GithubUserInfo:fillData', data, position, avatarSize);
		userMenu.style.top = Math.max(position.top - 10 - 1, 2) + 'px';
		userMenu.style.left = Math.max(position.left - 10 - 1, 2) + 'px';
		userMenu.style.display = 'block';

		userAvatar.setAttribute('href', 'https://github.com/' + data.username);
		userAvatarImg.style.height = avatarSize.height + 'px';
		userAvatarImg.style.width = avatarSize.width + 'px';
		window.setTimeout(function avatarAnimationTimeout() {
			userAvatarImg.style.height = '100px';
			userAvatarImg.style.width = '100px';
		}, 50);
		userAvatarImg.setAttribute('src', '');
		userAvatarImg.setAttribute('src', data.avatar);

		userName.setAttribute('title', data.username);
		userName.textContent = data.name;

		if (hasValue(data.company, userCompany)) {
			userCompanyText.textContent = data.company;
			userCompanyAdmin.style.display = data.admin ? 'inline' : 'none';
		}
		if (hasValue(data.location, userLocation)) {
			userLocationText.setAttribute('href', 'https://maps.google.com/maps?q=' + encodeURIComponent(data.location));
			userLocationText.textContent = data.location;
		}
		if (hasValue(data.mail, userMail)) {
			userMailText.setAttribute('href', 'mailto:' + data.mail);
			userMailText.textContent = data.mail;
		}
		if (hasValue(data.blog, userLink)) {
			userLinkText.setAttribute('href', data.blog);
			userLinkText.textContent = data.blog;
		}
		if (hasValue(data.created_at, userJoined)) {
			userJoinedText.setAttribute('datetime', data.created_at);
		}

		var userCountsHasValue = false;
		if (hasValue(data.followers, userFollowers)) {
			userCountsHasValue = true;
			userFollowers.setAttribute('href', 'https://github.com/' + data.username + '/followers');
			userFollowersCount.textContent = data.followers;
		}
		if (hasValue(data.following, userFollowing)) {
			userCountsHasValue = true;
			userFollowing.setAttribute('href', 'https://github.com/' + data.username + '/following');
			userFollowingCount.textContent = data.following;
		}
		if (hasValue(true, userRepos)) { // Always show repos count, as long another count is shown too;
			userCountsHasValue = userCountsHasValue ? true : !!data.repos;
			userRepos.setAttribute('href', 'https://github.com/' + data.username + '?tab=repositories');
			userReposCount.textContent = data.repos;
		}
		if (hasValue(data.orgs, userOrgs)) {
			userCountsHasValue = true;
			userOrgs.setAttribute('href', 'https://github.com/' + data.username);
			userOrgsCount.textContent = data.orgs;
		}
		if (hasValue(data.members, userMembers)) {
			userCountsHasValue = true;
			userMembers.setAttribute('href', 'https://github.com/orgs/' + data.username + '/people');
			userMembersCount.textContent = data.members;
		}
		if (hasValue(data.gists, userGists)) {
			userCountsHasValue = true;
			userGists.setAttribute('href', 'https://gist.github.com/' + data.username);
			userGistsCount.textContent = data.gists;
		}
		userCounts.style.display = userCountsHasValue ? 'block' : 'none';

		//if (data.type === 'Organization' || data.type === 'User') {}
	}

	function hasValue(property, elm) {
		elm.style.display = property ? 'block' : 'none';
		return !!property;
	}


	function init() {
		var avatars = document.querySelectorAll([
			'.avatar[alt^="@"]', // Logged-in user & commits author & issuse participant & users organization & organization member;
			'.timeline-comment-avatar[alt^="@"]', // GitHub comments author;
			'.gist-author img', // Gist author;
			'.gist .js-discussion .timeline-comment-avatar' // Gist comments author;
		].join(','));
		Array.prototype.forEach.call(avatars, function avatarsForEach(avatar) {
			avatar.addEventListener('mouseenter', function mouseenter() {
				console.log('GithubUserInfo:avatar', 'mouseenter');
				_timer = window.setTimeout(function mouseenterTimer() {
					console.log('GithubUserInfo:avatar', 'timeout');
					getData(this);
				}.bind(this), 500);
			});
			avatar.addEventListener('mouseleave', function mouseleave() {
				console.log('GithubUserInfo:avatar', 'mouseleave');
				window.clearTimeout(_timer);
			});
		});
	}

	// Page load;
	console.log('GithubUserInfo', 'page load');
	init();

	// On pjax;
	unsafeWindow.$(document).on("pjax:end", exportFunction(function pjaxEnd() {
		console.log('GithubUserInfo', 'pjax');
		init();
	}, unsafeWindow));

})();