Greasy Fork

Greasy Fork is available in English.

Plex now playing badge

Display a badge on favicon with a number of users streaming from the server

目前为 2017-09-06 提交的版本,查看 最新版本

// ==UserScript==
// @name        Plex now playing badge
// @namespace   V@no
// @description Display a badge on favicon with a number of users streaming from the server
// @include     http://localhost:32400/web/*
// @include     http://127.0.0.1:32400/web/*
// @include     https://app.plex.tv/desktop
// @include     https://app.plex.tv/desktop/*
// @icon        data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACVklEQVRYhc2Xy08TURTGZ6NAn7RMp1xo587ChVUTY9mCCfFPMFZicGlMNNGli7YoIsYBNDyCDyKmQVGpQMJC0aVuTKCkoFCbEhQoCNrUkBDWn4uWR8s0tHVu603O+vzuPed857scpRTFDO6/Bxi4VIWAhyjGpJvglENkC1B3UsSEWxkg4CHwX6liC0Apxc2ztowAU02VcNXZ2QIcPUIxmfYKU16CYLMVM3cEfG6yQpIYN2Grqzrl1tO3rfh6V8CcbMG3Dh5yI2ELIEkUn24QBLwE082J5KE2HuEHFYh0mRHpNMN5LPeGzGkMz9XaEbxViS+tAuaSyee7zfj+sByLT4x457awBaCUYvgawew9C8L3d5Mv9RkRfWbAqk+PC2eq2QI4HSJmZR6RTjMWek1YTCb/OaDD+qAW4UeGnBoyLyWUGwnme0z48bgc0f5E8l8vtYj5NYiPlKHrssAWQJIogjKPpT4jVnx6rA9qERvS4M9oKTbGShAfLUXNiey0Ie9d0FBvw/LT5O1faREfLsPGWAk23x7G1vtD+NhhYAtAKcW4h8facx1+v9bgA8flFf8EUHNcRNSnR2yo2AD+XYBsjyoA414eay90KS9QMICGehui/YZ9PbB9OI5TDFUAJIki2FaRMgUFBZAvEiz0mlJ04KASqAbgdIgItfM7SricVMKCAYxcJwi1798FmQBULYGr1o6ZFqviNmQOsG1IMvkB5iXYsWQZHBFTgHRTmu4JJ1oEtmOoZMv3uuLzp23sAA76mLy5mnDDzKQ4269ZUZbR3ijKOlYD4C/uwVlNS+Cv+wAAAABJRU5ErkJggg==
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js
// @author      V@no
// @version     2.3
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant GM_listValues

// ==/UserScript==

var prefsDefault = {
	position: 2, //0 = top-left, 1 = top-right, 2 = bottom-right, 3 = bottom-left
	offsetX: 0, //move badge away from x egde
	offsetY: 0, //move badge away from y edge
	textSize: 0, //text size, 0 = auto, 1 = 5px, 2 = 10px, so on
	textMargin: -1, //margin around text, use negative number for auto scale based on text size
	textColor: "#000000", // text color
	backgroundColor: "#FFFFFF", //background color
	borderColor: "#B90000", //border color
	borderWidth: -1, //border width, -1 = auto based on text size
	borderRadius: 0, //border corners radius
	sizeIcon: 16, //image size in pixels, 0 = original
};


var prefsConfig = {
	position: ["Top-Left", "Top-Right", "Bottom-Right", "Bottom-Left"],
	positionReal: ["0", "1", "2", "3"],
	offsetX: ["-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	offsetXReal: ["-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	offsetY: ["-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	offsetYReal: ["-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	sizeIcon: ["Original", "16x16", "32x32"],
	sizeIconReal: ["0", "16", "32"],
	textSize: ["Auto", "1", "2", "3"],
	textSizeReal: ["0", "1", "2", "3"],
	borderWidth: ["Auto", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	borderWidthReal: ["-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"],
	textMargin: ["Auto x4", "Auto x3", "Auto x2", "Auto", " 0", " 1", " 2", " 3", " 4"],
	textMarginReal: ["-4", "-3", "-2", "-1", " 0", " 1", " 2", " 3", " 4"],
	get: function(id, val, real)
	{
		let pos = this[id].indexOf(val);
		if (pos == -1)
		{
			if (this[id+"Real"])
				pos = this[id+"Real"].indexOf(String(val));

			if (pos == -1)
			{
				pos = prefs[id];
			}
		}
		if (real && this[id+"Real"])
		{
			pos = this[id+"Real"][pos];
			pos = fixType.do(id, pos);
		}
		else
			pos = this[id][pos];

		return pos;
	}
};

function clone(orig)
{
	let obj = {};
	for (let n in orig)
		obj[n] = orig[n];

	return obj;
}

function prefsUpdate (temp)
{
	for(let n in prefs)
	{
		let pref = prefs[n];
		if (typeof(pref) == "object" || typeof(pref) == "function")
			continue;

		pref = temp && GM_config.isOpen ? GM_config.fields[n].toValue() : GM_config.get(n);
		if (n in prefsConfig)
			pref = prefsConfig.get(n, pref, true);

		pref = fixType.do(prefs[n], pref);

		if (n.indexOf("Color") != -1)
			pref = pref.replace(/[^#a-zA-Z0-9\-]+/g, "");

		prefs[n] = pref;
	}
}
var  prefs = clone(prefsDefault),
		prefsClone = {},
		fixType = {
			string: String,
			number: Number,
			boolean: Boolean,
			do: function(o, n)
			{
				if (typeof(o) in this)
					n = this[typeof(o)](n);

				return n;
			}
		},
		link = null,
		head = document.getElementsByTagName('head')[0],
		prev = null,
		img = new Image(),
		canvas = document.createElement('canvas'),
		ctx = canvas.getContext('2d'),
		size = 16,
		multi,
		nav,
		configBlur = document.createElement("div"),
		title = "Plex Badge Config",
		testImg = {},
		testField = {value:""},
		style = document.createElement("style"),
		css = function(){/*
#configBlur
{
	z-index: 9998;
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	bottom: 0;
	background-color: grey;
	opacity: 0.6;
	display: none;
}
body[config] #configBlur
{
	display: block;
}
body[config] #plex > div
{
	filter: blur(5px);
}
*/};

style.innerHTML = css.toString().slice(14,-3).split("*//*").join("*/");
head.appendChild(style);
configBlur.id = "configBlur";
document.body.appendChild(configBlur);
configBlur.addEventListener("click", function(){GM_config.close();}, false);

/*
if (typeof GM_getMetadata === 'function') // Scriptish
	title = GM_getMetadata('name');
else if (typeof GM_info !== 'undefined') // Greasemonkey, Tampermonkey &c.
	title = GM_info.script.name;

title += (title ? ' ' : '') + 'Configuration';
*/
prefsConfig.menu = {
	id: "plexNowPlayingBadge",
	title: "<div><img id='testicon'></div>" + title,
	fields: // Fields object
	{
		position: {
				type: "select",
				label: "Position",
				labelPos: "left",
				options: prefsConfig.position,
				default: prefsConfig.position[prefsDefault.position]
		},
		offsetX: {
				type: "select",
				label: "Offset X",
				labelPos: "left",
				options: prefsConfig.offsetX,
				default:  prefsConfig.get("offsetX", prefsDefault.offsetX)
		},
		offsetY: {
				type: "select",
				label: "Offset Y",
				labelPos: "left",
				options: prefsConfig.offsetY,
				default:  prefsConfig.get("offsetY", prefsDefault.offsetY)
		},
		textSize: {
				type: "select",
				label: "Text size",
				labelPos: "left",
				options: prefsConfig.textSize,
				default: prefsConfig.textSize[prefsDefault.textSize]
		},
		textMargin: {
				type: "select",
				label: "Text margin",
				labelPos: "left",
				options: prefsConfig.textMargin,
				default: prefsConfig.get("textMargin", prefsDefault.textMargin)
		},
		textColor: {
				type: "text",
				label: "Text color",
				labelPos: "left",
				default: prefs.textColor
		},
		backgroundColor: {
				type: "text",
				label: "Background color",
				labelPos: "left",
				default: prefs.backgroundColor
		},
		borderColor: {
				type: "text",
				label: "Border color",
				labelPos: "left",
				default: prefs.borderColor
		},
		borderWidth: {
				type: "select",
				labelPos: "left",
				label: "Border width",
				options: prefsConfig.borderWidth,
				default: prefsConfig.get("borderWidth", prefsDefault.borderWidth)
		},
		borderRadius: {
				type: "select",
				label: "Border radius",
				labelPos: "left",
				options: ["0", "1", "2", "3", "4", "5"],
				default: prefs.borderRadius
		},
		sizeIcon: {
				type: "select",
				label: "Icon size",
				labelPos: "left",
				options: prefsConfig.sizeIcon,
				default: prefsConfig.get("sizeIcon", prefsDefault.sizeIcon)
		},
		test: {
				section: ["", '<input id="testinput" placeholder="Enter any number for test">'],
				type: ""
		},
	},
	events: {
		open: function()
		{
			prefsClone = clone(prefs);
			GM_config.frame.style.width = "26em";
			GM_config.frame.style.height = GM_config.frame.contentDocument.defaultView.document.body.firstChild.clientHeight + 20 + "px";
			GM_config.frame.style.border = "1px solid #000";
			GM_config.frame.style.boxShadow = "20px 20px 50px 3px #000";
			GM_config.center();
			document.body.setAttribute("config","");
			testImg = this.frame.contentDocument.getElementById("testicon");
			testImg.src = link.href;
			let that = this,
					input = function(e)
					{
						if (e.target.id == "testinput")
						{
							let pos = e.target.selectionEnd, i = 0;
							e.target.value = e.target.value.replace(/[^0-9]+/g, function(a,b,c,d)
							{
								if (pos >= b)
									i += a.length;
								return "";
							});
							e.target.selectionStart = e.target.selectionEnd = pos-i;
						}

						prefsUpdate(true);
						loop(true);
					};
			for(let f in GM_config.fields)
			{
				if (!(f in prefs) && f != "test")
					continue;

				let field = GM_config.fields[f];
				field.node.addEventListener("input", input, true);
			}
			testField = this.frame.contentDocument.getElementById("testinput");
			testField.addEventListener("input", input, true);
			function validateNumber(e)
			{
				let key = e.keyCode || e.which;
				if ((key < e.DOM_VK_0 || key > e.DOM_VK_9) &&
						[e.DOM_VK_BACK_SPACE, e.DOM_VK_DELETE, e.DOM_VK_TAB, e.DOM_VK_RETURN, e.DOM_VK_ESCAPE,
								e.DOM_VK_LEFT, e.DOM_VK_RIGHT, e.DOM_VK_UP, e.DOM_VK_DOWN, e.DOM_VK_PAGE_UP,
								e.DOM_VK_PAGE_DOWN, e.DOM_VK_HOME, e.DOM_VK_END, e.DOM_VK_INSERT].indexOf(key) == -1 &&
						(key < e.DOM_VK_F1 || key > e.DOM_VK_F22) &&
						!((e.ctrlKey || e.metaKey) && !key != e.DOM_VK_C) && //copy
						!((e.ctrlKey || e.metaKey) && !key != e.DOM_VK_V) && //paste
						!((e.ctrlKey || e.metaKey) && !key != e.DOM_VK_X) && //cut
						!((e.ctrlKey || e.metaKey) && !key != e.DOM_VK_Z) && //undo
						!((e.ctrlKey || e.metaKey) && !key != e.DOM_VK_Y) //redo
						)
				{
					e.returnValue = false;
					e.preventDefault();
				}
			}
			testField.addEventListener("keypress", validateNumber, false);
			loop(null);
		},
		close: function()
		{
			document.body.removeAttribute("config");
			prefs = clone(prefsClone);
			prefsUpdate();
			loop(false);
		},
		save: function()
		{
			prefsClone = clone(prefs);
			prefsUpdate();
			loop(true);
		},
		reset: function()
		{
			prefs = clone(prefsDefault);
			prefsUpdate(true);
			loop(true);
		}
	},
	css: [
		".config_header{margin-bottom:0.3em!important;text-align:center!important;margin-right:5px!important;}"+
		".config_header > div{display: inline-block;position: fixed;top: 0;left: 0;width: 32px;height: 32px;margin: 5px;text-align: start;}"+
		".config_var{display:table-row;}"+
		".config_var > label{display:table-cell;white-space:nowrap;text-align:end;width:0;vertical-align:middle;}"+
		"#testinput{width:100%;display:block;margin:0;}"+
		"#plexNowPlayingBadge_test_var{display:none;}"+
		"#plexNowPlayingBadge_header:{text-align: right}"+
		".config_var > *{margin:3px 3px 3px 0.5em;width:90%}"
	]
};
GM_config.init(prefsConfig.menu);

GM_registerMenuCommand(title, function () { GM_config.open(); });

function drawText(text)
{
	if (!img.loaded)
		return;

	if (prefs.sizeIcon)
		img.width = img.height = prefs.sizeIcon;
	else
	{
		img.width = img._width;
		img.height = img._height;
	}
	let size = img.height;
	canvas.width = canvas.height = size;
	ctx.save();
	ctx.drawImage(img, 0, 0, size, size);

	if (text)
	{
		let multi = prefs.textSize ? prefs.textSize : size / 16,
				textArray = text.toString().toUpperCase().split(''),
				textHeight = 0,
				textWidth = textArray.reduce(function (prev, cur)
				{
					let px = getPixelMap(cur);
					if (px.length * multi > textHeight)
						textHeight = px.length * multi;

					return prev + px[0].length * multi + 1;
				}, -1),
				borderWidth = prefs.borderWidth == -1 ? multi : prefs.borderWidth,
				textMargin = prefs.textMargin < 0 ? -prefs.textMargin * multi : prefs.textMargin,
				width = textWidth + textMargin * 2 + borderWidth,
				height = textHeight + textMargin * 2 + borderWidth,
				xy = -borderWidth / 2,
//				offsetX = prefs.offsetX < 0 ? -prefs.offsetX * multi : prefs.offsetX,
//				offsetY = prefs.offsetY < 0 ? -prefs.offsetY * multi : prefs.offsetY,
				offsetX = prefs.offsetX * multi,
				offsetY = prefs.offsetY * multi,
				x, y;

		switch (prefs.position)
		{
			case 0:
				x = borderWidth + offsetX;
				y = borderWidth + offsetY;
				break;
			case 1:
				x = size - width - offsetX;
				y = borderWidth + offsetY;
				break;
			default:
			case 2:
				x = size - width - offsetX;
				y = size - height - offsetY;
				break;
			case 3:
				x = borderWidth + offsetX;
				y = size - height - offsetX;
				break;
		}
		ctx.translate(x, y);

		// Draw Box
		ctx.fillStyle = hexToRgbA(prefs.backgroundColor);//backgborderRadius
		ctx.strokeStyle = hexToRgbA(prefs.borderColor);//border

		ctx.lineWidth = borderWidth;
		ctx.borderRadiusRect(xy, xy, width, height, prefs.borderRadius * multi).fill();
		if (borderWidth)
			ctx.borderRadiusRect(xy, xy, width, height, prefs.borderRadius * multi).stroke();

		// Draw Text
		ctx.fillStyle = hexToRgbA(prefs.textColor);
		ctx.translate(textMargin, textMargin);
		let pa = [];
		for(let i = 0; i < textArray.length; i++)
		{
			let px = getPixelMap(textArray[i]),
					_y = 0;

			for (let y = 0; y < px.length; y++)
			{
				let _x = 0;
				for (let x = 0; x < px[y].length; x++)
				{
					if (px[y] && px[y][x])
					{
						ctx.fillRect(_x, _y, 1, 1);
						for(let mx = 0; mx < multi; mx++)
						{
							for(let my = 0; my < multi; my++)
							{
								ctx.fillRect(_x + mx, _y + my, 1, 1);
							}
						}
					}
/*
// double, not bold
					if (multi)
					{
						if (px[y] && px[y][x])
						{
							if (x && px[y] && px[y][x-1])
								ctx.fillRect(_x-1, _y, 1, 1);

							if (y && px[y-1] && px[y-1][x])
								ctx.fillRect(_x, _y-1, 1, 1);
						}
						else
							if ((x && px[y] && px[y][x-1])
									&& (y && x && px[y-1] && px[y-1][x])
									&& !(y && x && px[y-1] && px[y-1][x-1])
								)
							{
								ctx.fillRect(_x-1, _y-1, 1, 1);
							}
					}
*/
					_x += multi;
				}
				_y += multi;

			}
			ctx.translate(px[0].length * multi + 1, 0);
		}
	}
	let data = canvas.toDataURL("image/x-icon");
	ctx.restore();
	ctx.clearRect(0, 0, size, size);
	return data;
}//drawText()

function hexToRgbA(hex)
{
	let c;
	if (/^#(([A-Fa-f0-9]{3}){1,2}|[A-Fa-f0-9]{7,8}|[A-Fa-f0-9]{4})$/.test(hex))
	{
		c = hex.substring(1).split('');
		if(c.length == 3)
				c= [c[0], c[0], c[1], c[1], c[2], c[2], "F", "F"];
		if(c.length == 4)
				c= [c[0], c[0], c[1], c[1], c[2], c[2], c[3], c[3]];

		if (c.length < 7)
			c = c.concat(["F", "F"]);
		else if (c.length < 8)
			c[7] = c[6];

		c = '0x' + c.join('');
		return 'rgba('+[(c>>24)&255, (c>>16)&255, (c>>8)&255, ((c&255)*100/255)/100].join(',') + ')';
	}
	return hex;
}

//borrowed from https://chrome.google.com/webstore/detail/favicon-badges/fjnaohmeicdkcipkhddeaibfhmbobbfm/related?hl=en-US
/**
 * Gets a character's pixel map
 */
function getPixelMap(sym)
{
	let px = PIXELMAPS[sym];
	if (!px)
		px = PIXELMAPS['0'];
	return px;
}
var PIXELMAPS = {
	'0': [
		[1,1,1],
		[1,0,1],
		[1,0,1],
		[1,0,1],
		[1,1,1]
	],
	'1': [
		[0,1,0],
		[1,1,0],
		[0,1,0],
		[0,1,0],
		[1,1,1]
	],
	'2': [
		[1,1,1],
		[0,0,1],
		[1,1,1],
		[1,0,0],
		[1,1,1]
	],
	'3': [
		[1,1,1],
		[0,0,1],
		[0,1,1],
		[0,0,1],
		[1,1,1]
	],
	'4': [
		[1,0,1],
		[1,0,1],
		[1,1,1],
		[0,0,1],
		[0,0,1]
	],
	'5': [
		[1,1,1],
		[1,0,0],
		[1,1,1],
		[0,0,1],
		[1,1,1]
	],
	'6': [
		[1,1,1],
		[1,0,0],
		[1,1,1],
		[1,0,1],
		[1,1,1]
	],
	'7': [
		[1,1,1],
		[0,0,1],
		[0,0,1],
		[0,1,0],
		[0,1,0]
	],
	'8': [
		[1,1,1],
		[1,0,1],
		[1,1,1],
		[1,0,1],
		[1,1,1]
	],
	'9': [
		[1,1,1],
		[1,0,1],
		[1,1,1],
		[0,0,1],
		[1,1,1]
	],
};

//https://stackoverflow.com/a/7838871/2930038
CanvasRenderingContext2D.prototype.borderRadiusRect = function (x, y, w, h, r)
{
	if (w < 2 * r) r = w / 2;
	if (h < 2 * r) r = h / 2;
	this.beginPath();
	this.moveTo(x+r, y);
	this.arcTo(x+w,  y,   x+w, y+h, r);
	this.arcTo(x+w,  y+h, x,   y+h, r);
	this.arcTo(x,    y+h, x,   y,   r);
	this.arcTo(x,    y,   x+w, y,   r);
	this.closePath();
	return this;
};

function loop(conf)
{
	let isConf = typeof(conf) != "undefined";
	if (!isConf)
		clearTimeout(loop.timer);

	if (!link || link.parentNode !== head)
	{
		let links = document.getElementsByTagName("link");
		for(let i = 0; i < links.length; i++)
		{
			if (links[i].getAttribute("rel") == "shortcut icon")
			{
				link = links[i];
				break;
			}
		}
		if (link && !img.loaded)
		{
			img.setAttribute('crossOrigin','anonymous');
			img.src = link.href;
			img.onload = function()
			{
				img._width = img.width;
				img._height = img.height;
				img.loaded = true;
			};
		}
	}
	let data,
			_badge = document.getElementsByClassName("activity-badge badge badge-transparent"),
			badge = _badge.length ? _badge[0].innerText : "",
			text = parseInt(badge);

	if(conf === true)
	{
		text = testField.value === "" ? prev : testField.value;
		prev = null;
	}
	else if ((isConf || GM_config.isOpen) && conf !== false)
		text = testField.value === "" ? Math.floor(Math.random() * 99) + 1 : testField.value;


	if (isNaN(text))
		text = 0;

	if (prev != text && (data = drawText(text)))
	{
		link.href = data;
		testImg.src = data;
		prev = text;
	}
	if (!nav)
	{
		nav = document.getElementById("nav-dropdown");
		if (nav)
		{
			let li = document.createElement("li"),
					lis = nav.getElementsByTagName("li"),
					a = document.createElement("a");
			for(let i = lis.length - 1; i >= 0; i--)
			{
				let c = lis[i];
				if (c.className == "divider")
				{
					a.innerText = "Plex Badge Config";
					a.href = "#";
					a.addEventListener("click", function(e)
					{
						e.preventDefault();
						GM_config.open();
					}, false);
					li.appendChild(a);
					c.parentNode.insertBefore(c.cloneNode(false), c);
					c.parentNode.insertBefore(li, c);
					break;
				}
			}
		}
	}
	if (!isConf)
		loop.timer = setTimeout(loop, 3000);
}
prefsUpdate();
loop();