code:
/*tomorrow*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #282a2e;
	--alt-font-color: #c5c8c6;
	--background-top: #1d1f21;
	--background-rest: #1d1f21;
	--navbar-color: #282a2e;
	--post-color: #282a2e;
	--post-outline-color: #282a2e;
	--label-color: #282a2e;
	--box-border-color: #111;
	--darken: #ffffff10;
	--highlighted-post-color: #2b1d1f;
	--highlighted-post-outline-color: #111;
	--board-title: #c5c8c6;
	--hr: #282a2e;
	--font-color: #c5c8c6;
	--name-color: #c5c8c6;
	--capcode-color: #f00;
	--subject-color: #b294bb;
	--link-color: #c5c8c6;
	--post-link-color: #5f89ac;
	--link-hover: #81a2be;
	--accent-color: #c5c8c6;
	--input-borders: #111;
	--input-color: #c5c8c6;
	--input-background: #1d1d21;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color:#E0727F;
}

.anchor:target + .post-container, .post-container.highlighted {
	border: 1px solid var(--highlighted-post-outline-color) !important;
}

#float .post-container, #livetext, #threadstats, .navbar, .catalog-tile, .pages, .post-container, .stickynav, .collapse, .bottom-reply {
	border-width: 1px!important;
	border-color: var(--box-border-color)!important;
}

.navbar{
	box-shadow: 0 0 3px 0px #00000030;
}

.post-container.op {
	border-color: transparent!important;
}

table {
	border-spacing: 0px;
}

tr:nth-child(2n+1) {
	background: unset;
}

.captcha {
	filter: invert(90%);
}

.box-wrap, .tabs li a {
	border: 1px solid var(--box-border-color);
}

@media only screen and (max-width: 600px) {

	.post-info{
		background: none;
		border-bottom: none;
	}

	.anchor:target + .post-container .post-info, .post-container.highlighted .post-info {
		border-bottom: none;
	}

}


code:
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--alt-label-color: #2d3351;
	--alt-font-color: #9ac6c9;
	--background-top: #232840;
	--background-rest: #232840;
	--navbar-color: #2d3351;
	--post-color: #2d3351;
	--post-outline-color: #232840;
	--label-color: #2d3351;
	--box-border-color: #232840;
	--darken: #ffffff00;
	--highlighted-post-color: #2d3351;
	--highlighted-post-outline-color: #9ac6c9;
	--board-title: #c5c8c6;
	--hr: #2d3351;
	--font-color: #9ac6c9;
	--name-color: #c5c8c6;
	--capcode-color: #f00;
	--subject-color: #b294bb;
	--link-color: #f649a7;
	--post-link-color: #f649a7;
	--link-hover: #f649a7;
	--accent-color: #c5c8c6;
	--input-borders: #232840;
	--input-color: #c5c8c6;
	--input-background: #2d3351;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color: #E0727F;
}
body {
	font-family: monospace;
}
.highlighted {
	border: 1px dashed var(--highlighted-post-outline-color)
}
a:hover {
	position: relative;
	text-shadow: 0 2px 10px rgba(246,73,167,.9);
}
a, .board-title {
	text-decoration: none;
	text-shadow: 0 2px 3px rgba(246,73,167,.5);
	font-weight: 800;
	transition: all .3s ease;
	position: relative;
}
a:hover::before {
	transform-origin: bottom left;
	transform: scaleX(1);
}
a::before {
	content: "";
	bottom: 0;
	left: 0;
	right: 0;
	position: absolute;
	height: 2px;
	background-color: #f649a7;
	transform-origin: bottom right;
	transform: scaleX(0);
	transition: transform .5s ease;
	box-shadow: 0 2px 5px rgba(246,73,167,.6);
	overflow: hidden;
}
a:active, a:hover {
	outline-width: 0;
}
.captcha {
	filter: invert(90%) sepia(100%) saturate(100) hue-rotate(194deg);
}

repo_name: Marzisoft/jschan

license: mit


code:
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--darken: #ffffff10;
	--board-title: white;
	--hr: #282a2e;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color: #E0727F;
	--alt-label-color: darkblue;
	--alt-font-color: white;
	--background-top: #008080;
	--background-rest: #008080;
	--navbar-color: #c0c0c0;
	--post-color: #c0c0c0;
	--post-outline-color: #c0c0c0;
	--label-color: transparent;
	--box-border-color: transparent;
	--highlighted-post-color: #c0c0c0;
	--highlighted-post-outline-color: #c0c0c0;
	--font-color: black;
	--post-link-color: blue;
	--link-color: #fffff50;
	--link-hover: #555;
	--accent-color: grey;
	--input-borders: #111;
	--input-color: black;
	--input-background: white;
	--name-color: #ddd;
	--capcode-color: #f00;
	--subject-color: #ddd;
}
.flag {
	image-rendering:pixelated;
}
.post-container:not(.op), #float .post-container, .stickynav, .pages, #action-menu, .catalog-tile, #livetext, #threadstats, .navbar, #postform, .op, table, .board-banner, .filelabel, .modal, .collapse, .bottom-reply {
	border: 2px outset lightgray!important;
}
input[type=button],
input[type=file],
input[type=number],
input[type=password],
input[type=range],
input[type=text],
select,
textarea,
.postform-style,
.code {
	 border:2px inset var(--post-color);
}
#postform {
	padding: 2px
}
.nav-item {
	line-height: 1.75em!important;
	margin: 2px;
}
.nav-item, .close, input[type=submit], .filelabel {
	color:black;
	background: transparent;
	border: 2px outset lightgray;
}
.close {
	font-weight: bold
}
.navbar a {
	color:black;
}
.anchor:target+.post-container .post-info, .post-container.highlighted .post-info, .post-container.hoverhighlighted .post-info, .anchor:target+table tbody tr th	{
	background: darkblue!important;
	color: white!important;
}
.op, .board-banner {
	background: var(--post-color)!important;
}
.navbar {
	top:unset!important;
	bottom: -3px;
}
.navbar a:first-of-type::before {
	content:'';
	position:absolute;
	background: url(/css/themes/assets/win95home.png);
	background-size: contain;
	height: 1.75em;
	width: 1.75em;
	left: 7px;
}
.navbar a:first-of-type {
	padding-left: 30px
}
.stickynav {
	top: 5px;
	bottom: unset
}
main {
	margin: 0;
}
table {
	background: var(--post-color);
	border-spacing:0;
}
.post-info, #postform-dragHandle, th:not(.alt){
	font-weight: bold;
	color: #ddd;
	background: #808080!important;
}
th a {
	color: #ddd!important;
}
hr {
	border-top: 2px ridge var(--post-outline-color);
}
.post-info {
	margin: -6px -6px 0px;
	padding: 3px 5px 3px 3px;
}
.board-description {
	color:var(--board-title)
}
.modal {
	padding:3px;
}
input[type=submit], input[type=submit], label[for=file] {
	background: var(--post-color)!important;
}
.footer {
	padding-bottom: 3em;
}
@media only screen and (max-width:600px) {
	.nav-item {
		padding-left: 5px;
		padding-right: 5px;
	}
}
.nav-item .rainbow {
	display: inline;
}
.catalog-thumb {
	margin-top:5px;
}
.op, .op .post-info {
	background:none!important;
}
.op .post-info {
	color:white;
}
.op .post-name {
	color:white;
}
.op.post-subject {
    color: white!important;
}
.op {
	border:2px outset transparent!important;
	--greentext-color:#3ae03a;
	--font-color:white;
	--subject-color: white;
	color:var(--font-color);
}
.anchor:target+.post-container.op {
	--greentext-color:unset;
	--font-color:black;
}
.op .postmenu {
	border-left-color: white;
}
.postmenu {
	color: black;
	border-left-color: #ddd;
}
.box-wrap, .tabs li a {
	border: 2px solid var(--post-outline-color);
	background: var(--post-color)!important;
}
.box-wrap .col {
	background: var(--post-color)!important;
}
#tab-10:target a[href="#tab-10"], #tab-1:target a[href="#tab-1"], #tab-2:target a[href="#tab-2"], #tab-3:target a[href="#tab-3"], #tab-4:target a[href="#tab-4"], #tab-5:target a[href="#tab-5"], #tab-6:target a[href="#tab-6"], #tab-7:target a[href="#tab-7"], #tab-8:target a[href="#tab-8"], #tab-9:target a[href="#tab-9"] {
  border: 2px inset var(--post-color);
}


code:
/*yotsuba b*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #98E;
	--alt-font-color: black;
	--background-top: #d6daf0;
	--background-rest: #eef2ff;
	--navbar-color: #D6DAF0;
	--post-color: #D6DAF0;
	--post-outline-color: #B7C5D9;
	--label-color: #98E;
	--box-border-color: #000;
	--darken: #00000010;
	--highlighted-post-color: #d6bad0;
	--highlighted-post-outline-color: #ba9dbf;
	--board-title: #af0a0f;
	--hr: lightgray;
	--font-color: black;
	--name-color: #117743;
	--capcode-color: #f00;
	--subject-color: #0F0C5D;
	--link-color: #34345C;
	--post-link-color: #d00;
	--link-hover: #d00;
	--input-borders: #a9a9a9;
	--input-color: #000;
	--input-background: white;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color:#E0727F;
}

.post-container, #float .post-container, .stickynav, .pages, #action-menu, .catalog-tile, #livetext, #threadstats, .collapse, .bottom-reply {
	border-width: 0 1px 1px 0;
}


code:
/*yotsuba b*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #800;
	--alt-font-color: #fff;
	--background-top: #fed6af90;
	--background-rest: #ffe;
	--navbar-color: #f0e0d6;
	--post-color: #f0e0d6;
	--post-outline-color: #d9bfb7;
	--label-color: #fca;
	--box-border-color: #000;
	--darken: #ead6ca;
	--highlighted-post-color: #f0c0b0;
	--highlighted-post-outline-color: #d99f91;
	--board-title: #af0a0f;
	--hr: #D9BFB7;
	--font-color: #800000;
	--name-color: #117743;
	--capcode-color: #f00;
	--subject-color: #f00;
	--link-color: #800;
	--post-link-color: navy;
	--link-hover: #f00;
	--accent-color: #800000;
	--input-borders: #a9a9a9;
	--input-color: #000;
	--input-background: white;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color:#E0727F;
}

.post-container, #float .post-container, .stickynav, .pages, #action-menu, .catalog-tile, #livetext, #threadstats, .collapse, .bottom-reply {
	border-width: 0 1px 1px 0;
}

tr, tr:nth-child(2n+1) {
    background: #fff;
}


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events"
	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 318.6 318.6"
	 style="enable-background:new 0 0 318.6 318.6;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#E2761B;stroke:#E2761B;stroke-linecap:round;stroke-linejoin:round;}
	.st1{fill:#E4761B;stroke:#E4761B;stroke-linecap:round;stroke-linejoin:round;}
	.st2{fill:#D7C1B3;stroke:#D7C1B3;stroke-linecap:round;stroke-linejoin:round;}
	.st3{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;}
	.st4{fill:#CD6116;stroke:#CD6116;stroke-linecap:round;stroke-linejoin:round;}
	.st5{fill:#E4751F;stroke:#E4751F;stroke-linecap:round;stroke-linejoin:round;}
	.st6{fill:#F6851B;stroke:#F6851B;stroke-linecap:round;stroke-linejoin:round;}
	.st7{fill:#C0AD9E;stroke:#C0AD9E;stroke-linecap:round;stroke-linejoin:round;}
	.st8{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;}
	.st9{fill:#763D16;stroke:#763D16;stroke-linecap:round;stroke-linejoin:round;}
</style>
<polygon class="st0" points="274.1,35.5 174.6,109.4 193,65.8 "/>
<g>
	<polygon class="st1" points="44.4,35.5 143.1,110.1 125.6,65.8 	"/>
	<polygon class="st1" points="238.3,206.8 211.8,247.4 268.5,263 284.8,207.7 	"/>
	<polygon class="st1" points="33.9,207.7 50.1,263 106.8,247.4 80.3,206.8 	"/>
	<polygon class="st1" points="103.6,138.2 87.8,162.1 144.1,164.6 142.1,104.1 	"/>
	<polygon class="st1" points="214.9,138.2 175.9,103.4 174.6,164.6 230.8,162.1 	"/>
	<polygon class="st1" points="106.8,247.4 140.6,230.9 111.4,208.1 	"/>
	<polygon class="st1" points="177.9,230.9 211.8,247.4 207.1,208.1 	"/>
</g>
<g>
	<polygon class="st2" points="211.8,247.4 177.9,230.9 180.6,253 180.3,262.3 	"/>
	<polygon class="st2" points="106.8,247.4 138.3,262.3 138.1,253 140.6,230.9 	"/>
</g>
<polygon class="st3" points="138.8,193.5 110.6,185.2 130.5,176.1 "/>
<polygon class="st3" points="179.7,193.5 188,176.1 208,185.2 "/>
<g>
	<polygon class="st4" points="106.8,247.4 111.6,206.8 80.3,207.7 	"/>
	<polygon class="st4" points="207,206.8 211.8,247.4 238.3,207.7 	"/>
	<polygon class="st4" points="230.8,162.1 174.6,164.6 179.8,193.5 188.1,176.1 208.1,185.2 	"/>
	<polygon class="st4" points="110.6,185.2 130.6,176.1 138.8,193.5 144.1,164.6 87.8,162.1 	"/>
</g>
<g>
	<polygon class="st5" points="87.8,162.1 111.4,208.1 110.6,185.2 	"/>
	<polygon class="st5" points="208.1,185.2 207.1,208.1 230.8,162.1 	"/>
	<polygon class="st5" points="144.1,164.6 138.8,193.5 145.4,227.6 146.9,182.7 	"/>
	<polygon class="st5" points="174.6,164.6 171.9,182.6 173.1,227.6 179.8,193.5 	"/>
</g>
<polygon class="st6" points="179.8,193.5 173.1,227.6 177.9,230.9 207.1,208.1 208.1,185.2 "/>
<polygon class="st6" points="110.6,185.2 111.4,208.1 140.6,230.9 145.4,227.6 138.8,193.5 "/>
<polygon class="st7" points="180.3,262.3 180.6,253 178.1,250.8 140.4,250.8 138.1,253 138.3,262.3 106.8,247.4 117.8,256.4
	140.1,271.9 178.4,271.9 200.8,256.4 211.8,247.4 "/>
<polygon class="st8" points="177.9,230.9 173.1,227.6 145.4,227.6 140.6,230.9 138.1,253 140.4,250.8 178.1,250.8 180.6,253 "/>
<g>
	<polygon class="st9" points="278.3,114.2 286.8,73.4 274.1,35.5 177.9,106.9 214.9,138.2 267.2,153.5 278.8,140 273.8,136.4
		281.8,129.1 275.6,124.3 283.6,118.2 	"/>
	<polygon class="st9" points="31.8,73.4 40.3,114.2 34.9,118.2 42.9,124.3 36.8,129.1 44.8,136.4 39.8,140 51.3,153.5 103.6,138.2
		140.6,106.9 44.4,35.5 	"/>
</g>
<polygon class="st6" points="267.2,153.5 214.9,138.2 230.8,162.1 207.1,208.1 238.3,207.7 284.8,207.7 "/>
<polygon class="st6" points="103.6,138.2 51.3,153.5 33.9,207.7 80.3,207.7 111.4,208.1 87.8,162.1 "/>
<polygon class="st6" points="174.6,164.6 177.9,106.9 193.1,65.8 125.6,65.8 140.6,106.9 144.1,164.6 145.3,182.8 145.4,227.6
	173.1,227.6 173.3,182.8 "/>
</svg>


code:
function pug_attr(t,e,n,r){if(!1===e||null==e||!e&&("class"===t||"style"===t))return"";if(!0===e)return" "+(r?t:t+'="'+t+'"');var f=typeof e;return"object"!==f&&"function"!==f||"function"!=typeof e.toJSON||(e=e.toJSON()),"string"==typeof e||(e=JSON.stringify(e),n||-1===e.indexOf('"'))?(n&&(e=pug_escape(e))," "+t+'="'+e+'"'):" "+t+"='"+e.replace(/'/g,"'")+"'"}
function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,c,n,s="";for(r=t.index,c=0;r<a.length;r++){switch(a.charCodeAt(r)){case 34:n=""";break;case 38:n="&";break;case 60:n="<";break;case 62:n=">";break;default:continue}c!==r&&(s+=a.substring(c,r)),c=r+1,s+=n}return c!==r?s+a.substring(c,r):s}
var pug_match_html=/["&<>]/;function banmessage(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
    var locals_for_with = (locals || {});

    (function (__, banmessage) {
      pug_mixins["banmessage"] = pug_interp = function(banmessage){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cp class=\"ban\"\u003E\u003Cspan class=\"message\"\u003E" + (pug_escape(null == (pug_interp = __('USER WAS BANNED FOR THIS POST')) ? "" : pug_interp)) + "\u003C\u002Fspan\u003E \u003Cspan" + (" class=\"reason\""+pug_attr("data-reason", banmessage, true, false)) + "\u003E" + (pug_escape(null == (pug_interp = banmessage) ? "" : pug_interp)) + "\u003C\u002Fspan\u003E\u003C\u002Fp\u003E";
};
pug_mixins["banmessage"](banmessage);
    }.call(this, "__" in locals_for_with ?
        locals_for_with.__ :
        typeof __ !== 'undefined' ? __ : undefined, "banmessage" in locals_for_with ?
        locals_for_with.banmessage :
        typeof banmessage !== 'undefined' ? banmessage : undefined));
    ;;return pug_html;}


code:
/* globals __ captchaOptions captchaformsection */
const captchaCookieRegex = /captchaid=(.[^;]*)/ig;
class CaptchaController {

	constructor() {
		this.captchaFields = [];
		this.refreshing = false;
	}

	init() {
		this.captchaFields = document.getElementsByClassName('captchafield');
		this.refreshing = false;
		for (let captcha of this.captchaFields) {
			this.setupCaptchaField(captcha);
		}
	}

	captchaAge() {
		const captchaCookieMatch = document.cookie.match(captchaCookieRegex);
		if (!captchaCookieMatch) { return; }
		const cookieParams = new URLSearchParams(captchaCookieMatch[0]);
		const captchaIdCookie = cookieParams.get('captchaid');
		if (captchaIdCookie) {
			const captchaExpiry = new Date(parseInt(captchaIdCookie.slice(0,8),16)*1000);
			return (Date.now() - captchaExpiry);
		}
	}

	startRefreshTimer() {
		clearTimeout(this.refreshTimer); //this wont throw an error if its null, so no need to check
		const captchaAge = this.captchaAge();
		if (captchaAge != null) {
			console.log('Refreshing captcha in ', 300000-captchaAge);
			this.refreshTimer = setTimeout(() => {
				this.refreshCaptchas();
			}, 300000-captchaAge);
		}
	}

	setupCaptchaField(captcha) {
		if (captcha.closest('form').dataset.captchaPreload == 'true') {
			return this.loadCaptcha(captcha);
		}
		if (captchaOptions.type === 'grid' || captchaOptions.type === 'grid2') {
			let hoverListener = captcha.closest('details') || captcha;
			//captcha.parentElement.previousSibling.previousSibling.tagName === 'SUMMARY' ? captcha.parentElement.previousSibling.previousSibling :  captcha.parentElement;
			hoverListener.addEventListener('mouseover', () => this.loadCaptcha(captcha), { once: true });
		} else { //captchaOptions.type === 'text'
			captcha.placeholder = __('focus to load captcha');
			captcha.addEventListener('focus', () => this.loadCaptcha(captcha), { once: true });
		}
	}

	refreshCaptchas(e) {
		if (this.refreshing) {
			return null;
		}
		this.refreshing = true;
		e && e.target.classList.add('spin');
		for (let captchacheck of document.querySelectorAll('input[name="captcha"]')) {
			captchacheck.checked = false;
		}
		document.cookie = 'captchaid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
		const xhr = new XMLHttpRequest();
		xhr.onload = () => {
			this.startRefreshTimer();
			for (let captcha of this.captchaFields) {
				const existingImage = captcha.previousSibling.children[0];
				if (existingImage) {
					captcha.previousSibling.children[0].src = xhr.responseURL;
				} else {
					this.loadCaptcha(captcha, xhr.responseURL);
				}
			}
			this.refreshing = false;
			e && e.target.classList.remove('spin');
		};
		xhr.onerror = () => {
			this.refreshing = false;
			e && e.target.classList.remove('spin');
		};
		xhr.open('GET', '/captcha', true);
		xhr.send(null);
	}

	removeCaptcha() {
		const postForm = document.getElementById('postform');
		const captchaField = postForm.querySelector('.captcha');
		if (captchaField) {
			//delete the whole row
			const captchaRow = captchaField.closest('.row');
			captchaRow.remove();
		}
	}

	addMissingCaptcha() {
		const postSubmitButton = document.getElementById('submitpost');
		const captchaFormSectionHtml = captchaformsection({ captchaOptions });
		postSubmitButton.insertAdjacentHTML('beforebegin', captchaFormSectionHtml);
		const captchaFormSection = postSubmitButton.previousSibling;
		const captchaField = captchaFormSection.querySelector('.captchafield');
		this.loadCaptcha(captchaField);
	}

	loadCaptcha(field, imgSrc = '/captcha') {
		const captchaDiv = field.previousSibling;
		if (captchaDiv.children.length > 0) {
			return;
		}
		const captchaImg = document.createElement('img');
		const refreshDiv = document.createElement('div');
		captchaDiv.style.display = '';
		captchaImg.style.margin = '0 auto';
		captchaImg.style.display = 'flex';
		//captchaImg.style.width = '100%';
		refreshDiv.classList.add('captcharefresh', 'noselect');
		refreshDiv.addEventListener('click', (e) => this.refreshCaptchas(e), true);
		refreshDiv.textContent = '↻';
		if (captchaOptions.type === 'text') {
			field.placeholder = __('loading');
		}
		captchaImg.src = imgSrc;
		captchaImg.onload = () => {
			if (captchaOptions.type === 'text') {
				field.placeholder = __('Captcha text');
			}
			captchaDiv.appendChild(captchaImg);
			captchaDiv.appendChild(refreshDiv);
			this.startRefreshTimer();
		};
	}

}

const captchaController = new CaptchaController();

window.addEventListener('DOMContentLoaded', () => {

	captchaController.init();

});


code:
/* global renderSheet rulesKey isCatalog */
if (isCatalog) {
	window.addEventListener('DOMContentLoaded', () => {

		const getFilterSelector = (filter) => {
			filter = filter.replace(/["\\]/g, '\\$&').toLowerCase();
			return `.catalog-tile:not([data-filter*="${filter}"])`;
		};
		const catalogFilter = document.getElementById('catalogfilter');
		catalogFilter.value = '';
		const filterCatalog = () => {
			for (let i = 0; i < renderSheet[rulesKey].length; i++) {
				if (renderSheet[rulesKey][i].selectorText.startsWith('.catalog-tile:not([data-filter*=')) {
					renderSheet.deleteRule(i);
					break;
				}
			}
			if (catalogFilter.value.length > 0) {
				renderSheet.insertRule(getFilterSelector(catalogFilter.value) + ' { display: none; }');
			}
		};
		catalogFilter.addEventListener('input', filterCatalog, false);

		const sorts = {
			date: (a, b) => {
				//date newest first
				return new Date(b.dataset.date) - new Date(a.dataset.date);
			},
			bump: (a, b) => {
				//bump date most recent first
				return a.dataset.bump - b.dataset.bump;
			},
			replies: (a, b) => {
				//replies most first
				return b.dataset.replies - a.dataset.replies;
			},
		};
		const tiles = document.getElementsByClassName('catalog-tile');
		const catalogSort = document.getElementById('catalogsort');
		catalogSort.value = '';
		const sortCatalog = (mode) => {
			console.log('sorting catalog', mode);
			const tilesArray = Array.from(tiles);
			tilesArray
				.sort(sorts[mode] || (() => 1))
				.forEach((tile, index) => {
					tile.style.order = index;
				});
		};
		catalogSort.addEventListener('change', (e) => { sortCatalog(e.target.value); }, false);

	});
}


code:
window.addEventListener('DOMContentLoaded', () => {

	const messageBox = document.getElementById('message');
	const parentForm = messageBox && messageBox.form;

	if (messageBox) {
		const messageBoxLabel = messageBox.previousSibling;
		const maxLength = messageBox.getAttribute('maxlength');
		const minLength = messageBox.getAttribute('minlength');
		let currentLength = messageBox.value.length;
		const counter = document.createElement('small');
		messageBoxLabel.appendChild(counter);

		const updateCounter = () => {
			counter.innerText = `(${currentLength}/${maxLength})`;
			if (currentLength >= maxLength || currentLength < minLength) {
				counter.style.color = 'red';
			} else {
				counter.removeAttribute('style');
			}
		};

		const updateLength = function() {
			if (messageBox.value.length > maxLength) {
				messageBox.value = messageBox.value.substring(0,maxLength);
			}
			currentLength = messageBox.value.length;
			updateCounter();
			parentForm && parentForm.dispatchEvent(new CustomEvent('messageBoxChange'));
		};

		updateCounter();

		messageBox.addEventListener('input', updateLength);

	}

});


code:
/* globals setLocalStorage setDefaultLocalStorage settings */
const renderCSSLink = document.createElement('style');
renderCSSLink.type = 'text/css';
renderCSSLink.id = 'rendercss';
document.head.appendChild(renderCSSLink);
const renderSheet = renderCSSLink.sheet;
const rulesKey = renderSheet.rules ? 'rules' : 'cssRules';

class CssToggle {
	constructor (settingId, localStorageKey, localStorageDefault, settingCss) {
		this.localStorageKey = localStorageKey;
		this.localStorageDefault = localStorageDefault;
		setDefaultLocalStorage(this.localStorageKey, this.localStorageDefault);
		this.settingBoolean = localStorage.getItem(this.localStorageKey) == 'true';
		this.settingCss = settingCss;
		window.addEventListener('settingsReady', () => {
			//on event fire, set boolean to correct checked stats
			this.setting = document.getElementById(settingId);
			this.setting.checked = this.settingBoolean;
			this.setting.addEventListener('change', () => {
				this.toggle();
			}, false);
		});
		this.apply();
	}
	toggle () {
		this.settingBoolean = !this.settingBoolean;
		console.log('toggling', this.localStorageKey, this.settingBoolean);
		this.apply();
		setLocalStorage(this.localStorageKey, this.settingBoolean);
	}
	apply () {
		if (this.settingBoolean) {
			renderSheet.insertRule(this.settingCss);
		} else {
			for (let i = 0; i < renderSheet[rulesKey].length; i++) {
				if (renderSheet[rulesKey][i].selectorText == this.settingCss.split(' {')[0]) {
					renderSheet.deleteRule(i);
				}
			}
		}
	}
}

//define the css
const hidePostStubsCss = '.post-container.hidden, .catalog-tile.hidden { display: none;margin-top: -1.5em;height: 0; }';
const hideDeletedPostContentCss = '.post-container.marked[data-mark="Deleted"] .post-data { display: none; }';
const hideThumbnailsCss = '.file-thumb, .catalog-thumb { visibility: hidden !important; }';
const hideRecursiveCss = '.op.hidden ~ .anchor, .op.hidden ~ .post-container { display: none; }';
const heightUnlimitCss = 'img, video { max-height: unset; }';
const crispCss = 'img { image-rendering: crisp-edges; }';
const nonColorIdsCss = '.user-id { background: transparent none repeat scroll 0% 0% !important; border-color: transparent; text-shadow: none; color: var(--font-color); }';
const alwaysShowSpoilersCss = '.spoiler { color: var(--font-color) !important; background: transparent none repeat scroll 0% 0%; outline: 1px solid black; cursor: auto; }';
const smoothScrollingCss = 'html { scroll-behavior: smooth; }';
const threadWatcherCss = '#threadwatcher { display: flex; }';

//make classes with css
new CssToggle('hiderecursive-setting', 'hiderecursive', settings.hideRecursive, hideRecursiveCss);
new CssToggle('heightlimit-setting', 'heightlimit', settings.heightUnlimit, heightUnlimitCss);
new CssToggle('crispimages-setting', 'crispimages', settings.crispImages, crispCss);
new CssToggle('hidethumbnails-setting', 'hidethumbnails', settings.hideThumbnails, hideThumbnailsCss);
new CssToggle('noncolorids-setting', 'noncolorids', settings.nonColorIds, nonColorIdsCss);
new CssToggle('alwaysshowspoilers-setting', 'alwaysshowspoilers', settings.alwaysShowSpoilers, alwaysShowSpoilersCss);
new CssToggle('hidepoststubs-setting', 'hidepoststubs', settings.hidePostStubs, hidePostStubsCss);
new CssToggle('hidedeletedpostcontent-setting', 'hidedeletedpostcontent', settings.hideDeletedPostContent, hideDeletedPostContentCss);
new CssToggle('smoothscrolling-setting', 'smoothscrolling', settings.smoothScrolling, smoothScrollingCss);
new CssToggle('threadwatcher-setting', 'threadwatcher', settings.threadWatcher, threadWatcherCss);


code:
/* globals setLocalStorage */
class Dragable {

	constructor(handle, target, updateCallback) {
		this.updateCallback = updateCallback;
		this.draging = false;
		this.xo = 0;
		this.yo = 0;
		this.targetId = target;
		this.handle = document.querySelector(handle);
		this.target = document.querySelector(target);
		const savedTop = localStorage.getItem(`${this.targetId}-dragtop`);
		if (savedTop != 'null') {
			this.target.style.top = savedTop;
			this.target.style.bottom = 'unset';
		}
		const savedLeft = localStorage.getItem(`${this.targetId}-dragleft`);
		if (savedLeft != 'null') {
			this.target.style.left = savedLeft;
		}
		this.target.style.right = 'unset';
		this.target.addEventListener('opened', e => this.updateMaxSizes(e));
		this.handle.addEventListener('mousedown', e => this.startDrag(e));
		this.handle.addEventListener('touchstart', e => this.startDrag(e), { passive: true });
		document.addEventListener('mouseup', e => this.stopDrag(e));
		document.addEventListener('touchend', e => this.stopDrag(e));
		window.addEventListener('resize', e => this.updateMaxSizes(e));
		//when resize: all css is used
		['mousedown', 'mousemove', 'mouseup', 'touchstart', 'click']
			.forEach(event => this.target.addEventListener(event, () => this.updateCallback && this.updateCallback()));
		window.addEventListener('orientationchange', e => this.updateMaxSizes(e));
		this.updateCallback && this.updateCallback();
	}

	//get a position in bounds
	inBounds(pos, offset, size, limit) {
		if (pos - offset <= 0) {
			return 0;
		} else if (pos - offset + size > limit) {
			return limit - size;
		} else {
			return pos - offset;
		}
	}

	updateMaxSizes() {
		let rect = this.target.getBoundingClientRect();
		if (rect.width === 0) {
			return;
		}
		//reset to top left if resized or rotated and and edge goes off the screen
		if (rect.right > document.documentElement.clientWidth) {
			this.target.style.left = 0;
		}
		if (rect.bottom > document.documentElement.clientHeight) {
			this.target.style.top = 0;
		}
		//set max widths, get rect again since it might have changed
		rect = this.target.getBoundingClientRect();
		this.target.style.maxHeight = `${document.documentElement.clientHeight - rect.top}px`;
		this.target.style.maxWidth = `${document.documentElement.clientWidth - rect.left}px`;
		this.updateCallback && this.updateCallback();
	}

	//start drag and attach appropriate listener for click/drag
	startDrag(e) {
		this.draging = true;
		this.handle.style.cursor = 'grabbing';
		this.target.style.position = 'fixed';
		const rect = this.target.getBoundingClientRect();
		switch (e.type) {
			case 'mousedown':
				this.xo = e.clientX - rect.left;
				this.yo = e.clientY - rect.top;
				window.addEventListener('mousemove', e => this.doDrag(e));
				break;
			case 'touchstart':
				e.preventDefault();
				e.stopPropagation();
				this.xo = e.targetTouches[0].clientX - rect.left;
				this.yo = e.targetTouches[0].clientY - rect.top;
				window.addEventListener('touchmove', e => this.doDrag(e));
				break;
			default:
				//user has alien technology
				break;
		}
	}

	//do the actual drag/movement
	doDrag(e) {
		if (!this.draging) {
			return;
		}
		this.updateMaxSizes();
		this.target.style.bottom = 'unset';
		switch (e.type) {
			case 'mousemove':
				this.target.style.left = `${this.inBounds(e.clientX, this.xo, this.target.offsetWidth, document.documentElement.clientWidth)}px`;
				this.target.style.top = `${this.inBounds(e.clientY, this.yo, this.target.offsetHeight, document.documentElement.clientHeight)}px`;
				break;
			case 'touchmove':
				e.preventDefault();
				e.stopPropagation();
				this.target.style.left = `${this.inBounds(e.targetTouches[0].clientX, this.xo, this.target.offsetWidth, document.documentElement.clientWidth)}px`;
				this.target.style.top = `${this.inBounds(e.targetTouches[0].clientY, this.yo, this.target.offsetHeight, document.documentElement.clientHeight)}px`;
				break;
			default:
				break;
		}
		this.target.style.bottom = 'unset';
		setLocalStorage(`${this.targetId}-dragtop`, this.target.style.top);
		setLocalStorage(`${this.targetId}-dragleft`, this.target.style.left);
		this.updateCallback && this.updateCallback();
	}

	//stopped dragging
	stopDrag() {
		if (this.draging) {
			this.handle.style.cursor = 'grab';
			window.removeEventListener('mousemove', e => this.doDrag(e));
			window.removeEventListener('touchmove', e => this.doDrag(e));
		}
		this.draging = false;
	}

}

if (document.getElementById('postform')) {
	new Dragable('#postform-dragHandle', '#postform');
}


code:
/* globals isCatalog */
if (!isCatalog) { //dont show embed buttons in catalog
	window.addEventListener('DOMContentLoaded', () => {

		const linkSelector = '.post-message a:not(.quote)'; //get links that arent quotes

		const supportedEmbeds = [
			{
				linkRegex: /^https?:\/\/(?:www\.|m\.)?(?:youtube\.com|youtu\.?be)\//i,
				toHtml: (url) => {
					try {
						const urlObject = new URL(url);
						const searchParams = urlObject.searchParams;
						const videoId = searchParams.get('v') // /watch?v=
							|| (urlObject.hostname === 'youtu.be' && urlObject.pathname.substring(1)) // /videoid
							|| (urlObject.pathname.startsWith('/shorts/') && urlObject.pathname.substring(8)); // /shorts/videoi
						if (videoId && videoId.length === 11) {
							return ['',
								`https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}`];
						}
					} catch (e) { /*invalid url*/ }
					return null;
				}
			},
			{
				linkRegex: /^https?:\/\/(?:www\.)?bitchute\.com\/video\/[a-z0-9]{12}\//i,
				toHtml: (url) => {
					try {
						const urlObject = new URL(url);
						const videoId = urlObject.pathname.split('/')[2];
						if (videoId) {
							return ['',
								`https://www.bitchute.com/embed/${encodeURIComponent(videoId)}/`];
						}
					} catch (e) { /*invalid url*/ }
					return null;
				}
			},
			{
				linkRegex: /^https?:\/\/(?:www\.)?odysee\.com\/.+\/.+/i,
				toHtml: (url) => {
					try {
						const urlObject = new URL(url);
						const videoId = urlObject.pathname;
						if (videoId && videoId.startsWith('/@')) {
							return ['',
								`https://odysee.com/$/embed${videoId}`];
						}
					} catch (e) { /*invalid url*/ }
					return null;
				}
			},
			//TODO: add more of these
		];

		const toggleEmbed = (embedSpan, embedHtml, embedSrc) => {
			if (embedSpan.dataset.open === 'true') {
				embedSpan.nextSibling.remove();
				embedSpan.firstElementChild.textContent = 'Embed';
			} else {
				embedSpan.insertAdjacentHTML('afterend', embedHtml);
				embedSpan.nextSibling.src = embedSrc;
				embedSpan.firstElementChild.textContent = 'Close';
			}
			embedSpan.dataset.open = embedSpan.dataset.open === 'true' ? 'false' : 'true';
		};

		const addEmbedButtons = (l) => {
			for (let i = 0; i < l.length; i++) {
				const embedHandler = supportedEmbeds.find(handler => handler.linkRegex.test(l[i].href));
				if (!embedHandler) { continue; }
				const [embedHtml, embedSrc] = embedHandler.toHtml(l[i].href);
				if (embedHtml) {
					const embedSpan = document.createElement('span');
					const openBracket = document.createTextNode('[');
					const embedLink = document.createElement('a');
					const closeBracket = document.createTextNode(']');
					embedSpan.classList.add('ml-5', 'noselect', 'bold');
					embedLink.classList.add('dummy-link');
					embedLink.textContent = 'Embed';
					embedSpan.appendChild(openBracket);
					embedSpan.appendChild(embedLink);
					embedSpan.appendChild(closeBracket);
					l[i].parentNode.insertBefore(embedSpan, l[i].nextSibling);
					embedLink.addEventListener('click', () => toggleEmbed(embedSpan, embedHtml, embedSrc, false));
				}
			}
		};

		const links = Array.from(document.querySelectorAll(linkSelector));
		addEmbedButtons(links);

		const updateEmbedLinks = (e) => {
			if (e.detail.hover) {
				return;
			}
			const newlinks = Array.from(e.detail.post.querySelectorAll(linkSelector))
				.filter(link => {
					//dont add for existing or during updatepostmessage
					return !(link.nextSibling
						&& link.nextSibling.classList
						&& link.nextSibling.classList.contains('dummy-link'));
				});
			addEmbedButtons(newlinks);
		};

		window.addEventListener('addPost', updateEmbedLinks);
		window.addEventListener('updatePostMessage', updateEmbedLinks);

	});
}


code:
/* globals setLocalStorage isCatalog */
window.addEventListener('DOMContentLoaded', () => {

	const actionFooter = document.querySelector('#action-menu');
	if (actionFooter) {
		actionFooter.addEventListener('click', () => {
			if (!actionFooter.parentElement.classList.contains('floatactions')) {
				actionFooter.scrollIntoView();
			} else {
				actionFooter.parentElement.classList.remove('floatactions');
			}
		}, false);
	}

	const volumeSetting = document.getElementById('volume-setting');
	let volumeLevel = localStorage.getItem('volume');
	const changeVolume = () => {
		volumeLevel = volumeSetting.value;
		const allMedia = document.querySelectorAll('audio,video');
		for (let i = 0; i < allMedia.length; i++) {
			allMedia[i].volume = volumeLevel/100;
		}
		console.log('adjusting default volume', volumeLevel);
		setLocalStorage('volume', volumeLevel);
	};
	volumeSetting.value = volumeLevel;
	volumeSetting.addEventListener('change', changeVolume, false);

	const loopSetting = document.getElementById('loop-setting');
	let loopEnabled = localStorage.getItem('loop') == 'true';
	const toggleLoop = () => {
		loopEnabled = loopSetting.checked;
		console.log('toggling video/audio looping', loopEnabled);
		setLocalStorage('loop', loopEnabled);
	};
	loopSetting.checked = loopEnabled;
	loopSetting.addEventListener('change', toggleLoop, false);

	const imageloadingbarsSetting = document.getElementById('imageloadingbars-setting');
	let imageloadingbarsEnabled = localStorage.getItem('imageloadingbars') == 'true';
	const toggleImageloadingbars = () => {
		imageloadingbarsEnabled = imageloadingbarsSetting.checked;
		console.log('toggling video/audio imageloadingbarsing', imageloadingbarsEnabled);
		setLocalStorage('imageloadingbars', imageloadingbarsEnabled);
	};
	imageloadingbarsSetting.checked = imageloadingbarsEnabled;
	imageloadingbarsSetting.addEventListener('change', toggleImageloadingbars, false);

	if (!isCatalog) { //dont expand on catalog
		const thumbs = document.getElementsByClassName('post-file-src');
		const toggle = function(thumb, expanded, filename, src) {
			if (thumb.style.display === 'none') { //closing
				thumb.style.display = '';
				expanded.style.display = 'none';
				filename.style.maxWidth = '';
			} else { //expanding
				thumb.style.display = 'none';
				expanded.style.display = '';
				if (expanded.offsetWidth >= filename.offsetWidth) {
					filename.style.maxWidth = expanded.offsetWidth+'px';
				}
			}
			//handle css thing for play icon on vid/audio
			const close = thumb.nextSibling.textContent === '[Close]' ? thumb.nextSibling : null;
			if (close) {
				expanded.loop = loopEnabled;
				expanded.volume = volumeLevel/100;
				if (src.style.visibility === 'hidden') {
					src.style.visibility = 'visible';
					close.style.display = 'none';
					expanded.pause();
				} else {
					src.style.visibility = 'hidden';
					close.style.display = 'block';
					expanded.play();
				}
			}
		};

		const expand = function(e) {
			if (e.target.nodeName === 'VIDEO' || e.target.nodeName === 'AUDIO') {
				e.stopPropagation();
				return;
			}
			if (this.dataset.attachment == 'true') {
				return;
			}
			e.preventDefault();
			const fileAnchor = this.firstChild;
			const fileHref = fileAnchor.href;
			const type = this.dataset.type;
			const thumbElement = fileAnchor.firstChild;
			const isSpoilered = thumbElement.classList.contains('spoilerimg');
			const fileName = this.previousSibling;
			const pfs = this.closest('.post-file-src');
			let expandedElement = type === 'image' ? thumbElement.nextSibling : fileAnchor.nextSibling;

			if (expandedElement) {
				toggle(thumbElement, expandedElement, fileName, pfs);
			} else if (thumbElement.style.opacity !== '0.5') {
				let source;
				switch (type) {
					case 'image':
						e.preventDefault();
						if (!isSpoilered) {
							fileAnchor.style.minWidth = fileAnchor.offsetWidth+'px';
							fileAnchor.style.minHeight = fileAnchor.offsetHeight+'px';
						}
						thumbElement.style.opacity = '0.5';
						thumbElement.style.cursor = 'wait';
						if (localStorage.getItem('imageloadingbars') == 'true'
							&& window.URL.createObjectURL) {
							const request = new XMLHttpRequest();
							request.onprogress = (e) => {
								const progress = Math.floor((e.loaded/e.total)*100);
								const progressWidth = Math.floor((e.loaded/e.total)*thumbElement.offsetWidth);
								if (progress >= 100) {
									pfs.removeAttribute('data-loading');
								} else {
									pfs.setAttribute('data-loading', progress);
									pfs.style = `--data-loading: ${progressWidth}px`;
								}
							};
							expandedElement = document.createElement('img');
							source = expandedElement;
							const loaded = function() {
								pfs.removeAttribute('data-loading');
								pfs.removeAttribute('style');
								const blob = this.response;
								source.onload = function() {
									thumbElement.style.opacity = '';
									thumbElement.style.cursor = '';
									fileAnchor.appendChild(expandedElement);
									toggle(thumbElement, expandedElement, fileName, pfs);
								};
								source.src = window.URL.createObjectURL(blob);
							};
							request.onload = loaded;
							request.responseType = 'blob';
							request.open('GET', fileHref, true);
							request.send(null);
						} else {
							expandedElement = document.createElement('img');
							source = expandedElement;
							source.onload = function() {
								thumbElement.style.opacity = '';
								thumbElement.style.cursor = '';
								fileAnchor.appendChild(expandedElement);
								toggle(thumbElement, expandedElement, fileName, pfs);
							};
							source.src = fileHref;
						}
						break;
					case 'video':
					case 'audio': {
						e.preventDefault();
						expandedElement = document.createElement(type);
						const closeSpan = document.createElement('span');
						const openBracket = document.createTextNode('[');
						const closeLink = document.createElement('a');
						const closeBracket = document.createTextNode(']');
						closeSpan.classList.add('noselect', 'bold');
						closeSpan.style.marginBottom = '3px';
						closeSpan.style.display = 'block';
						closeSpan.style.color = 'var(--font-color)';
						closeLink.classList.add('dummy-link');
						closeLink.textContent = 'Close';
						closeSpan.appendChild(openBracket);
						closeSpan.appendChild(closeLink);
						closeSpan.appendChild(closeBracket);
						closeSpan.addEventListener('click', function(e) {
							e.preventDefault();
							e.stopPropagation();
							toggle(thumbElement, expandedElement, fileName, pfs);
						}, true);
						expandedElement.controls = 'true';
						source = document.createElement('source');
						expandedElement.appendChild(source);
						expandedElement.setAttribute('playsinline', '');
						if (type === 'audio' && thumbElement.nodeName === 'IMG') {
							expandedElement.style.backgroundImage =
								`url("${encodeURI(thumbElement.src)}")`;
							expandedElement.style.backgroundRepeat = 'no-repeat';
							expandedElement.style.backgroundPosition = 'top';
							expandedElement.style.backgroundSize = 'contain';
							expandedElement.style.minWidth = thumbElement.width+'px';
							expandedElement.style.paddingTop = thumbElement.height+'px';
						} else if (!isSpoilered) {
							expandedElement.style.minWidth = fileAnchor.offsetWidth+'px';
							expandedElement.style.minHeight = fileAnchor.offsetHeight+'px';
						}
						pfs.appendChild(expandedElement);
						fileAnchor.appendChild(closeSpan);
						toggle(thumbElement, expandedElement, fileName, pfs);
						source.src = fileHref;
						return;
					}
				}
			}
		};

		const addExpandEvent = (t) => {
			for (let i = 0; i < t.length; i++) {
				t[i].addEventListener('click', expand, false);
			}
		};

		addExpandEvent(thumbs);

		window.addEventListener('addPost', function(e) {
			if (e.detail.hover) {
				return; //dont need to handle hovered posts for this
			}
			const post = e.detail.post;
			const newthumbs = post.getElementsByClassName('post-file-src');
			addExpandEvent(newthumbs);
		});
	}

});


code:
/* globals __ setLocalStorage pugfilters isCatalog captchaController threadWatcher */
const getFiltersFromLocalStorage = () => {
	const savedFilters = JSON.parse(localStorage.getItem('filters1'));
	return savedFilters.reduce((acc, filter) => {
		const regexFilter = filter.type.endsWith('r');
		if (regexFilter) {
			acc[filter.type].push(new RegExp(filter.val, 'i')); //todo: serialize flags? probs not necessary
		} else {
			acc[filter.type].add(filter.val);
		}
		return acc;
	}, {
		single: new Set(),
		fid: new Set(),
		fname: new Set(),
		ftrip: new Set(),
		fsub: new Set(),
		fmsg: new Set(),
		fflag: new Set(),
		fnamer: [],
		ftripr: [],
		fsubr: [],
		fmsgr: [],
		fflagr: [],
	});
};

let { single, fid, fname, ftrip, fsub, fmsg, fflag, fnamer, ftripr, fsubr, fmsgr, fflagr } = getFiltersFromLocalStorage();

let filtersTable;
const updateFiltersTable = () => {
	[...filtersTable.children].slice(3).forEach(row => row.remove());
	filtersTable.insertAdjacentHTML('beforeend', pugfilters({filterArr: JSON.parse(localStorage.getItem('filters1'))}));
	const closeButtons = filtersTable.querySelectorAll('.close');
	for (let elem of closeButtons) {
		let { type: closeType, data: closeData } = elem.dataset;
		if (closeType.endsWith('r')) {
			closeData = new RegExp(closeData, 'i');
		}
		elem.addEventListener('click', () => { toggleFilter(closeType, closeData); });
	}
};

const updateSavedFilters = () => {
	setLocalStorage('filters1', JSON.stringify([
		...([...single].map(x => ({type:'single', val:x}))),
		...([...fid].map(x => ({type:'fid', val:x}))),
		...([...ftrip].map(x => ({type:'ftrip', val:x}))),
		...([...fname].map(x => ({type:'fname', val:x}))),
		...([...fsub].map(x => ({type:'fsub', val:x}))),
		...([...fmsg].map(x => ({type:'fmsg', val:x}))),
		...([...fflag].map(x => ({type:'fflag', val:x}))),
		...fnamer.map(x => ({type:'fnamer', val:x.source.toString()})),
		...ftripr.map(x => ({type:'ftripr', val:x.source.toString()})),
		...fsubr.map(x => ({type:'fsubr', val:x.source.toString()})),
		...fmsgr.map(x => ({type:'fmsgr', val:x.source.toString()})),
		...fflagr.map(x => ({type:'fflagr', val:x.source.toString()})),
	]));
	updateFiltersTable();
};

const anyFilterMatches = (filteringPost) => {
	const { board, postId, userId, name, subject, tripcode, country } = filteringPost.dataset;
	const postMessage = filteringPost.querySelector('.post-message');
	const message = postMessage ? postMessage.textContent : null;
	const flag = country ? country.code : null;
	return single.has(`${board}-${postId}`)
		|| fid.has(userId)
		|| fname.has(name)
		|| ftrip.has(tripcode)
		|| fsub.has(subject)
		|| fmsg.has(message)
		|| fflag.has(flag)
		|| fnamer.some(r => r.test(name))
		|| ftripr.some(r => r.test(tripcode))
		|| fsubr.some(r => r.test(subject))
		|| fmsgr.some(r => r.test(message))
		|| fflagr.some(r => r.test(flag));
};

const togglePostsHidden = (posts, state, single) => {
	for (let elem of posts) {
		const showing = (!state && (!anyFilterMatches(elem) || single));
		if (showing) { //possible fix for multiple filters & unhiding conflicts
			elem.classList['remove']('hidden');
		} else {
			elem.classList['add']('hidden');
		}
		elem.querySelector('.postmenu').children[0].textContent = (showing ? __('Hide') : __('Show'));
	}
};

//I wish this wasn't necessary, but css selectors dont support regex :(
const getPostsByRegex = (attribute, regex) => {
	const matches = [];
	for (let elem of document.querySelectorAll(`[${attribute}]`)) {
		const value = elem.getAttribute(attribute).toString();
		if (regex.test(value) === true) {
			matches.push(elem);
		}
	}
	return matches;
};

const getPostsByMessage = (data, regex=false) => {
	//you asked for this
	const postMessages = [...document.querySelectorAll(`.${isCatalog ? 'catalog-tile': 'post-container' } .post-message`)];
	const matchingMessages = postMessages.filter(m => (regex ? data.test(m.textContent) : m.textContent.includes(data)));
	return matchingMessages.map(m => m.closest(`.${isCatalog ? 'catalog-tile': 'post-container' }`));
};

const getPostsByFilter = (type, data) => {
	let posts = [];
	switch (type) {
		case 'single': {
			const [dataBoard, dataPostId] = data.split('-');
			const singlePost = document.querySelector(`[data-board="${dataBoard}"][data-post-id="${dataPostId}"]`);
			posts = singlePost ? [singlePost] : [];
			break;
		}
		case 'fid':
			posts = document.querySelectorAll(`[data-user-id="${data}"]`);
			break;
		case 'fname':
			posts = document.querySelectorAll(`[data-name="${CSS.escape(data)}"]`);
			break;
		case 'fnamer':
			posts = getPostsByRegex('data-name', data);
			break;
		case 'ftrip':
			posts = document.querySelectorAll(`[data-tripcode="${CSS.escape(data)}"]`);
			break;
		case 'ftripr':
			posts = getPostsByRegex('data-tripcode', data);
			break;
		case 'fsub':
			posts = document.querySelectorAll(`[data-subject="${CSS.escape(data)}"]`);
			break;
		case 'fsubr':
			posts = getPostsByRegex('data-subject', data);
			break;
		case 'fmsg':
			posts = getPostsByMessage(data);
			break;
		case 'fmsgr':
			posts = getPostsByMessage(data, true);
			break;
		case 'fflag':
			posts = document.querySelectorAll(`[data-flag="${CSS.escape(data)}"]`);
			break;
		case 'fflagr':
			posts = getPostsByRegex('data-flag', data);
			break;
		default:
			break;
	}
	return [...posts];
};

const setFilterState = (type, data, state) => {
	const addOrDelete = state ? 'add' : 'delete';
	switch (type) {
		case 'single':
			single[addOrDelete](data);
			break;
		case 'fid':
			fid[addOrDelete](data);
			break;
		case 'fname':
			fname[addOrDelete](data);
			break;
		case 'fnamer':
			fnamer = fnamer.filter(r => r.source != data.source);
			if (state) {
				fnamer.push(data);
			}
			break;
		case 'ftrip':
			ftrip[addOrDelete](data);
			break;
		case 'ftripr':
			ftripr = ftripr.filter(r => r.source != data.source);
			if (state) {
				ftripr.push(data);
			}
			break;
		case 'fsub':
			fsub[addOrDelete](data);
			break;
		case 'fsubr':
			fsubr = fsubr.filter(r => r.source != data.source);
			if (state) {
				fsubr.push(data);
			}
			break;
		case 'fmsg':
			fmsg[addOrDelete](data);
			break;
		case 'fmsgr':
			fmsgr = fmsgr.filter(r => r.source != data.source);
			if (state) {
				fmsgr.push(data);
			}
			break;
		case 'fflag':
			fflag[addOrDelete](data);
			break;
		case 'fflagr':
			fflagr = fflagr.filter(r => r.source != data.source);
			if (state) {
				fflagr.push(data);
			}
			break;
		default:
			break;
	}
};

const toggleFilter = (filterType, filterData, state) => {
	const posts = getPostsByFilter(filterType, filterData);
	setFilterState(filterType, filterData, state);
	togglePostsHidden(posts, state, filterType === 'single');
	updateSavedFilters();
};

//i guess this works, lmfao and saves ton of time
let actionForm, modalBg, moderatingPost;
const cancelModeratePost = (e) => {
	if (!moderatingPost) {
		return;
	}
	e.preventDefault();
	moderatingPost.querySelector('.post-check').checked = false;
	moderatingPost.style.zIndex = 'unset';
	if (moderatingPost.classList.contains('op')) {
		moderatingPost.style.background = 'unset';
	}
	modalBg.style.display = 'none';
	modalBg.style.zIndex = 4;
	actionForm.removeAttribute('open');
	moderatingPost = null;
};
const moderatePost = (postContainer) => {
	moderatingPost = postContainer;
	actionForm.classList.add('floatactions');
	actionForm.setAttribute('open', 'open');
	actionForm.style.zIndex = 3;
	postContainer.style.zIndex = 3;
	if (postContainer.classList.contains('op')) {
		postContainer.style.background = 'var(--post-color)';
	}
	modalBg.style.display = 'unset';
	modalBg.style.zIndex = 3;
	const actionCaptcha = actionForm.querySelector('.captchafield');
	const postCheck = postContainer.querySelector('.post-check');
	Array.from(postCheck.form.elements)
		.filter(e => e.name === 'checkedposts')
		.forEach(e => e.checked = false);
	postCheck.checked = true;
	if (actionCaptcha) {
		captchaController.loadCaptcha(actionCaptcha);
	}
};

const postMenuChange = function() {
	const postContainer = this.closest(isCatalog ? '.catalog-tile': '.post-container');
	const postDataset = postContainer.dataset;
	const filterType = this.value;
	const hiding = !postContainer.classList.contains('hidden');
	this.value = '';
	let filterData;
	switch (filterType) {
		case 'single':
			filterData = `${postDataset.board}-${postDataset.postId}`;
			break;
		case 'fid':
			filterData = postDataset.userId;
			break;
		case 'fname':
			filterData = postDataset.name;
			break;
		case 'ftrip':
			filterData = postDataset.tripcode;
			break;
		case 'fsub':
			filterData = postDataset.subject;
			break;
		case 'fflag':
			filterData = postDataset.flag;
			break;
		case 'moderate':
			return moderatePost(postContainer);
		case 'edit':
			return window.location = `/${postDataset.board}/manage/editpost/${postDataset.postId}.html`;
		case 'watch': {
			const postMessage = postContainer.querySelector('.post-message');
			const watcherSubject = (postDataset.subject || (postMessage && postMessage.textContent) || `#${postDataset.postId}`).substring(0, 25);
			threadWatcher.add(postDataset.board, postDataset.postId, { subject: watcherSubject, unread: 0, updatedDate: new Date() });
			return;
		}
		case 'playlist':{
			console.log('creating playlist...');
			window.dispatchEvent(new CustomEvent('createPlaylist', {
				detail:{
					board:postDataset.board,
					postId:postDataset.postId
				}
			}));
			break;
		}
	}
	toggleFilter(filterType, filterData, hiding);
};

for (let menu of document.getElementsByClassName('postmenu')) {
	menu.value = '';
	menu.addEventListener('change', postMenuChange, false);
}

const getHiddenElems = () => {
	let posts = [];
	for (let elem of single) {
		posts = posts.concat(getPostsByFilter('single', elem));
	}
	for (let id of fid) {
		posts = posts.concat(getPostsByFilter('fid', id));
	}
	for (let name of fname) {
		posts = posts.concat(getPostsByFilter('fname', name));
	}
	for (let tripcode of ftrip) {
		posts = posts.concat(getPostsByFilter('ftrip', tripcode));
	}
	for (let subject of fsub) {
		posts = posts.concat(getPostsByFilter('fsub', subject));
	}
	for (let message of fmsg) {
		posts = posts.concat(getPostsByFilter('fmsg', message));
	}
	for (let flag of fflag) {
		posts = posts.concat(getPostsByFilter('fflag', flag));
	}
	for (let namer of fnamer) {
		posts = posts.concat(getPostsByFilter('fnamer', namer));
	}
	for (let tripcoder of ftripr) {
		posts = posts.concat(getPostsByFilter('ftripr', tripcoder));
	}
	for (let subr of fsubr) {
		posts = posts.concat(getPostsByFilter('fsubr', subr));
	}
	for (let messager of fmsgr) {
		posts = posts.concat(getPostsByFilter('fmsgr', messager));
	}
	for (let flagr of fflagr) {
		posts = posts.concat(getPostsByFilter('fflagr', flagr));
	}
	return posts;
};

togglePostsHidden(getHiddenElems(), true);

window.addEventListener('addPost', function(e) {
	const newPost = e.detail.post;
	if (anyFilterMatches(newPost)) {
		newPost.classList.add('hidden');
	}
	if (e.detail.hover) { return; }
	const menu = newPost.querySelector('.postmenu');
	menu.value = '';
	menu.addEventListener('change', postMenuChange, false);
});

window.addEventListener('updatePostMessage', function(e) {
	const newPost = e.detail.post;
	if (anyFilterMatches(newPost)) {
		newPost.classList.add('hidden');
	}
});

window.addEventListener('settingsReady', function() {

	actionForm = document.getElementById('actionform');
	if (actionForm) {
		modalBg = document.querySelector('.modal-bg');
		actionForm.firstChild.addEventListener('click', cancelModeratePost);
		modalBg.addEventListener('click', cancelModeratePost, false);
	}

	filtersTable = document.getElementById('advancedfilters');
	updateFiltersTable();

	const filtersForm = document.getElementById('filter-form');
	filtersForm.addEventListener('submit', (e) => {
		e.preventDefault();
		const isRegex = filtersForm.elements.regex.checked;
		const type = `${filtersForm.elements.type.value}${isRegex ? 'r' : ''}`;
		const val = isRegex ? new RegExp(filtersForm.elements.value.value, 'i') : filtersForm.elements.value.value;
		console.log('adding filter', type, val);
		toggleFilter(type, val, true);
	});

	const filterClearButton = document.getElementById('filters-clear');
	const clearFilters = () => {
		single = new Set(),
		fid = new Set(),
		fname = new Set(),
		ftrip = new Set(),
		fsub = new Set(),
		fmsg = new Set(),
		fflag = new Set(),
		fnamer = [],
		ftripr = [],
		fsubr = [],
		fmsgr = [],
		fflagr = [],
		updateFiltersTable();
		togglePostsHidden(document.querySelectorAll(`.${isCatalog ? 'catalog-tile': 'post-container' }`), false);
		updateSavedFilters();
		console.log('cleared hidden posts');
	};
	filterClearButton.addEventListener('click', clearFilters, false);

});


code:
/* globals __ __n modal Tegaki grecaptcha hcaptcha captchaController appendLocalStorageArray socket isThread setLocalStorage forceUpdate captchaController uploaditem isCanvasBlocked TegakiWrapper */
async function videoThumbnail(file) {
	return new Promise((resolve, reject) => {
		const hiddenVideo = document.createElement('video');
		hiddenVideo.setAttribute('src', URL.createObjectURL(file));
		hiddenVideo.load();
		hiddenVideo.addEventListener('error', err => {
			reject(err);
		});
		hiddenVideo.addEventListener('loadedmetadata', () => {
			//apparently 'loadedmetadata' is too early -.-
			setTimeout(() => {
				hiddenVideo.currentTime = 0;
			}, 500);
			hiddenVideo.addEventListener('seeked', () => {
				const canvas = document.createElement('canvas');
				canvas.width = hiddenVideo.videoWidth;
				canvas.height = hiddenVideo.videoHeight;
				const ctx = canvas.getContext('2d');
				ctx.drawImage(hiddenVideo, 0, 0, canvas.width, canvas.height);
				ctx.canvas.toBlob(blob => {
					resolve(blob);
				});
			});
		});
	});
}

function doModal(data, postcallback, loadcallback) {
	try {
		const modalHtml = modal({ modal: data });
		let checkInterval;
		document.body.insertAdjacentHTML('afterbegin', modalHtml);
		const modals = document.getElementsByClassName('modal');
		const modalBgs = document.getElementsByClassName('modal-bg');
		window.dispatchEvent(new CustomEvent('showModal', {
			detail: {
				modal: modals[0],
			}
		}));
		if (modals.length > 1) {
			const latestModalIndex = parseInt(document.defaultView.getComputedStyle(modals[1], null).getPropertyValue('z-index'));
			//from appeals, or holding enter. make sure they show up above the previous modal
			modals[0].style.zIndex = latestModalIndex + 3;
			modalBgs[0].style.zIndex = latestModalIndex + 2;
		}
		if (loadcallback != null) {
			loadcallback();
		}
		document.getElementById('modalclose').onclick = () => {
			removeModal();
			clearInterval(checkInterval);
		};
		document.getElementsByClassName('modal-bg')[0].onclick = () => {
			removeModal();
			clearInterval(checkInterval);
		};
		const modalframe = document.getElementById('modalframe');
		if (modalframe) {
			if (localStorage.getItem('theme') === 'default') {
				modalframe.onload = () => {
					//if theres a modal frame and user has default theme, style it
					const currentTheme = document.head.querySelector('#theme').href;
					modalframe.contentDocument.styleSheets[1].ownerNode.href = currentTheme;
				};
			}
			if (postcallback) {
				checkInterval = setInterval(() => {
					if (modalframe && modalframe.contentDocument.title == __('Success')) {
						clearInterval(checkInterval);
						removeModal();
						postcallback();
					}
				}, 100);
			}
		}
	} catch (e) {
		console.error(e);
	}
}

const modalClasses = ['modal', 'modal-bg'];
function removeModal() {
	modalClasses.forEach(c => document.getElementsByClassName(c)[0].remove());
}

let recaptchaResponse = null;
function recaptchaCallback (response) { // eslint-disable-line
	recaptchaResponse = response;
}

let tegakiWidth = localStorage.getItem('tegakiwidth-setting');
let tegakiHeight = localStorage.getItem('tegakiheight-setting');

class postFormHandler {

	constructor(form) {
		this.form = form;
		this.resetOnSubmit = this.form.dataset.resetOnSubmit == 'true';
		this.enctype = this.form.getAttribute('enctype');
		this.messageBox = this.form.querySelector('#message');
		this.recordTegaki = this.form.elements.tegakireplay;
		this.canvasBlocked = null;
		this.canvasCheckInterval = null;
		this.minimal = this.form.elements.minimal;
		this.files = [];
		this.submit = form.querySelector('input[type="submit"]');
		if (this.submit) {
			this.originalSubmitText = this.submit.value;
		}

		//get different element for diffeent captcha types
		this.captchaField = form.querySelector('.captchafield')
			|| form.querySelector('.g-recaptcha')
			|| form.querySelector('.h-captcha');

		//if tegaki button, attach the listener to open tegaki
		this.tegakiButton = form.querySelector('.tegaki-button');
		if (this.tegakiButton) {
			this.tegakiButton.addEventListener('click', () => this.doTegaki());
		}

		//if web3 signature button, attach the listeners for message signing
		this.web3SignButton = form.querySelector('.web3-sign');
		if (this.web3SignButton) {
			this.web3SignButton.addEventListener('click', () => this.doWeb3Sign());
		}

		//if web3 login button, do login procedure on click
		this.web3LoginButton = form.querySelector('.web3-login');
		if (this.web3LoginButton) {
			this.web3LoginButton.addEventListener('click', (e) => this.doWeb3Login(e));
		}

		//if file input, attach listeners for adding files, drag+drop, etc
		this.fileInput = form.querySelector('input[type="file"]');
		if (this.fileInput) {
			this.fileRequired = this.fileInput.required;
			this.fileLabel = this.fileInput.previousSibling;
			this.fileUploadList = this.fileInput.nextSibling;
			this.multipleFiles = this.fileLabel.parentNode.previousSibling.firstChild.textContent.endsWith('s');
			this.fileLabelText = this.fileLabel.childNodes[0];
			this.fileLabel.addEventListener('dragover', e => this.fileLabelDrag(e));
			this.fileLabel.addEventListener('drop', e => this.fileLabelDrop(e));
			this.fileInput.addEventListener('change', e => this.fileInputChange(e));
			this.fileLabel.addEventListener('auxclick', e => this.fileLabelAuxclick(e));
			form.addEventListener('paste', e => this.paste(e));
		}

		//if custom flag select, attach listener and set from local storage if saved
		this.customFlagInput = this.form.elements.customflag;
		this.selectedFlagImage = document.getElementById('selected-flag');
		if (this.customFlagInput && this.selectedFlagImage) {
			this.customFlagInput.addEventListener('change', () => this.updateFlagField(), false);
			this.updateFlagField();
		}

		//allow control+enter to submit when in message input
		if (this.messageBox) {
			this.messageBox.addEventListener('keydown', e => this.controlEnterSubmit(e));
		}

		form.addEventListener('submit', e => this.formSubmit(e));
		form.addEventListener('messageBoxChange', () => this.handleMessageChange());
	}

	reset() {
		this.form.reset();
		this.updateFlagField();
		this.updateMessageBox();
		this.files = [];
		this.updateFilesText();
		const captcha = this.form.querySelector('.captcharefresh');
		if (captcha) {
			captcha.dispatchEvent(new Event('click'));
		}
	}

	doTegaki() {
		clearInterval(this.canvasCheckInterval);
		if ((this.canvasBlocked = isCanvasBlocked())
			&& localStorage.getItem('checkedCanvas') === 'true') {
			return doModal({
				'title': 'Canvas Permission Error',
				'message': 'Your browser has canvas disabled or broken due to anti-fingerprinting measures.',
			});
		}
		if (localStorage.getItem('checkedCanvas') !== 'true' && this.canvasBlocked) {
			if (!this.canvasCheckInterval || this.canvasCheckInterval._destroyed == true) {
				this.canvasCheckInterval = setInterval(() => {
					if (!isCanvasBlocked()) {
						this.doTegaki();
					}
				}, 250);
			}
			return localStorage.setItem('checkedCanvas', 'true');
		}
		const saveReplay = this.recordTegaki && this.recordTegaki.checked;
		TegakiWrapper.init();
		Tegaki.open({
			target: TegakiWrapper.element,
			saveReplay,
			onCancel: () => {
				TegakiWrapper.remove();
			},
			onDone: () => {
				const now = Date.now();
				let replayBlob;
				if (saveReplay) {
					replayBlob = Tegaki.replayRecorder.toBlob();
				}
				//add tegaki image
				Tegaki.flatten().toBlob(imageBlob => {
					this.addFile(new File([imageBlob], `${now}-tegaki.png`, { type: 'image/png' }), { stripFilenames: false });
					//add replay file
					replayBlob && this.addFile(new File([replayBlob], `${now}-tegaki.tgkr`, { type: 'tegaki/replay' }), { stripFilenames: false });
				}, 'image/png');
				//update file list
				this.updateFilesText();
				//reset tegaki state
				Tegaki.resetLayers();
				TegakiWrapper.remove();
			},
			width: tegakiWidth,
			height: tegakiHeight,
		});
		Tegaki.resetLayers();
		Tegaki.setColorPalette(2); //picks a better default color palette
	}

	handleMessageChange() {
		if (!this.messageBox) { return; }
		const emptyMessage = this.messageBox.value.length === 0;
		if (this.web3SignButton) {
			this.form.elements.signature.value = '';
			this.web3SignButton.disabled = emptyMessage;
		}
	}

	async doWeb3Login(e) {
		e.target.style.pointerEvents = 'none'; //way of disabling dummy button to prevent double click
		try {
			const accounts = await window.jschanweb3.eth.requestAccounts();
			const nonceResponse = await fetch(`/nonce/${encodeURIComponent(accounts[0])}.json`)
				.then(res => res.json());
			const nonce = nonceResponse && nonceResponse.nonce;
			if (!nonce) { throw Error('Nonce request failed'); }
			const signingMesssage = `Nonce: ${nonce}`;
			const signature = await window.jschanweb3.currentProvider.request({
				method: 'personal_sign',
				params: [signingMesssage, accounts[0]],
			});
			this.form.elements.signature.value = signature;
			this.form.elements.address.value = accounts[0];
			this.form.elements.nonce.value = nonce;
			this.form.requestSubmit();
		} catch (e) {
			console.warn(e);
		} finally {
			e.target.style.pointerEvents = 'auto';
		}
	}

	async doWeb3Sign() {
		if (!this.messageBox.value || this.messageBox.value.length === 0) {
			return;
		}
		const messageContent = this.messageBox.value;
		try {
			const accounts = await window.jschanweb3.eth.requestAccounts();
			const signature = await window.jschanweb3.currentProvider.request({
				method: 'personal_sign',
				params: [messageContent, accounts[0]],
			});
			this.form.elements.signature.value = signature;
		} catch (e) {
			console.warn(e);
		}
	}

	updateFlagField() {
		if (this.customFlagInput && this.customFlagInput.options.selectedIndex !== -1) {
			this.selectedFlagImage.src = this.customFlagInput.options[this.customFlagInput.options.selectedIndex].dataset.src || '';
		}
	}

	controlEnterSubmit(e) {
		if (e.ctrlKey && e.key === 'Enter') {
			this.formSubmit(e);
		}
	}

	formSubmit(e) {

		//get the captcha response if any recaptcha
		const captchaResponse = recaptchaResponse;

		//build the form data based on form enctype
		let postData;
		if (this.enctype === 'multipart/form-data') {
			this.fileInput && (this.fileInput.disabled = true);
			postData = new FormData(this.form);
			if (captchaResponse) {
				postData.append('captcha', captchaResponse);
			}
			this.fileInput && (this.fileInput.disabled = false);
			if (this.files && this.files.length > 0) {
				/* add each file to data individually, since we handle multiple files in multiple sessions of
					selecting, not just the last time (see addFile()) */
				for (let i = 0; i < this.files.length; i++) {
					postData.append('file', this.files[i]);
				}
			}
		} else {
			postData = new URLSearchParams([...(new FormData(this.form))]);
			if (captchaResponse) {
				postData.set('captcha', captchaResponse);
			}
		}

		/* if it is a "minimal" form (used in framed bypases) or ticked the "edit" box in post actions,
			dont preventDefault because we just want to use non-js form submission */
		if (this.minimal
			|| (postData instanceof URLSearchParams && postData.get('edit') === '1')) {
			return true;
		} else {
			e.preventDefault();
		}

		//prepare new request
		const xhr = new XMLHttpRequest();

		if (this.submit) {
			//disable submit button to prevent submitting while one in progress
			this.submit.disabled = true;
			//update the text on the submit button, and show upload progress if form has files
			this.submit.value = 'Processing...';
			if (this.files && this.files.length > 0) {
				xhr.onloadstart = () => {
					this.submit.value = '0%';
				};
				xhr.upload.onprogress = (ev) => {
					const progress = Math.floor((ev.loaded / ev.total) * 100);
					this.submit.value = `${progress}%`;
				};
				xhr.onload = () => {
					this.submit.value = this.originalSubmitText;
				};
			}
		}

		xhr.onreadystatechange = () => {

			//request finished
			if (xhr.readyState === 4) {

				//if the google/hcaptcha/yandex was filled, reset it now
				if (captchaResponse && grecaptcha) {
					grecaptcha.reset();
				} else if (captchaResponse && hcaptcha) {
					hcaptcha.reset();
				} else if (captchaResponse && window.smartCaptcha) {
					window.smartCaptcha.reset();
				}

				//remove captcha if server says it is no longer enabled	(submitting one when not needed doesnt cause any problem)
				if (xhr.getResponseHeader('x-captcha-enabled') === 'false') {
					captchaController.removeCaptcha();
					this.captchaField = null;
				}

				//re-enable the submit button now, and set the submit button text back to original value
				if (this.submit) {
					this.submit.disabled = false;
					this.submit.value = this.originalSubmitText;
				}

				//try and parse json from the response if there is a body
				let json;
				if (xhr.responseText) {
					try {
						json = JSON.parse(xhr.responseText);
					} catch (e) {
						//wasnt json response
					}
				}

				if (json) {
					if (xhr.status == 200) {

						//response had a postId from successful post so set here for scrolling to new posts
						if (json.postId) {
							window.myPostId = json.postId;
						}

						//get board and postId to add to (you)s
						if (json.redirect) {
							const redirectBoard = json.redirect.split('/')[1];
							const redirectPostId = json.redirect.split('#')[1];
							if (redirectBoard && redirectPostId) {
								appendLocalStorageArray('yous', `${redirectBoard}-${redirectPostId}`);
							}
						}

						//do modal for errors/messages
						if (json.message || json.messages || json.error || json.errors) {
							doModal(json);
						} else if (socket && socket.connected) {
							//set hash to scroll to your post if you are connected to the socket (it will be in the DOM by this point)
							window.location.hash = json.postId;
						} else {
							//if we are not in a thread so follow the redirect to open the new thread
							if (!isThread) {
								return window.location = json.redirect;
							}
							//otherwise save the postId for you tracking after forceUpdate() finishes
							setLocalStorage('myPostId', json.postId);
							//not connected to socket, so force fetch the JSON
							forceUpdate();
						}

						//if the form has data attribute to reset on submission, clear it now (reset() handled stuff like saved name, flag, etc)
						if (this.resetOnSubmit) {
							this.reset();
						}

					} else {

						//not a 200 so probably error
						if (!this.captchaField && json.message === __('Incorrect captcha answer')) {
							/* add missing captcha field if we got an error about it and the form has no captcha field
								(must have been enabeld after we loaded the page) */
							captchaController.addMissingCaptcha();
							this.captchaField = true;
						} else if (json.message === __('Captcha expired')) {
							//if captcha is expired, just refresh the captcha
							const captcha = this.form.querySelector('.captcharefresh');
							if (captcha) {
								captcha.dispatchEvent(new Event('click'));
							}
						} else if (json.bans) {
							//if user is banned, display their bans table and appeal form in a special modal
							doModal(json, null, () => {
								const modalBanned = document.getElementById('modalbanned');
								const modalBanForm = modalBanned.querySelector('form');
								const modalAppealHandler = new postFormHandler(modalBanForm);
								for (let modalFormElement of modalBanForm.querySelectorAll('input[name="checkedbans"]')) {
									//for ease of appeal, pre-check all the bans in this case.
									modalFormElement.checked = true;
								}
								const appealCaptcha = modalAppealHandler.captchaField;
								if (appealCaptcha) {
									captchaController.setupCaptchaField(appealCaptcha);
								}
							});
						} else {
							/* otherwise just show modal with errors/messages, and callback will be optionally used
								for 403 if they have to do a bypass and submit the iframe */
							doModal(json, () => {
								this.formSubmit(e);
							});
						}

					}
				} else if (xhr.responseURL
					&& xhr.responseURL !== `${location.origin}${this.form.getAttribute('action')}`) {
					//not an json and a redirect not to current url (which would be edits), so redirect to new location.
					return window.location = xhr.responseURL;
				} else if (xhr.status === 413) {
					//413 but not a json, must be nginx so show generic error
					doModal({
						'title': 'Payload Too Large',
						'message': 'Your upload was too large',
					});
				} else {
					//something is completely wrong, usually no connection or server down
					doModal({
						'title': 'Error',
						'message': 'Something broke'
					});
				}

				if (this.submit) {
					this.submit.value = this.originalSubmitText;
				}

			}
		};

		//gives a useless error, so once again show generic "something broke"
		xhr.onerror = (err) => {
			console.error(err);
			doModal({
				'title': 'Error',
				'message': 'Something broke'
			});
			if (this.submit) {
				this.submit.disabled = false;
				this.submit.value = this.originalSubmitText;
			}
		};

		//open the request
		xhr.open(this.form.getAttribute('method'), this.form.getAttribute('action'), true);

		//if not a minimal form, send special header so server knows to send json in dynamicResponse()
		if (!this.minimal) {
			xhr.setRequestHeader('x-using-xhr', true);
		}

		//if using live and connected, send special header so server knows to
		const isLive = localStorage.getItem('live') == 'true' && socket && socket.connected;
		if (isLive) {
			xhr.setRequestHeader('x-using-live', true);
		}

		//if not multipart form set correct header
		if (this.enctype !== 'multipart/form-data') {
			xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		}

		//send the request
		xhr.send(postData);

	}

	//forcefully update message box of form for character counter used e.g. in reset() because input event would not be fired
	updateMessageBox() {
		this.messageBox && this.messageBox.dispatchEvent(new Event('input'));
	}

	removeFile(fileElem, name, size) {
		fileElem.remove();
		let fileIndex;
		this.files.find((f, index) => {
			if (f.name === name && f.size === size) {
				fileIndex = index;
			}
		});
		this.files.splice(fileIndex, 1);
		this.updateFilesText();
	}

	async addFile(file, fileOptions = {}) {
		if (this.fileRequired) { //prevent drag+drop issues by removing required
			this.fileInput.removeAttribute('required');
		}
		this.files.push(file);
		console.log('got file', file.name);
		let fileHash;
		if (window.crypto.subtle) {
			let fileBuffer;
			if (file.arryaBuffer) {
				fileBuffer = await file.arrayBuffer();
			} else {
				//can old browsers just fuck off please?
				const bufferFileReader = new FileReader();
				await new Promise(res => {
					bufferFileReader.addEventListener('loadend', res);
					bufferFileReader.readAsArrayBuffer(file);
				});
				if (bufferFileReader.result) {
					fileBuffer = bufferFileReader.result;
				}
			}
			const fileDigest = await window.crypto.subtle.digest('SHA-256', fileBuffer);
			fileHash = Array.from(new Uint8Array(fileDigest))
				.map(c => c.toString(16).padStart(2, '0'))
				.join('');
			console.log('file hash', fileHash);
		}
		const item = {
			spoilers: this.fileUploadList.dataset.spoilers === 'true',
			stripFilenames: this.fileUploadList.dataset.stripFilenames === 'true',
			name: file.name,
			hash: fileHash,
			...fileOptions,
		};
		switch (file.type.split('/')[0]) {
			case 'image':
				item.url = URL.createObjectURL(file);
				break;
			case 'audio':
				item.url = '/file/audio.png';
				break;
			case 'video':
				try {
					const thumbnailBlob = await videoThumbnail(file);
					item.url = URL.createObjectURL(thumbnailBlob);
				} catch (err) {
					//couldnt create video thumb for some reason
					item.url = '/file/video.png';
				}
				break;
			default:
				item.url = '/file/attachment.png';
				break;
		}
		const uploadItemHtml = uploaditem({ uploaditem: item });
		this.fileUploadList.insertAdjacentHTML('beforeend', uploadItemHtml);
		const fileElem = this.fileUploadList.lastChild;
		const lastClose = fileElem.querySelector('.close');
		lastClose.addEventListener('click', () => {
			this.removeFile(fileElem, file.name, file.size);
		});
		this.fileUploadList.style.display = 'unset';
	}

	//show number of files on new label
	updateFilesText() {
		if (!this.fileLabelText) {
			return;
		}
		if (this.files && this.files.length === 0) {
			this.fileUploadList.textContent = '';
			this.fileUploadList.style.display = 'none';
			this.fileLabelText.nodeValue = __n('Select/Drop/Paste files', this.multipleFiles ? 2 : 1);
		} else {
			this.fileLabelText.nodeValue = __n('%s files selected', this.files.length);
		}
		this.fileInput.value = null;
	}

	//remove all files from this form
	clearFiles() {
		if (!this.fileInput) {
			return;
		}
		this.files = []; //empty file list
		this.fileInput.value = null; //remove the files for real
		if (this.fileRequired) { //reset to required if clearing files
			this.fileInput.setAttribute('required', true);
		}
		this.updateFilesText();
	}

	//paste files from clipboard
	paste(e) {
		const clipboard = e.clipboardData;
		if (clipboard.items && clipboard.items.length > 0) {
			const items = clipboard.items;
			for (let i = 0; i < items.length; i++) {
				const item = items[i];
				if (item.kind === 'file') {
					const file = new File([item.getAsFile()], 'ClipboardImage.png', { type: item.type });
					this.addFile(file);
				}
			}
			this.updateFilesText();
		}
	}

	//change cursor on hover when drag+dropping files
	fileLabelDrag(e) {
		e.stopPropagation();
		e.preventDefault();
		e.dataTransfer.dropEffect = 'copy';
	}

	//add file on drag+drop
	fileLabelDrop(e) {
		e.stopPropagation();
		e.preventDefault();
		const newFiles = e.dataTransfer.files;
		for (let i = 0; i < newFiles.length; i++) {
			this.addFile(newFiles[i]);
		}
		this.updateFilesText();
	}

	//add file by normal file form, but add instead of replacing files
	fileInputChange() {
		const newFiles = this.fileInput.files;
		for (let i = 0; i < newFiles.length; i++) {
			this.addFile(newFiles[i]);
		}
		this.updateFilesText();
	}

	//middle click to clear files
	fileLabelAuxclick(e) {
		if (e.button !== 1) { //middle click only
			return;
		}
		this.clearFiles();
	}

}

window.addEventListener('DOMContentLoaded', () => {

	const myPostId = localStorage.getItem('myPostId');
	if (myPostId) {
		window.location.hash = myPostId;
		localStorage.removeItem('myPostId');
	}

	window.addEventListener('addPost', (e) => {
		if (e.detail.hover) {
			return; //dont need to handle hovered posts for this
		}
		if (window.myPostId == e.detail.postId) {
			window.location.hash = e.detail.postId;
			e.detail.post.previousSibling.scrollIntoView();
		}
	});

});

window.addEventListener('settingsReady', () => {

	const forms = document.getElementsByTagName('form');

	for (let i = 0; i < forms.length; i++) {
		if (forms[i].method === 'post') {
			new postFormHandler(forms[i]);
		}
	}

	const tegakiWidthSetting = document.getElementById('tegakiwidth-setting');
	const changeTegakiWidthSetting = (e) => {
		tegakiWidth = parseInt(e.target.value);
		setLocalStorage('tegakiwidth-setting', tegakiWidth);
	};
	tegakiWidthSetting.value = tegakiWidth;
	tegakiWidthSetting.addEventListener('change', changeTegakiWidthSetting, false);

	const tegakiHeightSetting = document.getElementById('tegakiheight-setting');
	const changeTegakiHeightSetting = (e) => {
		tegakiHeight = parseInt(e.target.value);
		setLocalStorage('tegakiheight-setting', tegakiHeight);
	};
	tegakiHeightSetting.value = tegakiHeight;
	tegakiHeightSetting.addEventListener('change', changeTegakiHeightSetting, false);

});


code:
document.querySelectorAll('input[type="file"]').forEach(fileInput => {
	//not using display: none because we still want to show the browser prompt for a "required" file
	fileInput.style.position = 'absolute';
	fileInput.style.border = 'none';
	fileInput.style.height = '1px';
	fileInput.style.width = '1px';
//	fileInput.style.opacity = '0'; // same effect as display:none in some browsers, ugh...
});


code:
/* globals __ setLocalStorage */
let imageSources = new Set(JSON.parse(localStorage.getItem('hiddenimages')));
let imageSourcesList;

const toggleAllHidden = (state) => imageSources.forEach(i => toggleSource(i, state));

const toggleSource = (source, state) => {
	const images = document.querySelectorAll(`img.file-thumb[src="${source}"], img.catalog-thumb[src="${source}"]`);
	images.forEach(i => i.classList[state?'add':'remove']('vh'));
	const buttons = document.querySelectorAll(`a.hide-image[data-src="${source}"]`);
	buttons.forEach(b => b.textContent = state ? __('Show') : __('Hide'));
};

toggleAllHidden(true);

const toggleHandler = (e) => {
	const thumbSource = e.target.dataset.src;
	const hidden = imageSources.has(thumbSource);
	imageSources[hidden?'delete':'add'](thumbSource);
	imageSourcesList.value = [...imageSources];
	setLocalStorage('hiddenimages', JSON.stringify([...imageSources]));
	toggleSource(thumbSource, !hidden);
};

Array.from(document.getElementsByClassName('hide-image')).forEach(el => {
	el.addEventListener('click', toggleHandler, false);
});

const handleHiddenImages = (e) => {
	//hide any images from this post that should already be hidden
	e.detail.json.files.forEach(f => {
		let hideFilename = '/file/';
		if (f.hasThumb) {
			hideFilename += `thumb/${f.hash}${f.thumbextension}`;
		} else {
			hideFilename += f.filename;
		}
		if (imageSources.has(hideFilename)) {
			toggleSource(hideFilename, true);
		}
	});
	//add the hide toggle link and event listener
	if (!e.detail.hover) {
		const hideButtons = e.detail.post.querySelectorAll('.hide-image');
		for (let i = 0; i < hideButtons.length; i++) {
			hideButtons[i].addEventListener('click', toggleHandler, false);
		}
	}
};

window.addEventListener('addPost', handleHiddenImages, false);

window.addEventListener('settingsReady', () => {

	imageSourcesList = document.getElementById('hiddenimages-setting');
	imageSourcesList.value = [...imageSources];
	const imageSourcesListClearButton = document.getElementById('hiddenimages-clear');
	const clearImageSources = () => {
		toggleAllHidden(false);
		imageSources = new Set();
		imageSourcesList.value = '';
		setLocalStorage('hiddenimages', '[]');
		console.log('cleared hidden images list');
	};
	imageSourcesListClearButton.addEventListener('click', clearImageSources, false);

});


code:
/* globals isManage isModView extraLocals deleteStartsWith setLocalStorage post */
window.addEventListener('DOMContentLoaded', () => {

	const quotes = document.getElementsByClassName('quote');
	let hoverLoading = {};
	let hovering = false;
	let lastHover;

	const toggleDottedUnderlines = (hoveredPost, id) => {
		let uniqueQuotes = new Set();
		hoveredPost.querySelectorAll('.post-message .quote').forEach(q => uniqueQuotes.add(q.href));
		if (uniqueQuotes.size > 1) {
			const matchingQuotes = hoveredPost.querySelectorAll(`.post-message .quote[href$="${id}"]`);
			for (let i = 0; i < matchingQuotes.length; i++) {
				const mq = matchingQuotes[i];
				mq.style.borderBottom = mq.style.borderBottom == '' ? '1px dashed' : '';
				mq.style.textDecoration = mq.style.textDecoration == '' ? 'none' : '';
			}
		}
	};

	const isVisible = (e) => {
		const top = e.getBoundingClientRect().top;
		const bottom = e.getBoundingClientRect().bottom;
		const height = window.innerHeight;
		return top >= 38 && bottom <= height;
	};

	const setFloatPos = (quote, float, xpos, ypos) => {
		const quotepos = quote.getBoundingClientRect();
		const post = float.firstChild;
		const iw = document.body.offsetWidth-10;
		const ih = window.innerHeight;
		const left = xpos < iw/2;
		if (left) {
			float.style.left = `${quotepos.right+10}px`;
			if (quotepos.right+10+post.offsetWidth >= iw) {
				float.style.right = '5px';
			}
		} else {
			float.style.right = `${iw-quotepos.left+15}px`;
			if (quotepos.left-15 < post.offsetWidth) {
				float.style.left = '5px';
			}
		}
		const top = ypos < ih/2;
		if (top && quotepos.bottom+post.offsetHeight < ih) {
			float.style.top = `${quotepos.top}px`;
		} else if (!top && post.offsetHeight < ypos) {
			float.style.top = `${quotepos.bottom-post.offsetHeight}px`;
		} else {
			float.style.top = '42px';
		}
	};

	const floatPost = (quote, post, xpos, ypos) => {
		const clone = document.createElement('div');
		clone.id = 'float';
		clone.classList.remove('hoverhighlighted');
		clone.appendChild(post.cloneNode(true));
		document.body.appendChild(clone);
		setFloatPos(quote, clone, xpos, ypos);
		return clone;
	};

	const toggleHighlightPost = async function (e) {
		hovering = e.type === 'mouseover';
		let jsonParts = this.pathname.replace(/\.html$/, '.json').split('/');
		let jsonPath;
		if ((isManage || isModView) && jsonParts.length === 5) {
			jsonParts.splice(2,1); //remove manage from json url
		}
		jsonPath = jsonParts.join('/');
		if (!this.hash) {
			return; //non-post number board quote
		}
		const float = document.getElementById('float');
		if (float) {
			document.body.removeChild(float);
		}
		const parentPost = this.closest('.post-container');
		let thisId = 0;
		if (parentPost) {
			thisId = parentPost.dataset.postId;
		}
		const loading = Date.now();
		lastHover = loading;
		const hash = this.hash.substring(1);
		const anchor = document.getElementById(hash);
		let hoveredPost, postJson;
		if (anchor
			&& jsonPath.split('/')[1] === anchor.nextSibling.dataset.board) {
			hoveredPost = anchor.nextSibling;
		} else {
			let hovercache = localStorage.getItem(`hovercache-${jsonPath}`);
			if (hovercache) {
				hovercache = JSON.parse(hovercache);
				if (hovercache.postId == hash) {
					postJson = hovercache;
				} else if (hovercache.replies.length > 0) {
					postJson = hovercache.replies.find(r => r.postId == hash);
				}
			}
			if (!postJson) { //wasnt cached or cache outdates
				this.style.cursor = 'wait';
				let json;
				try {
					if (!hoverLoading[jsonPath]) {
						hoverLoading[jsonPath] = fetch(jsonPath).then(res => res.json());
					}
					json = await hoverLoading[jsonPath];
				} catch (e) {
					return console.error(e);
				} finally {
					this.style.cursor = '';
				}
				if (json) {
					setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json));
					hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
					if (json.postId == hash) {
						postJson = json;
					} else {
						postJson = json.replies.find(r => r.postId == hash);
					}
				} else {
					return localStorage.removeItem(`hovercache-${jsonPath}`); //thread deleted
				}
			}
			if (lastHover !== loading) {
				return; //dont show for ones not hovering
			}
			if (!postJson) {
				return; //post was deleted or missing
			}
			const postHtml = post({
				post: postJson,
				...extraLocals,
			});
			const wrap = document.createElement('div');
			wrap.innerHTML = postHtml;
			hoveredPost = wrap.firstChild.nextSibling;
		}
		if (hovering && !isVisible(hoveredPost)) {
			hoveredPost = floatPost(this, hoveredPost, e.clientX, e.clientY);
		} else {
			hovering ? hoveredPost.classList.add('hoverhighlighted') : hoveredPost.classList.remove('hoverhighlighted');
		}
		if (postJson) {
			//need this event so handlers like post hiding still apply to hover introduced posts
			const newPostEvent = new CustomEvent('addPost', {
				detail: {
					json: postJson,
					post: hoveredPost,
					postId: postJson.postId,
					hover: true,
					nonotify: true,
				}
			});
			window.dispatchEvent(newPostEvent);
		}
		if (hovering || isVisible(hoveredPost)) {
			toggleDottedUnderlines(hoveredPost, thisId);
		}
	};

	for (let i = 0; i < quotes.length; i++) {
		quotes[i].addEventListener('mouseover', toggleHighlightPost, false);
		quotes[i].addEventListener('mouseout', toggleHighlightPost, false);
	}

	window.addEventListener('addPost', function(e) {
		if (e.detail.hover) {
			return; //dont need to handle hovered posts for this
		}
		const newquotes = document.getElementsByClassName('quote'); //to get backlinks from replying posts. just an easy way. could make more efficient and only do necessary ones later.
		for (let i = 0; i < newquotes.length; i++) {
			newquotes[i].removeEventListener('mouseover', toggleHighlightPost);
			newquotes[i].removeEventListener('mouseout', toggleHighlightPost);
			newquotes[i].addEventListener('mouseover', toggleHighlightPost, false);
			newquotes[i].addEventListener('mouseout', toggleHighlightPost, false);
		}
	});

	window.addEventListener('updatePostMessage', function(e) {
		const newquotes = e.detail.post.getElementsByClassName('quote');
		for (let i = 0; i < newquotes.length; i++) {
			newquotes[i].addEventListener('mouseover', toggleHighlightPost, false);
			newquotes[i].addEventListener('mouseout', toggleHighlightPost, false);
		}
	});

});

let hoverCacheList;
window.addEventListener('settingsReady', function() {
	hoverCacheList = document.getElementById('hovercachelist-setting');
	hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
	const hoverCacheListClearButton = document.getElementById('hovercachelist-clear');
	const clearHoverCacheList = () => {
		deleteStartsWith('hovercache');
		hoverCacheList.value = '';
		console.log('cleared cache');
	};
	hoverCacheListClearButton.addEventListener('click', clearHoverCacheList, false);
});


code:
let hoverExpandEnabled = localStorage.getItem('hoverexpandsmedia') === 'true';
let hoverExpandFollowEnabled = localStorage.getItem('hoverexpandsfollowcursor') === 'true';
const hoverMediaSelector = 'div.post-file-src[data-type="image"], div.post-file-src[data-type="video"], div.post-file-src[data-type="audio"]';
const mediaElements = document.querySelectorAll(hoverMediaSelector);
let hoverPopup = null;

const createHoverPopup = () => {
	hoverPopup = document.createElement('div');
	hoverPopup.id = 'hover-popup';
	document.body.appendChild(hoverPopup);
};

const updateHoverPopup = (mediaType, src, thumbElement) => {
	return new Promise(res => {
		const mediaElement = document.createElement(mediaType === 'image' ? 'img' : mediaType);
		const customOnLoad = () => {
			hideHoverPopup();
			hoverPopup.appendChild(mediaElement);
			hoverPopup.style.display = 'block';
			mediaElement.play && mediaElement.play();
			res();
		};
		mediaElement.onload = customOnLoad;
		mediaElement.onloadedmetadata = customOnLoad;
		mediaElement.src = src;
		if (mediaType !== 'image') {
			mediaElement.controls = true;
			mediaElement.loop = true;
			mediaElement.volume = localStorage.getItem('volume') / 100; //todo: necessary to change?
			mediaElement.load();
			if (mediaType === 'audio' && thumbElement) {
				mediaElement.style.backgroundImage = `url("${encodeURI(thumbElement.src)}")`;
				mediaElement.style.backgroundRepeat = 'no-repeat';
				mediaElement.style.backgroundPosition = 'top';
				mediaElement.style.backgroundSize = 'contain';
				mediaElement.style.minWidth = `${thumbElement.width}px`;
				mediaElement.style.paddingTop = `${thumbElement.height}px`;
			}
		}
	});
};

const positionHoverPopup = (mouseX, mouseY) => {
	if (!hoverExpandFollowEnabled) {
		hoverPopup.style.right = '0px';
		hoverPopup.style.top = '0px';
		return;
	}

	const popupWidth = hoverPopup.offsetWidth;
	const popupHeight = hoverPopup.offsetHeight;

    // Calculate the position to center the popup next to the cursor
	let left = mouseX + 10; // Offset to the right of the cursor
	let top = mouseY - (popupHeight / 2); // Center vertically

    //bound to left/right
	if (left + popupWidth > window.innerWidth) {
		left = window.innerWidth - popupWidth;
	}

    //bound to top/bottom
	if (top < 0) {
		top = 10; // Prevent going off the top of the screen
	} else if (top + popupHeight > window.innerHeight) {
		top = window.innerHeight - popupHeight; // Prevent going off the bottom of the screen
	}

	hoverPopup.style.left = `${left}px`;
	hoverPopup.style.top = `${top}px`;
};

const hideHoverPopup = () => {
	if (hoverPopup) {
		while (hoverPopup.firstChild) {
			hoverPopup.removeChild(hoverPopup.firstChild); //remove all children
		}
		hoverPopup.style.left = '';
		hoverPopup.style.top = '';
		hoverPopup.style.right = '';
		hoverPopup.style.display = 'none';
	}
};

const handleMouseOver = async (event) => {
	if (!hoverExpandEnabled) { return; }
	const thumbElement = event.currentTarget.querySelector('.file-thumb');
	if (thumbElement //if its already expanded, or is spoilered, don't allow hover
        && (thumbElement.style.display === 'none'
            || thumbElement.classList.contains('spoilerimg'))) { return; }
	const mediaType = event.currentTarget.dataset.type;
	const mediaLink = event.currentTarget.querySelector('a').href;

	await updateHoverPopup(mediaType, mediaLink, thumbElement);
	positionHoverPopup(event.x, event.y);
};

const handleMouseMove = (event) => {
	if (hoverExpandFollowEnabled && hoverPopup.style.display === 'block') {
		positionHoverPopup(event.x, event.y);
	}
};

const toggleHoverExpand = () => {
	hoverExpandEnabled = !hoverExpandEnabled;
	localStorage.setItem('hoverexpandsmedia', hoverExpandEnabled);
	console.log('hover expand setting:', hoverExpandEnabled);
};

const toggleHoverExpandFollow = () => {
	hoverExpandFollowEnabled = !hoverExpandFollowEnabled;
	localStorage.setItem('hoverexpandsfollowcursor', hoverExpandFollowEnabled);
	console.log('hover expand follow setting:', hoverExpandFollowEnabled);
};

const attachHoverListeners = (elements) => {
	elements.forEach(media => {
		media.addEventListener('mouseover', handleMouseOver);
		media.addEventListener('mouseout', hideHoverPopup);
		media.addEventListener('click', hideHoverPopup);
	});
};

document.addEventListener('mousemove', handleMouseMove);

window.addEventListener('settingsReady', function() {
	const hoverExpandSetting = document.getElementById('hover-expand-setting');
	hoverExpandSetting.checked = hoverExpandEnabled;
	hoverExpandSetting.addEventListener('change', toggleHoverExpand, false);

	const hoverExpandFollowSetting = document.getElementById('hover-expand-follow-setting');
	hoverExpandFollowSetting.checked = hoverExpandFollowEnabled;
	hoverExpandFollowSetting.addEventListener('change', toggleHoverExpandFollow, false);

	createHoverPopup();
	attachHoverListeners(mediaElements);
});

window.addEventListener('addPost', function(e) {
	const newMediaElements = e.detail.post.querySelectorAll(hoverMediaSelector);
	attachHoverListeners(newMediaElements);
});


code:
/* eslint-disable no-unused-vars */
/* globals TRANSLATIONS */

const pluralMap = {
	1: 'one',
	// two, three, few, many, ...
};

//simple translation
const __ = (key, replacement=null) => {
	const translation = TRANSLATIONS[key] || key;
	return replacement !== null ? translation.replace('%s', replacement) : translation;
};

//pluralisation
const __n = (key, count) => {
	const pluralKey = pluralMap[count] || 'other';
	const translationObj = TRANSLATIONS[key];
	if (!translationObj) {
		return key;
	}
	const translationPlural = translationObj[pluralKey] || translationObj['other'];
	return translationPlural.replace('%s', count);
};


code:
/* globals setLocalStorage */
window.addEventListener('settingsReady', () => {

	const settingNames = ['volume','loop','imageloadingbars','live','scroll','localtime','relative','24hour','notifications','hiddenimages', 'threadwatcher'
		,'notification-yous-only','yous-setting','filters1','name','theme','codetheme','customcss','disableboardcss','hiderecursive', 'watchlist'
		,'heightlimit','crispimages','hidethumbnails','noncolorids','alwaysshowspoilers','hidedeletedpostcontent','hidepoststubs','smoothscrolling'
		,'tegakiheight-setting','tegakiwidth-setting', 'hoverexpandsmedia'];

	const importExportText = document.getElementById('import-export-setting');

	const exportSettingsButton = document.getElementById('export-setting');
	const exportSettings = () => {
		const settingsData = settingNames.reduce((acc, val) => {
			acc[val] = localStorage.getItem(val);
			return acc;
		}, {});
		importExportText.value = JSON.stringify(settingsData);
	};
	exportSettingsButton.addEventListener('click', exportSettings, false);

	const importSettingsButton = document.getElementById('import-setting');
	const importSettings = () => {
		if (importExportText.value.length > 0) {
			try {
				const importedSettings = JSON.parse(importExportText.value);
				Object.entries(importedSettings).forEach(entry => {
					setLocalStorage(entry[0], entry[1]);
				});
				//should we have an update listener one day to import without refresh?
				location.reload();
			} catch (e) {
				//bad data
				console.error(e);
			}
		}
	};
	importSettingsButton.addEventListener('click', importSettings, false);

});


code:
/* eslint-disable no-unused-vars */
// Canvas Blocker & Firefox privacy.resistFingerprinting Detector. (c) 2018 // JOHN OZBAY // CRYPT.EE // MIT License: https://github.com/johnozbay/canvas-block-detector/blob/master/isCanvasBlocked.js
function isCanvasBlocked() {
	var canvas = document.createElement('canvas');
	var ctx = canvas.getContext('2d');
	if (!ctx) { return true; }
	var imageData = ctx.createImageData(1, 1);
	var originalImageData = imageData.data;
	originalImageData[0] = 128;
	originalImageData[1] = 128;
	originalImageData[2] = 128;
	originalImageData[3] = 255;
	ctx.putImageData(imageData, 1, 1);
	try {
		var checkData = ctx.getImageData(1, 1, 1, 1).data;
		if (
			originalImageData[0] !== checkData[0] &&
      originalImageData[1] !== checkData[1]
		) { return true; }
	} catch (error) {
		return true;
	}
	return false;
}