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.cn-greasyfork.org/scripts/389508/880219/GC%20Stats%20Banner%20Library.js

  1. // ==UserScript==
  2. // @exclude *
  3. // @supportURL https://github.com/Cryo99/GCStatsBannerLib
  4. // @version 1.0.2
  5. // @include /^https?://www\.geocaching\.com/(account/dashboard|my|default|geocache|profile|seek/cache_details|p)/
  6. // @exclude /^https?://www\.geocaching\.com/(login|about|articles|myfriends)/
  7. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  8.  
  9. // ==UserLibrary==
  10. // @name GC Stats Banner Library
  11. // @description This library provides the core functionality for adding a stats banner onto profile and cache pages on geocaching.com.
  12. // @copyright 2019-2020, Cryo99 (https://github.com/Cryo99)
  13. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  14.  
  15. // ==/UserScript==
  16.  
  17. // ==/UserLibrary==
  18.  
  19.  
  20. /*jshint esversion: 6 */
  21. var GCStatsBanner = function(cfg){
  22.  
  23. // ========================= Private members =========================
  24. var _cfg = {},
  25. _cfgDefault = {
  26. cacheTitles: false,
  27. callerVersion: 'Unknown',
  28. elPrefix: '',
  29. imgScript: 'find-badge.php',
  30. logLevel: 'normal',
  31. seriesLevels: ['None'],
  32. seriesLevelDefault: 'None',
  33. seriesName: false,
  34. seriesURL: false
  35. };
  36. // Internal vars.
  37. _cacheName = document.getElementById("ctl00_ContentBody_CacheName"),
  38. // Images can be wider when level names are long. overflow: hidden; on <series>-container prevents images from overlaying the div border.
  39. _css = '',
  40. _profileNameOld = document.getElementById("ctl00_ContentBody_ProfilePanel1_lblMemberName"),
  41. _profileName = document.getElementById("ctl00_ProfileHead_ProfileHeader_lblMemberName"),
  42. _userField = document.getElementsByClassName("user-name");
  43. // ========================= Private methods =========================
  44. // Constructor.
  45. function _const(){
  46. for(cfgItem in _cfgDefault){
  47. if(typeof cfg[cfgItem] === 'undefined' || cfg[cfgItem] === null){
  48. // Required variable is undefined or null
  49. if(_cfgDefault[cfgItem] === false){
  50. throw new Error('GCStatsBannerLib: ' + cfgItem + ' is a required configuration element.');
  51. }
  52. // Use the default value.
  53. _cfg[cfgItem] = _cfgDefault[cfgItem];
  54. // console.warn('GCStatsBannerLib: ' + cfgItem + ' undefined. Using value: ' + _cfgDefault[cfgItem] + '.')
  55. }else{
  56. // Use the configured value.
  57. _cfg[cfgItem] = cfg[cfgItem];
  58. }
  59. }
  60. // If elPrefix is still empty, generate it.
  61. if(!_cfg.elPrefix){
  62. _cfg.elPrefix = _getPrefix(cfg.seriesName);
  63. }
  64. _log(cfg, 'Passed config');
  65. _log(_cfg, 'Generated config');
  66.  
  67. _generateCSS();
  68. }
  69. // Run the constructor on creation.
  70. _const();
  71.  
  72. function _getPrefix(name){
  73. var matches = name.match(/\b(\w)/g);
  74. return matches.join('').toLowerCase().padStart(3, '_');
  75. }
  76.  
  77. function _generateCSS(){
  78. _css = 'div.' + _cfg.elPrefix + '-container { border: 1px solid #b0b0b0; margin-top: 1.5em; padding: 0; text-align: center; overflow: hidden;} ' +
  79. '.WidgetBody div.' + _cfg.elPrefix + '-container { border: none; } ' +
  80. '#ctl00_ContentBody_ProfilePanel1_pnlProfile div.' + _cfg.elPrefix + '-container { border: none; text-align: inherit;} ' +
  81. 'a.' + _cfg.elPrefix + '-badge { background-color: white;} ' +
  82. '#ctl00_ContentBody_ProfilePanel1_pnlProfile div.' + _cfg.elPrefix + '-container {float: left}' +
  83. '#StatsComponents {background: white;}';
  84. }
  85.  
  86. function _log(msg, desc){
  87. if(_cfg.logLevel === 'debug'){
  88. var msgStart = '%c' + _cfg.seriesName + ' Stats Debug (%s):';
  89. console.log(msgStart, "color: yellow; font-style: italic; background-color: red;padding: 2px", desc, msg);
  90. }
  91. }
  92.  
  93. function _getHiderName(){
  94. var i,
  95. links = document.getElementsByTagName("a"),
  96. pos;
  97. if(links){
  98. for(i = 0; i < links.length; i++){
  99. pos = links[i].href.indexOf("/seek/nearest.aspx?u=");
  100. if(pos !== -1){
  101. return decodeURIComponent(links[i].href.substr(pos + 21).replace(/\+/g, '%20'));
  102. }
  103. }
  104. }
  105. };
  106.  
  107. function _parseNames(names){
  108. // Filter out null or undefined entries, convert commas to semicolons, then convert to a comma-separated string.
  109. return encodeURIComponent(names
  110. .filter(function (n){
  111. return n !== undefined;
  112. })
  113. .map(function (n){
  114. return (n + "").replace(/,/g, ";");
  115. })
  116. .join());
  117. };
  118. function _getHtml(uname, brand){
  119. return "<a class='" + _cfg.elPrefix + "-badge' href='https://www." + _cfg.seriesURL + "' title='" + _cfg.seriesName +
  120. " stats.'><img src='https://img." + _cfg.seriesURL + "/awards/" + _cfg.imgScript + "?name=" + uname + "&brand=" + brand + "' /></a>";
  121. };
  122.  
  123. function _displayStats(stats, page, brand){
  124. var widget = document.createElement("div"),
  125. html = "",
  126. i,
  127. target;
  128.  
  129. for(i = 0; i < stats.length; i++){
  130. var name = (stats[i].name + "")
  131. .replace(/;/g, ",")
  132. .replace(/'/g, "&apos;")
  133. .replace(/"/g, "&quot;");
  134. if(i === 0 || stats[i].name !== stats[0].name){
  135. html += _getHtml(name, brand);
  136. }
  137. }
  138. _log(html, 'Banner HTML');
  139.  
  140. switch(page){
  141. case "my":
  142. target = document.getElementById("ctl00_ContentBody_lnkProfile");
  143. break;
  144. case "account":
  145. // New account dashboard.
  146. // The WidgetPanel is too slow to load and causes scripts to block so follow GClhII and just append the widget to the sidebar.
  147. target = document.querySelector(".sidebar-right");
  148. break;
  149. case "cache":
  150. target = document.getElementsByClassName('sidebar')[0];
  151. break;
  152. case "profile":
  153. if(_profileName){
  154. target = document.getElementById("ctl00_ContentBody_ProfilePanel1_lblProfile");
  155. if (target) {
  156. target = target.parentNode;
  157. }
  158. }else if(_profileNameOld){
  159. target = document.getElementById("HiddenProfileContent");
  160. }
  161. break;
  162. }
  163.  
  164. if(!target){
  165. console.warn(_cfg.seriesName + " Stats: Aborted - couldn't find where to insert widget. You might not be logged in.");
  166. return;
  167. }
  168.  
  169. if(html){
  170. widget.className = _cfg.elPrefix + "-container";
  171. widget.innerHTML = html;
  172. switch(page){
  173. case "my":
  174. case "profile":
  175. target.parentNode.insertBefore(widget, target.nextSibling);
  176. break;
  177. case "account":
  178. // If the StatsWidget isn't present, create it.
  179. var el = document.getElementById("StatsWidget");
  180. if(!el){
  181. _log('Creating widget.', 'StatsWidget');
  182. var divStats = document.createElement('div');
  183. divStats.id = "StatsWidget";
  184. divStats.classList.add("panel", "collapsible");
  185. divStats.innerHTML = '<div class="panel-header isActive" aria-expanded="true">\
  186. <h1 id="stats-widget-label" class="h5 no-margin">Statistics</h1>\
  187. <button aria-controls="StatsWidget" aria-labelledby="stats-widget-label">\
  188. <svg height="22" width="22" class="opener" role="img">\
  189. <use xlink:href="/account/app/ui-icons/sprites/global.svg#icon-expand-svg-fill"></use>\
  190. </svg>\
  191. </button>\
  192. </div>\
  193. <div id="StatsComponents" class="panel-body">\
  194. <div id="StatsPanel" class="widget-panel"></div>\
  195. </div>';
  196. target.append(divStats);
  197. // Hide the panel if it was previously hidden.
  198. if (!GM_getValue('statsWidget_visible', false)) {
  199. document.querySelector('#StatsWidget .panel-body').style.display = "none";
  200. document.querySelector('#StatsWidget .panel-header').classList.remove('isActive');
  201. _fadeOut(document.querySelector('#StatsWidget .panel-body'));
  202. }
  203. // Add the click handler.
  204. document.querySelector('#StatsWidget .panel-header').addEventListener('click', function() {
  205. if (GM_getValue('statsWidget_visible', true)) {
  206. document.querySelector('#StatsWidget .panel-header').classList.remove('isActive');
  207. _fadeOut(document.querySelector('#StatsWidget .panel-body'));
  208. GM_setValue('statsWidget_visible', false);
  209. }else{
  210. document.querySelector('#StatsWidget .panel-header').classList.add('isActive');
  211. _fadeIn(document.querySelector('#StatsWidget .panel-body'));
  212. GM_setValue('statsWidget_visible', true);
  213. }
  214. });
  215. }
  216. // Finally, append the banner.
  217. document.querySelector('#StatsPanel').appendChild(widget);
  218. break;
  219. default:
  220. target.insertBefore(widget, target.firstChild.nextSibling.nextSibling);
  221. break;
  222. }
  223. }else{
  224. console.warn(_cfg.seriesName + " Stats: didn't generate an award badge.");
  225. }
  226. };
  227.  
  228. function _fadeOut(element) {
  229. var op = 1; // initial opacity
  230. var timer = setInterval(function () {
  231. if (op <= 0.1){
  232. clearInterval(timer);
  233. element.style.display = "none";
  234. }
  235. element.style.opacity = op;
  236. op -= 0.1;
  237. }, 30);
  238. };
  239.  
  240. function _fadeIn(element) {
  241. var op = 0.1; // initial opacity
  242. element.style.display = "block";
  243. var timer = setInterval(function () {
  244. if (op >= 1){
  245. clearInterval(timer);
  246. }
  247. element.style.opacity = op;
  248. op += 0.1;
  249. }, 30);
  250. };
  251.  
  252. function _createConfigDlg(){
  253. // Register the menu item.
  254. GM_registerMenuCommand("Options", function(){
  255. GM_config.open();
  256. });
  257.  
  258. GM_config.init({
  259. 'id': _cfg.elPrefix + '_config', // The id used for this instance of GM_config
  260. 'title': _cfg.seriesName + ' Stats', // Panel Title
  261. 'fields': { // Fields object
  262. 'branding': { // This is the id of the field
  263. 'label': 'Branding', // Appears next to field
  264. 'type': 'select', // Makes this setting a dropdown
  265. 'options': _cfg.seriesLevels, // Possible choices
  266. 'default': _cfg.seriesLevelDefault // Default value if user doesn't change it
  267. }
  268. },
  269. // Dialogue internal styles.
  270. 'css': '#' + _cfg.elPrefix + '_config {position: static !important; width: 75% !important; margin: 1.5em auto !important; border: 10 !important;}' +
  271. '#' + _cfg.elPrefix + '_config_' + _cfg.elPrefix + '_branding_var {padding-top: 30px;} #' + _cfg.elPrefix + '_config button {color: black;}',
  272. 'events': {
  273. 'open': function(document, window, frame){
  274. // iframe styles.
  275. frame.style.width = '300px';
  276. frame.style.height = '250px';
  277. frame.style.left = parent.document.body.clientWidth / 2 - 150 + 'px';
  278. frame.style.borderWidth = '5px';
  279. frame.style.borderStyle = 'ridge';
  280. frame.style.borderColor = '#999999';
  281. },
  282. 'save': function(){
  283. GM_setValue(_cfg.elPrefix + '_branding', GM_config.get('branding'));
  284. location.reload(); // reload the page when configuration was changed
  285. }
  286. }
  287. });
  288. };
  289.  
  290. function _init(){
  291. var currentPage,
  292. elCSS = document.createElement("style"),
  293. userName = "",
  294. userNames = [],
  295. stats = [];
  296.  
  297. // Don't run on frames or iframes
  298. if(window.top !== window.self){
  299. return false;
  300. }
  301.  
  302. if(/\/my\//.test(location.pathname)){
  303. // On a My Profile page
  304. currentPage = "my";
  305. }else if(/\/account\//.test(location.pathname)){
  306. // On a Profile page
  307. currentPage = "account";
  308. }else{
  309. if(_cacheName){
  310. // On a Geocache page...
  311. // var matcher = new RegExp(_cfg.seriesName, "i");
  312. // if(!matcher.test(_cacheName.innerHTML)){
  313. // ...but not the right cache series.
  314. // return;
  315. // }
  316.  
  317. var titleFound = false;
  318. for(title in _cfg.cacheTitles){
  319. var matcher = new RegExp(_cfg.cacheTitles[title], "i");
  320. if(matcher.test(_cacheName.innerHTML)){
  321. titleFound = true;
  322. }
  323. }
  324. if(!titleFound){
  325. // ...but not the right cache series.
  326. return;
  327. }
  328.  
  329. currentPage = "cache";
  330. }else{
  331. currentPage = "profile";
  332. }
  333. }
  334. _log(currentPage, 'Detected page');
  335.  
  336. // We're going to display so we can announce ourselves and prepare the configuration dialogue.
  337. console.info(_cfg.seriesName + " Stats V" + _cfg.callerVersion);
  338.  
  339. //CONFIG
  340. _createConfigDlg();
  341. var brand = GM_getValue(_cfg.elPrefix + '_branding', _cfg.seriesLevelDefault);
  342. _log(brand, 'Stats branding');
  343. brand = brand.toLowerCase()
  344.  
  345. // Get hider details.
  346. var hider;
  347. switch(currentPage){
  348. case "profile":
  349. if(_profileName){
  350. userNames = [_profileName.textContent.trim()];
  351. }else if(_profileNameOld){
  352. userNames = [_profileNameOld.textContent.trim()];
  353. }
  354. break;
  355. default:
  356. if(_userField.length > 0){
  357. userNames.push(_userField[0].innerHTML.trim());
  358. }
  359. hider = _getHiderName();
  360. if(typeof hider !== 'undefined'){
  361. userNames.push(hider);
  362. }
  363. break;
  364. }
  365. _log(userNames[0], "Finder's name");
  366. _log(userNames[1], "Hider's name");
  367. for(var i = 0; i < userNames.length; i++){
  368. stats[i] = {name: userNames[i]};
  369. }
  370. _log(stats, 'Statistics');
  371. userName = _parseNames(userNames);
  372. if(!userName){
  373. console.error(_cfg.seriesName + " Stats: Aborted - couldn't work out user name");
  374. return;
  375. }
  376.  
  377. // Inject widget styling
  378. elCSS.setAttribute('type', 'text/css');
  379. if(elCSS.styleSheet){
  380. elCSS.styleSheet.cssText = _css;
  381. }else{
  382. elCSS.appendChild(document.createTextNode(_css));
  383. }
  384. document.head.appendChild(elCSS);
  385. _displayStats(stats, currentPage, brand);
  386. }
  387.  
  388. return {
  389. // ========================= Public members =========================
  390.  
  391. // ========================= Public methods =========================
  392. init: _init
  393. };
  394. };