TagPro Userscript Library

Functions that any TagPro script could benefit from

当前为 2018-10-03 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/371240/633764/TagPro%20Userscript%20Library.js

  1. // ==UserScript==
  2. // @name TagPro Userscript Library
  3. // @description Functions that any TagPro script could benefit from
  4. // @author Ko </u/Wilcooo> (https://greasyfork.org/users/152992)
  5. // @version 3.1
  6. // @license MIT
  7. // @include *.koalabeast.com*
  8. // @include *.jukejuice.com*
  9. // @include *.newcompte.fr*
  10. // @downloadURL https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/tpul.lib.js
  11. // @icon https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/icon.png
  12. // @supportURL https://www.reddit.com/message/compose/?to=Wilcooo
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_xmlhttpRequest
  16. // @connect koalabeast.com
  17. // ==/UserScript==
  18.  
  19.  
  20.  
  21. // ==UserLibrary==
  22. // @name TagPro Userscript Library
  23. // @description Functions that any TagPro script could benefit from
  24. // @version 3.0
  25. // @license MIT
  26. // ==/UserLibrary==
  27.  
  28.  
  29. var version = 3.0;
  30. console.log('Loading TPUL (TagPro Userscript Library) version '+version);
  31.  
  32.  
  33.  
  34. ////////////////////////////////////////////////////////////////
  35. ////////////////////////////////////////////////////////////////
  36. ////////////////////////////////////////////////////////////////
  37.  
  38. // To use this library, include these 5 lines in your userscripts' metadata block:
  39.  
  40. // @require https://greasyfork.org/scripts/371240/code/TagPro%20Userscript%20Library.js
  41. // @grant GM_setValue
  42. // @grant GM_getValue
  43. // @grant GM_xmlhttpRequest
  44. // @connect koalabeast.com
  45.  
  46. ////////////////////////////////////////////////////////////////
  47. ////////////////////////////////////////////////////////////////
  48. ////////////////////////////////////////////////////////////////
  49.  
  50.  
  51.  
  52.  
  53.  
  54.  
  55. /* TODO
  56.  
  57. Option to change the layout of the settings
  58.  
  59. Show a "scroll down" reminder
  60.  
  61. Notify "options cancld" when scrolling away
  62.  
  63. margin beneath buttons on scoreboard
  64.  
  65. option to disable notifications.
  66.  
  67. ESC cancels, option to Save when canceld (scroll away, esc)
  68.  
  69. */
  70.  
  71.  
  72.  
  73.  
  74.  
  75.  
  76.  
  77. var GM_configStruct = (function(){
  78.  
  79.  
  80. ////////////////////////////////////////////////////////////////
  81. // START OF ORIGINAL GM_CONFIG //
  82. ////////////////////////////////////////////////////////////////
  83.  
  84. /*
  85. Copyright 2009+, GM_config Contributors (https://github.com/sizzlemctwizzle/GM_config)
  86.  
  87. GM_config Contributors:
  88. Mike Medley <medleymind@gmail.com>
  89. Joe Simmons
  90. Izzy Soft
  91. Marti Martz
  92.  
  93. GM_config is distributed under the terms of the GNU Lesser General Public License.
  94.  
  95. GM_config is free software: you can redistribute it and/or modify
  96. it under the terms of the GNU Lesser General Public License as published by
  97. the Free Software Foundation, either version 3 of the License, or
  98. (at your option) any later version.
  99.  
  100. This program is distributed in the hope that it will be useful,
  101. but WITHOUT ANY WARRANTY; without even the implied warranty of
  102. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  103. GNU Lesser General Public License for more details.
  104.  
  105. You should have received a copy of the GNU Lesser General Public License
  106. along with this program. If not, see <http://www.gnu.org/licenses/>.
  107. */
  108.  
  109. function GM_configStruct(){if(arguments.length){GM_configInit(this,arguments);this.onInit()}}
  110. function GM_configInit(config,args){if(typeof config.fields=="undefined"){config.fields={};config.onInit=config.onInit||function(){};config.onOpen=config.onOpen||function(){};config.onSave=config.onSave||function(){};config.onClose=config.onClose||function(){};config.onReset=config.onReset||function(){};config.isOpen=false;config.title="User Script Settings";config.css={basic:["#GM_config * { font-family: arial,tahoma,myriad pro,sans-serif; }","#GM_config { background: #FFF; }","#GM_config input[type='radio'] { margin-right: 8px; }",
  111. "#GM_config .indent40 { margin-left: 40%; }","#GM_config .field_label { font-size: 12px; font-weight: bold; margin-right: 6px; }","#GM_config .radio_label { font-size: 12px; }","#GM_config .block { display: block; }","#GM_config .saveclose_buttons { margin: 16px 10px 10px; padding: 2px 12px; }","#GM_config .reset, #GM_config .reset a,"+" #GM_config_buttons_holder { color: #000; text-align: right; }","#GM_config .config_header { font-size: 20pt; margin: 0; }","#GM_config .config_desc, #GM_config .section_desc, #GM_config .reset { font-size: 9pt; }",
  112. "#GM_config .center { text-align: center; }","#GM_config .section_header_holder { margin-top: 8px; }","#GM_config .config_var { margin: 0 0 4px; }","#GM_config .section_header { background: #414141; border: 1px solid #000; color: #FFF;"," font-size: 13pt; margin: 0; }","#GM_config .section_desc { background: #EFEFEF; border: 1px solid #CCC; color: #575757;"+" font-size: 9pt; margin: 0 0 6px; }"].join("\n")+"\n",basicPrefix:"GM_config",stylish:""}}if(args.length==1&&typeof args[0].id=="string"&&typeof args[0].appendChild!=
  113. "function")var settings=args[0];else{var settings={};for(var i=0,l=args.length,arg;i<l;++i){arg=args[i];if(typeof arg.appendChild=="function"){settings.frame=arg;continue}switch(typeof arg){case "object":for(var j in arg){if(typeof arg[j]!="function"){settings.fields=arg;break}if(!settings.events)settings.events={};settings.events[j]=arg[j]}break;case "function":settings.events={onOpen:arg};break;case "string":if(/\w+\s*\{\s*\w+\s*:\s*\w+[\s|\S]*\}/.test(arg))settings.css=arg;else settings.title=
  114. arg;break}}}if(settings.id)config.id=settings.id;else if(typeof config.id=="undefined")config.id="GM_config";if(settings.title)config.title=settings.title;if(settings.css)config.css.stylish=settings.css;if(settings.frame)config.frame=settings.frame;if(settings.events){var events=settings.events;for(var e in events)config["on"+e.charAt(0).toUpperCase()+e.slice(1)]=events[e]}if(settings.fields){var stored=config.read(),fields=settings.fields,customTypes=settings.types||{},configId=config.id;for(var id in fields){var field=
  115. fields[id];if(field)config.fields[id]=new GM_configField(field,stored[id],id,customTypes[field.type],configId);else if(config.fields[id])delete config.fields[id]}}if(config.id!=config.css.basicPrefix){config.css.basic=config.css.basic.replace(new RegExp("#"+config.css.basicPrefix,"gm"),"#"+config.id);config.css.basicPrefix=config.id}}
  116. GM_configStruct.prototype={init:function(){GM_configInit(this,arguments);this.onInit()},open:function(){var match=document.getElementById(this.id);if(match&&(match.tagName=="IFRAME"||match.childNodes.length>0))return;var config=this;function buildConfigWin(body,head){var create=config.create,fields=config.fields,configId=config.id,bodyWrapper=create("div",{id:configId+"_wrapper"});head.appendChild(create("style",{type:"text/css",textContent:config.css.basic+config.css.stylish}));bodyWrapper.appendChild(create("div",
  117. {id:configId+"_header",className:"config_header block center"},config.title));var section=bodyWrapper,secNum=0;for(var id in fields){var field=fields[id],settings=field.settings;if(settings.section){section=bodyWrapper.appendChild(create("div",{className:"section_header_holder",id:configId+"_section_"+secNum}));if(Object.prototype.toString.call(settings.section)!=="[object Array]")settings.section=[settings.section];if(settings.section[0])section.appendChild(create("div",{className:"section_header center",
  118. id:configId+"_section_header_"+secNum},settings.section[0]));if(settings.section[1])section.appendChild(create("p",{className:"section_desc center",id:configId+"_section_desc_"+secNum},settings.section[1]));++secNum}section.appendChild(field.wrapper=field.toNode())}bodyWrapper.appendChild(create("div",{id:configId+"_buttons_holder"},create("button",{id:configId+"_saveBtn",textContent:"Save",title:"Save settings",className:"saveclose_buttons",onclick:function(){config.save()}}),create("button",{id:configId+
  119. "_closeBtn",textContent:"Close",title:"Close window",className:"saveclose_buttons",onclick:function(){config.close()}}),create("div",{className:"reset_holder block"},create("a",{id:configId+"_resetLink",textContent:"Reset to defaults",href:"#",title:"Reset fields to default values",className:"reset",onclick:function(e){e.preventDefault();config.reset()}}))));body.appendChild(bodyWrapper);config.center();window.addEventListener("resize",config.center,false);config.onOpen(config.frame.contentDocument||
  120. config.frame.ownerDocument,config.frame.contentWindow||window,config.frame);window.addEventListener("beforeunload",function(){config.close()},false);config.frame.style.display="block";config.isOpen=true}var defaultStyle="bottom: auto; border: 1px solid #000; display: none; height: 75%;"+" left: 0; margin: 0; max-height: 95%; max-width: 95%; opacity: 0;"+" overflow: auto; padding: 0; position: fixed; right: auto; top: 0;"+" width: 75%; z-index: 9999;";if(this.frame){this.frame.id=this.id;this.frame.setAttribute("style",
  121. defaultStyle);buildConfigWin(this.frame,this.frame.ownerDocument.getElementsByTagName("head")[0])}else{document.body.appendChild(this.frame=this.create("iframe",{id:this.id,style:defaultStyle}));this.frame.src="about:blank";this.frame.addEventListener("load",function(e){var frame=config.frame;var body=frame.contentDocument.getElementsByTagName("body")[0];body.id=config.id;buildConfigWin(body,frame.contentDocument.getElementsByTagName("head")[0])},false)}},save:function(){var forgotten=this.write();
  122. this.onSave(forgotten)},close:function(){if(this.frame.contentDocument){this.remove(this.frame);this.frame=null}else{this.frame.innerHTML="";this.frame.style.display="none"}var fields=this.fields;for(var id in fields){var field=fields[id];field.wrapper=null;field.node=null}this.onClose();this.isOpen=false},set:function(name,val){this.fields[name].value=val;if(this.fields[name].node)this.fields[name].reload()},get:function(name,getLive){var field=this.fields[name],fieldVal=null;if(getLive&&field.node)fieldVal=
  123. field.toValue();return fieldVal!=null?fieldVal:field.value},write:function(store,obj){if(!obj){var values={},forgotten={},fields=this.fields;for(var id in fields){var field=fields[id];var value=field.toValue();if(field.save)if(value!=null){values[id]=value;field.value=value}else values[id]=field.value;else forgotten[id]=value}}try{this.setValue(store||this.id,this.stringify(obj||values))}catch(e){this.log("GM_config failed to save settings!")}return forgotten},read:function(store){try{var rval=this.parser(this.getValue(store||
  124. this.id,"{}"))}catch(e){this.log("GM_config failed to read saved settings!");var rval={}}return rval},reset:function(){var fields=this.fields;for(var id in fields)fields[id].reset();this.onReset()},create:function(){switch(arguments.length){case 1:var A=document.createTextNode(arguments[0]);break;default:var A=document.createElement(arguments[0]),B=arguments[1];for(var b in B)if(b.indexOf("on")==0)A.addEventListener(b.substring(2),B[b],false);else if(",style,accesskey,id,name,src,href,which,for".indexOf(","+
  125. b.toLowerCase())!=-1)A.setAttribute(b,B[b]);else A[b]=B[b];if(typeof arguments[2]=="string")A.innerHTML=arguments[2];else for(var i=2,len=arguments.length;i<len;++i)A.appendChild(arguments[i])}return A},center:function(){var node=this.frame;if(!node)return;var style=node.style,beforeOpacity=style.opacity;if(style.display=="none")style.opacity="0";style.display="";style.top=Math.floor(window.innerHeight/2-node.offsetHeight/2)+"px";style.left=Math.floor(window.innerWidth/2-node.offsetWidth/2)+"px";
  126. style.opacity="1"},remove:function(el){if(el&&el.parentNode)el.parentNode.removeChild(el)}};
  127. (function(){var isGM=typeof GM_getValue!="undefined"&&typeof GM_getValue("a","b")!="undefined",setValue,getValue,stringify,parser;if(!isGM){setValue=function(name,value){return localStorage.setItem(name,value)};getValue=function(name,def){var s=localStorage.getItem(name);return s==null?def:s};stringify=JSON.stringify;parser=JSON.parse}else{setValue=GM_setValue;getValue=GM_getValue;stringify=typeof JSON=="undefined"?function(obj){return obj.toSource()}:JSON.stringify;parser=typeof JSON=="undefined"?
  128. function(jsonData){return(new Function("return "+jsonData+";"))()}:JSON.parse}GM_configStruct.prototype.isGM=isGM;GM_configStruct.prototype.setValue=setValue;GM_configStruct.prototype.getValue=getValue;GM_configStruct.prototype.stringify=stringify;GM_configStruct.prototype.parser=parser;GM_configStruct.prototype.log=window.console?console.log:isGM&&typeof GM_log!="undefined"?GM_log:window.opera?opera.postError:function(){}})();
  129. function GM_configDefaultValue(type,options){var value;if(type.indexOf("unsigned ")==0)type=type.substring(9);switch(type){case "radio":case "select":value=options[0];break;case "checkbox":value=false;break;case "int":case "integer":case "float":case "number":value=0;break;default:value=""}return value}
  130. function GM_configField(settings,stored,id,customType,configId){this.settings=settings;this.id=id;this.configId=configId;this.node=null;this.wrapper=null;this.save=typeof settings.save=="undefined"?true:settings.save;if(settings.type=="button")this.save=false;this["default"]=typeof settings["default"]=="undefined"?customType?customType["default"]:GM_configDefaultValue(settings.type,settings.options):settings["default"];this.value=typeof stored=="undefined"?this["default"]:stored;if(customType){this.toNode=
  131. customType.toNode;this.toValue=customType.toValue;this.reset=customType.reset}}
  132. GM_configField.prototype={create:GM_configStruct.prototype.create,toNode:function(){var field=this.settings,value=this.value,options=field.options,type=field.type,id=this.id,configId=this.configId,labelPos=field.labelPos,create=this.create;function addLabel(pos,labelEl,parentNode,beforeEl){if(!beforeEl)beforeEl=parentNode.firstChild;switch(pos){case "right":case "below":if(pos=="below")parentNode.appendChild(create("br",{}));parentNode.appendChild(labelEl);break;default:if(pos=="above")parentNode.insertBefore(create("br",
  133. {}),beforeEl);parentNode.insertBefore(labelEl,beforeEl)}}var retNode=create("div",{className:"config_var",id:configId+"_"+id+"_var",title:field.title||""}),firstProp;for(var i in field){firstProp=i;break}var label=field.label&&type!="button"?create("label",{id:configId+"_"+id+"_field_label","for":configId+"_field_"+id,className:"field_label"},field.label):null;switch(type){case "textarea":retNode.appendChild(this.node=create("textarea",{innerHTML:value,id:configId+"_field_"+id,className:"block",cols:field.cols?
  134. field.cols:20,rows:field.rows?field.rows:2}));break;case "radio":var wrap=create("div",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var radLabel=create("label",{className:"radio_label"},options[i]);var rad=wrap.appendChild(create("input",{value:options[i],type:"radio",name:id,checked:options[i]==value}));var radLabelPos=labelPos&&(labelPos=="left"||labelPos=="right")?labelPos:firstProp=="options"?"left":"right";addLabel(radLabelPos,radLabel,wrap,rad)}retNode.appendChild(wrap);
  135. break;case "select":var wrap=create("select",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var option=options[i];wrap.appendChild(create("option",{value:option,selected:option==value},option))}retNode.appendChild(wrap);break;default:var props={id:configId+"_field_"+id,type:type,value:type=="button"?field.label:value};switch(type){case "checkbox":props.checked=value;break;case "button":props.size=field.size?field.size:25;if(field.script)field.click=field.script;
  136. if(field.click)props.onclick=field.click;break;case "hidden":break;default:props.type="text";props.size=field.size?field.size:25}retNode.appendChild(this.node=create("input",props))}if(label){if(!labelPos)labelPos=firstProp=="label"||type=="radio"?"left":"right";addLabel(labelPos,label,retNode)}return retNode},toValue:function(){var node=this.node,field=this.settings,type=field.type,unsigned=false,rval=null;if(!node)return rval;if(type.indexOf("unsigned ")==0){type=type.substring(9);unsigned=true}switch(type){case "checkbox":rval=
  137. node.checked;break;case "select":rval=node[node.selectedIndex].value;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=radios.length;i<len;++i)if(radios[i].checked)rval=radios[i].value;break;case "button":break;case "int":case "integer":case "float":case "number":var num=Number(node.value);var warn='Field labeled "'+field.label+'" expects a'+(unsigned?" positive ":"n ")+"integer value";if(isNaN(num)||type.substr(0,3)=="int"&&Math.ceil(num)!=Math.floor(num)||unsigned&&
  138. num<0){alert(warn+".");return null}if(!this._checkNumberRange(num,warn))return null;rval=num;break;default:rval=node.value;break}return rval},reset:function(){var node=this.node,field=this.settings,type=field.type;if(!node)return;switch(type){case "checkbox":node.checked=this["default"];break;case "select":for(var i=0,len=node.options.length;i<len;++i)if(node.options[i].textContent==this["default"])node.selectedIndex=i;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=
  139. radios.length;i<len;++i)if(radios[i].value==this["default"])radios[i].checked=true;break;case "button":break;default:node.value=this["default"];break}},remove:function(el){GM_configStruct.prototype.remove(el||this.wrapper);this.wrapper=null;this.node=null},reload:function(){var wrapper=this.wrapper;if(wrapper){var fieldParent=wrapper.parentNode;fieldParent.insertBefore(this.wrapper=this.toNode(),wrapper);this.remove(wrapper)}},_checkNumberRange:function(num,warn){var field=this.settings;if(typeof field.min==
  140. "number"&&num<field.min){alert(warn+" greater than or equal to "+field.min+".");return null}if(typeof field.max=="number"&&num>field.max){alert(warn+" less than or equal to "+field.max+".");return null}return true}};var GM_config=new GM_configStruct;
  141.  
  142.  
  143. ////////////////////////////////////////////////////////////////
  144. // END OF ORIGINAL GM_CONFIG //
  145. ////////////////////////////////////////////////////////////////
  146.  
  147.  
  148.  
  149.  
  150. // I'm going to edit GM_config slightly.
  151. // Mostly to get rid of the 'alerts' when something is wrong.
  152. // (alerts pause the window, which causes you to disconnect from a game)
  153.  
  154.  
  155. // This function will return true when no errors were found.
  156.  
  157. GM_configStruct.prototype.valid = function() {
  158.  
  159.  
  160. for (var id in this.fields) {
  161.  
  162. var node = this.fields[id].node;
  163.  
  164. if (node.validity && !node.validity.valid) return false;
  165.  
  166. /*
  167. var field = this.fields[id],
  168. type = field.settings.type,
  169. unsigned = false;
  170.  
  171. if (type.indexOf('unsigned ') == 0) {
  172. type = type.substring(9);
  173. unsigned = true;
  174. }
  175.  
  176. if (['int','integer','float','number'].includes(type)) {
  177.  
  178. var num = Number(field.node.value);
  179.  
  180. var warn = 'Field labeled "' + field.label + '" expects a' +
  181. (unsigned ? ' positive ' : 'n ') + 'integer value';
  182.  
  183. if (isNaN(num) ||
  184. (type.substr(0, 3) == 'int' && Math.ceil(num) != Math.floor(num)) ||
  185. (unsigned && num < 0)) {
  186. // Add a few ways for scripters to know that there is an error
  187. field.error = true;
  188. field.wrapper.classList.add('error');
  189. correct = false;
  190. }
  191.  
  192. else if (typeof field.settings.min == "number" && num < field.settings.min) {
  193. // Add a few ways for scripters to know that there is an error
  194. field.error = true;
  195. field.wrapper.classList.add('error');
  196. correct = false;
  197. }
  198.  
  199. else if (typeof field.settings.max == "number" && num > field.settings.max) {
  200. // Add a few ways for scripters to know that there is an error
  201. field.error = true;
  202. field.wrapper.classList.add('error');
  203. correct = false;
  204. }
  205.  
  206. else {
  207. // Add a few ways for scripters to know that there is NO error
  208. field.error = false;
  209. field.wrapper.classList.remove('error');
  210. }
  211. }*/
  212. }
  213.  
  214. return true;
  215. };
  216.  
  217.  
  218. // Change the field prototype
  219.  
  220. var org_toNode = GM_configField.prototype.toNode;
  221.  
  222. GM_configField.prototype.toNode = function(){
  223.  
  224. var retNode = org_toNode.apply(this, ...arguments);
  225.  
  226. var unsigned = false,
  227. type = this.settings.type;
  228.  
  229. if (type.indexOf('unsigned ') === 0) {
  230. type = type.substring(9);
  231. unsigned = true;
  232. }
  233.  
  234. if (this.node.validity) {
  235. // Validity checks will work for ANY input, not only numbers.
  236. // For example, if you want a text field to have at least 3 characters,
  237. // manually set the 'minLength' tag to 3 and the rest will be done
  238. // automagically.
  239.  
  240. // Immediately show a validity report while typing / clicking
  241. this.node.addEventListener('input', this.node.reportValidity);
  242. this.node.addEventListener('click', this.node.reportValidity);
  243.  
  244. // The autocomplete covers the validity report (at least in Chrome)
  245. this.node.autocomplete = 'off';
  246. }
  247.  
  248. if (['int','integer','float','number'].includes(type)) {
  249.  
  250. // By default, GM_config makes most inputs a text field, even numbers.
  251. // Lets fix that, to be able to check min and max values better.
  252.  
  253. this.node.type = 'number';
  254.  
  255. if (this.settings.min) this.node.min = this.settings.min;
  256. if (this.settings.max) this.node.max = this.settings.max;
  257.  
  258. // unsigned means non-negative
  259. if (unsigned) this.node.min = Math.max(0,this.settings.min);
  260.  
  261. // integers are only whole numbers
  262. if (type.substr(0, 3) == 'int') this.node.step = 1;
  263. }
  264.  
  265. if (!['radio','select','checkbox','button','hidden'].includes(type)) {
  266. // Disable TagPro's controls when typing inside a field you can type in
  267. // You can set tpul.rollingChat.enable = true to make the Arrow keys move your ball, even when typing text.
  268. this.node.addEventListener('focus', function(){tagpro.disableControls = true;});
  269. this.node.addEventListener('blur', function(){tagpro.disableControls = false;});
  270. }
  271.  
  272. return retNode;
  273. };
  274.  
  275.  
  276.  
  277. return GM_configStruct;
  278. })();
  279.  
  280.  
  281.  
  282.  
  283.  
  284.  
  285.  
  286.  
  287. var tpul = (function(){
  288.  
  289.  
  290.  
  291.  
  292. // =====STYLE SECTION=====
  293.  
  294.  
  295.  
  296. // Create our own stylesheet to define the styles in:
  297.  
  298. var style = document.getElementById('tpul-style') || document.createElement('style');
  299. document.head.appendChild(style);
  300. style.id = 'tpul-style';
  301.  
  302. // Remove all existing rules of any previous TPUL version.
  303.  
  304. var styleSheet = style.sheet;
  305. Array.from(styleSheet.cssRules).forEach(rule => styleSheet.deleteRule(rule));
  306.  
  307. // THE SETTINGS MENU BUTTONS
  308.  
  309. // Container for settings buttons
  310. styleSheet.insertRule(` #tpul-settings-menu {
  311. text-align: center;
  312. margin: 0 10%;
  313. }`);
  314.  
  315. // A settings button
  316. styleSheet.insertRule(` .tpul-settings-btn {
  317. position: relative;
  318. width: 64px;
  319. height: 64px;
  320. padding: 10px;
  321. margin: 20px;
  322. background-size: contain !important;
  323. background-origin: content-box !important;
  324. background-repeat: no-repeat !important;
  325. outline: none;
  326. }`);
  327.  
  328. // Blue line around button when focussed
  329. styleSheet.insertRule(` .tpul-settings-btn:focus::after {
  330. content: "";
  331. position: absolute;
  332. width: 100%;
  333. height: 100%;
  334. border: 2px solid Highlight;
  335. top: 0;
  336. left: 0;
  337. }`);
  338.  
  339. // Tooltip of button
  340. styleSheet.insertRule(` .tpul-settings-btn span {
  341. position: absolute;
  342. z-index: 1;
  343. border-radius: 10px;
  344. margin-top: 10px;
  345. padding: 10px;
  346. background: #0E8AE0;
  347. border: 1px solid #095C96;
  348. box-shadow: 0 3px #095C96;
  349. font-size: small;
  350. top: 100%;
  351. left: 50%;
  352. transform: translateX(-50%);
  353. width: max-content;
  354. min-width: 64px;
  355. max-width: 128px;
  356. overflow-wrap: break-word;
  357. word-wrap: break-word;
  358. pointer-events: none;
  359. opacity: 0;
  360. transition: opacity .3s;
  361. }`);
  362.  
  363.  
  364. // Arrow of tooltip
  365. styleSheet.insertRule(` .tpul-settings-btn span::after {
  366. content: "";
  367. position: absolute;
  368. left: 50%;
  369. bottom: 100%;
  370. margin-left: -20px;
  371. border-width: 20px;
  372. border-style: solid;
  373. border-color: transparent transparent #0E8AE0 transparent;
  374. }`);
  375.  
  376. // Show tooltip when hovering/focussing
  377. styleSheet.insertRule(`.tpul-settings-btn:hover span, .tpul-settings-btn:focus span {
  378. opacity: 1;
  379. }`);
  380.  
  381.  
  382.  
  383. // THE SETTINGS PANEL
  384.  
  385. // The frame (gray, spans full page)
  386. styleSheet.insertRule(` .tpul-settings-frame {
  387. position: fixed;
  388. z-index: 1;
  389. left: 0;
  390. top: 0;
  391. width: 100%;
  392. height: 100%;
  393. overflow: auto;
  394. background-color: rgba(0,0,0,0.4);
  395.  
  396. transition: opacity .5s;
  397. opacity: 0;
  398. pointer-events: none;
  399. }`);
  400.  
  401. // The frame when shown
  402. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame {
  403. opacity: 1;
  404. pointer-events: auto;
  405. }`);
  406.  
  407. // The settings window itself
  408. styleSheet.insertRule(` .tpul-settings-frame > div {
  409. width: 80%;
  410. max-width: 800px;
  411. margin: auto;
  412. margin-bottom: 10%;
  413.  
  414. position: relative;
  415. padding: 20px;
  416.  
  417. border: 1px solid #888;
  418. border-radius: 15px;
  419. background: #353535;
  420.  
  421. font-size: 16px;
  422.  
  423. top: 200%;
  424. transition: top .5s;
  425. }`);
  426.  
  427. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame > div { top: 120%; }`);
  428. // In a game we want to have an 80% gap to be able to keep playing.
  429. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame.in-game > div { top: 180%; }`);
  430.  
  431.  
  432. styleSheet.insertRule(`.tpul-settings-frame .config_header {
  433. font-size: 2em;
  434. font-weight: bold;
  435. }`);
  436.  
  437. styleSheet.insertRule(`.tpul-settings-frame .section_header {
  438. font-size: 1.5em;
  439. font-weight: bold;
  440. }`);
  441.  
  442. styleSheet.insertRule(`.tpul-settings-frame .config_var {
  443. }`);
  444.  
  445.  
  446. // ERRORS in fields:
  447. styleSheet.insertRule(`.tpul-settings-frame .config_var input:invalid {
  448. box-shadow: inset 0 0 10px rgba(255,0,0,1), 0 0 10px rgba(255, 0, 0, 1);
  449. }`);
  450.  
  451. /*styleSheet.insertRule(`.tpul-settings-frame .config_var.error:before {
  452. content: attr(data-min) ' - ' attr(data-max);
  453. display: block;
  454. text-align: right;
  455. margin: 5px 20px;
  456. color: #FFA9A2;
  457. font-style: italic;
  458. }`);*/
  459.  
  460. styleSheet.insertRule(`.tpul-settings-frame .field_label {
  461. font-weight: bold;
  462. }`);
  463. styleSheet.insertRule(`.tpul-settings-frame .form-control {
  464. background: #212121;
  465. border-color: #5f5f5f;
  466. }`);
  467. styleSheet.insertRule(`.tpul-settings-frame .form-control[type="checkbox"] {
  468. width: auto;
  469. }`);
  470. styleSheet.insertRule(`.tpul-settings-frame .btn-default {
  471. border-color: #888888;
  472. }`);
  473. styleSheet.insertRule(`.tpul-settings-frame textarea.form-control {
  474. resize: vertical;
  475. }`);
  476.  
  477. styleSheet.insertRule(`.tpul-settings-frame .btn-primary {
  478. margin-left: 10px;
  479. }`);
  480.  
  481.  
  482.  
  483. styleSheet.insertRule(`.tpul-settings-frame .tab-list {
  484. border-bottom-color: #888888;
  485. }`);
  486. styleSheet.insertRule(`.tpul-settings-frame .tab-list li {
  487. cursor: pointer;
  488. color: #8BC34A;
  489. font-size: 1.5em;
  490. }`);
  491. styleSheet.insertRule(`.tpul-settings-frame .tab-list li:hover {
  492. color: #689F38;
  493. }`);
  494. styleSheet.insertRule(`.tpul-settings-frame .tab-list li.active {
  495. border-color: #888888;
  496. border-bottom-color: transparent;
  497. background-color: #353535;
  498. }`);
  499.  
  500.  
  501.  
  502. // save/close/etc buttons
  503.  
  504. styleSheet.insertRule(`.tpul-settings-frame-buttons-holder {
  505. height: 0;
  506. text-align: right;
  507. }`);
  508.  
  509. styleSheet.insertRule(`.tpul-settings-frame-buttons-holder button {
  510. padding: 4px .5em;
  511. }`);
  512.  
  513.  
  514. /*
  515.  
  516. //Bad design notice:
  517.  
  518. styleSheet.insertRule(` .tpul-settings-frame > div::after {
  519. content: "Sorry for the bad design, I'm working on it!";
  520. font-style: italic;
  521. color: gray;
  522. }`);
  523.  
  524. */
  525.  
  526. // Stop the body from scrolling when the settings panel is shown
  527. styleSheet.insertRule(`body.tpul-settings-shown {
  528. overflow:hidden !important;
  529. }`);
  530.  
  531.  
  532.  
  533.  
  534. // Notifications
  535. styleSheet.insertRule(` .tpul-notification-success {
  536. border-color: #8BC34A;
  537. background: #4C6D25;
  538. color: black;
  539. }`);
  540.  
  541. styleSheet.insertRule(` .tpul-notification-error {
  542. border-color: #BD0E0B;
  543. background: #6B2121;
  544. color: #FFA9A2;
  545. }`);
  546.  
  547. styleSheet.insertRule(` .tpul-notification-warning {
  548. border-color: Olive;
  549. background: DarkKhaki;
  550. color: black;
  551. }`);
  552.  
  553. styleSheet.insertRule(` .tpul-notification {
  554. position: fixed;
  555. bottom: 0px;
  556.  
  557. padding: 10px;
  558.  
  559. width: 100%;
  560.  
  561. text-align: center;
  562.  
  563. cursor: pointer;
  564. z-index: 2;
  565.  
  566. border-top: 1px solid #404040;
  567. background: #353535;
  568. color: #fff;
  569.  
  570. animation: slideUp 1s;
  571. transform: translateY(0);
  572. transition: transform 1s;
  573. }`);
  574.  
  575. styleSheet.insertRule(` .tpul-notification.vanish {
  576. transform: translateY(100%);
  577. }`);
  578.  
  579. styleSheet.insertRule(` @keyframes slideUp {
  580. 0% { transform: translateY(100%); }
  581. 100% { transform: translateY(0%); }
  582. }`);
  583.  
  584.  
  585.  
  586. // =====NOITCES ELYTS=====
  587.  
  588.  
  589.  
  590.  
  591.  
  592. // =====DOM SECTION=====
  593.  
  594.  
  595.  
  596. var SettingsMenu = document.getElementById('tpul-settings-menu') || document.createElement('div');
  597. SettingsMenu.id = 'tpul-settings-menu';
  598.  
  599. var SettingsFrame = document.getElementsByClassName('tpul-settings-frame')[0] || document.createElement('div');
  600. SettingsFrame.className = 'tpul-settings-frame';
  601. if(location.port) SettingsFrame.classList.add('in-game');
  602. document.body.appendChild(SettingsFrame);
  603.  
  604.  
  605. // =====NOITCES MOD=====
  606.  
  607.  
  608.  
  609.  
  610.  
  611. // =====LOGIC SECTION=====
  612.  
  613.  
  614.  
  615. var GM_storage = typeof GM_setValue === 'function' && typeof GM_getValue === 'function',
  616. all_settings = [],
  617. profileId = null,
  618. last_opened = null,
  619. rollingChatEnabled = false;
  620.  
  621.  
  622. // THE TPUL OBJECT!!
  623.  
  624. var tpul = {
  625. get version(){return version;},
  626.  
  627. settings: {
  628. addSettings: function({id, title, fields, icon, tooltipText, buttonText}) {
  629.  
  630. var config = arguments[0];
  631.  
  632. if (config.allowLocal && !id && !GM_storage) throw "TPUL: A unique id is required, because localStorage will be used! By the way; it is better to @grant GM_getValue and GM_setValue and set 'allowLocal:false' to use private storage instead.";
  633.  
  634. if (!config.allowLocal && !GM_storage) throw "TPUL: Please @grant GM_setValue and GM_getValue in your userscripts metadata (recommended) or use 'allowLocal:true' (not recommended)";
  635.  
  636.  
  637. if (arguments.length != 1 || typeof config != 'object')
  638. throw Error("addSettings() takes one object as an argument! Example: addSettings( {id:'MySettings', title:'Hello World'} )");
  639.  
  640. // Create a new GM_config instance
  641. let settings = new GM_configStruct({
  642.  
  643. frame: SettingsFrame,
  644.  
  645. ...config,
  646.  
  647. id: String(config.id) || 'defaultId',
  648.  
  649. events: {
  650. ...(config.events||{}),
  651.  
  652. open: function(){
  653.  
  654. //Remove the default inline style of the GM_config frame
  655. this.frame.setAttribute('style', '');
  656.  
  657. //Apply some TagPro/Bootstrap styles
  658. SettingsFrame.firstChild.classList.add('form-horizontal');
  659. for (let el of SettingsFrame.getElementsByClassName('config_header')) el.classList.add('header-title');
  660. for (let el of SettingsFrame.getElementsByClassName('config_var')) el.classList.add('form-group');
  661. for (let el of SettingsFrame.getElementsByClassName('field_label')) {
  662. el.classList.add('col-xs-4');
  663. el.classList.add('control-label');
  664. }
  665. for (let el of SettingsFrame.getElementsByClassName('radio_label')) el.classList.add('radio');
  666. for (let el of [...SettingsFrame.getElementsByTagName('input'),
  667. ...SettingsFrame.getElementsByTagName('select'),
  668. ...SettingsFrame.getElementsByTagName('textarea')]) {
  669.  
  670. switch (el.type) {
  671. case 'radio':
  672. el.parentElement.classList.add('col-xs-8');
  673. el.parentElement.style.paddingLeft = '30px';
  674. el.nextElementSibling.prepend(el);
  675. continue;
  676. case 'button':
  677. el.classList.add('btn');
  678. el.classList.add('btn-default');
  679. break;
  680. default:
  681. el.classList.add('form-control');
  682.  
  683. }
  684.  
  685. var div = document.createElement('div');
  686. el.parentElement.appendChild(div);
  687. div.appendChild(el);
  688.  
  689. div.classList.add('col-xs-8');
  690. div.classList.add('pull-right');
  691. }
  692.  
  693. // The footer with the buttons:
  694.  
  695. var buttonsHolder = SettingsFrame.firstElementChild.lastElementChild;
  696. buttonsHolder.classList.add('col-sm-12');
  697. buttonsHolder.classList.add('tpul-settings-frame-buttons-holder');
  698.  
  699. // Place the "footer" on top
  700. buttonsHolder.parentElement.insertBefore(buttonsHolder, buttonsHolder.parentElement.firstElementChild);
  701.  
  702. for (var btn of [...buttonsHolder.getElementsByClassName('saveclose_buttons'),
  703. ...buttonsHolder.getElementsByClassName('reset')]) {
  704. btn.classList.add('btn');
  705. btn.classList.add('btn-primary');
  706. }
  707.  
  708. buttonsHolder.innerHTML = '';
  709.  
  710. for (var type of this.buttons || ['ok','cancel','reset']) {
  711. var button = document.createElement('button');
  712. button.className = 'btn btn-primary';
  713. button.settings = settings;
  714. buttonsHolder.appendChild(button);
  715.  
  716. switch(type.toLowerCase()) {
  717. case 'ok':
  718. button.onclick = function(){
  719. if(this.settings.valid()) {this.settings.save(); this.settings.close(); tpul.notify('Options saved!','success');}
  720. else {tpul.notify('Please fix any issues before saving', 'error');}
  721. };
  722. button.innerText = 'Ok';
  723. break;
  724. case 'cancel':
  725. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  726. button.innerText = 'Cancel';
  727. break;
  728. case 'reset':
  729. button.onclick = function(){ this.settings.reset(); tpul.notify('All options are reset to their defaults','');};
  730. button.innerText = 'Reset';
  731. break;
  732. case 'save':
  733. button.onclick = function(){
  734. if(this.settings.valid()) {this.settings.save(); tpul.notify('Options saved!','success');}
  735. else {tpul.notify('Please fix any issues before saving', 'error');}
  736. };
  737. button.innerText = 'Save';
  738. break;
  739. case 'close':
  740. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  741. button.innerText = 'Close';
  742. break;
  743. }
  744. }
  745.  
  746.  
  747. if (this.tabs) {
  748.  
  749. var tablist = document.createElement('ul');
  750. tablist.classList.add('tab-list');
  751. SettingsFrame.firstElementChild.insertBefore(tablist, SettingsFrame.firstElementChild.lastElementChild);
  752.  
  753. var tabcontent = document.createElement('div');
  754. tabcontent.classList.add('tab-content');
  755. SettingsFrame.firstElementChild.insertBefore(tabcontent, SettingsFrame.firstElementChild.lastElementChild);
  756.  
  757. for (let el of [...SettingsFrame.getElementsByClassName('section_header_holder')]) {
  758.  
  759. var header = el.getElementsByClassName('section_header')[0];
  760.  
  761. tablist.innerHTML += '<li data-target="#'+el.id+'">' + header.innerText;
  762.  
  763. tabcontent.appendChild(el);
  764. el.classList.add('tab-pane');
  765.  
  766. el.removeChild(header);
  767. }
  768.  
  769. tablist.firstElementChild.click();
  770.  
  771. } else {
  772. for (let el of SettingsFrame.getElementsByClassName('section_header')) el.classList.add('header-title');
  773. }
  774.  
  775. //Open the settings on our way (animated, blocking scroll of body etc.)
  776. this.frame.style.display = '';
  777. SettingsFrame.scrollTop = SettingsFrame.offsetHeight;
  778. document.body.classList.add('tpul-settings-shown');
  779.  
  780. last_opened = settings;
  781.  
  782. if (this.events && typeof this.events.open == "function")
  783. this.events.open.call(this,...arguments);
  784. },
  785.  
  786. close: function(){
  787. if(this.isOpen){}//TODO: Check whether unsaved?
  788.  
  789. SettingsFrame.removeEventListener('scroll', settings.scroll);
  790.  
  791. //close the settings in our way (animated)
  792. this.frame.style.display = '';
  793. document.body.classList.remove('tpul-settings-shown');
  794.  
  795. if (this.events && typeof this.events.close == "function")
  796. this.events.close.call(this,...arguments);
  797. },
  798. }
  799. });
  800.  
  801. // Remove all other default styles of GM_config
  802. delete settings.css.basic;
  803.  
  804. // Create a button using the function below
  805. var button = tpul.settings.addButton({
  806. onclick: ()=>settings.open(),
  807. icon: icon,
  808. tooltipText: tooltipText,
  809. buttonText: buttonText,
  810. });
  811.  
  812. settings.button = button;
  813.  
  814. for (let c in config) if(settings[c] === undefined) settings[c] = config[c];
  815.  
  816. all_settings.push(settings);
  817.  
  818. return settings;
  819.  
  820. },
  821. addButton: function({onclick, icon, tooltipText, buttonText}) {
  822.  
  823. if (!SettingsMenu) {
  824. console.error('TPUL: Could not find a place to add the settings button for '+name);
  825. return null;
  826. }
  827.  
  828. var button = document.createElement('button');
  829. button.className = 'btn tpul-settings-btn';
  830.  
  831. if (icon) {
  832. if (icon.search(/^url\((.*)\)$/) == -1) icon = 'url("'+icon+'")';
  833. button.style.backgroundImage = icon;
  834. button.innerHTML = '&nbsp;';
  835. } else button.innerText = buttonText || '?';
  836.  
  837. var tooltip = document.createElement('span');
  838. tooltip.innerText = tooltipText || "Configure this script's settings" ;
  839. button.appendChild(tooltip);
  840.  
  841. SettingsMenu.appendChild(button);
  842.  
  843. button.addEventListener('click',function(click){
  844. button.blur();
  845. onclick(click);
  846. });
  847.  
  848. return button;
  849. },
  850. get parent() {return SettingsMenu.parentElement;},
  851. set parent(container) {
  852.  
  853. if (container) console.warn('You are repositioning the tpul settings menu. This will affect all settings buttons, not only for your script!');
  854.  
  855. container = container ||
  856. document.getElementById('tpul-settings-container') || // Try to add it to a position pre-defined by another script (such as ModFather)
  857. document.getElementById('userscript-top') || // Try to add it on top of any page on the server
  858. document.getElementById('options'); // Try to add it to the scoreboard in-game
  859.  
  860. if (container) {
  861. container.classList.remove('hidden');
  862. container.appendChild(SettingsMenu);
  863. } else console.error('Couldn\'t find a parent element.');
  864.  
  865. return container;
  866. },
  867. get menu(){ return SettingsMenu; },
  868. set menu(_){ throw "You can't change the TPUL settings menu object. You might mean to change the tpul.settings.parent"; },
  869. },
  870.  
  871. profile: {
  872. getId: function() {
  873.  
  874. if (!tpul_promises.getProfileId) {
  875. tpul_promises.getProfileId = new Promise(function(resolve,reject) {
  876.  
  877. GM_xmlhttpRequest({
  878. method: "GET",
  879. url: "http://"+document.location.hostname+"/",
  880. onload: function(){
  881. var match = this.responseText.match(/profile\/([0-9a-f]+)/i);
  882. if (match) {
  883. profileId = match[1];
  884. resolve(profileId);
  885. } else reject({error:"not logged in"});
  886. },
  887. onerror: ()=> reject({error:"request error", request:this}),
  888. });
  889. });
  890. }
  891.  
  892. return tpul_promises.getProfileId;
  893. },
  894.  
  895. getInfo: function() {
  896.  
  897. if (!tpul_promises.getProfileInfo) {
  898.  
  899. tpul_promises.getProfileInfo = new Promise(function(resolve,reject) {
  900.  
  901. tpul.profile.getId().then( function(id){
  902.  
  903. GM_xmlhttpRequest({
  904. method: "GET",
  905. url: "http://"+document.location.hostname+"/profiles/"+id,
  906. onload: function(r){
  907. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  908.  
  909. var arr;
  910. try{ arr = JSON.parse(r.response); }
  911. catch(e){ reject({error:"/profiles/ responded invalid JSON", request:this}); }
  912.  
  913. if(arr.error) reject(arr);
  914.  
  915. if(Array.isArray( arr ) && arr.length == 1) {
  916. resolve(arr[0]);
  917. }
  918. else reject({error:"unknown error", response:arr, request:this});
  919. },
  920. onerror: ()=> reject({error:"request error", request:this}),
  921. });
  922. });
  923.  
  924. tpul.profile.getId().catch( reject );
  925.  
  926. });
  927. }
  928.  
  929. return tpul_promises.getProfileInfo;
  930. },
  931.  
  932. getPage: function() {
  933.  
  934. if (!tpul_promises.getProfilePage) {
  935.  
  936. tpul_promises.getProfilePage = new Promise(function(resolve,reject) {
  937.  
  938. tpul.profile.getId().then( function(id){
  939.  
  940. GM_xmlhttpRequest({
  941. method: "GET",
  942. url: "http://"+document.location.hostname+"/profile/"+id,
  943. onload: function(r){
  944. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  945. if(r.response.error) reject(r.response);
  946.  
  947. var match,
  948. profile = {
  949. settings: {
  950. allChat: undefined,
  951. teamChat: undefined,
  952. groupChat: undefined,
  953. systemChat: undefined,
  954. tutorialChat: undefined,
  955.  
  956. names: undefined,
  957. degrees: undefined,
  958. matchState: undefined,
  959. performanceInfo: undefined,
  960. spectatorInfo: undefined,
  961.  
  962. stats: undefined,
  963. },
  964.  
  965. flair: [],
  966. };
  967.  
  968. // If the 'settings' div cannot be found, assume to not be logged in.
  969. if( !/<div(?: [^>]*)? id="settings"/i.test(this.responseText) ) return reject({error:"not logged in", request:this});
  970.  
  971. // Get the global settings
  972. // (ball spin, respawn warnings and video settings are NOT stored on the TP server,
  973. // only in a cookie on your device)
  974. for (var setting in profile.settings) {
  975. match = RegExp('<input(?: [^>]*)? id="' +setting+ '"(?: [^>]*)? (checked)?', 'i').exec(this.responseText);
  976. if (match) {
  977. profile.settings[setting] = Boolean(match[1]);
  978. } else return reject({error:"unknown error", request:this});
  979. }
  980.  
  981. // Get the 'Custom Team Names' setting (the only non-boolean setting)
  982. /*
  983. <select id="teamNames" name="teamNames" class="form-control">
  984. <option value="always" >Always</option>
  985. <option value="spectating" >When Spectating</option>
  986. <option value="never" selected>Never</option>
  987. </select>
  988. */
  989.  
  990. var teamNamesOptions = /<select(?: [^>]*)? id="teamNames"(?: [^>]*)?>((?:\s*?.*?)*?)<\/select>/i.exec(this.responseText);
  991. if (teamNamesOptions) {
  992. var teamNamesOpt_rgx = /<option(?: [^>]*)? value="([^>]*)"(?: [^>]*)? (selected)?(?: [^>]*)?>/ig;
  993. while ( (match = teamNamesOpt_rgx.exec(teamNamesOptions[1])) ){
  994.  
  995. if (match[2]) {
  996. profile.settings.teamNames = match[1];
  997. break;
  998. }
  999. }
  1000. } else return reject({error:"unknown error", request:this});
  1001.  
  1002. // Get both names
  1003. for (var name of ['reservedName','displayedName']) {
  1004. match = RegExp('<input(?: [^>]*)? id="' +name+ '"(?: [^>]*)? value="(.*?)"', 'i').exec(this.responseText);
  1005. if (match) {
  1006. profile[name] = match[1];
  1007. } else return reject({error:"unknown error", request:this});
  1008. }
  1009.  
  1010. // Get your email
  1011. match = /<span(?: [^>]*)? class="hidden-email"(?: [^>]*)?>[^<]*?\b([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})\b<\/span>/i.exec(this.responseText);
  1012. if (match) {
  1013. profile.email = match[1];
  1014. } else return reject({error:"unknown error", request:this});
  1015.  
  1016. // Get all flairs, and whether they are available, and which one is selected
  1017. var flair_rgx = /<li class="(.*?)" data-flair="(.*?)">/ig;
  1018. while ( (match = flair_rgx.exec(this.responseText)) ) {
  1019. var i = profile.flair.push({
  1020. id: match[2],
  1021. selected: match[1].includes('selected'),
  1022. available: match[1].includes('flair-available'),
  1023. });
  1024. if (profile.flair[i-1]) profile.selectedFlair = profile.flair[i-1];
  1025. }
  1026.  
  1027. // Remove duplicate flairs (because there are 3 tabs)
  1028. var flair_ids = [];
  1029. profile.flair = profile.flair.filter(flair => !flair_ids.includes(flair.id) && flair_ids.push(flair.id));
  1030.  
  1031. resolve(profile);
  1032.  
  1033. },
  1034.  
  1035. onerror: ()=> reject({error:"request error", request:this}),
  1036. });
  1037. });
  1038.  
  1039. tpul.profile.getId().catch( reject );
  1040. });
  1041. }
  1042.  
  1043. return tpul_promises.getProfilePage;
  1044. },
  1045.  
  1046. getRolling: function() {
  1047.  
  1048. if (!tpul_promises.getProfileRolling) {
  1049.  
  1050. tpul_promises.getProfileRolling = new Promise(function(resolve,reject) {
  1051.  
  1052. tpul.profile.getId().then( function(id){
  1053.  
  1054. GM_xmlhttpRequest({
  1055. method: "GET",
  1056. url: "http://"+document.location.hostname+"/profile_rolling/"+id,
  1057. onload: function(r){
  1058. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1059. if(r.response.error) reject(r.response);
  1060. if(Array.isArray( r.response )) {
  1061. resolve(r.response);
  1062. }
  1063. else reject({error:"unknown error", request:this});
  1064. },
  1065. onerror: ()=> reject({error:"request error", request:this}),
  1066. });
  1067. });
  1068.  
  1069. tpul.profile.getId().catch( reject );
  1070.  
  1071. });
  1072. }
  1073.  
  1074. return tpul_promises.getProfileRolling;
  1075. },
  1076.  
  1077. getReservedName: function(fallbackTimeout=5e3) {
  1078.  
  1079. /*
  1080.  
  1081. Where to get the Reserved name from?
  1082.  
  1083. - in-game when auth
  1084. - getInfo /profiles/...
  1085. - getPage /profile/...
  1086.  
  1087. Logic:
  1088.  
  1089. 1. if getInfo was called before: use that
  1090. 2. if getPage was called before: use that
  1091. 3. if in-game and auth: get it that way
  1092. 4. call getInfo() to get the name
  1093.  
  1094. */
  1095.  
  1096. if (!tpul_promises.getReservedName) {
  1097.  
  1098. tpul_promises.getReservedName = new Promise(function(resolve,reject) {
  1099.  
  1100. // The fallback: get the reserved name using getInfo()
  1101. var fallback = function(){
  1102.  
  1103. done = true;
  1104.  
  1105. tpul.profile.getInfo().then(function(profileInfo) {
  1106.  
  1107. resolve(profileInfo.reservedName);
  1108. });
  1109.  
  1110. tpul.profile.getInfo().catch( reject );
  1111. };
  1112.  
  1113. if (tpul_promises.getProfileInfo) {
  1114.  
  1115. tpul_promises.getProfileInfo.then(function(profileInfo){
  1116. resolve(profileInfo.reservedName);
  1117. });
  1118.  
  1119. tpul_promises.getProfileInfo.catch( reject );
  1120.  
  1121. } else if (tpul_promises.getProfilePage) {
  1122.  
  1123. tpul_promises.getProfilePage.then(function(profilePage){
  1124. resolve(profilePage.reservedName);
  1125. });
  1126.  
  1127. tpul_promises.getProfilePage.catch( reject );
  1128.  
  1129. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1130. tagpro.ready(function(){
  1131.  
  1132. if (tagpro.players) {
  1133. if (tagpro.players[tagpro.playerId]) {
  1134.  
  1135. if (tagpro.players[tagpro.playerId].auth) {
  1136. resolve (tagpro.players[tagpro.playerId].name);
  1137. } else fallback();
  1138.  
  1139. } else {
  1140.  
  1141. tagpro.socket.on('p',function(playerId) {
  1142. if (tagpro.players[tagpro.playerId]) {
  1143.  
  1144. if (tagpro.players[tagpro.playerId].auth) {
  1145. resolve (tagpro.players[tagpro.playerId].name);
  1146. } else fallback();
  1147.  
  1148. }
  1149. });
  1150.  
  1151. }
  1152. } else fallback();
  1153. });
  1154. } else fallback();
  1155.  
  1156. var done = false;
  1157. setTimeout(function(){
  1158. tpul_promises.getReservedName.then(()=>done=true);
  1159. });
  1160.  
  1161. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1162.  
  1163. });
  1164. }
  1165.  
  1166. return tpul_promises.getReservedName;
  1167. },
  1168.  
  1169. getDisplayedName: function(fallbackTimeout = 5e3) {
  1170.  
  1171. /*
  1172.  
  1173. Where to get the Displayed name from?
  1174.  
  1175. - in-game
  1176. - getProfile /profile/...
  1177.  
  1178. Logic:
  1179.  
  1180. 1. if getPage was called before: use that
  1181. 2. if in-game: get it that way
  1182. 3. call getPage() to get the name
  1183.  
  1184. */
  1185.  
  1186. if (!tpul_promises.getDisplayedName) {
  1187.  
  1188. tpul_promises.getDisplayedName = new Promise(function(resolve,reject) {
  1189.  
  1190. // The fallback: get the displayed name using getPage()
  1191. var fallback = function(){
  1192.  
  1193. done = true;
  1194.  
  1195. tpul.profile.getPage().then(function(profilePage) {
  1196.  
  1197. resolve(profilePage.displayedName);
  1198. });
  1199.  
  1200. tpul.profile.getPage().catch( reject );
  1201. };
  1202.  
  1203. if (tpul_promises.getProfilePage) {
  1204.  
  1205. tpul_promises.getProfilePage.then(function(profilePage){
  1206. resolve(tpul_promises.getProfilePage.displayedName);
  1207. });
  1208.  
  1209. tpul_promises.getProfilePage.catch( reject );
  1210.  
  1211. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1212. tagpro.ready(function(){
  1213.  
  1214. if (tagpro.players) {
  1215. if (tagpro.players[tagpro.playerId]) {
  1216.  
  1217. resolve (tagpro.players[tagpro.playerId].name);
  1218.  
  1219. } else {
  1220.  
  1221. tagpro.socket.on('p',function(playerId) {
  1222. if (tagpro.players[tagpro.playerId]) {
  1223.  
  1224. resolve (tagpro.players[tagpro.playerId].name);
  1225.  
  1226. }
  1227. });
  1228.  
  1229. }
  1230. } else fallback();
  1231. });
  1232. } else fallback();
  1233.  
  1234. var done = false;
  1235. setTimeout(function(){
  1236. tpul_promises.getDisplayedName.then(()=>done=true);
  1237. });
  1238.  
  1239. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1240.  
  1241. });
  1242. }
  1243.  
  1244. return tpul_promises.getDisplayedName;
  1245.  
  1246. },
  1247.  
  1248. getSettings: function(fallbackTimeout = 5e3) {
  1249.  
  1250. /*
  1251.  
  1252. Where to get the settings from?
  1253.  
  1254. - in-game
  1255. - getPage /profile/...
  1256.  
  1257. Logic:
  1258.  
  1259. 1. if in-game: get it that way
  1260. 2. call getPage() to get the settings
  1261.  
  1262. */
  1263.  
  1264. var top_args = arguments;
  1265.  
  1266. if (!tpul_promises.getProfileSettings) {
  1267.  
  1268. tpul_promises.getProfileSettings = new Promise(function(resolve,reject) {
  1269.  
  1270. var fallback = function(){
  1271.  
  1272. done = true;
  1273.  
  1274. tpul.profile.getPage().then(function(profilePage){
  1275. resolve(profilePage.settings);
  1276. });
  1277.  
  1278. tpul.profile.getPage().catch( reject );
  1279. };
  1280.  
  1281. if (top_args[0] && top_args[0].__settings) {
  1282. resolve(top_args[0].__settings);
  1283. } else if (tpul_promises.getProfilePage) {
  1284.  
  1285. tpul_promises.getProfilePage.then(function(profilePage){
  1286. resolve(profilePage.settings);
  1287. });
  1288.  
  1289. tpul_promises.getProfilePage.catch( reject );
  1290.  
  1291. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1292. tagpro.ready(function(){
  1293.  
  1294. if (tagpro.socket && tagpro.socket.on) {
  1295.  
  1296. tagpro.socket.on('settings', function(settings) {
  1297. resolve(Object.assign(settings.ui, {stats:settings.stats}));
  1298. });
  1299.  
  1300. } else fallback();
  1301. });
  1302. } else fallback();
  1303.  
  1304. var done = false;
  1305. setTimeout(function(){
  1306. tpul_promises.getProfileSettings.then(()=>done=true);
  1307. });
  1308.  
  1309. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1310.  
  1311. });
  1312.  
  1313. }
  1314.  
  1315. return tpul_promises.getProfileSettings;
  1316.  
  1317. },
  1318.  
  1319. setSettings: function(newSettings, persistent=true, immediately=false) {
  1320.  
  1321. if (immediately) console.warn("Most settings will NOT take effect immediately, I might add this functionality in the future. Only chat settings work at the moment.");
  1322.  
  1323. return new Promise(function(resolve, reject){
  1324.  
  1325. // Step 1: set any local (cookie) settings
  1326. // These don't have to be send to the server, easy!
  1327.  
  1328. if (persistent) {
  1329.  
  1330. for (let setting in newSettings) {
  1331. if (['sound',
  1332. 'music',
  1333. 'volume',
  1334.  
  1335. 'textures',
  1336.  
  1337. 'disableBallSpin',
  1338. 'tileRespawnWarnings',
  1339. 'disableTutorialChat', // This cookie seems to be unused
  1340. // Setting it anyway \(^.^)/
  1341.  
  1342. 'disableParticles',
  1343. 'forceCanvasRenderer',
  1344. 'disableViewportScaling',
  1345. ].includes(setting)) {
  1346.  
  1347. var expires = new Date(Date.now() + 31536e8).toUTCString(); // A century from now (same as TagPro uses)
  1348. document.cookie = setting + '=' + newSettings[setting] + '; expires='+expires+'; path=/; domain=.koalabeast.com';
  1349. }
  1350. }
  1351.  
  1352. // Step 2: send any server-sided settings to the server
  1353.  
  1354. if (['reservedName',
  1355. 'displayedName',
  1356.  
  1357. 'allChat',
  1358. 'teamChat',
  1359. 'groupChat',
  1360. 'systemChat',
  1361. 'tutorialChat',
  1362.  
  1363. 'names',
  1364. 'degrees',
  1365. 'matchState',
  1366. 'performanceInfo',
  1367. 'spectatorInfo',
  1368.  
  1369. 'teamNames',
  1370. 'stats',
  1371. ].some( s => s in newSettings ) ){
  1372.  
  1373. // Call these to let them run in parallel
  1374. tpul.profile.getSettings();
  1375. tpul.profile.getReservedName();
  1376. tpul.profile.getDisplayedName();
  1377.  
  1378. tpul.profile.getSettings().then( function(settings){
  1379. tpul.profile.getReservedName().then( function(reservedName){
  1380. tpul.profile.getDisplayedName().then( function(displayedName){
  1381.  
  1382. console.log(param({reservedName: reservedName, // Your reservedName
  1383. displayedName: displayedName, // Your displayedName
  1384. //...settings, // The current settings
  1385. //...newSettings}));
  1386. }));
  1387. var req = GM_xmlhttpRequest({
  1388. data: param(Object.assign({},
  1389. settings, // The current settings
  1390. {reservedName: reservedName, // Your reservedName
  1391. displayedName: displayedName}, // Your displayedName
  1392. newSettings // Overwrite with the settings that you want to edit.
  1393. )),
  1394. method: "POST",
  1395. headers: {"Content-Type": "application/x-www-form-urlencoded"},
  1396. url: "http://"+document.location.hostname+"/profile/update",
  1397. onload: function(r){
  1398. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1399.  
  1400. var arr;
  1401. try{ arr = JSON.parse(r.response); }
  1402. catch(e){ reject({error:"/profile/update responded invalid JSON", request:this}); }
  1403.  
  1404. if(arr.error) reject(arr);
  1405. else if(arr.success) {
  1406. resolve(arr);
  1407. } else reject({error:'unknown error',response: arr, request:this});
  1408. },
  1409. onerror: reject,
  1410. });
  1411.  
  1412. });
  1413. });
  1414. });
  1415.  
  1416. }
  1417.  
  1418. }
  1419.  
  1420. // Step 3: In case we are in-game, let the settings go into effect immediately.
  1421. // To update the reserved name, a refresh is required. TPUL won't do this!
  1422.  
  1423. if (immediately && tagpro) {
  1424. if (!tagpro.settings) tagpro.settings = {ui:{}};
  1425. if (!tagpro.settings.ui) tagpro.settings.ui = {};
  1426.  
  1427. for (let setting in newSettings) {
  1428. if (['allChat',
  1429. 'teamChat',
  1430. 'groupChat',
  1431. 'systemChat',
  1432. 'tutorialChat',
  1433. ].includes(setting)){
  1434.  
  1435. tagpro.settings.ui[setting] = newSettings[setting];
  1436. }
  1437. }
  1438.  
  1439. if (setting == 'tutorialChat') {
  1440. var tutorialButton = document.getElementById('tutorialButton');
  1441.  
  1442. if (tutorialButton) {
  1443. var action = tutorialButton.innerText === "Enable Tips";
  1444. if (newSettings[setting] == action) tutorialButton.click();
  1445. }
  1446. }
  1447.  
  1448.  
  1449. }
  1450.  
  1451. });
  1452. }
  1453.  
  1454. },
  1455.  
  1456. rollingChat: {
  1457.  
  1458. _init: function initRollingChat(enable = false){
  1459.  
  1460. // In case you don't want to load the full TPUL library,
  1461. // You can add RollingChat to your own script by copying this function
  1462. // Usage:
  1463. // initRollingChat(true);
  1464.  
  1465. if (!tagpro.rollingChat) {
  1466.  
  1467. tagpro.rollingChat = {
  1468. enabled: false,
  1469. get handler() {
  1470. return function(event) {
  1471.  
  1472. // Return if not enabled
  1473. if (!tagpro.rollingChat.enabled) return;
  1474.  
  1475. // Whether you are releasing instead of pressing the key:
  1476. var releasing = event.type == 'keyup';
  1477.  
  1478. // Check if any modifier keys where held down during a keyDown
  1479. if (!releasing && (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)) return;
  1480.  
  1481. // The key that is pressed/released (undefined when it is any other key)
  1482. var arrow = ['left','up','right','down'][[37,38,39,40].indexOf(event.keyCode)];
  1483.  
  1484. // Only if the controls are disabled (usually while composing a message)
  1485. // AND the key is indeed an arrow (not undefined)
  1486. if (tagpro.disableControls && arrow) {
  1487.  
  1488. // Prevent the 'default' thing to happen, which is the cursor moving through the message you are typing
  1489. event.preventDefault();
  1490.  
  1491. // Return if already pressed/released
  1492. if (tagpro.players[tagpro.playerId].pressing[arrow] != releasing) return;
  1493.  
  1494. // Send the key press/release to the server!
  1495. tagpro.sendKeyPress(arrow, releasing);
  1496.  
  1497. // Not necesarry, but useful for other scripts to 'hook onto'
  1498. if (!releasing && tagpro.events.keyDown) tagpro.events.keyDown.forEach(f => f.keyDown(arrow));
  1499. if (releasing && tagpro.events.keyUp) tagpro.events.keyUp.forEach(f => f.keyUp(arrow));
  1500. if(tagpro.ping.avg)setTimeout(()=>(tagpro.players[tagpro.playerId][arrow]=!releasing),tagpro.ping.avg/2);
  1501. }
  1502. };
  1503. }
  1504. };
  1505.  
  1506. // intercept all key presses and releases:
  1507. document.addEventListener('keydown', tagpro.rollingChat.handler);
  1508. document.addEventListener('keyup', tagpro.rollingChat.handler);
  1509. }
  1510.  
  1511. if (enable) tagpro.rollingChat.enabled = true;
  1512. },
  1513.  
  1514. get enabled(){
  1515. tpul.rollingChat._init();
  1516. return tagpro.rollingChat.enabled;
  1517. },
  1518.  
  1519. set enabled(e){
  1520. tpul.rollingChat._init();
  1521. if (!e) console.warn('Disabling Rolling Chat! This will disable Rolling Chat for all scripts, not only yours! Please enable it again asap to not get users confused.');
  1522. tagpro.rollingChat.enabled = Boolean(e);
  1523. },
  1524. },
  1525.  
  1526. notify: function(text, type="message", timeout=Math.max(4000, 50*text.length) ){
  1527.  
  1528. // Accepted types: message, success, error, warning
  1529. // ( white green red yellow )
  1530. // For more types, the only thing you need to add is some CSS
  1531.  
  1532. var notification = document.createElement('div');
  1533. notification.className = 'tpul-notification tpul-notification-' + type;
  1534. notification.innerText = text;
  1535. document.body.appendChild(notification);
  1536.  
  1537. // Hide after a while (timeout)
  1538. setTimeout(function(notification){
  1539. if(notification)notification.classList.add('vanish');
  1540. }, timeout, notification);
  1541.  
  1542. // Hide on click
  1543. notification.onclick = function(){ this.classList.add('vanish'); };
  1544.  
  1545. // Clear up the DOM once the notification is vanished
  1546. notification.addEventListener('transitionend',function(){ this.remove(); });
  1547.  
  1548. // Return the element, for scripters to "play" with
  1549. return notification;
  1550.  
  1551. },
  1552.  
  1553. groupcomm: {
  1554.  
  1555. emit: function ( script, command, ...args ) {
  1556. if (tagpro && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {
  1557.  
  1558. var full_command = "/" + [...arguments].map(a=>(a||"").replace(/([\^\/:;])/g,"^$1")).join("/") + ";";
  1559.  
  1560. tagpro.group.socket.emit( "touch", full_command.substr( 0,12 ) );
  1561. for (var i = 12; i < full_command.length; i += 11) {
  1562. tagpro.group.socket.emit( "touch", ":" + full_command.substr( i,11 ) );
  1563. }
  1564.  
  1565. tagpro.group.socket.emit( "touch", tagpro.group.socket.playerLocation );
  1566. }
  1567. else throw "Not connected to a group";
  1568. },
  1569.  
  1570. oncommand: function oncommand( callback ) {
  1571. if (tagpro && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {
  1572. if (!tpul.groupcomm._active) tpul.groupcomm._init();
  1573. tpul.groupcomm._callbacks.push(callback);
  1574. }
  1575. else throw "Not connected to a group";
  1576. },
  1577.  
  1578. _callbacks: [],
  1579.  
  1580. _commands: {},
  1581.  
  1582. _active: false,
  1583.  
  1584. _init: function (){
  1585.  
  1586. if (tpul.groupcomm._active) return;
  1587.  
  1588. tpul.groupcomm._active = true;
  1589.  
  1590. tagpro.group.socket.on( "member", function(member) {
  1591.  
  1592. function handleCommand(command){
  1593. var args = [...command
  1594. .replace(/\^(.)/g, "$1^")
  1595. .match(/\/(.*);(?=(?:\^\^)*(?!\^))/,1)[1]
  1596. .split(/\/(?=(?:\^\^)*(?!\^))/)
  1597. .map(a=>a.replace(/\(.)\^(?=(?:\^\^)*(?!\^))/g, "$1"))
  1598. ];
  1599.  
  1600. for (var c in tpul.groupcomm._callbacks) {
  1601. var callback = tpul.groupcomm._callbacks[c];
  1602. try { callback({
  1603. member: member,
  1604. script: args.shift() || null,
  1605. command: args.shift() || null,
  1606. args: args,
  1607. raw:command } ); }
  1608. catch(e) {
  1609. console.error("Unhandled GroupComm error. Mod makers, handle your errors!", e);
  1610. tpul.groupcomm._callbacks.splice(c,1);
  1611. }
  1612. }
  1613. }
  1614.  
  1615. var raw = member.location,
  1616. commands = tpul.groupcomm._commands;
  1617.  
  1618. if (typeof raw !== "string") return;
  1619.  
  1620. // A full one-line command: / ... ;
  1621. if ( raw.match(/^\/.*[^^];/) ) {
  1622. handleCommand( raw );
  1623. delete commands[member.id];
  1624. }
  1625.  
  1626. // The start of a multi-line command: / ...
  1627. else if ( raw.match(/^\//) ) {
  1628. commands[member.id] = raw;
  1629. }
  1630.  
  1631. // The end of a multi-line command: : ... ;
  1632. else if ( raw.match(/^:.*[^^];/) ) {
  1633. if (!commands[member.id]) throw "Did not receive start of command.";
  1634. var com = commands[member.id] + raw.slice(1);
  1635. handleCommand( com );
  1636. delete commands[member.id];
  1637. }
  1638.  
  1639. // A middle part of a multi-line command: : ...
  1640. else if ( raw.match(/^:/) ) {
  1641. if (!commands[member.id]) throw "Did not receive start of command.";
  1642. commands[member.id] += raw.slice(1);
  1643. }
  1644.  
  1645. // Not a GroupComm command:
  1646. else delete commands[member.id];
  1647. });
  1648. }
  1649. }
  1650. };
  1651.  
  1652. if (!SettingsMenu.parentElement) tpul.settings.parent = null;
  1653.  
  1654.  
  1655.  
  1656.  
  1657.  
  1658. // OPENING AND CLOSING
  1659.  
  1660. SettingsFrame.onclick = function(click) {
  1661.  
  1662. // Close all settings when clicking outside the panel
  1663. if (SettingsFrame == click.target) for (var settings of all_settings) settings.close();
  1664.  
  1665. };
  1666.  
  1667. SettingsFrame.addEventListener('scroll', function(wheel) {
  1668.  
  1669. // Open when scrolling down (only in game)
  1670. if (location.port && wheel.deltaY > 0 && last_opened && !last_opened.isOpen) last_opened.open();
  1671.  
  1672. // Close all settings when scrolling up far enough
  1673. setTimeout(function(){
  1674. if (SettingsFrame.firstElementChild &&
  1675. SettingsFrame.scrollTop + SettingsFrame.offsetHeight <= SettingsFrame.firstElementChild.offsetTop + 20)
  1676. for (var settings of all_settings) settings.close();
  1677. },200);
  1678. });
  1679.  
  1680.  
  1681.  
  1682.  
  1683. // Section tabs
  1684.  
  1685. SettingsFrame.addEventListener('click', function(click) {
  1686. var tablist = click.target.parentElement;
  1687. if (tablist.classList.contains('tab-list')) {
  1688.  
  1689. var scrollTop = SettingsFrame.scrollTop;
  1690. console.log(scrollTop);
  1691.  
  1692. for (let li of tablist.getElementsByTagName('li'))
  1693. li.classList.remove('active');
  1694. for (let pane of tablist.parentElement.getElementsByClassName('tab-pane'))
  1695. pane.classList.remove('active');
  1696. click.target.classList.add('active');
  1697. document.querySelector(click.target.dataset.target).classList.add('active');
  1698.  
  1699. SettingsFrame.scrollTop = scrollTop;
  1700. }
  1701. }, true);
  1702.  
  1703.  
  1704.  
  1705. // Get settings from socket:
  1706.  
  1707. if (tagpro && tagpro.ready) {
  1708. tagpro.ready(function(){
  1709. if (tagpro.socket && tagpro.socket.on) {
  1710. tagpro.socket.on('settings', function(settings) {
  1711. // Don't try to tamper with this, or copy this in your own script.
  1712. // It will affect all scripts using TPUL.
  1713. tpul.profile.getSettings( {__settings:Object.assign(settings.ui, {stats: settings.stats})} );
  1714. });
  1715. }
  1716. });
  1717. }
  1718.  
  1719. // Some helper functions
  1720.  
  1721. function param(o){
  1722.  
  1723. return Object.keys(o).map(function(k) {
  1724. return encodeURIComponent(k) + '=' + encodeURIComponent(o[k.replace(' ','+')]);
  1725. }).join('&').replace(/%20/g, '+');
  1726. }
  1727.  
  1728.  
  1729.  
  1730.  
  1731. // =====NOITCES CIGOL=====
  1732.  
  1733.  
  1734.  
  1735.  
  1736.  
  1737. if (typeof tpul_promises == 'undefined') {
  1738. try{
  1739. window.tpul_promises = {};
  1740. unsafeWindow.tpul_promises = window.tpul_promises;
  1741. }catch(e){}
  1742. }
  1743.  
  1744.  
  1745. return tpul;
  1746. })();