Github User Info

Show inline user information on avatar hover.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name             Github User Info
// @id               Github_User_Info@https://github.com/jerone/UserScripts
// @namespace        https://github.com/jerone/UserScripts
// @description      Show inline user information on avatar hover.
// @author           jerone
// @copyright        2015+, jerone (https://github.com/jerone)
// @license          CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license          GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @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
// @icon             https://github.githubassets.com/pinned-octocat.svg
// @version          0.4.1
// @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==

// cSpell:ignore leaderboard, vcard, transform
/* eslint security/detect-object-injection: "off" */

(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("div");
	userName.style =
		"padding-left: 24px;" +
		"white-space: nowrap;" +
		"overflow: hidden;" +
		"text-overflow: ellipsis;" +
		"font-weight: bold;";
	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 "));
	var userJoinedText = unsafeWindow.document.createElement("relative-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 =
		"border-top: 1px solid #EEE;" +
		"clear: left;" +
		"display: flex;" +
		"justify-content: space-around;" +
		"text-align: center;" +
		"white-space: nowrap;";
	userMenu.appendChild(userCounts);

	var userFollowers = document.createElement("a");
	userFollowers.style = "display: none;" + "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;" + "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;" + "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;" + "text-decoration: none;";
	userOrgs.classList.add("vcard-stat");
	userOrgs.setAttribute("target", "_blank");
	userOrgs.setAttribute("title", "Public organizations");
	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;" + "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;" + "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(),
				upDate = new Date(
					now.setDate(now.getDate() - UPDATE_INTERVAL_DAYS),
				);
			if (date > upDate) {
				var data = users[username].data;
				// console.log('GithubUserInfo:getData', 'CACHED', data);
				fillData(defaultData(data), position, avatarSize);
			} else {
				// console.log('GithubUserInfo:getData', 'AJAX - OUTDATED', username, date, upDate);
				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.warn(
				"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);

		userAvatar.setAttribute("href", "https://github.com/" + data.username);
		userAvatarImg.style.height = avatarSize.height + "px";
		userAvatarImg.style.width = avatarSize.width + "px";
		userAvatarImg.addEventListener("load", function () {
			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";
			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
			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 === 30 ? "30+" : 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 ? "flex" : "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 & issue participant & users organization & organization member
				'.avatar-child[alt^="@"]', // Authored committed users
				'.gravatar[alt^="@"]', // Following & followers page
				'.timeline-comment-avatar[alt^="@"]', // GitHub comments author
				'.commits img[alt^="@"]', // Commits on user activity tab
				'.leaderboard-gravatar[alt^="@"]', // Trending developer: https://github.com/trending/developers
				".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);
			});
		});
	}

	// Init
	init();

	// Pjax
	document.addEventListener("pjax:end", init);
})();