Unlock Hath Perks

Unlock Hath Perks and add other helpers

Versión del día 22/10/2017. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name               Unlock Hath Perks
// @name:zh-TW         解鎖 Hath Perks
// @name:zh-CN         解锁 Hath Perks
// @description        Unlock Hath Perks and add other helpers
// @description:zh-TW  解鎖 Hath Perks 及增加一些小工具
// @description:zh-CN  解锁 Hath Perks 及增加一些小工具
// @namespace          https://github.com/FlandreDaisuki
// @version            1.0.3
// @match              *://e-hentai.org/*
// @match              *://exhentai.org/*
// @icon               https://i.imgur.com/JsU0vTd.png
// @run-at             document-start
// @grant              GM_setValue
// @grant              GM_getValue
// @noframes
//
// Addition metas
//
// @supportURL    https://github.com/FlandreDaisuki/My-Browser-Extensions/issues
// @homepageURL   https://github.com/FlandreDaisuki/My-Browser-Extensions/blob/master/userscripts/UnlockHathPerks.md
// @author        FlandreDaisuki
// @license       MPLv2
// @compatible    firefox 52+
// @compatible    chrome 55+
// @incompatible  any not support async/await, CSS-grid browsers
// ==/UserScript==

'use strict';

/************************************/
/*****     Before DOM Ready     *****/
/************************************/

Set.prototype.difference = function(setB) {
	const difference = new Set(this);
	for(const elem of setB) {
		difference.delete(elem);
	}
	return difference;
};

function $find(el, selector, cb = () => {}) {
	const found = el.querySelector(selector);
	cb(found);
	return found;
}

function $$find(el, selector, cb = () => {}) {
	const found = Array.from(el.querySelectorAll(selector));
	cb(found);
	return found;
}

function $(selector) {
	return $find(document, selector);
}

function $$(selector) {
	return $$find(document, selector);
}

function $el(name, attr = {}, cb = () => {}) {
	const el = document.createElement(name);
	Object.assign(el, attr);
	cb(el);
	return el;
}

function $style(textContent) {
	$el('style', {textContent}, el => document.head.appendChild(el));
}

// sessionStorage namespace:
// in tab && in domain
function $scrollYTo(n) {
	n = parseFloat(n | 0);
	const id = setInterval(() => {
		scrollTo(scrollX, n);
		if (scrollY >= n) {
			clearInterval(id);
		}
	}, 100);
}

class API {
	// ref: https://github.com/tommy351/ehreader-android/wiki/E-Hentai-JSON-API

	static gInfo(href) {
	// pathname = '/g/{gallery_id}/{gallery_token}/'
		const a = $el('a', {href});
		const path = a.pathname.split('/').filter(x => x);
		if (path[0] !== 'g') {
			return null;
		}
		// [{gallery_id}, {gallery_token}]
		return path.slice(1);
	}

	static async gData(gInfos) {
		const queue = [];
		const result = [];

		while(gInfos.length) {
			const toQ = gInfos.slice(0, 25);
			gInfos.splice(0, 25);
			queue.push(toQ);
		}

		for(const glist of queue) {
			const r = await fetch('/api.php', {
				method: 'POST',
				credentials: 'same-origin',
				body: JSON.stringify({
					method: 'gdata',
					gidlist: glist,
				}),
			});

			const json = await r.json();

			if (json.error) {
				console.error('API.gdata(): glist', glist);
				throw new Error(json.error);
			} else {
				result.push(...json.gmetadata);
			}
		}

		return result;
	}

	static async sPage(href) {
		const r = await fetch(href, {credentials: 'same-origin'});
		const html = await r.text();
		const imgsrc = html.replace(/[\s\S]*id="img" src="([^"]+)"[\s\S]*/g, '$1');
		return {
			imgsrc,
		};
	}
}

const uhpConfig = {
	abg: true,
	mt: true,
	tf: false,
	pe: true,
	mpv: false,
	fw: false,
	rth: false,
	sr: false,
	pi: false,
	tpf: false,
	flaggingTags: {
		red: {
			hide: false,
			tags:[],
		},
		green: {
			hide: false,
			tags:[],
		},
		brown: {
			hide: false,
			tags:[],
		},
		blue: {
			hide: false,
			tags:[],
		},
		yellow: {
			hide: false,
			tags:[],
		},
		purple: {
			hide: false,
			tags:[],
		},
	},
};

function uhpSaveConfig() {
	GM_setValue('uhp', uhpConfig);
}

function uhpLoadConfig() {
	return GM_getValue('uhp', uhpConfig);
}

Object.assign(uhpConfig, uhpLoadConfig());
uhpSaveConfig();

if (uhpConfig.abg) {
	Object.defineProperty(window, 'adsbyjuicy', {
		enumerable: false,
		configurable: false,
		writable: false,
		value: null,
	});
}

document.onreadystatechange = function() {
	if (document.readyState === 'interactive') {
		main();
		$style(cssText);
		$style(materialCSS);
		$el('link', {
			href: 'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
			rel: 'stylesheet',
			integrity: 'sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN',
			crossOrigin: 'anonymous',
		}, el => document.head.appendChild(el));
	}
};

/*****************************/
/*****     DOM Ready     *****/
/*****************************/

function main() {
	if (!location.pathname.startsWith('/s/')) {
	/* Make nav button */
		const nb = $('#nb');
		const mr = $el('img', {src: '//ehgt.org/g/mr.gif'});
		const uhpBtnEl =
		$el('a', {
			textContent: 'Unlock Hath Perks',
			id: 'uhp-btn',
		}, el => {
			el.addEventListener('click', () => {
				$('#uhp-panel-container').classList.remove('hidden');
			});
		});
		nb.appendChild(mr);
		nb.appendChild(document.createTextNode(' '));
		nb.appendChild(uhpBtnEl);

		/* Setup UHP Panel */
		const uhpPanelContainerEl = $el('div', {
			className: 'hidden',
			id: 'uhp-panel-container',
		}, el => {
			el.addEventListener('click', () => {
				if($$('#uhp-panel input[pattern]').every(el=>el.validity.valid)) {
					el.classList.add('hidden');
				}
			});
		});
		document.body.appendChild(uhpPanelContainerEl);

		const uhpPanelEl = $el('div', {
			id: 'uhp-panel',
		}, el => {
			if(location.host === 'exhentai.org') {
				el.classList.add('dark');
			}
			el.addEventListener('click', event => { event.stopPropagation(); });
		});
		uhpPanelContainerEl.appendChild(uhpPanelEl);

		/* Setup UHP Configs */
		uhpPanelEl.innerHTML = uhpPanelElHTML + uhpTagFlaggingHTML;

		$$('#uhp-panel input[id^="uhp-conf-"]').forEach(el => {
			const abbr = el.id.replace('uhp-conf-', '');
			el.checked = uhpConfig[abbr];
			el.addEventListener('change', () => {
				uhpConfig[abbr] = el.checked;
				uhpSaveConfig();
			});
		});

		$$('#uhp-panel input[pattern]').forEach(el => {
			// tag color
			const tc = el.id.replace('uhp-tf-', '');
			el.addEventListener('change', () => {
				const newTags = el.value.split(',').map(x => x.trim()).filter(x => x);
				const oldTags = uhpConfig.flaggingTags[tc].tags;
				const allTags = Object.values(uhpConfig.flaggingTags).reduce((acc, val) => acc.concat(val.tags), []);
				const newAllSet = new Set(allTags).difference(oldTags);
				const newSet = new Set(newTags).difference(newAllSet);

				el.value = [...newSet].join(', ');
				uhpConfig.flaggingTags[tc].tags = [...newSet];
				uhpSaveConfig();
			});
		});

		$$('.uhp-tf-options input[type="checkbox"]').forEach(el => {
			// tag color
			const tc = el.id.replace(/uhp-tf-(\w+)-hide/, '$1');
			el.checked = uhpConfig.flaggingTags[tc].hide;
			el.addEventListener('change', () => {
				uhpConfig.flaggingTags[tc].hide = el.checked;
				uhpSaveConfig();
			});
		});

		/* Setup Reactable UHP Configs */
		$('#uhp-conf-fw').addEventListener('change', event => {
			const fwc = $('#uhp-full-width-container');
			if(fwc) {
				if(event.target.checked) {
					fwc.classList.add('fullwidth');
				} else {
					fwc.classList.remove('fullwidth');
				}
			}
		});

		$('#uhp-conf-tf').addEventListener('change', event => {
			const tfops = $$('.uhp-tf-options');
			if(event.target.checked) {
				tfops.forEach(el => el.classList.remove('hidden'));
			} else {
				tfops.forEach(el => el.classList.add('hidden'));
			}
		});
	}

	if ($('#searchbox')) {
		const ido = $('div.ido');
		ido.id = 'uhp-full-width-container';
		if (uhpConfig.fw) {
			ido.classList.add('fullwidth');
		}
	}

	/* Main Functions by Configs */

	/**************/
	/* Ad-Be-Gone */
	/**************/
	if (uhpConfig.abg) {
		// if "No hits found", there is no mode
		if ($('#searchbox') && $('#dmi>span')) {
			const mode = $('#dmi>span').textContent === 'Thumbnails' ? 't' : 'l';
			if (mode === 'l') {
				$$('table.itg tr:nth-of-type(n+2)')
					.forEach(el => {
						if (!el.className) {
							el.remove();
						}
					});
			}
		}

		$$('script[async]').forEach(el => el.remove());
		$$('iframe').forEach(el => el.remove());
	}

	/**********************/
	/* Paging Enlargement */
	/**********************/
	async function getNextPage(nextURL, mode) {
		const selector = mode === 't' ? 'div.id1' : 'table.itg tr:nth-of-type(n+2)';

		const result = {
			mode,
			elements: [],
			nextURL: null,
		};

		if (!nextURL) {
			return result;
		}

		const response = await fetch(nextURL, {
			credentials: 'same-origin',
		});
		if (response.ok) {
			const html = await response.text();
			const doc = new DOMParser().parseFromString(html, 'text/html');
			result.elements = Array.from($$find(doc, selector));
			if (uhpConfig.abg) {
				result.elements = result.elements.filter(el => el.className);
			}
			result.elements =
			result.elements
				.filter(el => {
					if(uhpConfig.rth) {
						if (mode === 't') {
							return !$find(el, '.id3 img').src.endsWith('blank.gif');
						} else {
							return $find(el, '.it5 > a').getAttribute('onmouseover');
						}
					}
					return true;
				})
				.map(el => {
					el.removeAttribute('style');
					return el;
				});

			const nextEl = $find(doc, '.ptb td:last-child > a');
			result.nextURL = nextEl ? nextEl.href : null;
		}
		console.log(result);
		return result;
	}

	async function addTagFlags(page) {
		const selector = page.mode === 't' ? '.id3 > a' : '.it5 > a';
		const gLinks = page.elements.map(el => $find(el, selector).href);
		const gInfos = gLinks.map(a => API.gInfo(a));
		const gData = await API.gData(gInfos);
		const tagsMap = {};
		for(const i in gLinks) {
			const gLink = gLinks[i];
			// tag1;tag2;tag3
			tagsMap[gLink] = gData[i].tags.join(';');
		}

		for(const pageEl of page.elements) {
			const parent = (page.mode === 't') ?
				$find(pageEl, '.id44') :
				$el('div', {className: 'it4t'}, el => {
					if($find(pageEl, '.it4t')) {
						$find(pageEl, '.it4t').replaceWith(el);
					} else {
						$find(pageEl, '.it4').appendChild(el);
					}
				});

			const aLink = $find(pageEl, selector);
			// remove exists
			$$find(parent, `.tf${page.mode}`).forEach(el => el.remove());

			for (const c in uhpConfig.flaggingTags) {
				const tags = uhpConfig.flaggingTags[c].tags;
				const matchs = tags.filter(t => tagsMap[aLink.href].includes(t));
				if (matchs.length) {
					const flagEl = $el('div', {title: matchs.join(', '), className:`tf${page.mode} ${c}`});
					parent.appendChild(flagEl);
					if (uhpConfig.flaggingTags[c].hide) {
						if (page.mode === 't') {
							$find(aLink, 'img').src = '//ehgt.org/g/blank.gif';
						} else {
							aLink.removeAttribute('onmouseover');
							aLink.removeAttribute('onmouseout');
						}
					}
				}
			}
		}
		page.elements = page.elements.filter(el => {
			if(uhpConfig.rth) {
				if (page.mode === 't') {
					return !$find(el, '.id3 img').src.endsWith('blank.gif');
				} else {
					return $find(el, '.it5 > a').getAttribute('onmouseover');
				}
			}
			return true;
		});
	}

	// if "No hits found", there is no mode
	if ($('#searchbox') && $('#dmi>span')) {
		(async() => {
			const nextEl = $('.ptb td:last-child > a');
			let nextURL = nextEl ? nextEl.href : null;
			const mode = $('#dmi>span').textContent === 'Thumbnails' ? 't' : 'l';
			const parent = mode === 't' ? $('div.itg') : $('table.itg tbody');
			const status = $el('h1', {
				textContent: 'Loading...',
				id: 'uhp-status',
			});

			const urlSet = new Set();

			if (mode === 'l') {
				if (location.hostname.startsWith('exh')) {
					parent.classList.add('uhp-list-parent-exh');
				} else {
					parent.classList.add('uhp-list-parent-eh');
				}
			} else {
				parent.style.borderBottom = 'none';
				$$('div.id1').forEach(el => el.removeAttribute('style'));
			}

			// this page
			const thisPage = await getNextPage(location.href, mode);
			if(uhpConfig.tf) {
				await addTagFlags(thisPage);
			}
			while (parent.firstChild) {
				parent.firstChild.remove();
			}

			thisPage.elements.forEach(el => parent.appendChild(el));
			nextURL = thisPage.nextURL;
			if (!nextURL) {
				status.textContent = 'End';
			}

			// next page
			if (uhpConfig.pe) {
				$('table.ptb').replaceWith(status);

				// remove popular section
				$$('div.c, #pt, #pp').forEach(el => el.remove());

				document.addEventListener('scroll', async() => {
					const anchorTop = status.getBoundingClientRect().top;
					const windowHeight = window.innerHeight;

					if (anchorTop < windowHeight * 2 && nextURL && !urlSet.has(nextURL)) {
						urlSet.add(nextURL);
						const nextPage = await getNextPage(nextURL, mode);
						if(uhpConfig.tf) {
							await addTagFlags(nextPage);
						}

						//// work around first ////
						if(uhpConfig.pi) {
							if (mode === 'l') {
								parent.appendChild($el('tr', {
									className: 'uhp-open-in-new-page',
								}, el => {
									el.innerHTML = `<td colspan="4" style="font-size: 4rem;">
														<a href="${nextURL}" style="text-decoration: none; display: inline-flex; align-items: flex-end;">
															P${~~nextURL.replace(/.*(?:page=(\d+)|\/(\d+)$).*/g, '$1$2') + 1}
														</a>
													</td>`;
								}));
							} else {
								parent.appendChild($el('div', {
									className: 'uhp-open-in-new-page',
									style: 'grid-column: 1; display: flex; align-items: center; justify-content: center;',
								}, el => {
									el.innerHTML = `<div style="position: sticky;top: 0;font-size: 4rem;">
														<a href="${nextURL}" style="text-decoration: none; display: inline-flex; align-items: flex-end;">
															P${~~nextURL.replace(/.*(?:page=(\d+)|\/(\d+)$).*/g, '$1$2') + 1}
														</a>
													</div>`;
								}));
							}
						}

						if(uhpConfig.tpf) {
							parent.classList.add('uhp-tpf-dense');
						}
						//// work around first ////


						nextPage.elements.forEach(el => parent.appendChild(el));
						nextURL = nextPage.nextURL;
						if (!nextURL) {
							status.textContent = 'End';
						}
					}
				});
			}
		})();
	}


	/***************/
	/* More Thumbs */
	/***************/
	async function getNextGallaryPage(nextURL) {
		const result = {
			elements: [],
			nextURL: null,
		};
		if (!nextURL) {
			return result;
		}
		const response = await fetch(nextURL, {
			credentials: 'same-origin',
		});
		if (response.ok) {
			const html = await response.text();
			const doc = new DOMParser().parseFromString(html, 'text/html');
			result.elements = $$find(doc, '#gdt > div');
			const nextEl = $find(doc, '.ptb td:last-child > a');
			result.nextURL = nextEl ? nextEl.href : null;
		}
		console.log(result);
		return result;
	}

	if (uhpConfig.mt && location.pathname.startsWith('/g/')) {
		(async() => {
			$('#gdo1').style.display = 'none';
			const nextEl = $('.ptb td:last-child > a');
			let nextURL = nextEl ? nextEl.href : null;
			const parent = $('#gdt');
			parent.classList.add('uhp-page-parent');
			const urlSet = new Set();

			// this page
			const thisPage = await getNextGallaryPage(location.href);
			while (parent.firstChild) {
				parent.firstChild.remove();
			}
			thisPage.elements.forEach(el => parent.appendChild(el));

			// next page
			document.addEventListener('scroll', async() => {
				const anchorTop = $('#cdiv').getBoundingClientRect().top;
				const windowHeight = window.innerHeight;

				if (anchorTop < windowHeight * 2 && !urlSet.has(nextURL)) {
					urlSet.add(nextURL);
					const nextPage = await getNextGallaryPage(nextURL);
					nextPage.elements.forEach(el => parent.appendChild(el));
					nextURL = nextPage.nextURL;
				}
			});
		})();
	}

	/**********************/
	/* Scroll Restoration */
	/**********************/
	if(uhpConfig.sr) {
		history.scrollRestoration = 'manual';

		window.addEventListener('beforeunload', () => {
			history.replaceState(scrollY, null);
		});

		window.addEventListener('load', () => {
			if (history.state) {
				$scrollYTo(history.state);
			}
		});
	}
}

var uhpPanelElHTML = `
<h1>Hath Perks</h1>
<div class="option-grid">
	<div class="material-switch">
		<input id="uhp-conf-abg" type="checkbox">
		<label for="uhp-conf-abg"></label>
	</div>
	<span id="uhp-conf-abg-title">Ads-Be-Gone</span>
	<span id="uhp-conf-abg-desc">Make ad scripts won't work before request.</span>

	<div class="material-switch">
		<input id="uhp-conf-tf" type="checkbox">
		<label for="uhp-conf-tf"></label>
	</div>
	<span id="uhp-conf-tf-title">Tag Flagging</span>
	<span id="uhp-conf-tf-desc">Can flag 6 color for tags.<br/>
		Hide thumbnail of search results when the switch turn on.<br/>
		Conflict with official "Tag Flagging".
	</span>

	<div class="material-switch">
		<input id="uhp-conf-mpv" type="checkbox" disabled>
		<label for="uhp-conf-mpv"></label>
	</div>
	<span id="uhp-conf-mpv-title">Multi-Page Viewer</span>
	<span id="uhp-conf-mpv-desc">Work in Progress</span>

	<div class="material-switch">
		<input id="uhp-conf-mt" type="checkbox">
		<label for="uhp-conf-mt"></label>
	</div>
	<span id="uhp-conf-mt-title">More Thumbs</span>
	<span id="uhp-conf-mt-desc">Make thumbnails in book page infinitely scroll.</span>

	<div class="material-switch">
		<input id="uhp-conf-pe" type="checkbox">
		<label for="uhp-conf-pe"></label>
	</div>
	<span id="uhp-conf-pe-title">Paging Enlargement</span>
	<span id="uhp-conf-pe-desc">Make search results page infinitely scroll.<br/>Popular section will be removed.</span>
</div>

<h1>Others</h1>
<div class="option-grid">
	<div class="material-switch">
		<input id="uhp-conf-fw" type="checkbox">
		<label for="uhp-conf-fw"></label>
	</div>
	<span id="uhp-conf-fw-title">Full Width</span>
	<span id="uhp-conf-fw-desc">Make search results fitting browser width.<br/>Only affect on thumb display mode.</span>

	<div class="material-switch">
		<input id="uhp-conf-rth" type="checkbox">
		<label for="uhp-conf-rth"></label>
	</div>
	<span id="uhp-conf-rth-title">Remove Tag Hidden</span>
	<span id="uhp-conf-rth-desc">Remove search results which tagged with hidden when "Tag Flagging" work.</span>

	<div class="material-switch">
		<input id="uhp-conf-sr" type="checkbox">
		<label for="uhp-conf-sr"></label>
	</div>
	<span id="uhp-conf-sr-title">Scroll Restoration</span>
	<span id="uhp-conf-sr-desc">Scroll last position you seen in last page when "Paging Enlargement" work.</span>

	<div class="material-switch">
		<input id="uhp-conf-pi" type="checkbox">
		<label for="uhp-conf-pi"></label>
	</div>
	<span id="uhp-conf-pi-title">Page Indicator</span>
	<span id="uhp-conf-pi-desc">Add page indicator link to prevent "Scroll Restoration" work too hard.</span>

	<div class="material-switch">
		<input id="uhp-conf-tpf" type="checkbox">
		<label for="uhp-conf-tpf"></label>
	</div>
	<span id="uhp-conf-tpf-title">Thumb Page Flow</span>
	<span id="uhp-conf-tpf-desc">Make dense flow when "Page Indicator" work.<br/>Only affect on thumb display mode.</span>
</div>
`;

var uhpTagFlaggingHTML = `
<h1 class="uhp-tf-options ${uhpConfig.tf ? '' : 'hidden'}">Tag Flagging</h1>
<div class="uhp-tf-options tf-option-grid ${uhpConfig.tf ? '' : 'hidden'}">
	<div class="tfl red"></div>
	<input id="uhp-tf-red" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.red.tags.join(', ')}" placeholder="e.g. touhou, flandre scarlet"/>
	<div class="material-switch">
		<input id="uhp-tf-red-hide" type="checkbox">
		<label for="uhp-tf-red-hide"></label>
	</div>

	<div class="tfl green"></div>
	<input id="uhp-tf-green" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.green.tags.join(', ')}"/>
	<div class="material-switch">
		<input id="uhp-tf-green-hide" type="checkbox">
		<label for="uhp-tf-green-hide"></label>
	</div>

	<div class="tfl brown"></div>
	<input id="uhp-tf-brown" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.brown.tags.join(', ')}"/>
	<div class="material-switch">
		<input id="uhp-tf-brown-hide" type="checkbox">
		<label for="uhp-tf-brown-hide"></label>
	</div>

	<div class="tfl blue"></div>
	<input id="uhp-tf-blue" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.blue.tags.join(', ')}"/>
	<div class="material-switch">
		<input id="uhp-tf-blue-hide" type="checkbox">
		<label for="uhp-tf-blue-hide"></label>
	</div>

	<div class="tfl yellow"></div>
	<input id="uhp-tf-yellow" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.yellow.tags.join(', ')}"/>
	<div class="material-switch">
		<input id="uhp-tf-yellow-hide" type="checkbox">
		<label for="uhp-tf-yellow-hide"></label>
	</div>

	<div class="tfl purple"></div>
	<input id="uhp-tf-purple" pattern="(\\w(?:[^:]|[\\w\\s])+)(?:,\\s*\\1)*" value="${uhpConfig.flaggingTags.purple.tags.join(', ')}"/>
	<div class="material-switch">
		<input id="uhp-tf-purple-hide" type="checkbox">
		<label for="uhp-tf-purple-hide"></label>
	</div>
</div>
`;

var cssText = `
#uhp-btn {
	cursor: pointer;
}
#uhp-panel-container {
	position:fixed;
	top: 0;
	height: 100vh;
	width: 100vw;
	background-color: rgba(200, 200, 200, 0.7);
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: center;
}
#uhp-panel-container.hidden {
	visibility: hidden;
	opacity: 0;
}
#uhp-panel {
	padding: 1.2rem;
	background-color: floralwhite;
	border-radius: 1rem;
	font-size: 1rem;
	color: darkred;
	max-width: 650px;
}
#uhp-panel.dark {
	background-color: dimgray;
	color: ghostwhite;
}
#uhp-panel > .option-grid {
	display: grid;
	grid-template-columns: max-content max-content 1fr;
	grid-gap: 0.5rem 1rem;
}
#uhp-panel > .tf-option-grid {
	display: grid;
	grid-template-columns: 20px 1fr max-content;
	grid-gap: 0.5rem 1rem;
}
#uhp-panel > .option-grid > *,
#uhp-panel > .tf-option-grid > * {
	display: flex;
	justify-content: center;
	align-items: center;
}
#uhp-panel > .tf-option-grid > .tfl {
	margin: auto;
}
#uhp-panel > .uhp-tf-options.hidden {
	display: none;
}
#uhp-full-width-container.fullwidth,
#uhp-full-width-container.fullwidth div.itg {
	max-width: none;
}
#uhp-full-width-container div.itg {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
	grid-gap: 2px;
}
#uhp-full-width-container div.itg.uhp-tpf-dense {
	grid-auto-flow: dense;
}
#uhp-full-width-container div.id1 {
	height: 345px;
	float: none;
	display: flex;
	flex-direction: column;
	margin: 3px auto;
	padding: 4px 0;
}
#uhp-full-width-container div.id2 {
	overflow: visible;
	height: initial;
	margin: 4px auto;
}
#uhp-full-width-container div.id3 {
	flex: 1;
	display: flex;
	justify-content: center;
	align-items: center;
}
.uhp-list-parent-eh tr:nth-of-type(2n+1){
	background-color: #EDEBDF;
}
.uhp-list-parent-eh tr:nth-of-type(2n+2){
	background-color: #F2F0E4;
}
.uhp-list-parent-exh tr:nth-of-type(2n+1) {
	background-color: #363940;
}
.uhp-list-parent-exh tr:nth-of-type(2n+2){
	background-color: #4F535B;
}
#uhp-status {
	text-align: center;
	font-size: 3rem;
	clear: both;
	padding: 2rem 0;
}
/* replace */
div#pp,
div#gdt.uhp-page-parent {
	display: flex;
	flex-wrap: wrap;
}
div#gdt.uhp-page-parent>div{
	float: initial;
}
div.it4t {
	width: 102px;
}
div.tfl.red,
div.tft.red {
	background-position: 0 -1px;
}
div.tfl.green,
div.tft.green {
	background-position: 0px -52px;
}
div.tfl.brown,
div.tft.brown {
	background-position: 0px -18px;
}
div.tfl.blue,
div.tft.blue {
	background-position: 0px -69px;
}
div.tfl.yellow,
div.tft.yellow {
	background-position: 0px -35px;
}
div.tfl.purple,
div.tft.purple {
	background-position: 0px -86px;
}`;

/* https://bootsnipp.com/snippets/featured/material-design-switch */
var materialCSS = `
.material-switch {
	display: inline-block;
}

.material-switch > input[type="checkbox"] {
	display: none;
}

.material-switch > input[type="checkbox"] + label {
	display: inline-block;
	position: relative;
	margin: 6px;
	border-radius: 8px;
	width: 40px;
	height: 16px;
	opacity: 0.3;
	background-color: rgb(0, 0, 0);
	box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
	transition: all 0.4s ease-in-out;
}

.material-switch > input[type="checkbox"] + label::after {
	position: absolute;
	top: -4px;
	left: -4px;
	border-radius: 16px;
	width: 24px;
	height: 24px;
	content: "";
	background-color: rgb(255, 255, 255);
	box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
	transition: all 0.3s ease-in-out;
}

.material-switch > input[type="checkbox"]:checked + label {
	background-color: #0e0;
	opacity: 0.7;
}

.material-switch > input[type="checkbox"]:checked + label::after {
	background-color: inherit;
	left: 20px;
}
.material-switch > input[type="checkbox"]:disabled + label::after {
	content: "\\f023";
	line-height: 24px;
	font-size: 0.8em;
	font-family: FontAwesome;
	color: initial;
}`;