GC Stats Banner Library

This library provides the core functionality to add a stats badge onto profile and cache pages on geocaching.com.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/389508/880219/GC%20Stats%20Banner%20Library.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @exclude     *
// @supportURL	https://github.com/Cryo99/GCStatsBannerLib
// @version     1.0.2
// @include     /^https?://www\.geocaching\.com/(account/dashboard|my|default|geocache|profile|seek/cache_details|p)/
// @exclude     /^https?://www\.geocaching\.com/(login|about|articles|myfriends)/
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js

// ==UserLibrary==
// @name        GC Stats Banner Library
// @description This library provides the core functionality for adding a stats banner onto profile and cache pages on geocaching.com.
// @copyright   2019-2020, Cryo99 (https://github.com/Cryo99)
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html

// ==/UserScript==

// ==/UserLibrary==


/*jshint esversion: 6 */
var GCStatsBanner = function(cfg){

    // ========================= Private members =========================
	var _cfg = {},
		_cfgDefault = {
			cacheTitles: false,
			callerVersion: 'Unknown',
			elPrefix: '',
			imgScript: 'find-badge.php',
			logLevel: 'normal',
			seriesLevels: ['None'],
			seriesLevelDefault: 'None',
			seriesName: false,
			seriesURL: false
		};
		
		// Internal vars.
		_cacheName = document.getElementById("ctl00_ContentBody_CacheName"),
		// Images can be wider when level names are long. overflow: hidden; on <series>-container prevents images from overlaying the div border.
		_css = '',
		_profileNameOld = document.getElementById("ctl00_ContentBody_ProfilePanel1_lblMemberName"),
		_profileName = document.getElementById("ctl00_ProfileHead_ProfileHeader_lblMemberName"),
		_userField = document.getElementsByClassName("user-name");
	
		
	// ========================= Private methods =========================
	// Constructor.
	function _const(){
		for(cfgItem in _cfgDefault){
			if(typeof cfg[cfgItem] === 'undefined' || cfg[cfgItem] === null){
				// Required variable is undefined or null
				if(_cfgDefault[cfgItem] === false){
					throw new Error('GCStatsBannerLib: ' + cfgItem + ' is a required configuration element.');
				}
				// Use the default value.
				_cfg[cfgItem] = _cfgDefault[cfgItem];
				// console.warn('GCStatsBannerLib: ' + cfgItem + ' undefined. Using value: ' + _cfgDefault[cfgItem] + '.')
			}else{
				// Use the configured value. 
				_cfg[cfgItem] = cfg[cfgItem];
			}
		}
		// If elPrefix is still empty, generate it.
		if(!_cfg.elPrefix){
			_cfg.elPrefix = _getPrefix(cfg.seriesName);
		}
		_log(cfg, 'Passed config');
		_log(_cfg, 'Generated config');

		_generateCSS();
	}
	// Run the constructor on creation.
	_const();

	function _getPrefix(name){
		var matches = name.match(/\b(\w)/g);
		return matches.join('').toLowerCase().padStart(3, '_');
	}

	function _generateCSS(){
		_css = 'div.' + _cfg.elPrefix + '-container { border: 1px solid #b0b0b0; margin-top: 1.5em; padding: 0; text-align: center; overflow: hidden;} ' + 
			'.WidgetBody div.' + _cfg.elPrefix + '-container { border: none; } ' +
			'#ctl00_ContentBody_ProfilePanel1_pnlProfile div.' + _cfg.elPrefix + '-container { border: none; text-align: inherit;} ' +
			'a.' + _cfg.elPrefix + '-badge { background-color: white;} ' +
			'#ctl00_ContentBody_ProfilePanel1_pnlProfile div.' + _cfg.elPrefix + '-container {float: left}' +
			'#StatsComponents {background: white;}';
	}

	function _log(msg, desc){
		if(_cfg.logLevel === 'debug'){
			var msgStart = '%c' + _cfg.seriesName + ' Stats Debug (%s):';
			console.log(msgStart,  "color: yellow; font-style: italic; background-color: red;padding: 2px", desc, msg);
		}
	}

	function _getHiderName(){
		var i,
			links = document.getElementsByTagName("a"),
            pos;
            
		if(links){
			for(i = 0; i < links.length; i++){
				pos = links[i].href.indexOf("/seek/nearest.aspx?u=");
				if(pos !== -1){
					return decodeURIComponent(links[i].href.substr(pos + 21).replace(/\+/g, '%20'));
				}
			}
		}
	};

	function _parseNames(names){
		// Filter out null or undefined entries, convert commas to semicolons, then convert to a comma-separated string.
		return encodeURIComponent(names
				.filter(function (n){
					return n !== undefined;
				})
				.map(function (n){
					return (n + "").replace(/,/g, ";");
				})
				.join());
    };
    
    function _getHtml(uname, brand){
		return "<a class='" + _cfg.elPrefix + "-badge' href='https://www." + _cfg.seriesURL + "' title='" + _cfg.seriesName +
			" stats.'><img src='https://img." + _cfg.seriesURL + "/awards/" + _cfg.imgScript + "?name=" + uname + "&brand=" + brand + "' /></a>";
    };

	function _displayStats(stats, page, brand){
		var widget = document.createElement("div"),
			html = "",
			i,
			target;

		for(i = 0; i < stats.length; i++){
			var name = (stats[i].name + "")
				.replace(/;/g, ",")
				.replace(/'/g, "&apos;")
				.replace(/"/g, "&quot;");
			if(i === 0 || stats[i].name !== stats[0].name){
				html += _getHtml(name, brand);
			}
		}
		_log(html, 'Banner HTML');

		switch(page){
			case "my":
				target = document.getElementById("ctl00_ContentBody_lnkProfile");
				break;
			case "account":
				// New account dashboard.
				// The WidgetPanel is too slow to load and causes scripts to block so follow GClhII and just append the widget to the sidebar.
				target = document.querySelector(".sidebar-right");
				break;
			case "cache":
                target = document.getElementsByClassName('sidebar')[0];
                break;
			case "profile":
				if(_profileName){
					target = document.getElementById("ctl00_ContentBody_ProfilePanel1_lblProfile");
					if (target) {
						target = target.parentNode;
					}
				}else if(_profileNameOld){
					target = document.getElementById("HiddenProfileContent");
				}
				break;
		}

		if(!target){
			console.warn(_cfg.seriesName + " Stats: Aborted - couldn't find where to insert widget. You might not be logged in.");
			return;
		}

		if(html){
			widget.className = _cfg.elPrefix + "-container";
			widget.innerHTML = html;
            switch(page){
                case "my":
                case "profile":
                    target.parentNode.insertBefore(widget, target.nextSibling);
                    break;
				case "account":
					// If the StatsWidget isn't present, create it.
					var el = document.getElementById("StatsWidget");
					if(!el){
						_log('Creating widget.', 'StatsWidget');
						var divStats = document.createElement('div');
						divStats.id = "StatsWidget";
						divStats.classList.add("panel", "collapsible");
						divStats.innerHTML = '<div class="panel-header isActive" aria-expanded="true">\
	<h1 id="stats-widget-label" class="h5 no-margin">Statistics</h1>\
	<button aria-controls="StatsWidget" aria-labelledby="stats-widget-label">\
		<svg height="22" width="22" class="opener" role="img">\
			<use xlink:href="/account/app/ui-icons/sprites/global.svg#icon-expand-svg-fill"></use>\
		</svg>\
	</button>\
</div>\
<div id="StatsComponents" class="panel-body">\
	<div id="StatsPanel" class="widget-panel"></div>\
</div>';
				
						target.append(divStats);
						// Hide the panel if it was previously hidden. 
						if (!GM_getValue('statsWidget_visible', false)) {
							document.querySelector('#StatsWidget .panel-body').style.display = "none";
							document.querySelector('#StatsWidget .panel-header').classList.remove('isActive');
							_fadeOut(document.querySelector('#StatsWidget .panel-body'));
						}
		
						// Add the click handler.
						document.querySelector('#StatsWidget .panel-header').addEventListener('click', function() {
							if (GM_getValue('statsWidget_visible', true)) {
								document.querySelector('#StatsWidget .panel-header').classList.remove('isActive');
								_fadeOut(document.querySelector('#StatsWidget .panel-body'));
								GM_setValue('statsWidget_visible', false);
							}else{
								document.querySelector('#StatsWidget .panel-header').classList.add('isActive');
								_fadeIn(document.querySelector('#StatsWidget .panel-body'));
								GM_setValue('statsWidget_visible', true);
							}
						});
					}
					// Finally, append the banner.
					document.querySelector('#StatsPanel').appendChild(widget);				
					break;
				default:
					target.insertBefore(widget, target.firstChild.nextSibling.nextSibling);
					break;
            }
        }else{
			console.warn(_cfg.seriesName + " Stats: didn't generate an award badge.");
		}
	};

	function _fadeOut(element) {
		var op = 1;  // initial opacity
		var timer = setInterval(function () {
			if (op <= 0.1){
				clearInterval(timer);
				element.style.display = "none";
			}
			element.style.opacity = op;
			op -= 0.1;
		}, 30);
	};

	function _fadeIn(element) {
		var op = 0.1;  // initial opacity
		element.style.display = "block";
		var timer = setInterval(function () {
			if (op >= 1){
				clearInterval(timer);
			}
			element.style.opacity = op;
			op += 0.1;
		}, 30);
	};

	function _createConfigDlg(){
		// Register the menu item.
		GM_registerMenuCommand("Options", function(){
			GM_config.open();
		});

		GM_config.init({
			'id': _cfg.elPrefix + '_config', 				// The id used for this instance of GM_config
			'title': _cfg.seriesName + ' Stats',			// Panel Title
			'fields': { 									// Fields object
				'branding': {								// This is the id of the field
					'label': 'Branding', 					// Appears next to field
					'type': 'select', 						// Makes this setting a dropdown
					'options': _cfg.seriesLevels,			// Possible choices
					'default': _cfg.seriesLevelDefault		// Default value if user doesn't change it
				}
			},
			// Dialogue internal styles.
			'css': '#' + _cfg.elPrefix + '_config {position: static !important; width: 75% !important; margin: 1.5em auto !important; border: 10 !important;}' +
				'#' + _cfg.elPrefix + '_config_' + _cfg.elPrefix + '_branding_var {padding-top: 30px;} #' + _cfg.elPrefix + '_config button {color: black;}',
			'events': {
				'open': function(document, window, frame){
					// iframe styles.
					frame.style.width = '300px';
					frame.style.height = '250px';
					frame.style.left = parent.document.body.clientWidth / 2 - 150 + 'px';
					frame.style.borderWidth = '5px';
					frame.style.borderStyle = 'ridge';
					frame.style.borderColor = '#999999';
				},
				'save': function(){
					GM_setValue(_cfg.elPrefix + '_branding', GM_config.get('branding'));
					location.reload();                      // reload the page when configuration was changed
				}
			}
		});
	};

	function _init(){
		var currentPage,
			elCSS = document.createElement("style"),
			userName = "",
			userNames = [],
			stats = [];

		// Don't run on frames or iframes
		if(window.top !== window.self){
			return false;
		}

		if(/\/my\//.test(location.pathname)){
			// On a My Profile page
			currentPage = "my";
		}else if(/\/account\//.test(location.pathname)){
			// On a Profile page
			currentPage = "account";
		}else{
			if(_cacheName){
				// On a Geocache page...
				// var matcher = new RegExp(_cfg.seriesName, "i");
				// if(!matcher.test(_cacheName.innerHTML)){
					// ...but not the right cache series.
				// 	return;
				// }

				var titleFound = false;
				for(title in _cfg.cacheTitles){
					var matcher = new RegExp(_cfg.cacheTitles[title], "i");
					if(matcher.test(_cacheName.innerHTML)){
						titleFound = true;
					}
				}
				if(!titleFound){
					// ...but not the right cache series.
					return;
				}

				currentPage = "cache";
			}else{
				currentPage = "profile";
			}
		}
		_log(currentPage, 'Detected page');

		// We're going to display so we can announce ourselves and prepare the configuration dialogue.
		console.info(_cfg.seriesName + " Stats V" + _cfg.callerVersion);

		//CONFIG
		_createConfigDlg();
		var brand = GM_getValue(_cfg.elPrefix + '_branding', _cfg.seriesLevelDefault);
		_log(brand, 'Stats branding');
		brand = brand.toLowerCase()

		// Get hider details.
		var hider;
		switch(currentPage){
			case "profile":
				if(_profileName){
					userNames = [_profileName.textContent.trim()];
				}else if(_profileNameOld){
					userNames = [_profileNameOld.textContent.trim()];
				}
				break;
			default:
				if(_userField.length > 0){
					userNames.push(_userField[0].innerHTML.trim());
				}
				hider = _getHiderName();
				if(typeof hider !== 'undefined'){
					userNames.push(hider);
				}
				break;
		}
		_log(userNames[0], "Finder's name");
		_log(userNames[1], "Hider's name");
	
		for(var i = 0; i < userNames.length; i++){
			stats[i] = {name: userNames[i]};
		}
		_log(stats, 'Statistics');
	
		userName = _parseNames(userNames);
		if(!userName){
			console.error(_cfg.seriesName + " Stats: Aborted - couldn't work out user name");
			return;
		}

		
		// Inject widget styling
		elCSS.setAttribute('type', 'text/css');
		if(elCSS.styleSheet){
			elCSS.styleSheet.cssText = _css;
		}else{
			elCSS.appendChild(document.createTextNode(_css));
		}
		document.head.appendChild(elCSS);
		_displayStats(stats, currentPage, brand);
	
	}

    return {
      // ========================= Public members =========================

	  // ========================= Public methods =========================
	  init: _init
    };
  
};