BP Funcs

Small script to be @require-d, providing useful functions and extensions I like to refularly refer to

目前為 2022-06-26 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/447081/1064792/BP%20Funcs.js

// ==UserScript==
// @name		BP Funcs
// @description	Small script to be @require-d, providing useful functions and extensions I like to refularly refer to
// @version		1.0.0
// @namespace	BP
// @author		Benjamin Philipp <dev [at - please don't spam] benjamin-philipp.com>
// ==/UserScript==

/*
	BP Funcs, as of 2022-06-26 19:18:49 (GMT +02:00)
*/

String.prototype.after = function(str, fromRight, returnAll){
	if(fromRight === undefined)
		fromRight = false;
	if(returnAll === undefined)
		returnAll = false;
	var os = this.indexOf(str);
	if(fromRight)
		os = this.lastIndexOf(str);
	if(os<0)
		return returnAll?this:"";
	return this.substring(os + str.length);
};
String.prototype.before = function(str, fromRight, returnAll){
	if(fromRight === undefined)
		fromRight = false;
	if(returnAll === undefined)
		returnAll = false;
	var os = this.indexOf(str);
	if(fromRight)
		os = this.lastIndexOf(str);
	if(os<0)
		return returnAll?this:"";
	return this.substr(0, os);
};
function bpMenu(){
	var r = {};
	r.obj = null;
	r.items = {};
	r.styles = {
		default: {
			colors : {
				background : "#fff",
				text : "#333",
				item : "#555",
				itemHover : "#46a",
				itemBack : "transparent",
				frame : "#fff"
			},
			paddingFrame : "10px",
			radiusFrame : "10px",
			fontSize : "18px"
		}
	};
	r.style = r.styles.default;
	r.setup = function(override=false){
		var head, script;
		head = document.getElementsByTagName("head")[0];
//		if(typeof $ !== "function"){
//			console.log("jQuery not available?\nTrying to insert & load...", typeof $);
//			script = document.createElement("script");
//			script.type = "text/javascript";
//			script.onload = function(){
//				r.setup();
//			};
//			script.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js";
//			head.appendChild(script);
//			return;
//		}
		if(typeof bpModal !== "function"){
			console.log("BP Modal not available?\nTrying to insert & load...", typeof bpModal);
			script = document.createElement("script");
			script.type = "text/javascript";
			script.onload = function(){
				r.setup();
			};
			script.src = "https://benjamin-philipp.com/js/gm/funcs.js?funcs=bpModal";
			head.appendChild(script);
			return;
		}
//		console.log("Setup BP Menu");
		if(override){
			$("body>#bpMenu").remove();
		}
		r.injectStyles(override);

		if($("body>#bpMenu").length <=0)
			$("body").append("<div id='bpMenu'><div class='inner'></div></div>");
		r.obj = $("body>#bpMenu");
	};
	r.injectStyles = function(override=false){
		if(override)
			$("#bpMenuStyle").remove();
		if($("#bpMenuStyle").length<=0)
			$("head").append(`<style id="bpMenuStyle">
			.bpbutton{
				padding: 5px;
				display: inline-block;
				background: #aaa;
				color: #333;
				cursor: pointer;
				font-weight: 600;
				line-height: 1em;
			}
			.bpbutton:hover, .bpbutton.on{
				background: #1c82fa;
				color: #fff;
			}
			
			#bpMenu{
				position: fixed;
				z-index: 99999;
				top: -50px;
				right: 0px;
				height: 70px;
				display: inline-block;
				transition: top 0.5s;
				padding: 0px 0px 10px;
			}
			#bpMenu .inner{
				display: inline-block;
				background-color: #fff;
				color: #aaa;
				padding: 0px 10px 5px;
				border-radius: 0 0 10px 10px;
				box-shadow: 0 0 10px rgba(0,0,0,0.5);
			}
			#bpMenu:hover{
				top: 0px;
			}
			#bpMenu .bp{
				display: inline-block;
				padding: 5px;
				font-size: ' + r.style.fontSize + ';
			}
			#bpMenu .bp.item{
				color: ' + r.style.colors.item + ';
				font-weight: bold;
				cursor: pointer;
			}
			#bpMenu .bp.item:hover{
				color: ' + r.style.colors.itemHover + ';
			}
			#bpMenu .bp+.bp{
				margin-left: 10px;
			}
			</style>`);
	};
	r.add = function(id, html, cb=null, title="", override=false, sel=""){
		let l = $("body>#bpMenu>.inner #" + id);
		let add = true;
		if(l.length >0){
			add = false;
			if(override){
				l.remove();
				add = true;
			}
		}
		if(add){
			if(title)
				title = " title='" + title + "'";
			$("body>#bpMenu .inner").append("<div id='" + id + "' class='bp" + (cb?" item":"") + "'" + title + ">" + html + "</div>");
			r.items[id] = $("#bpMenu #" + id);
			if(cb)
				$("#bpMenu #" + id).click(function(e){
					cb(e);
				});
		}
	};
	r.changeStyle = function(obj){
		mergeDeep(r.style, obj);
		r.injectStyles(true);
	};
	r.setup();
	return r;
}
//if("undefined" === typeof bpMenuHelper){ // jshint ignore:line
//	var bpMenuHelper = bpMenu(); // jshint ignore:line
//}
function dateString(date, format){
     if(date===undefined || date===null || date === "")
        date = new Date();
     if(format===undefined)
        format="YYYY-MM-DD HH:mm:SS";
     else if(format==="file")
        format="YYYY-MM-DD HH-mm-SS";
	date = new Date(date);
     var year = date.getUTCFullYear();
     var months = pad(date.getUTCMonth() + 1);
     var days =  pad(date.getUTCDate());
     var hours = pad(date.getUTCHours());
     var minutes = pad(date.getUTCMinutes());
     var seconds = pad(date.getUTCSeconds());
     return format.replace("YYYY", year)
        .replace("MM", months)
        .replace("DD", days)
        .replace("HH", hours)
        .replace("mm", minutes)
        .replace("SS", seconds);
}
function pad(num){
    var r = String(num);
    if(r.length<=1)
        r = "0" + r;
    return r;
}
var bpTitleFormats = {
	movies: [
		"[title] [[year]]",
		"[title] ([year])",
		"[title] - [year]"
	],
	series: [
		"[name] - Season [season] Episode [episode] - [title]",
		"[name] - Season [season] Episode [episode]",
		"[name] - S[lzseason]E[lzepisode] - [title]",
		"[name] - S[lzseason]E[lz3episode] - [title]"
	]
};

var bpMediaTitleRegex = {
	movies: [
		/(.+) \[(\d{4})\]$/mi,
		/(.+) \((\d{4})\)$/mi,
		/(.+) - (\d{4})$/mi,
	],
	series: [
		// /(.+?) ?-? (?:(?:season |S)?0?0?(\d+))? ?(?:episode |E|x)0*(\d+(?:-\d+)?)(?: ?[:-]? (.+))?/mi
		/(.+?) ?-? (?:(?:season |S)?0*(\d+))?(?: -|,)?\s*(?:episode |E|x)0*(\d+(?:-\d+)?)(?: ?[:-]? (.+))?/mi
	]
};
{//// Check RegEx against:
	// Some series name 1x12
	// Some series name 1x12 and a title
	// Some series name 1x12 - and a title
	// Some series name S01E12
	// Some series name S01E12 and a title
	// Some series name S01E12 - and a title
	// Some series name S01E12-13
	// Some series name S01E12-13 and a title
	// Some series name S01E12-13 - and a title
	// Some series name - 1x12
	// Some series name - 1x12 and a title
	// Some series name - 1x12 - and a title
	// Some series name - S01E12
	// Some series name - S01E12 and a title
	// Some series name - S01E12 - and a title
	// Some series name - S01E12-13
	// Some series name - S01E12-13 and a title
	// Some series name - S01E12-13 - and a title
	// Some series name Episode 12
	// Some series name Episode 12 and a title
	// Some series name Episode 12 - and a title
	// Some series name Episode 12-13
	// Some series name Episode 12-13 and a title
	// Some series name Episode 12-13 - and a title
	// Some series name Season 1 Episode 12
	// Some series name Season 1 Episode 12 and a title
	// Some series name Season 1 Episode 12 - and a title
	// Some series name Season 1 Episode 12-13
	// Some series name Season 1 Episode 12-13 and a title
	// Some series name Season 1 Episode 12-13 - and a title
	// Some series name - Episode 12
	// Some series name - Episode 12 and a title
	// Some series name - Episode 12 - and a title
	// Some series name - Episode 12-13
	// Some series name - Episode 12-13 and a title
	// Some series name - Episode 12-13 - and a title
	// Some series name - Season 1 Episode 12
	// Some series name - Season 1 Episode 12 and a title
	// Some series name - Season 1 Episode 12 - and a title
	// Some series name - Season 1 Episode 12-13
	// Some series name - Season 1 Episode 12-13 and a title
	// Some series name - Season 1 Episode 12-13 - and a title
	// Some series name - Season 1 Episode 12: and a title
	// Some series name - Season 1 Episode 12 : and a title
	// Some series name - Season 1 Episode 12-13: and a title
	// Some series name - Season 1 Episode 12-13 : and a title
	// Some series name - Season 1, Episode 12
	// Some series name - Season 1, Episode 12 and a title
	// Some series name - Season 1, Episode 12 - and a title
	// Some series name - Season 1, Episode 12-13
	// Some series name - Season 1, Episode 12-13 and a title
	// Some series name - Season 1, Episode 12-13 - and a title
	// Some series name - Season 1, Episode 12: and a title
	// Some series name - Season 1, Episode 12 : and a title
	// Some series name - Season 1, Episode 12-13: and a title
	// Some series name - Season 1 - Episode 12
	// Some series name - Season 1 - Episode 12 and a title
	// Some series name - Season 1 - Episode 12 - and a title
	// Some series name - Season 1 - Episode 12-13
	// Some series name - Season 1 - Episode 12-13 and a title
	// Some series name - Season 1 - Episode 12-13 - and a title
	// Some series name - Season 1 - Episode 12: and a title
	// Some series name - Season 1 - Episode 12 : and a title
	// Some series name - Season 1 - Episode 12-13: and a title
	// Some series name - Season 1 - Episode 12-13 : and a title
}

function guessMovieOrTV(title){
	var tit = title.replace(/[—–]/g, "-"); // em-dash, en-dash
	for(let rex of bpMediaTitleRegex.series){
		if(rex.test(tit))
			return "TV";
	}
	for(let rex of bpMediaTitleRegex.movies){
		if(rex.test(tit))
			return "Movie";
	}
	return false;
}

function formatMovieTV(tit, templateSeries, templateMovie){
	switch(guessMovieOrTV(tit)){
		case "TV":
			return formatEpisodeTitle(tit, templateSeries);
		case "Movie":
			return formatMovieTitle(tit, templateMovie);
		default:
			console.log("Could not identify TV or Movie title");
			return tit;
	}
}

function formatMovieTitle(tit, template){
	if(!template)
		template = bpTitleFormats.movies[0];
		
//	console.log("preferred format: " + template);
	var match = false;
	for(let rex of bpMediaTitleRegex.movies){
		match = tit.match(rex);
		if(match){
//			match = rex.exec(tit);
			console.log("title matches format " + rex.toString(), match);
			break;
		}
	}
	if(!match){
		console.log("Title format not recognized", tit);
		return tit;
	}
	var name = match[1];
	var year = match[2];
	
	tit = template.replace("[title]", name)
		.replace("[year]", year);
	console.log("formatted title:", tit);
	return tit;
}

function formatEpisodeTitle(tit, template){
	if(!template)
		template = bpTitleFormats.series[0];
		
//	console.log("preferred format: " + template);
	
	tit = tit.replace(/[—–]/g, "-"); // em-dash, en-dash
	var match = false;
	for(let rex of bpMediaTitleRegex.series){
		match = tit.match(rex);
		if(match){
//			match = rex.exec(tit);
			console.log("title matches format " + rex.toString(), match);
			break;
		}
	}
	if(!match){
		console.log("Title format not recognized", tit);
		return tit;
	}
	var name = match[1];
	var season = match[2];
	if(!season)
		season = 1;
	var episode = match[3];
	var title = (match.length>=5 && match[4] !== undefined)? match[4] : "";
	if((/(Episode #? ?\d+|S\d+ ?E\d+)/i).test(title))
		title = "";
//	console.log({"name" : name, "season" : season, "episode" : episode, "title" : title});
	
	
	if(title===""){
		template = template.replace(/ ?-? \[title\]/, "");
		console.log("no title:", template);
	}
	
	tit = template.replace("[name]", name)
		.replace("[season]", season)
		.replace("[episode]", episode)
		.replace(/\[lz(\d*)season]/i, function(_,p){
			if(p==="")
				p = 2;
			return lz(season, p);
		})
		.replace(/\[lz(\d*)episode]/i, function(_,p){
			if(p==="")
				p = 2;
			return lz(episode, p);
		})
		.replace("[title]", title);
	console.log("formatted title:", tit);
	return tit;
}

function sanitize(str){
	str = str.replace(/[\\"]/g, "-");
	str = str.replace(/\?/g, "");
	str = str.replace(/[\/:]/g, " - ");
	str = str.replace(/[\/:]/g, " - ");
	str = str.replace(/\s+-\s*(?:-+\s+)+/g, " - ");
	str = str.replace(/\s\s+/g, " ");
	return str;
}

function lz(num, places = 2){
	return ("0000" + num).slice(-places);
}

function toTitleCase(str, preserveCaps=false, preserveAllCaps=false){
	return str.replace(/\w[^\s_:-]*/g, function(txt){
		var rest = txt.substr(1);
		if(!preserveCaps){
			if(preserveAllCaps){
				if(txt.charAt(0) != txt.charAt(0).toUpperCase()|| rest != rest.toUpperCase())
					rest = rest.toLowerCase();
			}
			else
				rest = rest.toLowerCase();
		}
		return txt.charAt(0).toUpperCase() + rest;
	});
}
const mimeTypes = {
	".aac": "audio/aac",
	".abw": "application/x-abiword",
	".arc": "application/x-freearc",
	".avif": "image/avif",
	".avi": "video/x-msvideo",
	".azw": "application/vnd.amazon.ebook",
	".bin": "application/octet-stream",
	".bmp": "image/bmp",
	".bz": "application/x-bzip",
	".bz2": "application/x-bzip2",
	".cda": "application/x-cdf",
	".csh": "application/x-csh",
	".css": "text/css",
	".csv": "text/csv",
	".doc": "application/msword",
	".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	".eot": "application/vnd.ms-fontobject",
	".epub": "application/epub+zip",
	".gz": "application/gzip",
	".gif": "image/gif",
	".htm": "text/html",
	".html": "text/html",
	".ico": "image/vnd.microsoft.icon",
	".ics": "text/calendar",
	".jar": "application/java-archive",
	".jpeg.jpg": "image/jpeg",
	".js": "text/javascript",
	".json": "application/json",
	".jsonld": "application/ld+json",
	".mid.midi": "audio/midi",
	".mjs": "text/javascript",
	".mp3": "audio/mpeg",
	".mp4": "video/mp4",
	".mpeg": "video/mpeg",
	".mpkg": "application/vnd.apple.installer+xml",
	".odp": "application/vnd.oasis.opendocument.presentation",
	".ods": "application/vnd.oasis.opendocument.spreadsheet",
	".odt": "application/vnd.oasis.opendocument.text",
	".oga": "audio/ogg",
	".ogv": "video/ogg",
	".ogx": "application/ogg",
	".opus": "audio/opus",
	".otf": "font/otf",
	".png": "image/png",
	".pdf": "application/pdf",
	".php": "application/x-httpd-php",
	".ppt": "application/vnd.ms-powerpoint",
	".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
	".rar": "application/vnd.rar",
	".rtf": "application/rtf",
	".sh": "application/x-sh",
	".svg": "image/svg+xml",
	".swf": "application/x-shockwave-flash",
	".tar": "application/x-tar",
	".tif": "image/tiff",
	".tiff": "image/tiff",
	".ts": "video/mp2t",
	".ttf": "font/ttf",
	".txt": "text/plain",
	".vsd": "application/vnd.visio",
	".wav": "audio/wav",
	".weba": "audio/webm",
	".webm": "video/webm",
	".webp": "image/webp",
	".woff": "font/woff",
	".woff2": "font/woff2",
	".xhtml": "application/xhtml+xml",
	".xls": "application/vnd.ms-excel",
	".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
	".xml": "application/xml",
	".xul": "application/vnd.mozilla.xul+xml",
	".zip": "application/zip",
	".3gp": "video/3gpp",
	".3g2": "video/3gpp2",
	".7z": "application/x-7z-compressed"
};

function getContentTypeByExtension(ext){
	var defaultType = "application/octet-stream";
	if(typeof ext != "string")
		return console.warn('[getContentTypeByExtension] Invalid type supplied, please supply string', ext) || defaultType;
	if(ext.indexOf(".")<0)
		ext = "." + ext;
	else
		ext = ext.replace(/^.*(?=[.]\w+$)/, '');
	var mime = mimeTypes[ext.toLowerCase()];
	if(!mime)
		return console.warn('[getContentTypeByExtension] Failed to resolve for content name: %s', ext) || defaultType;
	return mime;
}
function getParam(s){
	return getParamFromString(location.href, s);
}

function getParamFromString(u, s){
	var url = new URL(u);
	return url.searchParams.get(s);
}
function getSelectedElements(){
	var allSelected = [];
	try{
		var selection = window.getSelection();
		var range = selection.getRangeAt(0);
		if(range.startOffset == range.endOffset)
			return allSelected;
		var cont = range.commonAncestorContainer;
		if(!cont){
//			console.log("no parent container?");
			return range.startContainer;
		}
		if(!cont.nodeName || cont.nodeName == "#text" || !cont.getElementsByTagName){
			var p = cont.parentElement;
//			console.log("weird container or text node; return parent", cont, p);
			if(!p){
//				console.log("actually, never mind; has no parent. Return element instead");
				return [cont];
			}
			return [p];
		}
		var allWithinRangeParent = cont.getElementsByTagName("*");

		for (var i=0, el; el = allWithinRangeParent[i]; i++){ // jshint ignore:line
			// The second parameter says to include the element 
			// even if it's not fully selected
			if (selection.containsNode(el, true))
				allSelected.push(el);
		}
	}catch(e){
		console.log(e);
	}
	return allSelected;
}
function htmlEntities(str){
	return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
		return '&#' + i.charCodeAt(0) + ';';
	});
}
function isNativeFunction(value) {
	// Used to resolve the internal `[[Class]]` of values
	var toString = Object.prototype.toString;

	// Used to resolve the decompiled source of functions
	var fnToString = Function.prototype.toString;

	// Used to detect host constructors (Safari > 4; really typed array specific)
	var reHostCtor = /^\[object .+?Constructor\]$/;

	// Compile a regexp using a common native method as a template.
	// We chose `Object#toString` because there's a good chance it is not being mucked with.
	var reNative = RegExp('^' +
		// Coerce `Object#toString` to a string
		String(toString)
		// Escape any special regexp characters
		.replace(/[.*+?^${}()|[\]\/\\]/g, '\\$&')
		// Replace mentions of `toString` with `.*?` to keep the template generic.
		// Replace thing like `for ...` to support environments like Rhino which add extra info
		// such as method arity.
		.replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
	);
	var type = typeof value;
	return type == 'function'
		// Use `Function#toString` to bypass the value's own `toString` method
		// and avoid being faked out.
		? // jshint ignore:line
		reNative.test(fnToString.call(value))
		// Fallback to a host object check because some environments will represent
		// things like typed arrays as DOM methods which may not conform to the
		// normal native pattern.
		:
		(value && type == 'object' && reHostCtor.test(toString.call(value))) || false;
}
function jqExtend(jQ=false){
	if(jQ || ("undefined" != typeof(jQuery) && "undefined" != typeof(jQuery.fn)) || ("undefined" != typeof($) && "undefined" != typeof($.fn))){
		var fn;
		if("undefined" != typeof(jQ) && "undefined" != typeof(jQ.fn))
			fn = jQ.fn;
		else if("undefined" != typeof(jQuery) && "undefined" != typeof(jQuery.fn))
			fn = jQuery.fn;
		else
			fn = $.fn;
		fn.selectText = function(){
			var doc = document;
			for(var i = 0; i<this.length; i++){
				var element = this[i];
				var range;
				if (doc.body.createTextRange){
					range = document.body.createTextRange();
					range.moveToElementText(element);
					range.select();
				} else if (window.getSelection){
					var selection = window.getSelection();
					range = document.createRange();
					range.selectNodeContents(element);
					selection.removeAllRanges();
					selection.addRange(range);
				}
			}
		};

		fn.fare = function(){
			$(this).fadeOut(function(){
				$(this).remove();
			});
		};

		fn.textOnly = function(trim=true){
			var c = this.clone();
			c.children().remove();
			if(trim)
				return c.text().trim();
			return c.text();
		};
	}
	else
	console.log("no jQuery, no extensions :(");
}
jqExtend();
class BPLogger {
	constructor(name = false, prefix = false, debugging = false, level = 1){
		if(!name && typeof GM_info !== "undefined")
			name = GM_info.script.name;
		
		if(prefix === false && name)
			prefix = name;
		
		if(!name)
			name = "Logger";
		
		this.name = name;
		this.prefix = prefix;
		this.debugging = debugging;
		this.level = level;

		this.colors = {
			default: [180, 100],
			warn: [60, 100],
			error: [0, 100],
			success: [150, 100]
		};
		this.history = [];
		this.keepHistory = false;

		if (typeof name == "object"){
			Object.assign(this, name);
		}
		return this;
	}

	writeLog(args, type = "default", level = 1){
		if (this.keepHistory)
			this.history.push([Date.now(), type, level, args]);
		if(this.prefix)
			args = ["%c" + this.prefix + ":", `color: hsl(${this.colors[type][0]},${this.colors[type][1]}%,80%); background-color: hsl(${this.colors[type][0]},${this.colors[type][1]}%,15%); font-weight: 900!important`, ...args];
		if (this.debugging)
			args = [...args, new Error().stack.replace(/^\s*(Error|Stack trace):?\n/gi, "").replace(/^([^\n]*\n)/, "\n")];
		
		if(["warn", "error"].includes(type))
			console[type](...args);
		else
			console.log(...args);
	}
	log(...args){
		this.writeLog(args);
	}
	warn(...args){
		this.writeLog(args, "warn");
	}
	error(...args){
		this.writeLog(args, "error");
	}
	success(...args){
		this.writeLog(args, "success");
	}
}

function BPLogger_default(...args){
	if(args.length<=0)
		args = "";
	var logger = new BPLogger(args);
	log = function(...args){
		logger.log(...args);
	};
	warn = function(...args){
		logger.warn(...args);
	};
	error = function(...args){
		logger.error(...args);
	};
	success = function(...args){
		logger.success(...args);
	};
	return logger;
}
String.prototype.matches = function(rex){
	if(!(rex instanceof RegExp))
		return log("Not a regular Expression:", rex);
	return rex.exec(this);
};

function mergeDeep(target, source, mutate=true){
	let output = mutate ? target : Object.assign({}, target);
	if(typeof target == "object"){
		if(typeof source != "object")
			source = {source};
			
		Object.keys(source).forEach(key => {
			if(typeof source[key] == "object"){
				if(!(key in target))
					Object.assign(output, { [key]: source[key] });
				else
				output[key] = mergeDeep(target[key], source[key]);
			}else{
				Object.assign(output, { [key]: source[key] });
			}
		});
	}
	return output;
}
function bpModal(){
	
	var r = {};
	r.messages = [];
	r.count = 0;
	r.setup = function(){
//		if(typeof $ !== "function"){
//			console.log("jQuery not available?\nTrying to insert & load...", typeof $);
//			var head = document.getElementsByTagName("head")[0];
//			var script = document.createElement("script");
//			script.type = "text/javascript";
//			script.onload = function(){
//				r.setup();
//			};
//			script.id="bpJQ";
//			script.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js";
//			head.appendChild(script);
//			return;
//		}
//		console.log("Actual setup with jQuery");
		if($("head #bpModalStyle").length<=0){
			$("head").append(`<style id="bpModalStyle">
				#messageoverlays{
					position:fixed;
					top:20vh;
					z-index:100;
					text-align:left;
					margin: 0 auto;
					left:50%;
					transform: translate(-50%, 0px);
				}

				#messageoverlays>.table{
					margin: 0px auto;
				}

				#messageoverlays .msg{
					display:inline-block;
					width:auto;
					margin: 5px auto;
					position:relative;
					padding: 10px;
					box-sizing: border-box;
					border-radius: 5px;
				}
				#messageoverlays .msg{
					border: 1px solid #666;
					background-color: #ddd;
					color: #333;
				}
				#messageoverlays .msg.dark,
				.dark #messageoverlays .msg,
				#messageoverlays.dark .msg{
					border: 1px solid #ccc;
					background-color: #333;
					color: #ccc;
				}

				#messageoverlays .msg.error{
					border-color: #a10;
					color: #710;
					background-color: #ffcabf;
				}
				#messageoverlays .msg.dark.error,
				.dark #messageoverlays .msg.error,
				#messageoverlays.dark .msg.error{
					background-color: #300;
					color: #b66;
					border: 1px solid #b66;
				}

				#messageoverlays .msg.warn{
					border-color: #dd0;
					color: #bb0;
					background-color: #fec;
				}
				#messageoverlays .msg.dark.warn,
				.dark #messageoverlays .msg.warn,
				#messageoverlays.dark .msg.warn{
					background-color: #330;
					color: #bb6;
					border: 1px solid #bb6;
				}

				#messageoverlays .msg.success{
					border-color: #190;
					color: #070;
					background-color: #bf9;
				}
				#messageoverlays .msg.dark.success,
				.dark #messageoverlays .msg.success,
				#messageoverlays.dark .msg.success{
					background-color:#030;
					color:#6b6;
					border:1px solid #6b6;
				}

				.closebutton{
					font-weight: 900;
					font-size: 12px;
					cursor: pointer;
					z-index: 20;
					opacity: 0.75;
					color: #fff;
					background-color: #a10;
					padding: 0 5px 1px;
					border-radius: 100%;
					position: absolute;
					right: -5px;
					top: -2px;
					line-height: 16px;
				}

				.closebutton:hover,
				.bpModback .modclose:hover{
					opacity: 1;
				}


				.bpModback{
					position:fixed;
					width:100%;
					height:100%;
					display:table;
					left:0;
					top:0;
					z-index:99000;
				}
				
				.bpModback.tint{
					background-color:rgba(0,0,0,0.5);
				}
				
				.bpModback.nomodal{
					display:block;
					width: auto;
					height: auto;
					left: 50%;
					top: 20px;
					transform: translateX(-50%);
				}
				
				.bpModback .modcent{
					display:table-cell;
					vertical-align:middle;
					height:100%;
					max-height:100%;
					min-height:100%;
				}
				
				.bpModback.nomodal .modcent{
					height:auto;
					max-height:auto;
				}
				
				.bpModback .modtable{
					display:table;
					margin:auto;
					position:relative;
					left:0;
				}
				
				.bpModback .modframe{
					border-radius: 6px;
					border:10px solid #fff;
					display:block;
					background-color: #fff;
					box-shadow: 0 0 20px rgba(0,0,0,0.5);
					max-height: 90vh!important;
					max-width: 90vw!important;
					overflow-y:auto;
				}
				
				.bpModback .modclose{
					display:block;
					background-color: #000;
					color: #fff;
					opacity:0.7;
					position:absolute;
					right:-12px;
					top:-12px;
					-webkit-border-radius: 20px;
					-moz-border-radius: 20px;
					border-radius: 20px;
					border:4px solid #fff;
					font-weight:900;
					font-size:12pt;
					padding:0px 7px;
					cursor:pointer;
					z-index:400;
				}
				
				.bpModback .modbox{
					position:relative;
					display: table;
					padding:20px 20px 10px;
					color:#666;
					overflow:hidden;
					display:block;
					/*text-align:center;*/
				}
				.bpModback .table{
					display:table;
				}
				
				.bpModback .tr{
					display:table-row;
				}
				
				.bpModback .td{
					display: table-cell;
				}
				#watch #player{
					height: 200px;
				}
				</style>`);
		}
	};
	r.msg = function(instr, id="", modal=true, callback=null){
		r.count++;
		if(!id){
			id = "bpmod" + r.count;
		}
		var noclose = false;
		if(typeof(modal)=="string" && modal == "noclose"){
			noclose = true;
			modal = true;
		}
		var m = {
			id : id,
			msg: instr,
			callback: callback,
			modal: modal,
			obj: $("<div class='bpModback " + (!!modal?"tint":"nomodal") + (noclose?" noclose":"") + "' id='" + id + "'><div class='tr'><div class='modcent'><div class='modtable'><div class='modclose'>X</div><div class='modframe'><div class='modbox'>" + instr + "</div></div></div></div></div></div>"),
			close: function(){
				this.obj.remove();
				delete r.messages[this.id];
				if(this.callback)
					this.callback(this);
			}
		};

		$("body").append(m.obj);
		
		$("#" + id + ":not('.noclose') .modcent").click(function(e){
			if(e.target == this)
				m.close(this);
		});
		$("#" + id + " .modclose").click(function(e){
			m.close(this);
		});
		r.messages[id] = m;
		return m;
	};
	r.close = function(el="all"){
		if(el=="all"){
			
		}
	};
	r.setup();
	return r;
}
//if("undefined" === typeof bpModHelper){ // jshint ignore:line
//	var bpModHelper = bpModal(); // jshint ignore:line
//}
function message(content, classname, id, expirein, closable){
	expirein = typeof expirein !== 'undefined' ? expirein : 0;
	if(closable===undefined)
		closable = true;
	var expires = expirein !== 0 ? true : false;
	if (id === undefined || id === ""){
		for (var i = 0; i < 512; i++){
			if ($(document).find("#message-" + i)[0] !== undefined){} else {
				this.id = "message-" + i;
				break;
			}
		}
	} else {
		this.id = id;
	}
	var fid = this.id;
	this.expire = function(){
		if (expirein > 0){
			if(this.extimer)
				window.clearTimeout(this.extimer);
			this.extimer = window.setTimeout(function(){
				$("#" + fid).fadeOut(function(){
					$("#" + fid).remove();
				});
			}, expirein);
		}
	};
	this.html = "<div id='" + this.id + "' class='table'><div class='msg " + classname + "'>" + content + (closable?"<div class='closebutton' id='c-" + this.id + "'>x</div>":"") + "</div></div>";
}

function overlaymessage(content, classname, id, expirein, closable){
	expirein = typeof expirein !== 'undefined' ? expirein : 5000;
	classname = classname || "hint";
	id = id || "";
	var curmes = new message(content, classname, id, expirein, closable);
	//console.log(curmes);
	if($("#messageoverlays").length<=0)
		$("body").append("<div id='messageoverlays'></div>");
	$("#messageoverlays").append(curmes.html);
	$(".msg .closebutton").off("click").on("click", function(){
		console.log("close", $(this).parent().parent());
		$(this).parent().parent().fare();
	});
	curmes.expire();
}

function msg(content, classname, id, expirein){
	overlaymessage(content, classname, id, expirein);
}

function msgbox(content, classname="", id=""){
	if (id === undefined || id === ""){
		for (var i = 0; i < 512; i++){
			if ($(document).find("#message-" + i)[0] !== undefined){} else {
				id = "message-" + i;
				break;
			}
		}
	} else {
		id = id;
	}
	return "<div id='" + id + "' class='msg " + classname + "'>" + content + "</div>";
}
String.prototype.rIndexOf = function(regex, startpos) {
    var indexOf = this.substring(startpos || 0).search(regex);
    return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
};
var regEsc = regEsc?regEsc : function(str) {
  return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};

String.prototype.replaceAll = function(str1, str2, insensitive)
{
    return this.replace(new RegExp(regEsc(str1),(insensitive?"gi":"g")),(typeof(str2)=="string")?str2.replace(/\$/g,"$$$$"):str2);
};
function scrollIntoView(object, offsetTop = 20, offsetLeft = 20){
	object = $(object);
	if(object.length<=0)
		return;
	object = object[0];
	var offset = $(object).offset();
	$('html, body').animate({
		scrollTop: offset.top - offsetTop,
		scrollLeft: offset.left - offsetLeft
	});
	object.scrollIntoView();
}
/* globals isElement, isNativeFunction, uneval */
function stringify(obj, forHTML, onlyOwnProperties, completeFunctions, level, maxLevel, skipEmpty){
	if(!level) level = 0;
	var r = "";
	if(obj===undefined) r = "[undefined]";
	else if(obj === null) r = "[null]";
	else if(obj === false) r = "FALSE";
	else if(obj === true) r = "TRUE";
	else if(obj==="") r = "[empty]";
	else if(typeof obj == "object"){
		var isDOMElement = isElement(obj);
		if(onlyOwnProperties === undefined) onlyOwnProperties = true;
		if(completeFunctions === undefined) completeFunctions = false;
		if(maxLevel === undefined) maxLevel = 5;
		if(skipEmpty === undefined) skipEmpty = false;
		
		r = "[object] ";
		var level_padding = "";
		var padString = "    ";
		for(var j = 0; j < level; j++) level_padding += padString;
		
		if(isDOMElement){
			r = "[DOMElement " + obj.nodeName + "] ";
			skipEmpty = true;
			completeFunctions = false;
		}
		
		if(level<maxLevel){
			r += "{\n";
			if(isDOMElement){
				r += level_padding + padString + "HTML => " + obj.outerHTML.replace(/\r?\n/g, "\\n").replace(/\s+/g, " ") + "\n";
			}
			for(var item in obj){
				try{
					var value = obj[item];
					if(onlyOwnProperties && obj.hasOwnProperty && !obj.hasOwnProperty(item) || isNativeFunction(value) || skipEmpty && (value===undefined || value === null || value===""))
						continue;
					
					if(typeof(value) == 'object'){
						r += level_padding + padString + "'" + item + "' => ";
						r += stringify(value, forHTML, onlyOwnProperties, completeFunctions, level+1, maxLevel, skipEmpty) + "\n";
					}else if(typeof(value) == 'undefined'){
						r += level_padding + padString + "'" + item + "' => [undefined]\n";
					}else{
						if(typeof(value.toString)=="function")
							value = value.toString();
						if(!completeFunctions){
							let m = value.match(/function\s*\(([^\)]*)\)\s*\{/i);
							if(m)
								value = "function(" + m[1] + ")";
						}
						r += level_padding + padString + "'" + item + ("' => \"" + value).replace(/\r?\n/g, "\\n").replace(/\s+/g, " ") + "\"\n";
					}
				}catch(e){
					console.log(e);
				}
			}
			r += level_padding + "}";
		}else
			r += "[Max depth of " + maxLevel + " exceeded]";
	}
	else if(typeof obj == "function"){
		if(typeof(obj.toString)=="function")
			r = obj.toString();
		else
			r = uneval(obj);
		if(!completeFunctions){
			let m = r.match(/function\s*\(([^\)]*)\)\s*\{/i);
			if(m)
				r = "function(" + m[1] + ")";
		}
	}
	else
		r = obj + "";
	
	if(level===0){
		if(!!forHTML){
			r = r.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
			r = "<pre>" + r + "</pre>";
		}
	}
	return r;
}
function isNode(o){
	return (
		typeof Node === "object" ? o instanceof Node : 
		o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
	);
}
/* globals HTMLDocument */
function isElement(o){
	return (
		((typeof HTMLElement === "object" && o instanceof HTMLElement) || (typeof Element === "object" && o instanceof Element) || (typeof HTMLDocument === "object" && o instanceof HTMLDocument))? true : //DOM2
		o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
	);
}
function stringifyWithFuncs(val, withTabs = true){
	return JSON.stringify(val, function(key, value){
		if (typeof value === 'function') {
			return value.toString();
		}
		return value;
	}, withTabs?"\t":" ");
}
function toText(str, maxBreaks=2){
//	if($ === undefined){
//		$ = jQuery = require( "jquery" )(new JSDOM("").window);
//	}
	var hack = "__break god dammit__";
	str = str.replace(/(<(?:br|hr) ?\/?>|<\/(?:p|li|div|td|h\d)>)/gi, hack + "$1");
	str = $("<div/>").append(str).text();
	var rex = new RegExp(hack, "gi");
	str = str.replace(rex, "\n");
	rex = new RegExp("(\\n{" + maxBreaks + "})\\n+", "g");
	str = str.replace(rex, "$1");
	return str.trim();
}
function trim(s, what="\\s"){
	var rex = new RegExp("^(?:[" + what + "])*((?:[\\r\\n]|.)*?)(?:[" + what + "])*$");
	var m = s.match(rex);
//	log(m);
	if(m !== null && m.length>=2)
		return m[1];
	return "";
}
function varToPretty(str){
	return str.replace(/(.+?)([A-Z])/g, "$1 $2").replace(/_|-/g, " ").replace(/\s\s+/g, " ").replace(/\b([a-z])/g, function(v,i){return v.toUpperCase();});
}
class eleWaiter{
	constructor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, autoStart=true, debug = false){
		this.sel = "";
		this.cb = null;
		this.cbFail = null;
		this.findIn = "document";
		this.delay = 500;
		this.maxTries = 50;
		this.alwaysOn = false;
		this.autoStart = true;
		this.debug = false;
		this.colors = {
			label: [180,100],
			warn: [60,100],
			error: [0,100]
		};

		this.__running = false;
		this.__tries = 0;
		this.__timer = 0;
		this.__jqo = {};

		if(typeof sel == "object" && !(sel instanceof Array)){ // 2022-04-16 : Now allowing array of selectors
			// log("got object");
			Object.assign(this, sel);
		}
		else{
			this.sel = sel;
			this.cb = cb;
			if(cbFail!== undefined || cbFail!== null)
			this.cbFail = cbFail;
			if(findIn!== undefined || findIn!== null)
				this.findIn = findIn;
			this.delay = delay;
			this.maxTries = maxTries;
			this.alwaysOn = alwaysOn;
			this.autoStart = autoStart;
			this.debug = debug;
		}
		
		if(typeof this.sel == "string"){  // 2022-04-16 : Now allowing array of selectors
			this.sel = [this.sel];
		}
		
		if(this.debug){
			if(typeof this.debug == "string"){
				this.debug = {
					prefix: this.debug + " ",
					level: 1
				};
			}
			else if(typeof this.debug == "number"){
				this.debug = {
					prefix: "",
					level: this.debug
				};
			}
			else if(typeof this.debug == "object"){
				if(!this.debug.prefix)
					this.debug.prefix = "";
				else
					this.debug.prefix += " ";
			
				if(!this.debug.level)
					this.debug.level = 1;
			}
			else{
				this.debug = {
					prefix: "",
					level: 1
				};
			}
		}
//		this.log(this.cb);
		this.log(this);
		if(this.autoStart)
			this.__wait();
	}
	
	log(...args){
		if(!this.debug)
			return;
		
		if(typeof args == "object" && args.length>=2 && typeof args[args.length-1] == "string" && args[args.length-1].toLowerCase().indexOf("loglevel:")===0){
			var level = args[args.length-1].substr(9)*1;
			if(level>this.debug.level){
				return;
			}
			args.pop();
		}
		console.log(this.debug.prefix + "EleWaiter:", ...args);
	}

	start(){
		if(!this.__running){
			this.log("Start waiting", this.findIn, this.sel);
			this.__wait();
		}
	}
	stop(){
		clearTimeout(this.__timer);
		this.__running = false;
	}

	__wait(){
		if(this.findIn == "document" && !!document)
			this.findIn = document;
		
		this.__running = true;
		if(this.maxTries!=-1)
			this.__tries++;
		var triesLeft = this.alwaysOn?1:(this.maxTries - this.__tries);
		this.log("tries left:", triesLeft, "loglevel:3");
		var hasAll = true;
		this.__jqo = $();
		for(let sel of this.sel){
			var jqo = $(this.findIn).find(sel);

			if(jqo.length<=0){
				if(this.debug && this.debug.level>2 || !this.alwaysOn)
					this.log("Not found: " + sel, "in", this.findIn);
				if(triesLeft!==0){
					this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
					if(this.alwaysOn)
						this.__result(false);
				}
				else
					this.__result(false);
				return;
			}
			else{
				this.__jqo = this.__jqo.add(jqo);
				this.log("Found something, is now:", this.__jqo, "loglevel:3");
			}
		}
		this.__result(this.__jqo);

		if(this.alwaysOn){
			this.log("Always on, repeat", "loglevel:3");
			this.__timer = setTimeout(function(){this.__wait();}.bind(this), this.delay);
		}
	}
	__result(success=false){
		if(!this.alwaysOn){
			this.__running = false;
			this.log("Result:", success, "loglevel:2");
		}else if(this.debug.level>2)
			this.log("Result:", success);
		
		if(success){
			if(this.cb!==undefined && typeof this.cb == "function")
				this.cb(this.__jqo);
			else
				console.log("Warning: callback cb not function", this.cb);
		}
		else{
			if(this.cbFail!==undefined && typeof this.cbFail == "function")
				this.cbFail(this.__jqo);
		}
	}
}
if("undefined" === typeof eleWaiters){ // jshint ignore:line
	var eleWaiters ={}; // jshint ignore:line
}

function waitFor(sel, cb, cbFail=null, findIn="document", delay=500, maxTries=50, alwaysOn=false, debug = false){ // 2021-01-29
	return new eleWaiter(sel, cb, cbFail, findIn, delay, maxTries, alwaysOn, true, debug);
}