Greasy Fork is available in English.

GC Stats Banner Library

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

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/389508/880219/GC%20Stats%20Banner%20Library.js

// ==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
    };
  
};