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;
	--input-borders: #111;
	--input-color: #c5c8c6;
	--input-background: #1d1d21;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color:#E0727F;
}

table {
	border: none;
	border-spacing: 2px;
}

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

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

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

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

}


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;
	--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;
}

.post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats {
	border-bottom: 1px solid var(--post-outline-color);
	border-right: 1px solid var(--post-outline-color);
	border-top: 1px solid var(--post-color);
	border-left: 1px solid var(--post-color);
}

table {
	border: none;
	border-spacing: 2px;
}

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

@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;
	}

}

repo_name: dolphin/jschan

license: mit


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;
    --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);
}


code:
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--darken: #ffffff10;
	--board-title: #c5c8c6;
	--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;
    --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, .toggle-summary, .catalog-tile, #livetext, #threadstats, .navbar, #postform, .op, table, .board-banner, .filelabel, .modal {
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;
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, .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('/file/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, #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;
}
.op {
width:unset!important;
}
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;
  }
}
.rainbow {
display: inline
}
.nav-item:nth-of-type(3) {
line-height: 1.75em
}


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, .toggle-summary, .catalog-tile, #livetext, #threadstats {
	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: #EA8;
	--box-border-color: #000;
	--darken: #00000010;
	--highlighted-post-color: #f0c0b0;
	--highlighted-post-outline-color: #d99f91;
	--board-title: #af0a0f;
	--hr: #D9BFB7;
	--font-color: #800000;
	--name-color: #800000;
	--capcode-color: #f00;
	--subject-color: #f00;
	--link-color: #800;
	--post-link-color: navy;
	--link-hover: #f00;
	--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, .toggle-summary, .catalog-tile, #livetext, #threadstats {
	border-width: 0 1px 1px 0;
}


code:
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="/file/mstile-150x150.png"/>
            <TileColor>#00aba9</TileColor>
        </tile>
    </msapplication>
</browserconfig>


type: image/png

theme_color: #ffffff

background_color: #ffffff

display: standalone


code:
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 (captchaType === 'grid') {
			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 {
			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;
				}
			}
			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);
	}

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

	loadCaptcha(field) {
		const captchaDiv = field.previousSibling;
		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 (captchaType === 'text') {
			field.placeholder = 'loading';
		}
		captchaImg.src = '/captcha';
		captchaImg.onload = () => {
			if (captchaType === 'text') {
				field.placeholder = 'Captcha text';
			}
			captchaDiv.appendChild(captchaImg);
			captchaDiv.appendChild(refreshDiv);
			this.startRefreshTimer();
		}
	}

}

const captchaController = new CaptchaController();

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

	captchaController.init();

});


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 captchaformsection(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
    var locals_for_with = (locals || {});

    (function (captchaGridSize, captchaType, googleRecaptchaSiteKey, minimal) {
      pug_mixins["captchaexpand"] = pug_interp = function(){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdetails class=\"row label mr-0\"\u003E\u003Csummary class=\"pv-5\"\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fsummary\u003E";
switch (captchaType){
case 'google':
pug_html = pug_html + "\u003Cdiv" + (" class=\"g-recaptcha\""+pug_attr("data-sitekey", `${googleRecaptchaSiteKey}`, true, false)+" data-theme=\"dark\" data-size=\"compact\" data-callback=\"recaptchaCallback\"") + "\u003E\u003C\u002Fdiv\u003E\u003Cnoscript\u003EPlease enable JavaScript to solve the captcha.\u003C\u002Fnoscript\u003E";
  break;
case 'text':
pug_html = pug_html + "\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe" + (" class=\"captcha\""+" src=\"\u002Fcaptcha.html\""+pug_attr("width=210", true, true, false)+" height=\"80\" scrolling=\"no\" loading=\"lazy\"") + "\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none;\"\u003E\u003C\u002Fdiv\u003E\u003Cinput" + (" class=\"captchafield\""+" type=\"text\" name=\"captcha\" autocomplete=\"off\" placeholder=\"Captcha text\" pattern=\".{6}\""+pug_attr("required", true, true, false)+" title=\"6 characters\"") + "\u002F\u003E";
  break;
case 'grid':
if (!(minimal)) {
pug_html = pug_html + "\u003Ca class=\"text-center\" href=\"\u002Ffaq.html#captcha\"\u003EInstructions\u003C\u002Fa\u003E";
}
pug_html = pug_html + "\u003Cdiv class=\"catalog\"\u003E\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe class=\"captcha\" src=\"\u002Fcaptcha.html\" width=\"150\" height=\"150\" scrolling=\"no\" loading=\"lazy\"\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none\"\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"captchafield noselect\"\u003E";
for(let i = 0; i < captchaGridSize**2; i++) {
pug_html = pug_html + "\u003Clabel class=\"captchachecklabel\"\u003E\u003Cinput" + (" type=\"checkbox\" name=\"captcha\""+pug_attr("value", i, true, false)) + "\u002F\u003E\u003Cspan class=\"captchacheckbox\"\u003E\u003C\u002Fspan\u003E\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
  break;
}
pug_html = pug_html + "\u003C\u002Fdetails\u003E";
};
pug_mixins["captchaexpand"]();
    }.call(this, "captchaGridSize" in locals_for_with ?
        locals_for_with.captchaGridSize :
        typeof captchaGridSize !== 'undefined' ? captchaGridSize : undefined, "captchaType" in locals_for_with ?
        locals_for_with.captchaType :
        typeof captchaType !== 'undefined' ? captchaType : undefined, "googleRecaptchaSiteKey" in locals_for_with ?
        locals_for_with.googleRecaptchaSiteKey :
        typeof googleRecaptchaSiteKey !== 'undefined' ? googleRecaptchaSiteKey : undefined, "minimal" in locals_for_with ?
        locals_for_with.minimal :
        typeof minimal !== 'undefined' ? minimal : undefined));
    ;;return pug_html;}


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

		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 tiles = document.getElementsByClassName('catalog-tile');
			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', (event) => {

	const messageBox = document.getElementById('message');

	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(e) {
			if (messageBox.value.length > maxLength) {
				messageBox.value = messageBox.value.substring(0,maxLength);
			}
			currentLength = messageBox.value.length;
			updateCounter();
		};

		updateCounter();

		messageBox.addEventListener('input', updateLength);

	}

});


code:
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 { visibility: hidden;margin-top: -1.5em;height: 0; }`;
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; }`;

//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('smoothscrolling-setting', 'smoothscrolling', settings.smoothScrolling, smoothScrollingCss);


code:
class Dragable {

	constructor(handle, target) {
		this.draging = false;
		this.xo = 0;
		this.yo = 0;
		this.handle = document.querySelector(handle);
		this.target = document.querySelector(target);
		const savedTop = localStorage.getItem('dragtop');
		if (savedTop != 'null') {
			this.target.style.top = savedTop;
			this.target.style.bottom = 'unset';
		}
		const savedLeft = localStorage.getItem('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));
		document.addEventListener('mouseup', e => this.stopDrag(e));
		document.addEventListener('touchend', e => this.stopDrag(e));
		window.addEventListener('resize', e => this.updateMaxSizes(e));
		window.addEventListener('orientationchange', e => this.updateMaxSizes(e));
	}

	//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`;
	}

	//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('dragtop', this.target.style.top);
		setLocalStorage('dragleft', this.target.style.left);
	}

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

}

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


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

		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') || (urlObject.hostname === 'youtu.be' ? urlObject.pathname.substring(1) : null);
						if (videoId && videoId.length === 11) {
							return `<iframe class="embed-video" src="https://www.youtube.com/embed/${encodeURIComponent(videoId)}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" style="display:block;" allowfullscreen></iframe>`;
						}
					} 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 `<iframe class="embed-video" src="https://www.bitchute.com/embed/${encodeURIComponent(videoId)}/" frameborder="0" scrolling="no" style="display:block;" allowfullscreen></iframe>`;
						}
					} catch (e) { /*invalid url*/ }
					return null;
				}
			},
			//TODO: add more of these
		];

		const toggleEmbed = (embedSpan, embedHtml) => {
			if (embedSpan.dataset.open === 'true') {
				embedSpan.nextSibling.remove();
				embedSpan.firstElementChild.textContent = 'Embed';
			} else {
				embedSpan.insertAdjacentHTML('afterend', embedHtml);
				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 = 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), false);
				}
			}
		};

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

		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:
window.addEventListener('DOMContentLoaded', (event) => {

	const actionFooter = document.querySelector('summary.toggle-summary');
	if (actionFooter) {
		actionFooter.onclick = () => {
			actionFooter.scrollIntoView();
		}
	}

	const volumeSetting = document.getElementById('volume-setting');
	let volumeLevel = localStorage.getItem('volume');
	const changeVolume = (change) => {
		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 = (change) => {
		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 = (change) => {
		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 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();
						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') {
							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(e) {
								pfs.removeAttribute('data-loading');
								pfs.removeAttribute('style');
								const blob = this.response;
								source.src = window.URL.createObjectURL(blob);
								thumbElement.style.opacity = '';
								thumbElement.style.cursor = '';
								fileAnchor.appendChild(expandedElement);
								toggle(thumbElement, expandedElement, fileName, pfs);
							}
							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);
						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 {
							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;
						break;
					deault:
						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:
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(),
		fsub: new Set(),
		ftrip: new Set(),
		fmsg: new Set(),
		fnamer: [],
		ftripr: [],
		fsubr: [],
		fmsgr: [],
	});
};

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

let filtersTable;
const updateFiltersTable = () => {
	[...filtersTable.children].slice(3).forEach(row => row.remove());
	filtersTable.insertAdjacentHTML('beforeend', filters({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}))),
		...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()})),
	]));
	updateFiltersTable();
};

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

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');
		}
		if (!isCatalog) {
			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;
		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);
			}
		case 'fmsg':
			fmsg[addOrDelete](data);
			break;
		case 'fmsgr':
			fmsgr = fmsgr.filter(r => r.source != data.source);
			if (state) {
				fmsgr.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();
}

const postMenuChange = function(e) {
	const postContainer = this.parentElement.parentElement.parentElement;
	const postDataset = postContainer.dataset
	const filterType = this.value;
	const hiding = !postContainer.classList.contains('hidden');
	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;
	}
	toggleFilter(filterType, filterData, hiding);
	this.value = '';
};

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 subject of fsub) {
		posts = posts.concat(getPostsByFilter('fsub', subject));
	}
	for (let message of fmsg) {
		posts = posts.concat(getPostsByFilter('fmsg', message));
	}
	for (let tripcode of ftrip) {
		posts = posts.concat(getPostsByFilter('ftrip', tripcode));
	}
	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));
	}
	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(e) {

	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(),
		fsub = new Set(),
		fmsg = new Set(),
		ftrip = new Set(),
		fnamer = [],
		ftripr = [],
		fsubr = [],
		fmsgr = [],
		updateFiltersTable();
		togglePostsHidden(document.querySelectorAll(`.${isCatalog ? 'catalog-tile': 'post-container' }`), false);
		updateSavedFilters();
		console.log('cleared hidden posts');
	}
	filterClearButton.addEventListener('click', clearFilters, false);

});


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

function doModal(data, postcallback) {
	try {
		const modalHtml = modal({ modal: data });
		let checkInterval;
		document.body.insertAdjacentHTML('afterbegin', modalHtml);
		document.getElementById('modalclose').onclick = () => {
			removeModal();
			clearInterval(checkInterval);
		};
		document.getElementsByClassName('modal-bg')[0].onclick = () => {
			removeModal();
			clearInterval(checkInterval);
		};
		const modalframe = document.getElementById('modalframe');
		if (modalframe) {
			//if theres a modal frame and user has default theme, style it
			if (localStorage.getItem('theme') === 'default') {
				modalframe.onload = () => {
					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 checkTypes = ['checkbox', 'radio'];
function isCheckBox(element) {
	return checkTypes.includes(element.type)
}

function formToJSON(form) {
	const data = {};
	for (element of form.elements) {
		if (element.name /*&& element.value*/ && (!isCheckBox(element) || element.checked)) {
			if (isCheckBox(element) && data[element.name]) {
				if (Array.isArray(data[element.name])) {
					data[element.name] = data[element.name].push(element.value);
				} else {
					data[element.name] = [data[element.name], element.value];
				}
			} else {
				data[element.name] = element.value;
			}
		}
	}
	return JSON.stringify(data);
}

let recaptchaResponse = null;
function recaptchaCallback(response) {
	recaptchaResponse = response;
}
class formHandler {

	constructor(form) {
		this.form = form;
		this.enctype = this.form.getAttribute('enctype');
		this.messageBox = form.querySelector('#message');
		this.captchaField = form.querySelector('.captchafield') || form.querySelector('.g-recaptcha') || form.querySelector('.h-captcha');

		this.submit = form.querySelector('input[type="submit"]');
		if (this.submit) {
			this.originalSubmitText = this.submit.value;
		}
		this.minimal = this.form.elements.minimal;
		this.files = [];
		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));
		}
		this.messageBox && this.messageBox.addEventListener('keydown', e => this.controlEnterSubmit(e));
		form.addEventListener('paste', e => this.paste(e));
		form.addEventListener('submit', e => this.formSubmit(e));
	}

	reset() {
		const savedName = this.form.elements.name && this.form.elements.name.value;
		this.form.reset();
		if (this.form.elements.name) {
			this.form.elements.name.value = savedName
		}
		if (this.form.elements.postpassword) {
			this.form.elements.postpassword.value = localStorage.getItem('postpassword');
		}
		this.updateMessageBox();
		this.files = [];
		this.updateFilesText();
		const captcha = this.form.querySelector('.captcharefresh');
		if (captcha) {
			captcha.dispatchEvent(new Event('click'));
		}
	}

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

	formSubmit(e) {
		const xhr = new XMLHttpRequest();
		let postData;
		const captchaResponse = recaptchaResponse || hcaptchaResponse;
		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 files to file input element
				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 (this.banned
			|| this.minimal
			|| (postData instanceof URLSearchParams && postData.get('edit') === '1')) {
			return true;
		} else {
			e.preventDefault();
		}
		this.submit.disabled = true;
		this.submit.value = 'Processing...';
		if (this.files && this.files.length > 0) {
			//show progress on file uploads
			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 = () => {
			if (xhr.readyState === 4) {
				if (captchaResponse && grecaptcha) {
					grecaptcha.reset();
				} else if(captchaResponse && hcaptcha) {
					hcaptcha.reset();
				}
				this.submit.disabled = false;
				this.submit.value = this.originalSubmitText;
				let json;
				if (xhr.responseText) {
					try {
						json = JSON.parse(xhr.responseText);
					} catch (e) {
						//wasnt json response
					}
				}
				if (xhr.status == 200) {
					if (!json) {
						if (xhr.responseURL
							&& xhr.responseURL !== `${location.origin}${this.form.getAttribute('action')}`) {
							window.location = xhr.responseURL;
							return;
						} else if (xhr.responseText) {
							//
						}
					} else {
						if (json.postId) {
							window.myPostId = json.postId;
						}
						if (json.redirect) {
							const redirectBoard = json.redirect.split('/')[1];
							const redirectPostId = json.redirect.split('#')[1];
							if (redirectBoard && redirectPostId) {
								appendLocalStorageArray('yous', `${redirectBoard}-${redirectPostId}`);
							}
						}
						if (json.message || json.messages || json.error || json.errors) {
							doModal(json);
						} else if (socket && socket.connected) {
							window.location.hash = json.postId
						} else {
							if (!isThread) {
								return window.location = json.redirect;
							}
							setLocalStorage('myPostId', json.postId);
							forceUpdate();
						}
					}
					if (this.form.getAttribute('action') !== '/forms/editpost'
						&& !this.form.getAttribute('action').endsWith('/settings')) { //dont reset on edit, keep the new values in there. todo: add exceptions/better handling for this situation
						this.reset();
					}
				} else {
					if (xhr.status === 413) {
						this.clearFiles();
						//not json, must be nginx response
						doModal({
							'title': 'Payload Too Large',
							'message': 'Your upload was too large',
						});
					} else if (json) {
						if (!this.captchaField && json.message === 'Incorrect captcha answer') {
							captchaController.addMissingCaptcha();
							this.captchaField = true;
						} else if (json.message === 'Captcha expired') {
							const captcha = this.form.querySelector('.captcharefresh');
							if (captcha) {
								captcha.dispatchEvent(new Event('click'));
							}
						}
						doModal(json, () => {
							this.formSubmit(e);
						});
					} else {
//for bans, post form to show TODO: make modal support bans json and send dynamicresponse from it (but what about appeals, w/ captcha, etc?)
						this.clearFiles(); //dont resubmit files
						this.banned = true;
						this.form.dispatchEvent(new Event('submit'));
					}
				}
				this.submit.value = this.originalSubmitText;
			}
		}
		xhr.onerror = (err) => {
			console.error(err); //why is this error fucking useless
			doModal({
				'title': 'Error',
				'message': 'Something broke'
			});
			this.submit.disabled = false;
			this.submit.value = this.originalSubmitText;
		}
		xhr.open(this.form.getAttribute('method'), this.form.getAttribute('action'), true);
		if (!this.minimal) {
			xhr.setRequestHeader('x-using-xhr', true);
		}
		const isLive = localStorage.getItem('live') == 'true' && socket && socket.connected;
		if (isLive) {
			xhr.setRequestHeader('x-using-live', true);
		}
		if (this.enctype !== 'multipart/form-data') {
			xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		}
		xhr.send(postData);
	}

	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();
	}

	addFile(file) {
		if (this.fileRequired) { //prevent drag+drop issues by removing required
			this.fileInput.removeAttribute('required');
		}
		this.files.push(file);
		const item = {
			spoilers: this.fileUploadList.dataset.spoilers === 'true',
			name: file.name
		}
		switch (file.type.split('/')[0]) {
			case 'image':
				item.url = URL.createObjectURL(file);
				break;
			case 'audio':
				item.url = '/file/audio.png'
				break;
			case 'video':
				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 = `Select/Drop/Paste file${this.multipleFiles ? 's' : ''}`;
		} else {
			this.fileLabelText.nodeValue = `${this.files.length} file${this.files.length > 1 ? 's' : ''} selected`;
		}
	}

	//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 file 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
	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(e) {
		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;
		}
	});

});

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

	const forms = document.getElementsByTagName('form');
	for(let i = 0; i < forms.length; i++) {
		if (forms[i].method === 'post' /*&& forms[i].encoding === 'multipart/form-data'*/) {
			new formHandler(forms[i]);
		}
	}

})


code:
const fileInput = document.getElementById('file');
if (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';
}


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

	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);
	};

	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;
		if (anchor
			&& jsonPath.split('/')[1] === anchor.nextSibling.dataset.board) {
			hoveredPost = anchor.nextSibling;
		} else {
			let hovercache = localStorage.getItem(`hovercache-${jsonPath}`);
			let postJson;
			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 });
			const wrap = document.createElement('div');
			wrap.innerHTML = postHtml;
			hoveredPost = wrap.firstChild.nextSibling;
			//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
				}
			});
			window.dispatchEvent(newPostEvent);
		}
		toggleDottedUnderlines(hoveredPost, thisId);
		if (hovering && !isVisible(hoveredPost)) {
			floatPost(this, hoveredPost, e.clientX, e.clientY);
		} else {
			hovering ? hoveredPost.classList.add('hoverhighlighted') : hoveredPost.classList.remove('hoverhighlighted');
		}
	}

	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 post = e.detail.post;
		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);
		}
	});

});

window.addEventListener('settingsReady', function(e) {
	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 liveEnabled = localStorage.getItem('live') == 'true';
let scrollEnabled = localStorage.getItem('scroll') == 'true';
let socket;
let forceUpdate;

window.addEventListener('settingsReady', function(event) { //after domcontentloaded

	let supportsWebSockets = 'WebSocket' in window || 'MozWebSocket' in window;
	const livecolor = document.getElementById('livecolor');
	const livetext = isThread && document.getElementById('livetext') ? document.getElementById('livetext').childNodes[1] : null;
	const updateButton = livetext ? livetext.nextSibling : null;
	const updateLive = (message, color, showRelativeTime) => {
		livecolor.style.backgroundColor = color;
		livetext.nodeValue = `${message}`;
	}
	let lastPostId;
	let liveTimeout;
	const anchors = document.getElementsByClassName('anchor');
	if (anchors.length > 0) {
		lastPostId = anchors[anchors.length - 1].id;
	}
	const thread = document.querySelector('.thread');

	const newPost = (data) => {
		console.log('got new post');
		lastPostId = data.postId;
		const postData = data;
		//create a new post
		const postHtml = post({ post: postData, modview:isModView, upLevel:isThread });
		//add it to the end of the thread
		thread.insertAdjacentHTML('beforeend', postHtml);
		for (let j = 0; j < postData.quotes.length; j++) {
			const quoteData = postData.quotes[j];
			//add backlink to quoted posts
			const quotedPost = document.getElementById(quoteData.postId).nextSibling;
			let replies = quotedPost.querySelector('.replies');
			if (!replies) {
				const quotedPostData = quotedPost.querySelector('.post-data');
				const newRepliesDiv = document.createElement('div');
				newRepliesDiv.textContent = 'Replies: ';
				newRepliesDiv.classList.add('replies', 'mt-5', 'ml-5');
				quotedPostData.appendChild(newRepliesDiv);
				replies = newRepliesDiv;
			}
			if (new RegExp(`>>${postData.postId}(\s|$)`).test(replies.innerText)) {
				//reply link already exists (probably from a late catch up)
				continue;
			}
			const newReply = document.createElement('a');
			const space = document.createTextNode(' ');
			newReply.href = `${window.location.pathname}#${postData.postId}`;
			newReply.textContent = `>>${postData.postId}`;
			newReply.classList.add('quote');
			replies.appendChild(newReply);
			replies.appendChild(space);
		}
		const newPostAnchor = document.getElementById(postData.postId);
		const newPost = newPostAnchor.nextSibling;
		if (scrollEnabled) {
			newPostAnchor.scrollIntoView(); //scroll to post if enabled;
		}
		const newPostEvent = new CustomEvent('addPost', {
			detail: {
				post: newPost,
				postId: postData.postId,
				json: postData
			}
		});
		//dispatch the event so quote click handlers, image expand, etc can be added in separate scripts by listening to the event
		setTimeout(() => {
			window.dispatchEvent(newPostEvent);
		}, 50);
	}

	let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/');
	let jsonPath;
	if (isModView) {
		jsonParts.splice(2,1); //remove manage from json url
	}
	jsonPath = jsonParts.join('/');
	const fetchNewPosts = async () => {
		console.log('fetching posts from api');
		updateLive('Fetching posts...', 'yellow');
		let json;
		let newPosts = [];
		try {
			json = await fetch(jsonPath).then(res => res.json());
		} catch (e) {
			console.error(e);
		}
		if (json && json.replies && json.replies.length > 0) {
			newPosts = json.replies.filter(r => r.postId > lastPostId); //filter to only newer posts
			if (newPosts.length > 0) {
				for (let i = 0; i < newPosts.length; i++) {
					newPost(newPosts[i]);
				}
			}
		}
		updateLive('Updated', 'green');
		return newPosts.length;
	}

	let interval = 5000;
	let intervalStart;
	forceUpdate = async () => {
		updateButton.disabled = true;
		clearTimeout(liveTimeout);
		if ((await fetchNewPosts()) > 0) {
			interval = 5000;
		} else {
			interval = Math.min(interval*2, 90000);
		}
		setTimeout(() => {
			updateButton.disabled = false;
		}, 10000);
		if (liveEnabled) {
			intervalStart = Date.now();
			liveTimeout = setTimeout(forceUpdate, interval);
		}
	}

	setInterval(() => {
		if (liveEnabled && intervalStart) {
			const remaining = Math.abs((interval - (Date.now() - intervalStart))/1000);
			updateButton.value = `Update (${remaining.toFixed(0)}s)`;
		}
	}, 1000);

	const enableLive = () => {
		if (supportsWebSockets) {
			updateButton.style.display = 'none';
			const roomParts = window.location.pathname.replace(/\.html$/, '').split('/');
			const room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`;
			socket = io({
				transports: ['websocket'],
				reconnectionAttempts: 3,
				reconnectionDelay: 3000,
				reconnectionDelayMax: 15000,
			});
			socket.on('connect', async () => {
				console.log('socket connected');
				await fetchNewPosts();
				socket.emit('room', room);
			});
			socket.on('message', (message) => {
				console.log(message, room);
				if (message === 'joined') {
					updateLive('Connected for live posts', '#0de600');
				}
			});
			socket.on('pong', (latency) => {
				if (socket.connected) {
					updateLive(`Connected for live posts (${latency}ms)`, '#0de600');
				}
			});
			socket.on('reconnect_attempt', () => {
				updateLive('Attempting to reconnect...', 'yellow');
			});
			socket.on('disconnect', () => {
				console.log('lost connection to room');
				updateLive('Disconnected', 'red');
			});
			socket.on('reconnect', () => {
				console.log('reconnected to room');
				fetchNewPosts();
			});
			socket.on('error', (e) => {
				updateLive('Socket error', 'orange');
				console.error(e);
			});
			socket.on('connect_error', (e) => {
				updateLive('Error connecting', 'orange');
				console.error(e);
			});
			socket.on('reconnect_error', (e) => {
				updateLive('Error reconnecting', 'orange');
				console.error(e);
			});
			socket.on('reconnect_failed', (e) => {
				updateLive('Failed reconnecting', 'orange');
				console.error(e);
				console.log('failed to reconnnect, falling back to polling')
				socket.close();
				supportsWebSockets = false;
				enableLive();
			});
			socket.on('newPost', newPost);
		} else {
			//websocket not supported, update with polling to api
			updateButton.removeAttribute('style');
			forceUpdate();
		}
	};

	const disableLive = () => {
		updateButton.value = 'Update';
		updateButton.removeAttribute('style');
		clearTimeout(liveTimeout);
		if (socket && supportsWebSockets) {
			socket.disconnect();
		}
		updateLive('Live posts off', 'darkgray');
	};

	const liveSetting = document.getElementById('live-setting');
	const toggleLive = () => {
		liveEnabled = !liveEnabled;
		liveEnabled ? enableLive() : disableLive();
		console.log('toggling live posts', liveEnabled);
		setLocalStorage('live', liveEnabled);
	}
	liveSetting.checked = liveEnabled;
	liveSetting.addEventListener('change', toggleLive, false);

	const scrollSetting = document.getElementById('scroll-setting');
	const toggleScroll = () => {
		scrollEnabled = !scrollEnabled;
		console.log('toggling post scrolling', scrollEnabled);
		setLocalStorage('scroll', scrollEnabled);
	}
	scrollSetting.checked = scrollEnabled;
	scrollSetting.addEventListener('change', toggleScroll, false);

	if (isThread) {
		updateButton.addEventListener('click', forceUpdate);
		liveEnabled ? enableLive() : disableLive();
	}

});


code:
const isCatalog = window.location.pathname.endsWith('catalog.html');
const isThread = /\/\w+\/thread\/\d+.html/.test(window.location.pathname);
const isModView = /\/\w+\/manage\/(thread\/)?(index|\d+).html/.test(window.location.pathname);
const isManage = /\/(\w+\/manage|globalmanage)\/(recent|reports|bans|boards|logs|settings|banners|accounts|news).html/.test(window.location.pathname);

function setLocalStorage(key, value) {
	try {
		localStorage.setItem(key, value);
	} catch (e) {
		deleteStartsWith();
	} finally {
		localStorage.setItem(key, value);
	}
}

function appendLocalStorageArray(key, value) {
	const storedArray = JSON.parse(localStorage.getItem(key));
	storedArray.push(value);
	setLocalStorage(key, JSON.stringify(storedArray));
}

function deleteStartsWith(startString='hovercache') {
	//clears cache when localstorage gets full
	const hoverCaches = Object.keys(localStorage).filter(k => k.startsWith(startString));
	for(let i = 0; i < hoverCaches.length; i++) {
		localStorage.removeItem(hoverCaches[i]);
	}
}

function setDefaultLocalStorage(key, value) {
	if (!localStorage.getItem(key)) {
		setLocalStorage(key, value);
	}
}

//todo: just make the localstorage name match the names of settings and put a loop
setDefaultLocalStorage('volume', settings.defaultVolume);
setDefaultLocalStorage('loop', settings.loop);
setDefaultLocalStorage('imageloadingbars', settings.imageLoadingBars);
setDefaultLocalStorage('live', settings.live);
setDefaultLocalStorage('scroll', settings.sctollToPosts);
setDefaultLocalStorage('localtime', settings.localTime);
setDefaultLocalStorage('relative', settings.relativeTime);
setDefaultLocalStorage('24hour', settings.hour24Time);
setDefaultLocalStorage('notifications', settings.notificationsEnabled);
setDefaultLocalStorage('notification-yous-only', settings.notificationsYousOnly);
setDefaultLocalStorage('yous-setting', settings.showYous);

setDefaultLocalStorage('dragtop', null);
setDefaultLocalStorage('dragleft', null);
setDefaultLocalStorage('filters1', '[]');
setDefaultLocalStorage('yous', '[]');
setDefaultLocalStorage('name', '');
setDefaultLocalStorage('theme', 'default');
setDefaultLocalStorage('codetheme', 'default');
setDefaultLocalStorage('customcss', '');


code:
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/';
const generatePassword = () => {
	if (window.crypto) {
		const buf = new Uint8Array(20); //8 keeps charcodes within range
		window.crypto.getRandomValues(buf);
		return btoa(String.fromCharCode.apply(null, buf));
	} else {
		return new Array(20)
			.fill(null)
			.map(x => charset[Math.floor(Math.random()*charset.length)])
			.join('');
	}
}

setDefaultLocalStorage('postpassword', generatePassword());

class syncedField {
	constructor(selector, key, oneWay=false, persistent=true) {
		this.fields = []
		this.selector = selector;
		this.key = key;
		this.oneWay = oneWay;
		this.persistent = persistent;
		this.init();
	}
	init() {
		const settingsFields = document.getElementById('settingsmodal').querySelectorAll(this.selector);
		this.fields = this.fields.concat([...settingsFields]);

		const postForm = document.getElementById('postform');
		if (postForm) {
			const postformFields = postForm.querySelectorAll(this.selector);
			this.fields = this.fields.concat([...postformFields]);
		}

		const actionForm = document.getElementById('actionform');
		if (actionForm) {
			const actionFields = actionForm.querySelectorAll(this.selector);
			this.fields = this.fields.concat([...actionFields]);
		}

		if (this.oneWay) {
			settingsFields[0].addEventListener('input', (e) => { this.update(e) }, false);
		}
		for (let field of this.fields) {
			field.value = localStorage.getItem(this.key);
			!this.oneWay && field.addEventListener('input', (e) => { this.update(e) }, false);
		}
	}
	update(e) {
		if (this.persistent) {
			setLocalStorage(this.key, e.target.value);
		}
		for (let field of this.fields) {
			field.value = e.target.value;
		}
	}
}

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

	new syncedField('input[name="postpassword"]', 'postpassword');
	new syncedField('input[name="name"]', 'name');

});


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 filters(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
    var locals_for_with = (locals || {});

    (function (filterArr) {
      pug_mixins["filters"] = pug_interp = function(filterArr){
var block = (this && this.block), attributes = (this && this.attributes) || {};
const filterTypeMap = { single: 'Single', fid: 'ID', fname: 'Name', ftrip: 'Tripcode', fnamer: 'Name', ftripr: 'Tripcode', fsub: 'Subject', fsubr: 'Subject', fmsg: 'Message', fmsgr: 'Message' }
if (filterArr.length > 0) {
// iterate filterArr
;(function(){
  var $$obj = filterArr;
  if ('number' == typeof $$obj.length) {
      for (var pug_index0 = 0, $$l = $$obj.length; pug_index0 < $$l; pug_index0++) {
        var filter = $$obj[pug_index0];
pug_html = pug_html + "\u003Ctr\u003E\u003Ctd\u003E" + (pug_escape(null == (pug_interp = filterTypeMap[filter.type]) ? "" : pug_interp)) + "\u003C\u002Ftd\u003E\u003Ctd\u003E" + (pug_escape(null == (pug_interp = filter.val.toString()) ? "" : pug_interp)) + "\u003C\u002Ftd\u003E\u003Ctd\u003E\u003Cinput" + (pug_attr("disabled", true, true, false)+" type=\"checkbox\""+pug_attr("checked", filter.type.endsWith('r'), true, false)) + "\u002F\u003E\u003C\u002Ftd\u003E\u003Ctd\u003E\u003Ca" + (" class=\"right close\""+pug_attr("data-type", filter.type, true, false)+pug_attr("data-data", filter.val, true, false)) + "\u003EX\u003C\u002Fa\u003E\u003C\u002Ftd\u003E\u003C\u002Ftr\u003E";
      }
  } else {
    var $$l = 0;
    for (var pug_index0 in $$obj) {
      $$l++;
      var filter = $$obj[pug_index0];
pug_html = pug_html + "\u003Ctr\u003E\u003Ctd\u003E" + (pug_escape(null == (pug_interp = filterTypeMap[filter.type]) ? "" : pug_interp)) + "\u003C\u002Ftd\u003E\u003Ctd\u003E" + (pug_escape(null == (pug_interp = filter.val.toString()) ? "" : pug_interp)) + "\u003C\u002Ftd\u003E\u003Ctd\u003E\u003Cinput" + (pug_attr("disabled", true, true, false)+" type=\"checkbox\""+pug_attr("checked", filter.type.endsWith('r'), true, false)) + "\u002F\u003E\u003C\u002Ftd\u003E\u003Ctd\u003E\u003Ca" + (" class=\"right close\""+pug_attr("data-type", filter.type, true, false)+pug_attr("data-data", filter.val, true, false)) + "\u003EX\u003C\u002Fa\u003E\u003C\u002Ftd\u003E\u003C\u002Ftr\u003E";
    }
  }
}).call(this);

}
else {
pug_html = pug_html + "\u003Ctd colspan=\"4\"\u003ENo Filters\u003C\u002Ftd\u003E";
}
};
pug_mixins["filters"](filterArr);
    }.call(this, "filterArr" in locals_for_with ?
        locals_for_with.filterArr :
        typeof filterArr !== 'undefined' ? filterArr : undefined));
    ;;return pug_html;}


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

	const postForm = document.querySelector('#postform');
	const newPostButton = document.querySelector('a[href="#postform"]');
	const openPostForm = (e) => {
		if (e) {
			e.preventDefault();
		}
		history.replaceState({}, '', '#postform');
		postForm.style.display = 'flex';
		newPostButton.style.visibility = 'hidden';
		postForm.dispatchEvent(new Event('opened'));
	};
	const closePostForm = (e) => {
		e.preventDefault();
		history.replaceState({}, '', location.pathname);
		postForm.style.display = 'none';
		newPostButton.style.visibility = 'visible';
	};
	if (postForm) {
		const closeButton = postForm ? postForm.querySelector('.close') : null;
		newPostButton.addEventListener('click', openPostForm, false);
		closeButton.addEventListener('click', closePostForm, false);
	}

	const messageBox = document.getElementById('message');

	const addToMessageBox = (str) => {
		const index = messageBox.selectionStart;
		messageBox.value = `${messageBox.value.substr(0,index)}${str}${messageBox.value.substr(index)}`;
		messageBox.setSelectionRange(index+str.length, index+str.length); //this scroll anyway, no need to set scrolltop
	}

	const addQuote = function(number) {
		openPostForm();
		let quoteText = `>>${number}\n`;
		let selection;
		if (window.getSelection) {
			selection = window.getSelection().toString();
		} else if (document.getSelection) {
			selection = document.getSelection().toString();
		} else if (document.selection) {
			selection = document.selection.createRange().text;
		}
		if (selection && selection.length > 0) {
			const quotedSelection = selection.split(/\r?\n/) //split by lines
				.map(line => line.trim().length > 0 ? `>${line}` : line) //make non empty lines greentext
				.join('\n'); //join it back together and newline
			quoteText += `${quotedSelection}\n`;
		}
		addToMessageBox(quoteText);
		messageBox.focus();
		messageBox.dispatchEvent(new Event('input'));
	}

	const quote = function(e) {
		const quoteNum = this.textContent.replace('[Reply]', '').split(' ')[0].trim();
		if (isThread && !e.ctrlKey) {
			addQuote(quoteNum);
		} else {
			setLocalStorage('clickedQuote', quoteNum);
		}
	};

	//on loading page open with js method if user has scripts
	if (location.hash === '#postform') {
		openPostForm();
	}
	if (isThread) {
		//add quote to postform if link clicked with quote
		const quoteNum = localStorage.getItem('clickedQuote');
		if (quoteNum != null) {
			addQuote(quoteNum);
			//scroll to the post you quoted
			const quotingPost = document.getElementById(quoteNum);
			if (quotingPost) {
				quotingPost.scrollIntoView();
			}
		}
		localStorage.removeItem('clickedQuote');
	}

	const addQuoteListeners = (l) => {
		for (let i = 0; i < l.length; i++) {
			l[i].addEventListener('click', quote, false);
		}
	};

	const links = document.getElementsByClassName('post-quoters');
	addQuoteListeners(links);

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

});


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

	let settingsModal;
	let settingsBg;

	const hideSettings = () => {
		settingsModal.style.display = 'none';
		settingsBg.style.display = 'none';
	}

	const openSettings = (data) => {
		settingsModal.style.display = 'unset';
		settingsBg.style.display = 'unset';
	}

	const modalHtml = modal({
		modal: {
			title: 'Settings',
			settings: {
				themes,
				codeThemes,
			},
			hidden: true,
		}
	});

	const inserted = document.body.insertAdjacentHTML('afterbegin', modalHtml);
	settingsBg = document.getElementsByClassName('modal-bg')[0];
	settingsModal = document.getElementsByClassName('modal')[0];

	settingsBg.onclick = hideSettings;
	settingsModal.getElementsByClassName('close')[0].onclick = hideSettings;

	const settings = document.getElementById('settings');
	if (settings) { //can be false if we are in minimal view
		settings.onclick = openSettings;
	}

	window.dispatchEvent(new CustomEvent('settingsReady'));

});


code:
let customCSSString = localStorage.getItem('customcss');

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

	//for main theme
	const themePicker = document.getElementById('theme-setting');
	themePicker.value = localStorage.getItem('theme');
	themePicker.addEventListener('change', () => {
		setLocalStorage('theme', themePicker.value);
		changeTheme('theme');
	}, false);

	//for code theme
	const codeThemePicker = document.getElementById('codetheme-setting');
	codeThemePicker.value = localStorage.getItem('codetheme');
	codeThemePicker.addEventListener('change', () => {
		setLocalStorage('codetheme', codeThemePicker.value);
		changeTheme('codetheme');
	}, false);

	//custom CSS for users
    const customCSSSetting = document.getElementById('customcss-setting');
    const editCustomCSS = (change) => {
        customCSSString = customCSSSetting.value;
        console.log('editing custom CSS', customCSSString.length);
        setLocalStorage('customcss', customCSSString); //what if this gets too long?
		changeTheme('customcss');
    }
    customCSSSetting.value = customCSSString;
    customCSSSetting.addEventListener('input', editCustomCSS, false);

});

const customCSSLink = document.createElement('style');
customCSSLink.rel = 'stylesheet';
customCSSLink.id = 'customcss';
document.head.appendChild(customCSSLink);

function changeTheme(type) {
	switch(type) {
		case 'theme':
		case 'codetheme':
			const theme = localStorage.getItem(type);
			let tempLink = document.getElementById(`custom${type}`);
			let defaultLink = document.getElementById(type);
			if (theme === 'default' || theme === defaultLink.dataset.theme) {
				defaultLink.rel = 'stylesheet';
				setTimeout(() => {
					tempLink && tempLink.remove();
				}, 100);
			} else {
				//use path and try to fetch from localstorage
				const path = `/css/${type}s/${theme}.css`;
				let css = localStorage.getItem(path);
				if (!tempLink) {
					tempLink = document.createElement('style');
					document.head.appendChild(tempLink);
				}
				if (css) {
					tempLink.innerHTML = css; //set as inline style temporarily
				}
				defaultLink.rel = ''; //disable default theme
				const themeLink = document.createElement('link');
				themeLink.rel = 'stylesheet';
				themeLink.id = `custom${type}`;
				themeLink.onload = function() {
					css = '';
					const rulesKey = themeLink.sheet.rules != null ? 'rules' : 'cssRules';
					for(let i = 0; i < themeLink.sheet[rulesKey].length; i++) {
						css += themeLink.sheet[rulesKey][i].cssText; //add all the rules to the css
					}
					//update localstorage with latest version
					setLocalStorage(path, css);
					tempLink.innerHTML = css;
					//remove temp inline style since we dont need it anymore
					tempLink.remove();
				}
				themeLink.href = path;
				document.head.appendChild(themeLink);
			}
			break;
		case 'customcss':
			customCSSLink.innerHTML = localStorage.getItem('customcss');
			break;
	}
}
changeTheme('theme');
changeTheme('codetheme');
changeTheme('customcss');


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

	const mainStyleSheet = document.querySelector('link[rel="stylesheet"]').sheet;
	const rulesKey = mainStyleSheet.rules != null ? 'rules' : 'cssRules';
	for (rule of mainStyleSheet[rulesKey]) {
		if(rule.selectorText == '.user-id') {
			rule.style.cursor = 'pointer'; //make ids like a link when hovering
		}
	}

	const statsElem = document.getElementById('threadstats');
	const idElems = [];
	const idMap = new Map();
	const isHighlightedSet = new Set();

	const incrementMap = (id) => {
		idMap.set(id, idMap.get(id)+1 || 1);
	}

	const updateCounts = (updateId) => {
		for (let i = 0; i < idElems.length; i++) {
			const idString = idElems[i].innerText;
			if (updateId && updateId !== idString) { continue; }
			const count = idMap.get(idString);
			idElems[i].setAttribute('data-count', ` (${count})`);
			idElems[i].setAttribute('mobiletitle', `Double tap highlight (${count})`);
			idElems[i].setAttribute('title', `Double click to highlight (${count})`);
		}
	}

	const toggleHighlightPosts = (e) => {
		const id = e.target.innerText;
		const idPosts = document.querySelectorAll(`.post-container[data-user-id="${id}"]`);
		const isHighlighted = isHighlightedSet.has(id);
		isHighlightedSet[isHighlighted ? 'delete' : 'add'](id);
		for (let i = 0; i < idPosts.length; i++) {
			idPosts[i].classList[isHighlighted ? 'remove' : 'add']('highlighted');
		}
	}

	//fetch starting ids
	const startElems = document.getElementsByClassName('user-id');
	for (let i = 0; i < startElems.length; i++) {
		idElems.push(startElems[i]);
		startElems[i].addEventListener('dblclick', toggleHighlightPosts);
	}
	//set counts
	for (let i = 0; i < idElems.length; i++) {
		incrementMap(idElems[i].innerText);
	}

	if (isThread) {
		//set the attr for css hover count
		updateCounts();

		window.addEventListener('addPost', function(e) {
			if (e.detail.hover) {
				return; //dont need to handle hovered posts for this
			}
			const newFiles = e.detail.json.files.length;
			const numPosts = +statsElem.children[0].innerText.match(/^(\d+)/g);
			const numFiles = +statsElem.children[1].innerText.match(/^(\d+)/g);
			const filesTotal = numFiles + newFiles;
			const postTotal = numPosts + 1;
			statsElem.children[0].innerText = `${postTotal} repl${postTotal === 1 ? 'y' : 'ies'}`;
			statsElem.children[1].innerText = `${filesTotal} file${filesTotal === 1 ? '' : 's'}`;
			if (e.detail.json.userId) {
				const userId = e.detail.post.querySelector('.user-id');
				idElems.push(userId);
				userId.addEventListener('dblclick', toggleHighlightPosts);
				incrementMap(e.detail.json.userId);
				updateCounts(e.detail.json.userId);
				if (isHighlightedSet.has(e.detail.json.userId)) {
					e.detail.post.classList.add('highlighted');
				}
				if (!statsElem.children[2]) {
					//UIDs enabled after thread generated
					const spacer = document.createTextNode(' |  ');
					const uidSpan = document.createElement('span');
					statsElem.appendChild(spacer);
					statsElem.appendChild(uidSpan);
				}
				statsElem.children[2].innerText = `${idMap.size} UID${idMap.size === 1 ? '' : 's'}`;
			}
		});
	}

});


code:
let relativeTime = localStorage.getItem('relative') == 'true';
let hour24 = localStorage.getItem('24hour') == 'true';
let localTime = localStorage.getItem('localtime') == 'true';

let dates = [];
const dateElems = document.getElementsByClassName('reltime');
for (let i = 0; i < dateElems.length; i++) {
	dates.push(dateElems[i]); //convert to array
}

const YEAR = 31536000000
	, MONTH = 2592000000
	, WEEK = 604800000
	, DAY = 86400000
	, HOUR = 3600000
	, MINUTE = 60000;

const relativeTimeString = (date) => {
	let difference = Date.now() - new Date(date).getTime();
	let amount = 0;
	let ret = '';
	let isFuture = false;
	if (difference < 0) {
		difference = Math.abs(difference);
		isFuture = true;
	}
	if (difference < MINUTE) {
		return 'Now';
	} else if (difference < MINUTE*59.5) {
		amount = Math.round(difference / MINUTE);
		ret = `${amount} minute`;
	} else if (difference < HOUR*23.5) {
		amount = Math.round(difference / HOUR);
		ret = `${amount} hour`;
	} else if (difference < DAY*6.5) {
		amount = Math.round(difference / DAY);
		ret = `${amount} day`;;
	} else if (difference < WEEK*3.5) {
		amount = Math.round(difference / WEEK);
		ret = `${amount} week`;
	} else if (difference < MONTH*11.5) {
		amount = Math.round(difference / MONTH);
		ret = `${amount} month`;
	} else {
		amount = Math.round(difference / YEAR);
		ret = `${amount} year`;
	}
	return `${ret}${amount > 1 ? 's' : ''} ${isFuture ? 'from now' :  'ago'}`;
}

const changeDateFormat = (date) => {
	const options = {
		hour12: !hour24
	};
	if (!localTime) {
		options.timeZone = SERVER_TIMEZONE;
	}
	const locale = hour24 ? 'en-US-u-hc-h23' : 'en-US';
	const dateString = new Date(date.dateTime).toLocaleString(locale, options);
	if (relativeTime) {
		date.innerText = relativeTimeString(date.dateTime);
		date.title = dateString;
	} else {
		date.innerText = dateString;
		date.removeAttribute('title');
	}
}

const updateDates = () => {
	for (let i = 0; i < dates.length; i++) {
		changeDateFormat(dates[i]);
	}
}

updateDates();

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

	let relativeInterval = relativeTime ? setInterval(updateDates, MINUTE) : void 0;

	const hour24Setting = document.getElementById('24hour-setting');
	const togglehour24 = () => {
		hour24 = !hour24;
		setLocalStorage('24hour', hour24);
		updateDates();
		console.log('toggling 24h time', hour24);
	}
	hour24Setting.checked = hour24;
	hour24Setting.addEventListener('change', togglehour24, false);

	const localTimeSetting = document.getElementById('localtime-setting');
	const toggleLocalTime = () => {
		localTime = !localTime;
		setLocalStorage('localtime', localTime);
		updateDates();
		console.log('toggling local time', localTime);
	}
	localTimeSetting.checked = localTime;
	localTimeSetting.addEventListener('change', toggleLocalTime, false);

	const relativeTimeSetting = document.getElementById('relative-setting');
	const togglerelativeTime = () => {
		relativeTime = !relativeTime;
		setLocalStorage('relative', relativeTime);
		updateDates();
		if (relativeTime) {
			relativeInterval = setInterval(updateDates, MINUTE);
		} else {
			clearInterval(relativeInterval);
		}
		console.log('toggling relative time', relativeTime);
	}
	relativeTimeSetting.checked = relativeTime;
	relativeTimeSetting.addEventListener('change', togglerelativeTime, false);

});

window.addEventListener('addPost', function(e) {

	const newPostDates = e.detail.post.querySelectorAll('.reltime');
	for (let date of newPostDates) {
		if (!e.detail.hover) {
			dates.push(date);
		}
		changeDateFormat(date);
	}

});


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

	let focused = document.hasFocus();
	let unread = [];
	const originalTitle = document.title;

	const changeFavicon = (href) => {
		const currentFav = document.head.querySelector('link[type="image/x-icon"]');
	 	const newFav = document.createElement('link');
		newFav.type = 'image/x-icon';
		newFav.rel = 'shortcut icon';
		newFav.href = href;
		currentFav.remove();
		document.head.appendChild(newFav);
	}

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

	const updateTitle = () => {
		if (unread.length === 0) {
			document.title = originalTitle;
			changeFavicon('/favicon.ico');
		} else {
			document.title = `(${unread.length}) ${originalTitle}`;
			changeFavicon('/file/favicon2.ico');
		}
	}

	const focusChange = () => {
		focused = !focused;
	}

	const updateVisible = () => {
		const unreadBefore = unread.length;
		unread = unread.filter(p => {
			if (isVisible(p)) {
				p.classList.remove('highlighted');
				return false;
			}
			return true;
		});
		if (unreadBefore !== unread.length) {
			updateTitle();
		}
	}

	window.onfocus = focusChange;
	window.onblur = focusChange;
	window.addEventListener('scroll', updateVisible);

	window.addEventListener('addPost', function(e) {
		if (e.detail.hover) {
			return; //dont need to handle hovered posts for this
		}
		const post = e.detail.post;
		//if browsing another tab or the post is out of scroll view
		if (!focused || !isVisible(post)) {
			post.classList.add('highlighted');
			unread.push(post);
			updateTitle();
		}
	});

});