您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Display a badge on favicon with a number of users streaming from the server
当前为
// ==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();