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 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();