☰

Better bateworld.com

21/10/2025, 20:41:33

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        Better bateworld.com
// @namespace   Circlejerk Scripts
// @version     1.9.1
// @author      Thick Bro
// @match       https://bateworld.com/html5-chat/chatroom.php
// @match       https://bateworld.com//html5-chat/chat2/*
// @grant       GM_addStyle
// @grant       GM.addStyle
// @grant       GM_getValue
// @grant       GM.getValue
// @grant       GM_setValue
// @grant       GM.setValue
// @grant       GM_setValues
// @grant       GM.setValues
// @grant       GM_listValues
// @grant       GM.listValues
// @grant       GM_deleteValue
// @grant       GM.deleteValue
// @require     https://unpkg.com/[email protected]/dist/preact.min.js#sha512-iMvQ2nmrBGovAh+dbYrh8gttIQ4Xa/aYwXhrgYdlhNHDXdyQA4tM0JyI81xJEm+laaTWnIhWGgawYgpEihg7AQ==
// @require     https://unpkg.com/[email protected]/hooks/dist/hooks.umd.js#sha512-lLsbsdkj5qskElcWFvsifiWNJH4y8GeUtEdI3a1KZoVF+TCDvk/UGCyf/2mKpJtwI1XKJh89hVCVI2J3rEytQQ==
// @description 21/10/2025, 20:41:33
// @license GPL-3.0-or-later
// ==/UserScript==

// file://./src/betterbw/features/cooldown.ts
function cooldownIt({ username, minutes = 15, panel }) {
	if (!username) return;
	const expiry = Date.now() + 60000 * minutes;
	GM.setValue(`${username}_cooldown`, expiry);
	if (panel) jsPanel.activePanels.getPanel(panel.id)?.close();
}
async function isOnCooldown(id) {
	const val = await GM.getValue(`${id}_cooldown`);
	if (!val) return null;
	if (Date.now() < Number(val)) return true;
	GM.deleteValue(`${id}_cooldown`);
	return false;
}
async function sweepExpiredCooldowns() {
	const keys = (await GM.listValues()).filter(key => key.endsWith("_cooldown"));
	const now = Date.now();
	for (const key of keys) if (now >= Number(await GM.getValue(key))) await GM.deleteValue(key);
}
const sessionCooldown = new Set();

// file://./src/betterbw/utils/filters.ts
const isDefined = v => null != v;

// file://./src/betterbw/utils/math.ts
const clamp = (min, mid, max) => Math.max(min, Math.min(mid, max));

// file://./src/betterbw/features/settings.ts
const DEFAULTS = {
	watchNotifications: "off",
	defaultVolume: 0.08,
	scrollToVolume: true,
	pinchToZoom: true,
	preferredAlgo: "top",
	layout: "adaptable",
	classicWidth: 365,
	adaptableRows: 3,
	camsMoveAway: true,
};
const getSetting = key => GM_getValue(`settings.${key}`, DEFAULTS[key]);
const setSetting = (key, value) => GM.setValue(`settings.${key}`, value);
const SITE_OPTIONS = [
	{ group: "Sidebar", key: "displayConnectedSince", label: "For how long buddies are online" },
	{ group: "Chat", key: "groupMessages", label: "Group messages in a row from the same buddy" },
	{ group: "Chat", key: "showMessageServer", label: "Enter, leave and kick notices" },
	{ group: "Chat", key: "hideMessageServerAfterNseconds", label: "Hide those notices after", type: "number", unit: "s (0: never)" },
	{ group: "Cams", key: "soundMutedAtStart", label: "Start cams muted" },
];
const FORCED_SITE_OPTIONS = { openMostPopularCamAutomatically: "0" };
const siteOptionKey = key => `settings.site.${key}`;
function applySiteOptions() {
	Object.assign(chatHTML5.config, FORCED_SITE_OPTIONS);
	for (const { key } of SITE_OPTIONS) {
		const value = GM_getValue(siteOptionKey(key), null);
		if (null !== value) chatHTML5.config[key] = value;
	}
}
function setSiteOption(key, value) {
	chatHTML5.config[key] = value;
	GM.setValue(siteOptionKey(key), value);
}

// file://./src/betterbw/utils/organizePanels.ts

const LAYOUT_LABELS = { classic: "Classic grid (3 rows)", adaptable: "Adaptable grid (N rows)" };
const organizePanels_base = { my: "right-top", at: "right-top" };
const panelRatio = 314 / 365;
const gridconf = {
	MARGIN_TOP: 30,
	MARGIN_RIGHT: 5,
	GAP_X: 4,
	GAP_Y: 4,
	WIDTH: +GM_getValue("config.webcamWidth", 365),
	HEIGHT: GM_getValue("config.webcamWidth", 365) * panelRatio,
};
const MAX_ROWS = 8;
function classicLayout() {
	const width = getSetting("classicWidth");
	return { width, height: width * panelRatio, cols: 3, rows: 3 };
}
function adaptableLayout() {
	const areaWidth = innerWidth - (document.getElementById("tabsAndFooter")?.getBoundingClientRect().right ?? 0) - gridconf.MARGIN_RIGHT;
	const areaHeight = innerHeight - gridconf.MARGIN_TOP;
	const rows = clamp(1, getSetting("adaptableRows"), 8);
	const fitHeight = (areaHeight - (rows - 1) * gridconf.GAP_Y) / rows;
	const width = Math.floor(Math.max(120, Math.min(gridconf.WIDTH, areaWidth, fitHeight / panelRatio)));
	const cols = Math.max(1, Math.floor((areaWidth + gridconf.GAP_X) / (width + gridconf.GAP_X)));
	return { width, height: width * panelRatio, cols, rows };
}
const computeLayout = () => ("classic" === getSetting("layout") ? classicLayout() : adaptableLayout());
function camsWidth() {
	return 3 * (("classic" === getSetting("layout") ? getSetting("classicWidth") : gridconf.WIDTH) + gridconf.GAP_X) - gridconf.GAP_X + gridconf.MARGIN_RIGHT;
}
function* gridPositions({ width, height, cols, rows }) {
	const place = (c, r) => ({
		...organizePanels_base,
		offsetX: -(gridconf.MARGIN_RIGHT + c * (width + gridconf.GAP_X)),
		offsetY: gridconf.MARGIN_TOP + r * (height + gridconf.GAP_Y),
	});
	for (let band = 0; band < rows; band += 2) for (let c = 0; c < cols; c++) for (let r = band; r < Math.min(band + 2, rows); r++) yield place(c, r);
	for (let c = cols; ; c++) for (let r = 0; r < rows; r++) yield place(c, r);
}
function placeInSlot(panel, index) {
	const layout = computeLayout();
	const positions = gridPositions(layout);
	for (let i = 0; i < index; i++) positions.next();
	const position = positions.next().value;
	if (position) jsPanel.activePanels.getPanel(panel.id)?.resize({ width: layout.width, height: layout.height }).reposition(position);
}
function organizePanels() {
	document.documentElement.style.setProperty("--bbw-cams-width", `${camsWidth()}px`);
	const opened = queryPanels();
	if (!opened.length) return;
	const layout = computeLayout();
	const baseCells = layout.cols * layout.rows;
	let lastIndex = 0;
	const groups = { prePositioned: [], newlyCreated: [] };
	for (const panel of opened) {
		const gridIndex = panel.dataset.gridIndex;
		if (gridIndex) {
			groups.prePositioned.push(panel);
			const gridIndexNumber = +gridIndex;
			if (gridIndexNumber > lastIndex) lastIndex = gridIndexNumber;
		} else groups.newlyCreated.push(panel);
	}
	const grid = Array(Math.max(+chatHTML5.roles.user.webcamMax, opened.length, lastIndex)).fill(null);
	for (const panel of groups.prePositioned)
		if (null != panel.dataset.gridIndex && "" !== panel.dataset.gridIndex)
			if (grid[+panel.dataset.gridIndex]) {
				panel.dataset.gridIndex = "";
				groups.newlyCreated.unshift(panel);
			} else grid[+panel.dataset.gridIndex] = panel;
	for (let i = grid.length - 1; i >= baseCells; i--) {
		if (!grid[i]) continue;
		const nextAvailableSlot = grid.indexOf(null);
		if (!(nextAvailableSlot < 0) && !(nextAvailableSlot >= i)) {
			grid[i].dataset.gridIndex = nextAvailableSlot.toString();
			grid[nextAvailableSlot] = grid[i];
			grid[i] = null;
		}
	}
	const newlyPlaced = new Set();
	for (const panel of groups.newlyCreated) {
		const nextAvailableSlot = grid.indexOf(null);
		if (nextAvailableSlot < 0) grid.push(panel);
		else grid[nextAvailableSlot] = panel;
		newlyPlaced.add(panel);
	}
	const positions = gridPositions(layout);
	const poppedIn = [];
	grid.forEach((place, index) => {
		const position = positions.next().value;
		if (!place || !position) return;
		const isNew = newlyPlaced.has(place);
		if (isNew) {
			place.style.transition = "none";
			poppedIn.push(place);
		}
		const panel = jsPanel.activePanels.getPanel(place.id);
		if (panel) panel.resize({ width: layout.width, height: layout.height }).reposition(position);
		if (isNew) place.dataset.gridIndex = index.toString();
	});
	if (poppedIn.length) {
		poppedIn[0].offsetHeight;
		requestAnimationFrame(() => {
			for (const panel of poppedIn) panel.style.transition = "";
		});
	}
}

// file://./src/betterbw/utils/scrappers.ts
function getUserId(panel) {
	return panel.id.split("_")[2] || $("[data-id]", panel)[0]?.dataset.id;
}
function scrappers_getUsername(panel) {
	const id = getUserId(panel);
	return id && getUserById(id)?.username?.split("_")?.[0];
}
function getUserObjectById(userId) {
	return chatHTML5.users[userId];
}
function getUserById(userId) {
	const user = getUserObjectById(userId);
	if (!user) return;
	return { obj: user, username: user.username.split("_")[0] };
}

// file://./src/betterbw/utils/sortFunctions.ts
const topRandom = (a, b) => b.bias - a.bias || Math.random() - 0.5;

// file://./src/betterbw/algo.ts
let algo_algo = "";
const getAlgo = () => algo_algo;
const setAlgo = val => {
	document.body.dataset.algo = algo_algo = val;
};

// file://./src/betterbw/utils/openPanel.ts

let openPanel_roomSelected = false;
const setRoomSelected = () => (openPanel_roomSelected = true);
const PANEL_SELECTOR = ".jsPanel.jsPanel-theme-default";
const queryPanels = () => document.querySelectorAll(PANEL_SELECTOR);
const queryPanel = (target = document) => target.querySelector(PANEL_SELECTOR);
function clickWebcamButton(button) {
	const menuWasOpen = $("#userMenu").is(":visible");
	button.trigger("click");
	if (!menuWasOpen) $("#userMenu").hide();
}
function tryToOpenPanel(candidate) {
	clickWebcamButton($(".webcamBtn", candidate.item));
}
const hasPanel = username => Array.from(document.querySelectorAll(PANEL_SELECTOR)).some(panel => scrappers_getUsername(panel) === username);
function verifyOpened(candidate, fallbacks) {
	setTimeout(() => {
		if (hasPanel(candidate.username)) return;
		const full_username = candidate.item.dataset.username;
		if (full_username) {
			sessionCooldown.add(full_username);
			setTimeout(() => sessionCooldown["delete"](full_username), 1800000);
		}
		if (
			"" === getAlgo()
			|| !(() => {
				const max = +chatHTML5.roles.user.webcamMax;
				return document.querySelectorAll(PANEL_SELECTOR).length < max && chatHTML5.getWebcamNumber() < max;
			})()
		)
			return;
		let next = fallbacks.shift();
		while (next && hasPanel(next.username)) next = fallbacks.shift();
		if (!next) return;
		tryToOpenPanel(next);
		verifyOpened(next, fallbacks);
	}, 6000);
}
const getCandidates = async (compareFn = topRandom, _biases = {}) => {
	const biases = { "-": 1, undefined: 2, "+": 3, "++": 4, ..._biases };
	const promises = Array.from(
		document.querySelectorAll('#userList [data-status="online"][data-webcam="true"]:not(:has(:is(.fa.fa-lock, .fa.fa-eye-slash)))').values(),
	).map(async item => {
		const full_username = item.dataset.username;
		const username = full_username?.split("_")[0];
		if (!username) return;
		if (sessionCooldown.has(full_username)) return;
		const status = await GM.getValue(`${username}_status`);
		if ("--" === status) return;
		if (await isOnCooldown(username)) return;
		return { item, username, status, bias: biases[status], onlineSince: (item.dataset.id && getUserById(item.dataset.id)?.obj.date) || 0 };
	});
	return await Promise.all(promises).then(list => list.filter(isDefined).sort(compareFn));
};
function openCandidates(candidates) {
	if (!openPanel_roomSelected) return;
	const opened = document.querySelectorAll(PANEL_SELECTOR);
	let openedLength = opened.length || 0;
	const maxToOpen = +chatHTML5.roles.user.webcamMax;
	if (openedLength >= maxToOpen) return void organizePanels();
	const openedIds = new Set(Array.from(opened).map(panel => scrappers_getUsername(panel)));
	while (openedLength < maxToOpen && candidates.length > 0) {
		const c = candidates.shift();
		if (openedIds.has(c.username)) continue;
		tryToOpenPanel(c);
		verifyOpened(c, candidates);
		openedLength++;
	}
	organizePanels();
}

// file://./src/betterbw/utils/waitToBe.ts
function waitToBe(selector, attributeFilter = ["aria-hidden"], predicate = el => "false" !== el.getAttribute("aria-hidden")) {
	return new Promise(resolve => {
		let attrObserver = null;
		let domObserver = null;
		function cleanup() {
			if (attrObserver) {
				attrObserver.disconnect();
				attrObserver = null;
			}
			if (domObserver) {
				domObserver.disconnect();
				domObserver = null;
			}
		}
		function attachAttrObserver(el) {
			if (!el) return false;
			if (predicate(el)) {
				cleanup();
				resolve(el);
				return true;
			}
			(attrObserver = new MutationObserver(muts => {
				for (const m of muts)
					if ("attributes" === m.type && m.attributeName && attributeFilter.includes(m.attributeName)) {
						if (predicate(el)) {
							cleanup();
							resolve(el);
							return;
						}
					}
			})).observe(el, { attributes: true, attributeFilter });
			return false;
		}
		const existing = document.querySelector(selector);
		if (attachAttrObserver(existing)) return;
		(domObserver = new MutationObserver(muts => {
			for (const m of muts)
				for (const node of m.addedNodes) {
					if (!(node instanceof HTMLElement)) continue;
					const found = node.matches(selector) ? node : node.querySelector(selector);
					if (found) {
						if (attachAttrObserver(found)) {
							if (domObserver) {
								domObserver.disconnect();
								domObserver = null;
							}
							return;
						}
					}
				}
		})).observe(document.body, { childList: true, subtree: true });
	});
}

const { h, Fragment } = window.preact;

// file://./src/betterbw/utils/formatters.ts
const dataUsername = id => `[data-username="${id}"],[data-username^="${id}_"]`;
const HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
const escapeHtml = text => text.replace(/[&<>"']/g, char => HTML_ESCAPES[char]);

// file://./src/betterbw/styles/tiers.css?raw
const tiers_css = `
#userList .userItem:where(.group_minus_minus) {
	opacity: 0.3 !important;
}
#tabs .userLabelBBW:where(.group_minus_minus) {
	text-decoration: line-through solid 1.8rem rgba(205, 0, 0, 0.15);
}
#tabs .userLabelBBW:where(.group_minus) {
	text-decoration: line-through solid 1.8rem rgba(237, 146, 0, 0.2);
}
#userList .userItem:where(.group_minus) .userLabel {
	background: rgba(237, 146, 0, 0.2);
}
#tabs .userLabelBBW:where(.group_plus) {
	text-decoration: line-through solid 1.8rem rgba(0, 100, 255, 0.2);
}
#userList .userItem:where(.group_plus) .userLabel {
	background: rgba(0, 100, 255, 0.2);
}
#tabs .userLabelBBW:where(.group_plus_plus) {
	text-decoration: line-through solid 1.8rem rgba(149, 50, 255, 0.2);
}
#userList .userItem:where(.group_plus_plus) .userLabel {
	background: rgba(149, 50, 255, 0.2);
}
`;

// file://./src/betterbw/utils/debounce.ts
function debounce(func, wait = 200) {
	let timeout;
	return function (...args) {
		const context = this;
		clearTimeout(timeout);
		timeout = setTimeout(() => func.apply(context, args), wait);
	};
}
function enqueueAsync(func) {
	let isRunning = false;
	let runAgain = false;
	return async () => {
		if (isRunning) {
			runAgain = true;
			return;
		}
		isRunning = true;
		try {
			do {
				runAgain = false;
				await func();
			} while (runAgain);
		} finally {
			isRunning = false;
		}
	};
}

// file://./src/betterbw/dynamicStyle.ts

const dataUserItems = group => group.map(dataUsername).join(",");
async function getCSS() {
	const keys = (await GM.listValues()).filter(key => key.endsWith("_status"));
	const valuesEntries = await Promise.all(keys.map(async key => [key, await GM.getValue(key)]));
	const groups = { "--": [], "-": [], "+": [], "++": [] };
	for (const [key, value] of valuesEntries) {
		const id = key.split("_")[0];
		groups[value]?.push(id);
	}
	const selectors = {
		group_minus_minus: dataUserItems(groups["--"]),
		group_minus: dataUserItems(groups["-"]),
		group_plus: dataUserItems(groups["+"]),
		group_plus_plus: dataUserItems(groups["++"]),
	};
	return tiers_css.replace(/\.(group_\w+)/gm, (_match, key, openStyle) => selectors[key] + openStyle);
}
const dynamicStyle = GM_addStyle("");
const refreshDynamicStyle = enqueueAsync(async () => {
	dynamicStyle.innerHTML = await getCSS();
});
refreshDynamicStyle();

// file://./src/betterbw/features/rotateCam.tsx
function getRotation(element) {
	if (!element || !element.dataset.rotation) return 0;
	return parseInt(element.dataset.rotation, 10) || 0;
}
async function rotateCam({ id, panel }) {
	const newRotation = (getRotation(panel) + 90) % 360;
	panel.dataset.rotation = newRotation;
	return GM.setValue(`${id}_rotation`, newRotation);
}

// file://./src/betterbw/features/audioMuted.ts
const sessionAudioMuted = new Map();

// file://./src/betterbw/features/sessionZoom.ts
const sessionZoom = new Map();

// file://./src/betterbw/utils/wheel.ts
const LINE_PX = 100 / 3;
const IS_MAC = /Mac/.test(navigator.userAgent);
function unitPx(mode) {
	return mode === WheelEvent.DOM_DELTA_LINE ? LINE_PX : mode === WheelEvent.DOM_DELTA_PAGE ? 800 : 1;
}
function deltaPx(event) {
	return event.deltaY * unitPx(event.deltaMode);
}
function upwardPx(event) {
	return (event.webkitDirectionInvertedFromDevice ?? IS_MAC) ? deltaPx(event) : -deltaPx(event);
}

// file://./src/betterbw/styles/videoControls.css?raw
const vc_css = `
.bbw-controls {
	position: absolute;
	z-index: 2;
	left: 0;
	right: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	gap: 6px;
	box-sizing: border-box;
	height: 30px;
	padding: 0 20px 0 8px;
	background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
	color: #fff;
	font-size: 12px;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.2s ease-out;
	&.visible {
		opacity: 1;
		pointer-events: auto;
	}
	button {
		all: unset;
		width: 20px;
		line-height: 30px;
		text-align: center;
		cursor: pointer;
		opacity: 0.85;
		&:hover {
			opacity: 1;
		}
		&:disabled {
			opacity: 0.3;
			cursor: default;
		}
		&[hidden] {
			display: none;
		}
	}
	.bbw-floating-controls {
		position: absolute;
		right: 2px;
		bottom: 100%;
		margin-bottom: 2px;
		display: flex;
		align-items: flex-end;
		gap: 4px;
	}
	.bbw-zoom-controls,
	.bbw-rotate {
		display: flex;
		flex-direction: column;
		border-radius: 4px;
		background: rgba(0, 0, 0, 0.5);
		button {
			line-height: 22px;
			font-size: 11px;
		}
	}
	input[type='range'] {
		width: 70px;
		height: 12px;
		margin: 0;
		accent-color: #fff;
		cursor: pointer;
	}
	.bbw-spacer {
		flex: 1;
	}
}
.webcamSwfContainer:fullscreen {
	align-items: center;
	background: #000;
}
`;

// file://./src/betterbw/features/videoControls.ts

let styleAdded = false;
function volumeOnScroll(video) {
	let target = null;
	const step = () => {
		if (null === target) return;
		const current = video.volume ** 0.3333333333333333;
		const next = current + (target - current) * 0.3;
		const arrived = Math.abs(target - next) < 0.001;
		video.volume = (arrived ? target : next) ** 3;
		if (arrived) target = null;
		else requestAnimationFrame(step);
	};
	return event => {
		const upward = upwardPx(event);
		if (video.muted && upward > 0) video.muted = false;
		const idle = null === target;
		target = clamp(0, (target ?? video.volume ** 0.3333333333333333) + upward / 6000, 1);
		if (idle) requestAnimationFrame(step);
	};
}
function makeButton(title, onClick) {
	const button = document.createElement("button");
	button.type = "button";
	button.title = title;
	button.addEventListener("click", event => {
		event.stopPropagation();
		onClick();
	});
	return button;
}
function setIcon(button, name) {
	if (button.dataset.icon === name) return;
	button.dataset.icon = name;
	button.innerHTML = `<i class="fa fa-${name}"></i>`;
}
function autoHide(container, bar, video) {
	let timer;
	let overBar = false;
	const hide = () => {
		if (!overBar && !video.paused) bar.classList.remove("visible");
	};
	const show = () => {
		bar.classList.add("visible");
		clearTimeout(timer);
		timer = setTimeout(hide, 2000);
	};
	container.addEventListener("pointermove", show);
	container.addEventListener("pointerleave", () => {
		clearTimeout(timer);
		overBar = false;
		hide();
	});
	bar.addEventListener("pointerenter", () => {
		overBar = true;
	});
	bar.addEventListener("pointerleave", () => {
		overBar = false;
		show();
	});
	video.addEventListener("play", show);
	video.addEventListener("pause", show);
	if (video.paused) bar.classList.add("visible");
}
function makeZoomButtons(zoom) {
	const zoomIn = makeButton("Zoom in", zoom.zoomIn);
	const zoomOut = makeButton("Zoom out", zoom.zoomOut);
	setIcon(zoomIn, "plus");
	setIcon(zoomOut, "minus");
	const sync = () => {
		zoomIn.disabled = !zoom.canZoomIn();
		zoomOut.disabled = !zoom.isZoomed();
	};
	zoom.onChange(sync);
	sync();
	const buttons = document.createElement("div");
	buttons.className = "bbw-zoom-controls";
	buttons.append(zoomIn, zoomOut);
	return buttons;
}
function makeRotateButton(onRotate) {
	const rotate = makeButton("Rotate", onRotate);
	rotate.textContent = "⟳";
	const group = document.createElement("div");
	group.className = "bbw-rotate";
	group.append(rotate);
	return group;
}
function attachVideoControls(video, zoom, onRotate) {
	if (!styleAdded) {
		GM.addStyle(vc_css);
		styleAdded = true;
	}
	video.controls = false;
	new MutationObserver(() => {
		if (video.controls) video.controls = false;
	}).observe(video, { attributes: true, attributeFilter: ["controls"] });
	const container = video.parentElement;
	if ("static" === getComputedStyle(container).position) container.style.position = "relative";
	const bar = document.createElement("div");
	bar.className = "bbw-controls";
	const play = makeButton("Play / pause", () => (video.paused ? video.play() : video.pause()));
	const mute = makeButton("Mute", () => {
		video.muted = !video.muted;
	});
	const volume = document.createElement("input");
	volume.type = "range";
	volume.min = "0";
	volume.max = "1";
	volume.step = "0.01";
	volume.title = "Volume\nScroll over the controls to adjust it";
	volume.addEventListener("input", () => {
		video.muted = false;
		video.volume = Number(volume.value) ** 3;
	});
	const spacer = document.createElement("span");
	spacer.className = "bbw-spacer";
	const toggleFullscreen = () => (document.fullscreenElement === container ? document.exitFullscreen() : container.requestFullscreen());
	const fullscreen = makeButton("Full screen\nor double-click the cam", toggleFullscreen);
	video.addEventListener("dblclick", toggleFullscreen);
	const floating = document.createElement("div");
	floating.className = "bbw-floating-controls";
	floating.append(makeRotateButton(onRotate), makeZoomButtons(zoom));
	bar.append(play, spacer, mute, volume, fullscreen, floating);
	container.appendChild(bar);
	const sync = () => {
		setIcon(play, video.paused ? "play" : "pause");
		setIcon(mute, video.muted || 0 === video.volume ? "volume-off" : "volume-up");
		setIcon(fullscreen, document.fullscreenElement === container ? "compress" : "expand");
		volume.value = String(video.muted ? 0 : video.volume ** 0.3333333333333333);
	};
	for (const type of ["play", "pause", "volumechange"]) video.addEventListener(type, sync);
	container.addEventListener("fullscreenchange", sync);
	sync();
	autoHide(container, bar, video);
	const onVolumeWheel = volumeOnScroll(video);
	bar.addEventListener(
		"wheel",
		event => {
			event.preventDefault();
			if (getSetting("scrollToVolume")) onVolumeWheel(event);
		},
		{ passive: false },
	);
}

// file://./src/betterbw/features/videoGestures.ts

function isWheelNotch(event) {
	const delta = Math.abs(event.deltaY);
	return event.deltaMode !== WheelEvent.DOM_DELTA_PIXEL || delta % 4.000244140625 === 0 || (Number.isInteger(delta) && delta >= 50);
}
function zoomAndPan(video, initial) {
	let scale = initial?.scale ?? 1;
	let x = initial?.x ?? 0;
	let y = initial?.y ?? 0;
	let drag = null;
	const clip = video.closest(".jsPanel-content") ?? video.parentElement;
	const listeners = new Set();
	const render = () => {
		const zoomed = scale > 1;
		video.style.scale = zoomed ? `${scale}` : "";
		video.style.translate = zoomed ? `${x}px ${y}px` : "";
		video.style.cursor = drag?.moved ? "grabbing" : zoomed ? "grab" : "";
		if (clip) clip.style.overflow = zoomed ? "hidden" : "";
		listeners.forEach(listener => listener());
	};
	const moveTo = (rect, nextX, nextY, nextScale = scale) => {
		const maxX = ((nextScale - 1) * rect.width) / scale / 2;
		const maxY = ((nextScale - 1) * rect.height) / scale / 2;
		x = clamp(-maxX, nextX, maxX);
		y = clamp(-maxY, nextY, maxY);
		scale = nextScale;
		render();
	};
	const zoomAt = (rect, cx, cy, factor) => {
		const next = clamp(1, scale * factor, 4);
		moveTo(rect, cx - (next / scale) * (cx - x), cy - (next / scale) * (cy - y), next);
	};
	video.addEventListener("pointerdown", event => {
		if (1 === scale || 0 !== event.button) return;
		event.preventDefault();
		video.setPointerCapture(event.pointerId);
		drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, fromX: x, fromY: y, moved: false };
	});
	video.addEventListener("pointermove", event => {
		if (drag?.pointerId !== event.pointerId) return;
		const dx = event.clientX - drag.startX;
		const dy = event.clientY - drag.startY;
		if (!drag.moved && Math.hypot(dx, dy) < 3) return;
		drag.moved = true;
		moveTo(video.getBoundingClientRect(), drag.fromX + dx, drag.fromY + dy);
	});
	const endDrag = event => {
		if (drag?.pointerId !== event.pointerId) return;
		drag = null;
		render();
	};
	video.addEventListener("pointerup", endDrag);
	video.addEventListener("pointercancel", endDrag);
	if (scale > 1) render();
	return {
		onPinchWheel: event => {
			const rect = video.getBoundingClientRect();
			const cx = event.clientX - (rect.left + rect.width / 2 - x);
			const cy = event.clientY - (rect.top + rect.height / 2 - y);
			zoomAt(rect, cx, cy, Math.exp(-clamp(-25, deltaPx(event), 25) / 100));
		},
		onPanWheel: event => {
			moveTo(video.getBoundingClientRect(), x - event.deltaX * unitPx(event.deltaMode), y - deltaPx(event));
		},
		zoomIn: () => zoomAt(video.getBoundingClientRect(), 0, 0, 1.25),
		zoomOut: () => zoomAt(video.getBoundingClientRect(), 0, 0, 0.8),
		isZoomed: () => scale > 1,
		canZoomIn: () => scale < 4,
		onChange: listener => listeners.add(listener),
		getState: () => ({ scale, x, y }),
	};
}
function attachVideoGestures(video, onRotate, initial) {
	const zoom = zoomAndPan(video, initial);
	attachVideoControls(video, zoom, onRotate);
	video.addEventListener(
		"wheel",
		event => {
			if (event.ctrlKey && isWheelNotch(event)) return;
			event.preventDefault();
			if (event.ctrlKey) {
				if (getSetting("pinchToZoom")) zoom.onPinchWheel(event);
			} else if (zoom.isZoomed()) zoom.onPanWheel(event);
		},
		{ passive: false },
	);
	return zoom;
}

// file://./src/betterbw/features/panelActions.tsx

const BtnClassify = ({ getUsername, panel, value, title, onClick }) =>
	h(
		"button",
		{
			"data-status-value": value,
			title: title,
			className: "panel-action-btn",
			onClick: e => {
				const username = getUsername();
				if (username) GM.setValue(`${username}_status`, value).then(() => refreshDynamicStyle());
				if (panel) panel.dataset.status = value;
				onClick?.(e);
			},
		},
		value,
	);
const BtnMinus2 = ({ getUsername, panel }) =>
	h(BtnClassify, {
		getUsername: getUsername,
		panel: panel,
		value: "--",
		title: "Nope\nDo not suggest this person.",
		onClick: () => {
			if (panel) jsPanel.activePanels.getPanel(panel.id)?.close();
		},
	});
const BtnMinus1 = props => h(BtnClassify, { value: "-", title: "So so\nIt depends on the day, on the mood...", ...props });
const BtnPlus1 = props => h(BtnClassify, { value: "+", title: "Yeah\nI liked you, buddy", ...props });
const BtnPlus2 = props => h(BtnClassify, { value: "++", title: "Ohhh Yeah!\nI liked you a lot, buddy!", ...props });
const BtnCooldown = ({ panel, getUsername }) =>
	h(
		"button",
		{
			title: "Cooldown 15m\nDo not suggest this person for the next 15 minutes\n\nShift + click: Cooldown 2h",
			className: "panel-action-btn",
			onClick: event => {
				event?.shiftKey ? cooldownIt({ username: getUsername(), panel, minutes: 120 }) : cooldownIt({ username: getUsername(), panel });
			},
		},
		"⏱",
	);
const PanelActions = ({ panel, username }) => {
	const props = { panel, getUsername: () => username };
	return h(
		Fragment,
		null,
		h("div", { class: "panel-action" }, h(BtnMinus2, props), h(BtnMinus1, props), h(BtnPlus1, props), h(BtnPlus2, props)),
		h("div", { class: "panel-cooldown" }, h(BtnCooldown, props)),
	);
};
const MenuActions = () => {
	const props = { getUsername: getLatestUser };
	return h("div", null, h(BtnCooldown, props), h(BtnPlus2, props), h(BtnPlus1, props), h(BtnMinus1, props), h(BtnMinus2, props));
};
function getLatestUser() {
	const selected = getUserById(chatHTML5.myUser.selectedUserid);
	if (selected) return selected.username;
	const muteItem = $('#userMenu [data-action="mute"]')[0];
	return muteItem?.textContent.split(" ").at(-1)?.split("_").at(0);
}
function resizePanel(panelJS) {
	const { width, height } = computeLayout();
	panelJS.resize({ width, height });
}
function monitorVideoReadiness(video, panel, panelJS) {
	let timeoutId;
	if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return;
	const cleanup = () => {
		clearTimeout(timeoutId);
		video.removeEventListener("loadeddata", onVideoReady);
	};
	const onVideoReady = () => {
		cleanup();
	};
	const originalClose = panelJS.close.bind(panelJS);
	panelJS.close = function () {
		cleanup();
		return originalClose();
	};
	timeoutId = setTimeout(() => {
		if (!panel.isConnected) return;
		cooldownIt({ username: scrappers_getUsername(panel), minutes: 5 });
		panelJS.close();
	}, 25000);
	video.addEventListener("loadeddata", onVideoReady);
}
async function attachPanelActions(panel) {
	if ("1" === panel.dataset.actionsAttached) return;
	panel.dataset.actionsAttached = "1";
	const panelJS = jsPanel.activePanels.getPanel(panel.id);
	if (!panelJS) return;
	resizePanel(panelJS);
	const header = $(".jsPanel-hdr .jsPanel-title", panel)[0];
	if (!header) return;
	const username = scrappers_getUsername(panel);
	if (!username) return;
	panel.dataset.username = username;
	panel.dataset.status = await GM.getValue(`${username}_status`);
	const actions = document.createElement("div");
	header.appendChild(actions);
	preact.render(h(PanelActions, { username: username, panel: panel }), actions);
	panel.dataset.rotation = await GM.getValue(`${username}_rotation`, "0");
	$(".jsPanel-btn.jsPanel-btn-close", panel)
		.attr("title", "Close\n\nShift + click: also reduce the # of cams")
		.on("click", event => {
			if (event.shiftKey) chatHTML5.roles.user.webcamMax = chatHTML5.getWebcamNumber();
			cooldownIt({ username: username, minutes: 1 });
		});
	$(header)
		.attr("title", "Middle click or Ctrl + click: close and reduce the # of cams")
		.on("mousedown", event => {
			if (1 === event.button) event.preventDefault();
		})
		.on("auxclick click", event => {
			if ("auxclick" === event.type ? 1 !== event.button : !event.ctrlKey) return;
			event.preventDefault();
			chatHTML5.roles.user.webcamMax = chatHTML5.getWebcamNumber();
			cooldownIt({ username, minutes: 1, panel });
		});
	$(".userAvatar", panel).on("click", event => {
		event.stopPropagation();
		if (!("pageX" in event && "pageY" in event)) return;
		if ($("#userMenu").is(":visible")) return void $("#userMenu").hide();
		$(`#userList .userItem[data-id=${JSON.stringify(panel.id.split("_")[2])}]`).trigger(
			new jQuery.Event("click", { pageX: event.pageX + 3, pageY: event.pageY }),
		);
	});
	const video = panel.querySelector("video");
	if (video) {
		if (sessionAudioMuted.get(username)) video.muted = true;
		video.volume = await GM.getValue(`${username}_volume`, getSetting("defaultVolume"));
		const zoom = attachVideoGestures(video, () => rotateCam({ id: username, panel }), sessionZoom.get(username));
		zoom.onChange(
			debounce(() => {
				sessionZoom.set(username, zoom.getState());
			}),
		);
		persistVolumeChange(video, username);
		monitorVideoReadiness(video, panel, panelJS);
	}
}
function persistVolumeChange(video, username) {
	video.addEventListener(
		"volumechange",
		debounce(async () => {
			sessionAudioMuted.set(username, video.muted);
			await GM.setValue(`${username}_volume`, video.volume);
		}),
	);
}

// file://./src/betterbw/utils/spyOn.ts
function spyOn(obj, watchers) {
	return Proxy.revocable(obj, {
		set(target, prop, value) {
			if (prop in watchers && target[prop] !== value) watchers[prop](value);
			target[prop] = value;
			return true;
		},
	});
}

// file://./src/betterbw/styles/openPanels.css?raw
const openPanels_css = `
[data-username="I_am_watching"] {
	#userList &.userItem .webcamBtn {
		background: rgba(80, 206, 133, 1) !important;
	}
	#tabs &.userItem:before,
	#tabs & .watchCam:before {
		content: '';
		display: block;
		position: absolute;
		width: 12px;
		height: 11px;
		top: 50%;
		transform: translateY(-50%) translateX(-130%);
		border-radius: 2px;
		border: rgba(178, 178, 178, 1) solid 1px;
		border-top-width: 3px;
	}
}
.jsPanel.watchingMe {
	--gridGap: 4px;
	box-shadow: #FFD700 0px 0px 1px var(--gridGap) !important;
	& .jsPanel-headerbar {
		background: gold;
		background: linear-gradient(gold 0%, transparent 30%, transparent 70%, gold 85%);
	}
	& .jsPanel-title::before {
		content: "👁️";
		content: "\\f06e";
		font-family: "Font Awesome 5 Free";
		font-weight: 400;
		color: gold;
		position: absolute;
		z-index: 1;
		left: -2px;
		top: -5px;
		text-shadow: 1px 1px 0 goldenrod;
		font-size: 14px;
	}
}
.watching_private_cam {
	display: block;
	position: absolute;
	color: red;
	content: ' \\f023';
	font-family: 'Font Awesome 5 Free';
	font-weight: 900;
	top: 1px;
	left: 45px;
}
`;

// file://./src/betterbw/dynamicOpenedStyle.ts

const dynamicOpenedStyle = GM_addStyle("");
function updateCssForOpenedPanels() {
	const opened = queryPanels();
	if (!opened?.length) return;
	const usernames = Array.from(opened)
		.map(panel => scrappers_getUsername(panel))
		.filter(isDefined);
	const selectorsIamWatching = usernames.map(dataUsername).join(",");
	const selectorsWatchingPrivateCams = usernames
		.map(
			id =>
				`body:has(#userList .userItem:where(${dataUsername(id)}) .webcamBtn.visible i.lock.fa-lock) .jsPanel[data-username="${id}"] .jsPanel-title>span:before`,
		)
		.join(",");
	dynamicOpenedStyle.innerHTML = openPanels_css.replace(/(\.watching_private_cam|\[data-username="I_am_watching"\])/g, arg => {
		switch (arg) {
			case ".watching_private_cam":
				return selectorsWatchingPrivateCams;
			case '[data-username="I_am_watching"]':
				return selectorsIamWatching;
			default:
				return arg;
		}
	});
}

// file://./src/betterbw/features/tabFocus.ts
const tabFocused = () => "visible" === document.visibilityState;
const whenTabFocused = callback => {
	document.addEventListener(
		"visibilitychange",
		() => {
			"visible" === document.visibilityState && callback();
		},
		{ once: true, passive: true },
	);
};

// file://./src/betterbw/utils/observeIt.ts
function iterate(nodes, selector, fn) {
	let found = false;
	for (const node of nodes) {
		if (!(node instanceof HTMLElement)) continue;
		const panels = node.matches(selector) ? [node] : node.querySelectorAll(selector);
		if (panels.length) {
			if (fn) panels.forEach(fn);
			found = true;
		}
	}
	return found;
}
function observeIt({ target, selector, forEachAddedNode, forEachRemovedNode, cleanup }) {
	const observerPanels = new MutationObserver(mutations => {
		let nodesAdded = false;
		let nodesRemoved = false;
		for (const mutation of mutations) {
			if (iterate(mutation.addedNodes, selector, forEachAddedNode)) nodesAdded = true;
			if (iterate(mutation.removedNodes, selector, forEachRemovedNode)) nodesRemoved = true;
		}
		cleanup?.({ nodesAdded, nodesRemoved });
	});
	observerPanels.observe(target, { childList: true, subtree: false });
	return {
		teardown: () => {
			observerPanels.disconnect();
		},
	};
}

const { useState, useEffect, useRef } = window.preactHooks;

// file://./src/betterbw/features/globalActions.tsx

const ALGO_DESCRIPTIONS = {
	top: { label: "Top", description: "Prioritize users you liked more" },
	new: { label: "New", description: "Prioritize users you haven't liked/disliked before" },
};
async function runAlgo(preferredAlgo) {
	setAlgo(preferredAlgo);
	openCandidates(await getCandidates(topRandom, "new" === preferredAlgo ? { undefined: 9, "+": 8 } : {}));
}
const clickOnCurrentAlgoButton = debounce(() => {
	const algo = getAlgo();
	if ("new" === algo || "top" === algo) runAlgo(algo);
	else organizePanels();
});
const ResetLayoutButton = () => h("button", { title: "Reset the cams' grid layout", onClick: organizePanels }, "▦");
const PlayPauseButton = () => {
	const [playing, setPlaying] = useState(true);
	return h(
		"button",
		{
			title: playing ? "Stop opening cams automatically" : "Start opening cams automatically",
			onClick: async () => {
				if (playing) {
					setAlgo("");
					organizePanels();
					setPlaying(false);
				} else {
					await runAlgo(getSetting("preferredAlgo"));
					setPlaying(true);
				}
			},
		},
		playing ? "⏸" : "▶",
	);
};
const NumberOfCams = () => {
	const [value, setValue] = useState(() => GM_getValue("user.webcamMax", 10));
	useEffect(() => {
		const maxWebcamreached = chatHTML5.maxWebcamreached;
		chatHTML5.maxWebcamreached = function () {
			if (!maxWebcamreached()) return false;
			chatHTML5.roles.user.webcamMax = chatHTML5.getWebcamNumber() + 1;
			return maxWebcamreached();
		};
		const { proxy, revoke } = spyOn(chatHTML5.roles.user, {
			webcamMax: n => {
				setValue(n);
				GM.setValue("user.webcamMax", n);
			},
		});
		chatHTML5.roles.user = proxy;
		return () => {
			chatHTML5.maxWebcamreached = maxWebcamreached;
			revoke();
		};
	}, []);
	useEffect(() => {
		clickOnCurrentAlgoButton();
	}, [value]);
	return h(
		"label",
		null,
		h("input", {
			title: "# of open cams",
			type: "number",
			className: "numberOfCams",
			value: value,
			onChange: e => {
				chatHTML5.roles.user.webcamMax = +e.currentTarget.value;
			},
		}),
	);
};

// file://./src/betterbw/features/watchingMe.ts

function markPanelsWatchingMe() {
	for (const panel of queryPanels()) {
		const id = getUserId(panel);
		panel.classList.toggle("watchingMe", Boolean(id && id in chatHTML5.watchingAtMe));
	}
}
async function notifyWatching(user) {
	const mode = getSetting("watchNotifications");
	if ("off" === mode) return;
	const username = user.username.split("_")[0];
	const status = await GM.getValue(`${username}_status`);
	if ("--" === status) return;
	if ("plusplus" === mode && "++" !== status) return;
	if ("liked" === mode && "+" !== status && "++" !== status) return;
	const label = `<span class="userLabelBBW" data-username="${escapeHtml(username)}">${escapeHtml(username)}</span>`;
	chatHTML5.serverMessageCurrentTab(`👁 ${label} started watching you`, "bbw-watch-notice");
}
function setupWatchingMe() {
	const notified = new Set();
	markPanelsWatchingMe();
	chatHTML5.socket.on("watched", (user, watching) => {
		markPanelsWatchingMe();
		const id = String(user.id);
		if (watching) {
			if (id !== String(chatHTML5.myUser.id) && !notified.has(id)) {
				notified.add(id);
				notifyWatching(user);
			}
		} else notified.delete(id);
	});
}

// file://./src/betterbw/features/camCleanup.ts

const recentlyStopped = new Set();
function markStopped(id) {
	recentlyStopped.add(id);
	setTimeout(() => recentlyStopped.delete(id), 2000);
}
function detach(media) {
	media.pause();
	media.srcObject = null;
}
const cc_hasPanel = id => Array.from(queryPanels()).some(panel => getUserId(panel) === id);
function stopWatching(id, why) {
	if (recentlyStopped.has(id)) return;
	chatHTML5.socket.emit("watch", chatHTML5.myUser.id, id, false);
	try {
		chatHTML5.removeWebcam(id, "bbw-cleanup");
	} catch (error) {}
}
function getMediaUserId(media) {
	const [, prefix, id] = media.id.match(/^(video_|remotevideo)(\d+)$/) ?? [];
	if (!id || "video_" === prefix) return id;
	const user = Object.values(chatHTML5.users).find(user => String(user.streamid ?? user.id) === id);
	return user ? String(user.id) : id;
}
function setupCamCleanup() {
	const { removeWebcam } = chatHTML5;
	chatHTML5.removeWebcam = (id, reason) => {
		markStopped(String(id));
		return removeWebcam(id, reason);
	};
	const play = HTMLMediaElement.prototype.play;
	HTMLMediaElement.prototype.play = function () {
		if (this.isConnected || !this.srcObject) return play.call(this);
		const id = getMediaUserId(this);
		if (id && cc_hasPanel(id)) return play.call(this);
		detach(this);
		if (id) stopWatching(id, "it connected after its panel closed");
		return Promise.resolve();
	};
}
function cleanupClosedCam(panel) {
	panel.querySelectorAll("video, audio").forEach(detach);
	const id = getUserId(panel);
	if (id && !cc_hasPanel(id)) stopWatching(id, "its panel closed");
}

// file://./src/betterbw/styles/camsMoveAway.css?raw
const cma_css = `
.jsPanel {
	translate: var(--bbw-peek-x, 0px) var(--bbw-peek-y, 0px);
}
.jsPanel:is(.ui-draggable-dragging, .bbw-dragging) {
	translate: none;
}
.jsPanel.bbw-peeking {
	z-index: 99 !important;
}
`;

// file://./src/betterbw/features/camsMoveAway.ts

const rectOf = (left, top, right, bottom) => new DOMRect(left, top, right - left, bottom - top);
const contains = (rect, x, y) => x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
const panelRects = () => Array.from(queryPanels(), panel => [panel, panel.getBoundingClientRect()]);
const PEEKING_CLASS = "bbw-peeking";
function startPanel(panel, property, shift) {
	panel.style.setProperty(property, `${shift}px`);
	panel.classList.add(PEEKING_CLASS);
}
function endPeek(peek, instant) {
	if (!peek) return null;
	for (const panel of peek.panels) {
		if (instant) panel.style.transition = "none";
		panel.style.removeProperty(peek.property);
		panel.classList.remove(PEEKING_CLASS);
	}
	if (instant && peek.panels[0]) {
		peek.panels[0].offsetHeight;
		for (const panel of peek.panels) panel.style.transition = "";
	}
	return null;
}
const sides = [
	{
		zone: function tabsZone() {
			const chat = document.getElementById("tabsAndFooter")?.getBoundingClientRect();
			if (!chat?.width) return null;
			const tabs = document.querySelector("#tabs .nav-tabs:not(.nav-tabs-clone)")?.getBoundingClientRect();
			return rectOf(chat.left, 0, chat.right, tabs?.bottom || 65);
		},
		start: function startTabsPeek(zone) {
			const covering = panelRects().filter(([, rect]) => rect.left < zone.right && rect.right > zone.left && rect.top < zone.bottom);
			if (!covering.length) return null;
			const shift = Math.max(...covering.map(([, rect]) => zone.bottom - rect.top)) + 4;
			for (const [panel] of covering) startPanel(panel, "--bbw-peek-y", shift);
			const left = Math.min(zone.left, ...covering.map(([, rect]) => rect.left));
			const right = Math.max(zone.right, ...covering.map(([, rect]) => rect.right));
			return { panels: covering.map(([panel]) => panel), property: "--bbw-peek-y", hold: rectOf(left, zone.top, right, zone.bottom + shift) };
		},
		peek: null,
		timer: null,
	},
	{
		zone: function sidebarZone() {
			const sidebar = document.getElementById("usersContainer")?.getBoundingClientRect();
			return sidebar?.width ? sidebar : null;
		},
		start: function startSidebarPeek(zone) {
			const covering = panelRects().filter(([, rect]) => rect.left < zone.right && rect.right > zone.left && rect.top < zone.bottom && rect.bottom > zone.top);
			if (!covering.length) return null;
			const hugsRight = zone.left > innerWidth - zone.right;
			const needed = Math.max(...covering.map(([, rect]) => (hugsRight ? rect.right - zone.left : zone.right - rect.left)));
			const shift = (hugsRight ? -1 : 1) * (needed + 4);
			for (const [panel] of covering) startPanel(panel, "--bbw-peek-x", shift);
			const top = Math.min(zone.top, ...covering.map(([, rect]) => rect.top));
			const bottom = Math.max(zone.bottom, ...covering.map(([, rect]) => rect.bottom));
			return {
				panels: covering.map(([panel]) => panel),
				property: "--bbw-peek-x",
				hold: rectOf(Math.min(zone.left, zone.left + shift), top, Math.max(zone.right, zone.right + shift), bottom),
			};
		},
		peek: null,
		timer: null,
	},
];
let enabled = true;
let pointerX = -1;
let pointerY = -1;
let onPanel = null;
let cma_frame = 0;
function cancelDwell(side) {
	if (side.timer) clearTimeout(side.timer);
	side.timer = null;
}
function endPeeks(instant = false) {
	for (const side of sides) {
		cancelDwell(side);
		side.peek = endPeek(side.peek, instant);
	}
}
const readyIn = zone => !onPanel && contains(zone, pointerX, pointerY);
function cma_step(side) {
	if (side.peek) {
		let peek;
		if (!(contains((peek = side.peek).hold, pointerX, pointerY) || (!!onPanel && peek.panels.includes(onPanel)))) side.peek = endPeek(side.peek, false);
		return;
	}
	const zone = side.zone();
	if (!zone || !readyIn(zone)) return cancelDwell(side);
	if (side.timer) return;
	side.timer = setTimeout(() => {
		side.timer = null;
		const zone = side.zone();
		if (zone && readyIn(zone)) side.peek = side.start(zone);
	}, 250);
}
function update() {
	cma_frame = 0;
	if (enabled) for (const side of sides) cma_step(side);
}
function onMouseMove(event) {
	if (event.buttons) return;
	pointerX = event.clientX;
	pointerY = event.clientY;
	onPanel = event.target instanceof Element ? event.target.closest(PANEL_SELECTOR) : null;
	if (!cma_frame) cma_frame = requestAnimationFrame(update);
}
function onPointerDown(event) {
	let target;
	for (const side of sides) cancelDwell(side);
	if (
		0 === event.button
		&& event.target instanceof Element
		&& !!(target = event.target).closest(".jsPanel-hdr")
		&& !target.closest("button, input, a, .jsPanel-btn, .userAvatar")
	)
		endPeeks(true);
}
function refreshCamsMoveAway() {
	enabled = getSetting("camsMoveAway");
	if (!enabled) endPeeks();
}
function setupCamsMoveAway() {
	GM.addStyle(cma_css);
	refreshCamsMoveAway();
	document.addEventListener("pointerdown", onPointerDown, true);
	document.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
	document.documentElement.addEventListener("mouseleave", () => endPeeks());
	addEventListener("blur", () => endPeeks());
	addEventListener("resize", () => endPeeks());
}

// file://./src/betterbw/features/observeOpenPanels.ts

let oop_nextUser = null;
const setNextCam = val => (oop_nextUser = val);
let clickRequested = false;
function observePanels() {
	return observeIt({
		target: document.body,
		selector: PANEL_SELECTOR,
		forEachAddedNode: attachPanelActions,
		forEachRemovedNode: cleanupClosedCam,
		cleanup: ({ nodesAdded, nodesRemoved }) => {
			if (!nodesAdded && !nodesRemoved) return;
			endPeeks();
			organizePanels();
			if (nodesRemoved) {
				if (oop_nextUser) {
					clickWebcamButton($(".webcamBtn.visible", oop_nextUser));
					oop_nextUser = null;
				} else if (tabFocused()) clickOnCurrentAlgoButton();
				else if (!clickRequested) {
					clickRequested = true;
					whenTabFocused(() => {
						clickRequested = false;
						clickOnCurrentAlgoButton();
					});
				}
			}
			updateCssForOpenedPanels();
			markPanelsWatchingMe();
		},
	});
}

// file://./src/betterbw/features/discovery.tsx

const SEEN_KEY = "discovery.headerControlsMoved";
function HeaderControlsDiscovery() {
	const [seen, setSeen] = useState(() => GM_getValue(SEEN_KEY, false));
	if (seen) return null;
	const dismiss = () => {
		GM.setValue(SEEN_KEY, true);
		setSeen(true);
	};
	return h("div", { class: "bbw-discovery", onClick: dismiss }, "Better BW's controls moved here", h("button", { onClick: dismiss }, "Got it"));
}

// file://./src/betterbw/features/spyOnConfig.ts

function spyOnConfig(watchers = {}) {
	const { proxy, revoke } = spyOn(chatHTML5.config, watchers);
	chatHTML5.config = proxy;
	return { proxy, revoke, watchers };
}

// file://./src/betterbw/features/watchStats.ts

const STORAGE_KEY = "stats.watchingMe";
const PEAK_KEYS = ["watchers", "onCam", "mutual", "roomShare", "camsShare"];
const emptyPeaks = () => Object.fromEntries(PEAK_KEYS.map(key => [key, { value: 0, at: 0 }]));
const emptyUnsaved = () => ({ watchedMs: 0, watcherMs: 0, sessions: 0, regulars: {}, watchTime: {} });
const emptyEver = () => ({ ...emptyUnsaved(), peaks: emptyPeaks(), since: Date.now() });
let ws_now = { watchers: 0, onCam: 0, mutual: 0, roomShare: 0, camsShare: 0, names: [] };
const ws_session = { watchedMs: 0, watcherMs: 0, peaks: emptyPeaks(), watchers: new Set() };
let ws_ever = emptyEver();
let unsaved = emptyUnsaved();
let ws_dirty = false;
let ws_paused = false;
let lastTickAt = Date.now();
let savedAt = Date.now();
const ws_listeners = new Set();
function subscribeWatchStats(listener) {
	ws_listeners.add(listener);
	return () => {
		ws_listeners.delete(listener);
	};
}
const getWatchStats = () => ({ now: ws_now, session: ws_session, ever: ws_ever, paused: ws_paused });
const isTransmitting = () => true === chatHTML5.myUser.webcam;
function isMainTabActive() {
	const tab = document.querySelector('[data-label="Bateworld"]');
	if (!tab) return false;
	return tab.matches('.active, [aria-selected="true"]') || Boolean(tab.parentElement?.matches("li.active"));
}
function takeSnapshot() {
	const me = String(chatHTML5.myUser.id);
	const userById = id => chatHTML5.users[id] ?? chatHTML5.watchingAtMe[id];
	const watcherIds = Object.keys(chatHTML5.watchingAtMe).filter(id => id !== me);
	const watchers = watcherIds.length;
	const names = [...new Set(watcherIds.map(id => userById(id)?.username?.split("_")[0]).filter(isDefined))];
	const onCam = watcherIds.filter(id => userById(id)?.webcam).length;
	const watching = new Set(Array.from(queryPanels(), getUserId));
	const mutual = watcherIds.filter(id => watching.has(id)).length;
	const othersOnline =
		(Number(document.querySelector("#onlineCounter")?.textContent?.trim()) || 0 || document.querySelectorAll("#userList .userItem").length) - 1;
	const othersOnCam = Object.values(chatHTML5.users).filter(user => user.webcam && String(user.id) !== me).length;
	return { watchers, onCam, mutual, roomShare: othersOnline > 0 ? watchers / othersOnline : 0, camsShare: othersOnCam > 0 ? onCam / othersOnCam : 0, names };
}
function record(at) {
	ws_now = takeSnapshot();
	if (ws_now.watchers > 0 && 0 === ws_session.peaks.watchers.value) {
		ws_ever.sessions++;
		unsaved.sessions++;
	}
	for (const key of PEAK_KEYS) {
		const value = ws_now[key];
		if (value > ws_session.peaks[key].value) ws_session.peaks[key] = { value, at };
		if (value > ws_ever.peaks[key].value) {
			ws_ever.peaks[key] = { value, at };
			ws_dirty = true;
		}
	}
	for (const name of ws_now.names)
		if (!ws_session.watchers.has(name)) {
			ws_session.watchers.add(name);
			ws_ever.regulars[name] = (ws_ever.regulars[name] ?? 0) + 1;
			unsaved.regulars[name] = (unsaved.regulars[name] ?? 0) + 1;
		}
}
function tick() {
	const at = Date.now();
	if (!ws_paused && ws_now.watchers > 0) {
		for (const totals of [ws_session, ws_ever, unsaved]) {
			totals.watchedMs += at - lastTickAt;
			totals.watcherMs += ws_now.watchers * (at - lastTickAt);
		}
		for (const name of ws_now.names)
			for (const watchTime of [ws_ever.watchTime, unsaved.watchTime]) watchTime[name] = (watchTime[name] ?? 0) + (at - lastTickAt);
		ws_dirty = true;
	}
	lastTickAt = at;
	ws_paused = !isMainTabActive();
	if (!ws_paused) record(at);
	if (ws_dirty && at - savedAt > 15000) save();
	ws_listeners.forEach(listener => listener());
}
async function load() {
	const stored = await GM.getValue(STORAGE_KEY);
	const empty = emptyEver();
	return { ...empty, ...stored, peaks: { ...empty.peaks, ...stored?.peaks } };
}
function addCounts(base, pending) {
	const sum = { ...base };
	for (const [name, count] of Object.entries(pending)) sum[name] = (sum[name] ?? 0) + count;
	return sum;
}
function merge(base, pending, peaks) {
	return {
		since: base.since,
		peaks: Object.fromEntries(PEAK_KEYS.map(key => [key, peaks[key].value > base.peaks[key].value ? peaks[key] : base.peaks[key]])),
		watchedMs: base.watchedMs + pending.watchedMs,
		watcherMs: base.watcherMs + pending.watcherMs,
		sessions: base.sessions + pending.sessions,
		regulars: addCounts(base.regulars, pending.regulars),
		watchTime: addCounts(base.watchTime, pending.watchTime),
	};
}
async function save() {
	ws_dirty = false;
	savedAt = Date.now();
	const pending = unsaved;
	unsaved = emptyUnsaved();
	const saved = merge(await load(), pending, ws_ever.peaks);
	await GM.setValue(STORAGE_KEY, saved);
	ws_ever = merge(saved, unsaved, ws_ever.peaks);
}
async function trackWatchStats() {
	ws_ever = await load();
	lastTickAt = Date.now();
	tick();
	setInterval(tick, 2000);
	chatHTML5.socket.on("watched", () => tick());
	window.addEventListener("pagehide", () => {
		if (ws_dirty) save();
	});
}

// file://./src/betterbw/features/fastUserList.ts
let batching = false;
let listedRoomId = null;
const isSelected = id => document.getElementById(id)?.classList.contains("selected") ?? false;
function sortUserList() {
	if (isSelected("sortWatchersBtn")) return chatHTML5.sortWatchersNumber();
	const byRole = isSelected("sortRoleBtn");
	const byWebcam = isSelected("sortWebcamtBtn");
	const byName = isSelected("sortBtn");
	const list = document.getElementById("userList");
	if (!list || (!byRole && !byWebcam && !byName)) return;
	const flag = (item, key) => Number("true" === item.dataset[key]);
	const items = Array.from(list.children);
	items.sort(
		(a, b) =>
			flag(b, "showtop") - flag(a, "showtop")
			|| (byRole ? Number(b.dataset.power) - Number(a.dataset.power) : 0)
			|| (byWebcam ? flag(b, "webcam") - flag(a, "webcam") : 0)
			|| (byName ? (a.dataset.username ?? "").localeCompare(b.dataset.username ?? "", void 0, { sensitivity: "base" }) : 0),
	);
	list.append(...items);
}
function batchUserListRebuilds() {
	const { getUserPositionInList, searchUsers } = chatHTML5;
	chatHTML5.getUserPositionInList = user => {
		if (!batching) return getUserPositionInList(user);
		return chatHTML5.showOnTopofUserList(user) ? 0 : -1;
	};
	chatHTML5.searchUsers = () => {
		if (!batching) searchUsers();
	};
	document.addEventListener("roomChanged", () => {
		batching = true;
		setTimeout(() => {
			batching = false;
			sortUserList();
			searchUsers();
			chatHTML5.updateNumberUsersDisplay();
		});
	});
}
function skipRedundantUserListRefresh() {
	const { socket } = chatHTML5;
	const tab = chatHTML5.getCurrentTab();
	if (tab.room) listedRoomId = String(tab.roomid);
	socket.on("getUsers", (_users, roomid) => {
		listedRoomId = String(roomid);
	});
	$(document).on("tabChanged", (_event, changedTab) => {
		if (changedTab?.room === false && "1" === chatHTML5.config.multiRoomEnter) listedRoomId = null;
	});
	const emit = socket.emit;
	socket.emit = function (event, ...args) {
		if ("getUsers" === event && String(args[0]) === listedRoomId) return this;
		return emit.call(this, event, ...args);
	};
}

// file://./src/betterbw/features/watchStatsDisplay.tsx

const wsd_int = n => Math.round(n).toLocaleString();
const pct = n => `${Math.round(100 * n)}%`;
const average = ({ watchedMs, watcherMs }) => (watchedMs ? (watcherMs / watchedMs).toFixed(1) : "–");
const wsd_date = at => new Date(at).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
function duration(ms) {
	const minutes = Math.floor(ms / 60000);
	if (minutes < 1) return `${Math.floor(ms / 1000)}s`;
	if (minutes < 60) return `${minutes}m`;
	return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
}
const PEAK_ROWS = [
	["watchers", "Watching you", wsd_int],
	["onCam", "…with their cam on", wsd_int],
	["mutual", "…whose cam you watch", wsd_int],
	["roomShare", "Share of the room", pct],
	["camsShare", "Share of the cams", pct],
];
function StatsCard({ anchor }) {
	const { now, session, ever, paused } = getWatchStats();
	const below = anchor.bottom + 340 < innerHeight;
	const style = {
		width: "330px",
		left: `max(8px, min(${anchor.left}px, calc(100vw - 338px)))`,
		top: `${below ? anchor.bottom + 6 : anchor.top - 6}px`,
		transform: below ? "" : "translateY(-100%)",
	};
	const regulars = Object.entries(ever.watchTime)
		.filter(([, ms]) => ms >= 60000)
		.sort(([, a], [, b]) => b - a)
		.slice(0, 5);
	return h(
		"div",
		{ class: "bbw-watch-card", style: style },
		h(
			"table",
			null,
			h("thead", null, h("tr", null, h("th", null), h("th", null, "Now"), h("th", null, "Session"), h("th", null, "Ever"))),
			h(
				"tbody",
				null,
				PEAK_ROWS.map(([key, label, format]) =>
					h(
						"tr",
						{ key: key },
						h("th", null, label),
						h("td", null, paused ? "–" : format(now[key])),
						h("td", null, format(session.peaks[key].value)),
						h("td", null, format(ever.peaks[key].value)),
					),
				),
				h(
					"tr",
					{ class: "bbw-separator" },
					h("th", null, "Unique watchers"),
					h("td", null),
					h("td", null, wsd_int(session.watchers.size)),
					h("td", null, wsd_int(Object.keys(ever.regulars).length)),
				),
				h("tr", null, h("th", null, "Watched for"), h("td", null), h("td", null, duration(session.watchedMs)), h("td", null, duration(ever.watchedMs))),
				h("tr", null, h("th", null, "Average watchers"), h("td", null), h("td", null, average(session)), h("td", null, average(ever))),
			),
		),
		paused && h("p", null, h("b", null, "Paused:"), " only counted while the Bateworld tab is open"),
		h("p", { class: "bbw-muted" }, "Top rows: the most at once. Averages count only the time someone was watching."),
		ever.peaks.watchers.at > 0
			&& h(
				"p",
				null,
				"Record: ",
				h("b", null, wsd_int(ever.peaks.watchers.value)),
				" watchers, on ",
				wsd_date(ever.peaks.watchers.at),
			),
		regulars.length > 0
			&& h("p", null, "Regulars: ", regulars.map(([name, ms]) => `${name} (${duration(ms)}, ${wsd_int(ever.regulars[name] ?? 1)}×)`).join(", ")),
		h("p", { class: "bbw-muted" }, "Watched in ", wsd_int(ever.sessions), " sessions since ", wsd_date(ever.since)),
	);
}
function WatchStats() {
	const [, setVersion] = useState(0);
	useEffect(() => subscribeWatchStats(() => setVersion(v => v + 1)), []);
	const [anchor, setAnchor] = useState(null);
	const { session, ever } = getWatchStats();
	const showSession = isTransmitting() || session.peaks.watchers.value > 0;
	return h(
		"span",
		{ class: "bbw-watch-stats", onMouseEnter: e => setAnchor(e.currentTarget.getBoundingClientRect()), onMouseLeave: () => setAnchor(null) },
		h(
			"span",
			{ class: "bbw-watch-summary" },
			"max ",
			wsd_int(ever.peaks.watchers.value),
			showSession && ` · session ${wsd_int(session.peaks.watchers.value)}`,
		),
		anchor && h(StatsCard, { anchor: anchor }),
	);
}

// file://./src/betterbw/features/settingsMenu.tsx

const NOTIFICATIONS = [
	["off", "Off"],
	["plusplus", "Only ++"],
	["liked", "Buddies you liked (+, ++)"],
	["all", "Everyone (except --)"],
];
const GROUPS = [...new Set(SITE_OPTIONS.map(option => option.group))];
const START_MUTED_OPTION = SITE_OPTIONS.find(option => "soundMutedAtStart" === option.key);
function SiteOptionInput({ option, onChange }) {
	const value = String(chatHTML5.config[option.key] ?? "");
	if ("number" === option.type)
		return h(
			"label",
			null,
			option.label,
			h("input", {
				type: "number",
				min: "0",
				value: value || "0",
				onChange: e => {
					setSiteOption(option.key, String(Math.max(0, parseInt(e.currentTarget.value) || 0)));
					onChange();
				},
			}),
			option.unit,
		);
	return h(
		"label",
		null,
		h("input", {
			type: "checkbox",
			checked: "1" === value,
			onChange: e => {
				setSiteOption(option.key, e.currentTarget.checked ? "1" : "0");
				onChange();
			},
		}),
		option.label,
	);
}
function VolumePercentInput() {
	const [percent, setPercent] = useState(() => Math.round(100 * getSetting("defaultVolume")));
	return h(
		"label",
		null,
		"Default volume for new cams",
		h("input", {
			type: "number",
			min: "0",
			max: "100",
			value: percent,
			onChange: e => {
				const value = Math.min(100, Math.max(0, parseInt(e.currentTarget.value) || 0));
				setPercent(value);
				setSetting("defaultVolume", value / 100);
			},
		}),
		"%",
	);
}
function ToggleSetting({ setting, label, explain, onChange }) {
	const [checked, setChecked] = useState(() => getSetting(setting));
	return h(
		Fragment,
		null,
		h(
			"label",
			null,
			h("input", {
				type: "checkbox",
				checked: checked,
				onChange: async e => {
					const value = e.currentTarget.checked;
					setChecked(value);
					await setSetting(setting, value);
					onChange?.();
				},
			}),
			label,
		),
		explain && h("span", { class: "bbw-explain" }, explain),
	);
}
function MemoInput({ storageKey, prefix, suffix, defaultValue, onChangeEffect, ...props }) {
	const [value, setValue] = useState(() => GM_getValue(storageKey, defaultValue));
	return h(
		"label",
		null,
		prefix,
		h("input", {
			...props,
			value: value,
			onChange: e => {
				const value = e.currentTarget.value;
				setValue(value);
				GM.setValue(storageKey, value);
				onChangeEffect(value);
			},
		}),
		suffix,
	);
}
function PanelSizeInput() {
	return h(
		"label",
		{ class: "bbw-stacked" },
		"Max size of the panel",
		h(MemoInput, {
			storageKey: "config.webcamWidth",
			type: "number",
			className: "numberOfCams",
			suffix: "px",
			min: "150",
			step: "5",
			defaultValue: gridconf.WIDTH,
			onChangeEffect: value => {
				gridconf.WIDTH = +value;
				gridconf.HEIGHT = value * panelRatio;
				clickOnCurrentAlgoButton();
			},
		}),
		h("span", { class: "bbw-explain" }, "Shrinks automatically when the cams don't fit the screen"),
	);
}
function AlgoSelect() {
	const [value, setValue] = useState(() => getSetting("preferredAlgo"));
	return h(
		"label",
		{ class: "bbw-stacked" },
		"Which cams to prioritize",
		h(
			"select",
			{
				value: value,
				onChange: e => {
					const next = e.currentTarget.value;
					setValue(next);
					setSetting("preferredAlgo", next);
					if ("" !== getAlgo()) runAlgo(next);
				},
			},
			Object.keys(ALGO_DESCRIPTIONS).map(key => h("option", { key: key, value: key }, ALGO_DESCRIPTIONS[key].label)),
		),
		h("span", { class: "bbw-explain" }, ALGO_DESCRIPTIONS[value].description),
	);
}
function ClassicWidthInput() {
	const [width, setWidth] = useState(() => getSetting("classicWidth"));
	return h(
		"label",
		{ class: "bbw-stacked" },
		"Exact size of the panel",
		h(
			"span",
			null,
			h("input", {
				type: "number",
				min: "150",
				step: "5",
				value: width,
				onChange: async e => {
					const value = Math.max(150, parseInt(e.currentTarget.value) || 0);
					setWidth(value);
					await setSetting("classicWidth", value);
					organizePanels();
				},
			}),
			" ",
			"px",
		),
		h("span", { class: "bbw-explain" }, "Every cam is exactly this wide, whatever the size of the window"),
	);
}
function RowsSelect() {
	const [rows, setRows] = useState(() => getSetting("adaptableRows"));
	return h(
		"label",
		{ class: "bbw-stacked" },
		"Rows",
		h(
			"select",
			{
				value: rows,
				onChange: async e => {
					const value = +e.currentTarget.value;
					setRows(value);
					await setSetting("adaptableRows", value);
					organizePanels();
				},
			},
			Array.from({ length: MAX_ROWS }, (_, i) => i + 1).map(n => h("option", { key: n, value: n }, n)),
		),
		h("span", { class: "bbw-explain" }, "The cams grow or shrink so this many rows fill the window's height"),
	);
}
function LayoutOptions({ layout }) {
	if ("classic" === layout) return h(ClassicWidthInput, null);
	return h(Fragment, null, h(RowsSelect, null), h(PanelSizeInput, null));
}
function LayoutSettings() {
	const [applied, setApplied] = useState(() => getSetting("layout"));
	const [selected, setSelected] = useState(applied);
	const dirty = selected !== applied;
	return h(
		"div",
		null,
		h(
			"div",
			{ class: `bbw-stacked bbw-layout-picker${dirty ? " bbw-dirty" : ""}` },
			h(
				"span",
				null,
				"Layout",
				dirty
					&& h(
						Fragment,
						null,
						" (current: ",
						h(
							"a",
							{
								href: "#",
								title: "Go back to the current layout",
								onClick: e => {
									e.preventDefault();
									setSelected(applied);
								},
							},
							LAYOUT_LABELS[applied],
						),
						")",
					),
			),
			h(
				"div",
				{ class: "bbw-layout-row" },
				h(
					"select",
					{ "aria-label": "Layout", value: selected, onChange: e => setSelected(e.currentTarget.value) },
					Object.keys(LAYOUT_LABELS).map(name => h("option", { key: name, value: name }, LAYOUT_LABELS[name])),
				),
				dirty
					&& h(
						"button",
						{
							type: "button",
							class: "bbw-apply-layout",
							onClick: async () => {
								await setSetting("layout", selected);
								setApplied(selected);
								organizePanels();
							},
						},
						"Apply layout",
					),
			),
			dirty
				&& h(
					"span",
					{ class: "bbw-explain" },
					"Switching layouts rearranges every cam, so it waits for this button. Everything else here saves and applies on its own.",
				),
		),
		h(ToggleSetting, {
			setting: "camsMoveAway",
			label: "Cams move away",
			explain: "Rest the pointer on the chat's tabs, or on the buddy list, and the cams in front of them slide out of the way",
			onChange: refreshCamsMoveAway,
		}),
		h(
			"fieldset",
			null,
			h("legend", null, LAYOUT_LABELS[selected]),
			h(LayoutOptions, { key: selected, layout: selected }),
			dirty && h("p", { class: "bbw-muted" }, "Saved already; used once you apply this layout."),
		),
	);
}
function SettingsPanel({ anchor }) {
	const [notifications, setNotifications] = useState(() => getSetting("watchNotifications"));
	const [, setVersion] = useState(0);
	const rerender = () => setVersion(v => v + 1);
	const style = {
		width: "640px",
		maxWidth: "calc(100vw - 16px)",
		maxHeight: `calc(100vh - ${anchor.bottom + 12}px)`,
		overflowY: "auto",
		top: `${anchor.bottom + 4}px`,
		left: `max(8px, min(${anchor.left}px, calc(100vw - 648px)))`,
	};
	return h(
		"div",
		{ class: "bbw-settings-panel", style: style },
		h("h4", null, "Better BW"),
		h(
			"div",
			{ class: "bbw-settings-columns" },
			h(
				"div",
				null,
				h(
					"label",
					{ class: "bbw-stacked" },
					"Tell me when someone starts watching me",
					h(
						"select",
						{
							value: notifications,
							onChange: e => {
								const value = e.currentTarget.value;
								setNotifications(value);
								setSetting("watchNotifications", value);
							},
						},
						NOTIFICATIONS.map(([value, label]) => h("option", { key: value, value: value }, label)),
					),
				),
				GROUPS.map(group =>
					h(
						"fieldset",
						{ key: group },
						h("legend", null, group),
						SITE_OPTIONS.filter(option => option.group === group && option !== START_MUTED_OPTION).map(option =>
							h(SiteOptionInput, { key: option.key, option: option, onChange: rerender }),
						),
						"Cams" === group
							&& h(
								Fragment,
								null,
								h(AlgoSelect, null),
								h(VolumePercentInput, null),
								h(SiteOptionInput, { option: START_MUTED_OPTION, onChange: rerender }),
								h(ToggleSetting, { setting: "scrollToVolume", label: "Scroll wheel changes the volume" }),
								h(ToggleSetting, { setting: "pinchToZoom", label: "Pinch (or Ctrl + scroll) zooms into the cam" }),
							),
					),
				),
				h("p", { class: "bbw-muted" }, "The chat's options apply to new messages, buddies and cams."),
			),
			h(LayoutSettings, null),
		),
	);
}
function SettingsMenu() {
	const [anchor, setAnchor] = useState(null);
	const [pinned, setPinned] = useState(false);
	const root = useRef(null);
	const closeTimer = useRef();
	const open = () => {
		clearTimeout(closeTimer.current);
		if (root.current) setAnchor(root.current.getBoundingClientRect());
	};
	const close = () => {
		clearTimeout(closeTimer.current);
		setAnchor(null);
		setPinned(false);
	};
	useEffect(() => {
		if (!anchor) return;
		const closeOnEscape = event => {
			if ("Escape" === event.key) close();
		};
		document.addEventListener("keydown", closeOnEscape);
		return () => document.removeEventListener("keydown", closeOnEscape);
	}, [anchor]);
	useEffect(() => {
		if (!pinned) return;
		const closeOnOutsideClick = event => {
			if (!root.current?.contains(event.target)) close();
		};
		document.addEventListener("mousedown", closeOnOutsideClick);
		return () => document.removeEventListener("mousedown", closeOnOutsideClick);
	}, [pinned]);
	return h(
		"span",
		{
			class: "bbw-settings",
			ref: root,
			onMouseEnter: open,
			onMouseLeave: () => {
				if (pinned) return;
				clearTimeout(closeTimer.current);
				closeTimer.current = setTimeout(() => setAnchor(null), 200);
			},
		},
		h(
			"button",
			{
				title: "Better BW settings",
				onClick: () => {
					if (pinned) close();
					else {
						open();
						setPinned(true);
					}
				},
			},
			"⚙",
		),
		anchor && h(SettingsPanel, { anchor: anchor }),
	);
}

// file://./src/betterbw/styles/watchStats.css?raw
const ws_css = `
#bbw_watch_stats {
	display: block;
	flex-basis: 100%;
}
.users-info {
	padding-block: 0 !important;
}
.watchingMeContainer:has(#bbw_watch_stats) {
	flex-wrap: wrap;
	line-height: 1;
	label[for="whoWatchesMeCheckbox"] {
		margin-bottom: 0;
	}
}
.bbw-watch-stats {
	white-space: nowrap;
	cursor: help;
}
.bbw-watch-summary {
	font-size: 0.85em;
	opacity: 0.7;
}
#tabs .bbw-watch-notice {
	color: goldenrod;
}
.bbw-watch-card {
	position: fixed;
	z-index: 100000;
	box-sizing: border-box;
	padding: 8px 10px;
	border-radius: 6px;
	background: rgba(20, 20, 20, 0.95);
	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
	color: #eee;
	font-size: 12px;
	line-height: 1.4;
	white-space: normal;
	text-align: left;
	pointer-events: none;
	table {
		width: 100%;
		border-collapse: collapse;
	}
	th,
	td {
		padding: 1px 0;
		border: none;
		font-weight: normal;
	}
	tbody th {
		opacity: 0.8;
	}
	thead th,
	td {
		padding-left: 10px;
		text-align: right;
		font-variant-numeric: tabular-nums;
	}
	thead th {
		font-weight: 600;
	}
	.bbw-separator > * {
		padding-top: 4px;
		border-top: 1px solid rgba(255, 255, 255, 0.15);
	}
	p {
		margin: 6px 0 0;
	}
	.bbw-muted {
		opacity: 0.6;
		font-size: 11px;
	}
}
`;

// file://./src/betterbw/styles/settings.css?raw
const settings_css = `
.bbw-settings-panel {
	position: fixed;
	z-index: 100000;
	box-sizing: border-box;
	padding: 10px 12px;
	border-radius: 6px;
	background: rgba(20, 20, 20, 0.96);
	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
	color: #eee;
	font-size: 12px;
	line-height: 1.4;
	white-space: normal;
	text-align: left;
	h4 {
		margin: 0 0 8px;
		font-size: 13px;
	}
	fieldset {
		margin: 8px 0 0;
		padding: 0;
		border: none;
	}
	legend {
		width: auto;
		margin: 0 0 2px;
		padding: 0;
		border: none;
		color: inherit;
		font-size: 11px;
		opacity: 0.6;
		text-transform: uppercase;
	}
	label {
		display: flex;
		align-items: center;
		gap: 6px;
		margin: 2px 0;
		color: inherit;
		font-weight: normal;
	}
	.bbw-stacked {
		flex-direction: column;
		align-items: stretch;
	}
	select,
	input[type='number'] {
		color: #111;
	}
	input[type='number'] {
		width: 4em;
	}
	.bbw-muted {
		margin: 8px 0 0;
		font-size: 11px;
		opacity: 0.6;
	}
	.bbw-explain {
		display: block;
		margin-top: 2px;
		font-size: 11px;
		opacity: 0.85;
	}
	.bbw-settings-columns {
		display: grid;
		grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
		gap: 0 20px;
	}
	.bbw-layout-picker {
		display: flex;
		a {
			color: #7fc8ff;
			text-decoration: underline;
		}
	}
	.bbw-layout-row {
		display: flex;
		align-items: center;
		gap: 6px;
		border-radius: 4px;
		select {
			flex: 1;
			min-width: 0;
		}
	}
	.bbw-dirty .bbw-layout-row {
		padding: 3px;
		outline: 1px solid #50c180;
	}
	.bbw-apply-layout {
		padding: 2px 10px;
		border: none;
		border-radius: 4px;
		background: #50c180;
		color: #111;
		font-weight: 600;
		white-space: nowrap;
		cursor: pointer;
	}
}
.bbw-discovery {
	position: absolute;
	z-index: 100000;
	top: 100%;
	left: 0;
	margin-top: 8px;
	padding: 8px 10px;
	border-radius: 6px;
	background: rgba(20, 20, 20, 0.96);
	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
	color: #eee;
	font-size: 12px;
	line-height: 1.4;
	white-space: nowrap;
	cursor: pointer;
	&:before {
		content: '';
		position: absolute;
		bottom: 100%;
		left: 16px;
		border: 6px solid transparent;
		border-bottom-color: rgba(20, 20, 20, 0.96);
	}
	button {
		margin-left: 8px;
		padding: 2px 8px;
		border-radius: 4px;
		border: none;
		background: #50c180;
		color: #111;
		cursor: pointer;
	}
}
`;

// file://./src/betterbw/setupTools.tsx

async function setupHeader() {
	const avatar = document.getElementById("myAvatar");
	if (!avatar) return;
	const controls = document.createElement("div");
	controls.id = "bbw_header_controls";
	avatar.after(controls);
	GM.addStyle(settings_css);
	preact.render(
		h(Fragment, null, h(SettingsMenu, null), h(ResetLayoutButton, null), h(NumberOfCams, null), h(PlayPauseButton, null), h(HeaderControlsDiscovery, null)),
		controls,
	);
}
async function setupUserMenu() {
	const userMenu = $("#userMenu")[0];
	if (!userMenu) return;
	const controls = document.createElement("div");
	controls.id = "bbw_menu_controls";
	userMenu.appendChild(controls);
	preact.render(h(MenuActions, null), controls);
	chatHTML5.myUser = spyOn(chatHTML5.myUser, {
		selectedUserid: async selectedUserid => {
			const nuser = getUserById(selectedUserid);
			if (!nuser) return;
			userMenu.dataset.status = await GM.getValue(`${nuser.username}_status`);
			userMenu.dataset.username = nuser.username;
			userMenu.dataset.isCooldown = Boolean(await isOnCooldown(nuser.username)).toString();
			userMenu.dataset.privateCam = Boolean(!nuser.obj.webcamPublic).toString();
		},
	}).proxy;
}
async function setupSidebar() {
	const userList = document.getElementById("userList");
	if (!userList) return;
	$(userList).on("click", ".webcamBtn", function (event) {
		if (event.shiftKey || !event.originalEvent?.isTrusted) return;
		const webcamNumber = chatHTML5.getWebcamNumber();
		const webcamMax = +chatHTML5.roles.user.webcamMax;
		if (webcamNumber < webcamMax) return;
		const nextUser = $(this).closest(".userItem")[0];
		if (!nextUser?.dataset.id) return;
		const user = getUserById(nextUser.dataset.id);
		if (!user) return;
		if (user.obj.webcamPublic) {
			event.stopPropagation();
			setNextCam(nextUser);
			const panel = getPanelToClose(webcamNumber);
			if (panel?.id) jsPanel.activePanels.getPanel(panel.id)?.close();
		}
	});
	$(userList).on("click", ".fa-eye.fa-2x", function (e) {
		if (e.shiftKey) return;
		e.preventDefault();
		e.stopPropagation();
		e.stopImmediatePropagation();
	});
}
async function setupWatchStats() {
	const counter = document.getElementById("watchAtMe");
	if (!counter) return;
	GM.addStyle(ws_css);
	const stats = document.createElement("span");
	stats.id = "bbw_watch_stats";
	counter.after(stats);
	preact.render(h(WatchStats, null), stats);
	await trackWatchStats();
}
const ConfigWatchers = {};
function getPanelToClose(webcamNumber) {
	if (webcamNumber <= 0) return null;
	const optionA = $(`${PANEL_SELECTOR}:not(.watchingMe)[data-status="--"]`)[0];
	if (optionA) return optionA;
	for (let i = webcamNumber; i > 0; i--) {
		const optionB = $(`${PANEL_SELECTOR}[data-grid-index="${i}"]`)[0];
		if (optionB) return optionB;
	}
	const panels = queryPanels();
	return panels[panels.length - 1] || null;
}
async function setupTools() {
	chatHTML5.config.timeBeforeWatchingCamAgain = "1000";
	chatHTML5.config.checkOwnStream = "1";
	chatHTML5.config.showCountryFlag = "1";
	chatHTML5.roles.user.webcamMax = GM_getValue("user.webcamMax", 10);
	applySiteOptions();
	const { revoke } = spyOnConfig(ConfigWatchers);
	chatHTML5.amIMuted = () => chatHTML5.myUser.mutedUntil > Date.now();
	await Promise.all([setupHeader(), setupUserMenu(), setupSidebar(), setupWatchStats()]);
	skipRedundantUserListRefresh();
	setupWatchingMe();
	if (!$("#sortWebcamtBtn").hasClass("selected")) $("#sortWebcamtBtn").trigger("click");
	return revoke;
}

// file://./src/betterbw/features/observeChat.tsx

function handleWebcamOpened(message) {
	const span = $("[data-id]", message)[0];
	if (!span) return;
	const user = getUserById(span.dataset.id);
	if (!user) return;
	message.dataset.username = user.username;
	const privateCam = Boolean(!user.obj.webcamPublic);
	message.dataset.privateCam = privateCam.toString();
	span.innerHTML = span.innerHTML.replace(
		/(User )(.*?)( has opened his webcam)/gi,
		`$1<span class="userLabelBBW" data-username=${JSON.stringify(user.username)}>$2</span>$3`,
	);
	if (!privateCam) clickOnCurrentAlgoButton();
}
function handleRegularMessage(message) {
	const msgHeader = $("[data-ip]", message)[0];
	if (!msgHeader) return;
	msgHeader.classList.add("userLabelBBW", "sender");
}
function handleWhisperOrPrivateMessage(message) {
	const msgHeader = $("[data-ip]", message)[0];
	if (!msgHeader || !msgHeader.childNodes.length) return;
	{
		const firstChild = msgHeader.childNodes[0];
		const span = document.createElement("span");
		span.dataset.username = msgHeader.dataset.username;
		span.classList.add("userLabelBBW", "sender");
		msgHeader.insertBefore(span, firstChild);
		span.appendChild(firstChild);
	}
	{
		const thirdChild = msgHeader.childNodes[2];
		const span = document.createElement("span");
		span.dataset.username = thirdChild.textContent?.trim();
		span.classList.add("userLabelBBW", "receiver");
		msgHeader.insertBefore(span, thirdChild);
		span.appendChild(thirdChild);
	}
}
function handlePrivateRequested(message) {
	const textNode = Array.from(message.childNodes).find(n => 3 === n.nodeType && n.textContent?.trim());
	if (!textNode || !textNode.textContent) return;
	const textContent = textNode.textContent;
	const match = textContent.match(/\s*(\S+?)\s+has invited you to watch his cam/);
	if (!match) return;
	const full_username = match[1];
	if (!full_username) return;
	const span = document.createElement("span");
	span.className = "userLabelBBW";
	span.dataset.username = full_username;
	span.textContent = full_username;
	message.insertBefore(span, textNode);
	textNode.textContent = textContent.replace(/(^\s*\S+?)(\s+.*$)/, "$2");
}
function handleWebcamRequest(message) {
	const textNode = Array.from(message.childNodes).find(n => 3 === n.nodeType && n.textContent?.trim());
	if (!textNode || !textNode.textContent) return;
	const textContent = textNode.textContent;
	const match = textContent.match(/You requested webcam of\s*(\S+)/);
	if (!match) return;
	const full_username = match[1];
	if (!full_username) return;
	const user = chatHTML5.getUserByUsername(full_username);
	if (!user) return;
	textNode.textContent = textContent.replace(/(^.*? of )(\s+.*$)/, "$1");
	const span = document.createElement("span");
	span.className = "userLabelBBW";
	span.dataset.username = full_username;
	span.textContent = full_username;
	message.appendChild(span);
	if (user.webcamPublic) {
		sessionCooldown.add(full_username);
		setTimeout(() => sessionCooldown["delete"](full_username), 1800000);
		message.append(" (unexpected)");
	}
}
function handleChatMessage(message) {
	if (message.matches(".serverMessage.webcamOpened")) handleWebcamOpened(message);
	else if (message.matches(".message.msg-box:not(.whisper)")) handleRegularMessage(message);
	else if (message.matches(".message.addPrivateMessage,.message.msg-box.whisper")) handleWhisperOrPrivateMessage(message);
	else if (message.matches(".serverMessage.privateRequested")) handlePrivateRequested(message);
	else if (message.matches(".serverMessage.webcamRequest")) handleWebcamRequest(message);
}
function observeChat(room) {
	return observeIt({ target: room, selector: ".message,.serverMessage", forEachAddedNode: handleChatMessage });
}
function observeChatNav() {
	const tabContent = $("#tabs .tab-content")[0];
	if (!tabContent) return;
	const tabs = {};
	return observeIt({
		target: tabContent,
		selector: ".tab-pane",
		forEachAddedNode: room => {
			tabs[room.id] = observeChat(room);
		},
		forEachRemovedNode: room => {
			if (room.id in tabs) {
				tabs[room.id]?.teardown?.();
				delete tabs[room.id];
			}
		},
	});
}

// file://./src/betterbw/features/responsiveLayout.ts

const CHAT_WIDTH_KEY = "config.chatWidth";
function setupResponsiveLayout() {
	const reorganize = debounce(() => organizePanels(), 100);
	window.addEventListener("resize", reorganize);
	const chat = document.getElementById("tabsAndFooter");
	if (!chat) return;
	const savedWidth = GM_getValue(CHAT_WIDTH_KEY, "");
	if (savedWidth) chat.style.width = savedWidth;
	const saveWidth = debounce(() => {
		if (chat.style.width) GM.setValue(CHAT_WIDTH_KEY, chat.style.width);
	}, 300);
	new ResizeObserver(() => {
		saveWidth();
		reorganize();
	}).observe(chat);
}

// file://./src/betterbw/features/lighterTimers.ts
const TIMESTAMPS = "#chatContainer .timeStamp, div.windowChat .timeStamp";
function displayDateAgo() {
	const selector = "1" === chatHTML5.config.displayConnectedSince ? `${TIMESTAMPS}, #userList .userSince` : TIMESTAMPS;
	for (const element of document.querySelectorAll(selector)) {
		const date = element.dataset.date;
		const text = chatHTML5.getDateAgo(date && /^\d+$/.test(date) ? Number(date) : date);
		if (element.textContent !== text) element.textContent = text;
	}
}
function lightenTimers() {
	const { removeServerMessages } = chatHTML5;
	chatHTML5.removeServerMessages = () => {
		if (!document.hidden) removeServerMessages();
	};
	chatHTML5.displayDateAgo = () => {
		if (!document.hidden) displayDateAgo();
	};
	document.addEventListener("visibilitychange", () => {
		if (!document.hidden) chatHTML5.displayDateAgo();
	});
}

// file://./src/betterbw/features/noCamsBeforeRoom.ts

function closeCam(panel) {
	const id = getUserId(panel);
	if (id) chatHTML5.removeWebcam(id, "bbw-before-room");
	else panel.remove();
}
function closeCamsUntil(roomSelected) {
	queryPanels().forEach(closeCam);
	const observer = new MutationObserver(mutations => {
		for (const { addedNodes } of mutations) for (const node of addedNodes) if (node instanceof HTMLDivElement && node.matches(PANEL_SELECTOR)) closeCam(node);
	});
	observer.observe(document.body, { childList: true, subtree: true });
	roomSelected.then(() => observer.disconnect());
}

// file://./src/betterbw/styles/dragToSwap.css?raw
const dts_css = `
.bbw-swap-highlight {
	position: fixed;
	z-index: 100000;
	box-sizing: border-box;
	border: 2px solid rgba(40, 140, 255, 0.9);
	border-radius: 4px;
	background: rgba(40, 140, 255, 0.35);
	pointer-events: none;
}
`;

// file://./src/betterbw/features/dragToSwap.ts

let dts_drag = null;
const distance = (a, b) => Math.hypot(a.left - b.left, a.top - b.top);
function highlight(rect) {
	const el = document.createElement("div");
	el.className = "bbw-swap-highlight";
	Object.assign(el.style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
	document.body.appendChild(el);
	return el;
}
function stopHover(d) {
	d.timers.forEach(clearTimeout);
	d.timers = [];
	d.highlights.forEach(el => el.remove());
	d.highlights = [];
	d.hovered = null;
}
function startHover(d, target) {
	const slot = d.slots.get(target);
	d.hovered = target;
	d.timers = [
		setTimeout(() => {
			d.highlights = [highlight(slot), highlight(d.origin)];
		}, 250),
		setTimeout(() => {
			stopHover(d);
			d.pending = { target, index: target.dataset.gridIndex, slot };
			placeInSlot(target, +d.index);
		}, 1000),
	];
}
function dts_update() {
	const d = dts_drag;
	if (!d) return;
	d.frame = 0;
	const rect = d.panel.getBoundingClientRect();
	if (!d.moved) {
		if (distance(rect, d.origin) < 3) return;
		d.moved = true;
		d.panel.classList.add("bbw-dragging");
	}
	if (d.pending) {
		if (distance(rect, d.pending.slot) <= 24) return;
		placeInSlot(d.pending.target, +d.pending.index);
		d.pending = null;
	}
	let hovered = null;
	for (const [other, slot] of d.slots)
		if (other.isConnected && distance(rect, slot) <= 20) {
			hovered = other;
			break;
		}
	if (hovered === d.hovered) return;
	stopHover(d);
	if (hovered) startHover(d, hovered);
}
function dts_onPointerDown(event) {
	if (0 !== event.button || !(event.target instanceof Element)) return;
	if (!event.target.closest(".jsPanel-hdr") || event.target.closest("button, input, a, .jsPanel-btn, .userAvatar")) return;
	const panel = event.target.closest(PANEL_SELECTOR);
	const index = panel?.dataset.gridIndex;
	if (!panel || !index) return;
	const slots = new Map();
	for (const other of queryPanels()) if (other !== panel && other.dataset.gridIndex) slots.set(other, other.getBoundingClientRect());
	dts_drag = {
		panel,
		index,
		origin: panel.getBoundingClientRect(),
		slots,
		moved: false,
		hovered: null,
		timers: [],
		highlights: [],
		pending: null,
		frame: 0,
	};
}
function onPointerMove() {
	if (dts_drag && !dts_drag.frame) dts_drag.frame = requestAnimationFrame(dts_update);
}
function onPointerUp() {
	const d = dts_drag;
	if (!d) return;
	dts_drag = null;
	cancelAnimationFrame(d.frame);
	stopHover(d);
	if (!d.moved) return;
	setTimeout(() => {
		d.panel.classList.remove("bbw-dragging");
		const { pending } = d;
		if (!pending) return;
		if (distance(d.panel.getBoundingClientRect(), pending.slot) <= 24) {
			d.panel.dataset.gridIndex = pending.index;
			pending.target.dataset.gridIndex = d.index;
			organizePanels();
		} else placeInSlot(pending.target, +pending.index);
	});
}
function setupDragToSwap() {
	GM.addStyle(dts_css);
	document.addEventListener("pointerdown", dts_onPointerDown, true);
	document.addEventListener("pointermove", onPointerMove, { capture: true, passive: true });
	document.addEventListener("pointerup", onPointerUp, true);
	document.addEventListener("pointercancel", onPointerUp, true);
}

// file://./src/betterbw/styles/static.css?raw
const static_css = `
body>div:nth-child(1) {
	height: calc(100% - 4px);
}
body:not([data-algo]) button[data-sort-order=''],
body[data-algo=''] button[data-sort-order=''],
body[data-algo='new'] button[data-sort-order='new'],
body[data-algo='top'] button[data-sort-order='top'] {
	color: white;
	background-color: #50c180;
}
input.numberOfCams {
	width: 4em;
}
.panel-action-btn {
	cursor: pointer;
	margin-left: 8px;
	background: rgba(255, 255, 255, 0.8);
	color: inherit;
	font-size: 14px;
	float: right;
	position: relative;
	border: 1px solid #ddd;
	top: 2px;
	height: 15px;
	line-height: 0;
	opacity: 1;
	transition: opacity 0.2s ease-out;
}
.panel-action-btn:where([data-status-value='--'], [data-status-value='-'], [data-status-value='+'], [data-status-value='++']) {
	margin-left: 0;
}
.panel-action,
.panel-cooldown {
	position: absolute;
	z-index: 1;
	display: flex;
	border-radius: 4px;
	overflow: hidden;
	background: rgba(0, 0, 0, 0.5);
	opacity: 0;
	transition: opacity 0.2s ease-out;
	.panel-action-btn {
		all: unset;
		box-sizing: border-box;
		cursor: pointer;
		color: #fff;
		font-size: 12px;
		line-height: 20px;
		padding: 0 6px;
		text-align: center;
		opacity: 0.85;
		transition: opacity 0.2s ease-out;
		&:hover {
			opacity: 1;
		}
	}
}
.panel-action {
	left: 4px;
	top: 24px;
}
.panel-cooldown {
	right: 2px;
	top: 24px;
}
.jsPanel-hdr {
	container-type: inline-size;
}
@container (max-width: 240px) {
	:is(.panel-action, .panel-cooldown) .panel-action-btn {
		padding: 0 3px;
		font-size: 11px;
	}
}
.jsPanel[data-grid-index][data-actions-attached]:not(.ui-draggable-dragging, .bbw-dragging) {
	transition:
		top 0.15s linear,
		right 0.15s linear,
		bottom 0.15s linear,
		left 0.15s linear,
		translate 0.15s linear;
}
.jsPanel:hover :is(.panel-action, .panel-cooldown) {
	opacity: 1;
}
.jsPanel .speaks {
	width: 3px;
	rotate: 180deg;
	top: 0;
	bottom: 0;
	left: 0;
	.volume {
		background: yellow;
		box-shadow:
			1px 1px 1px rgb(0, 0, 0),
			2px 2px 1px rgb(255, 255, 255);
	}
}
.jsPanel .jsPanel-content {
	background: #111;
}
:where(#userMenu, .jsPanel)[data-status='--'] [data-status-value='--'] {
	background: rgba(205, 0, 0, 1);
	color: #fff;
}
:where(#userMenu, .jsPanel)[data-status='-'] [data-status-value='-'] {
	background: rgba(237, 146, 0, 1);
	color: #fff;
}
:where(#userMenu, .jsPanel)[data-status='+'] [data-status-value='+'] {
	background: rgba(0, 167, 228, 1);
	color: #fff;
}
:where(#userMenu, .jsPanel)[data-status='++'] [data-status-value='++'] {
	background: rgba(149, 50, 255, 1);
	color: #fff;
}
:where(#userMenu, .jsPanel)[data-is-cooldown='true'] [data-status-value='⏱'] {
	background: rgba(50, 50, 50, 1);
	color: #fff;
}
#userMenu .menuUserItem[data-action="whisper"] {
	display: block !important;
}
#userMenu[data-private-cam='true'] [data-action='viewWebcam'] {
	color: red;
	.fa.fa-video-camera:after {
		color: red;
		content: ' \\f023';
		font-family: 'Font Awesome 5 Free';
		font-weight: 900;
	}
}
#tabs .tab-pane.tab-pane {
	padding: 10px 10px 10px 20px !important;
}
#tabs .addPrivateMessage {
	.mention,
	.userLabelBBW.receiver {
		pointer-events: none;
	}
	.name-time {
		flex-wrap: wrap;
		.content {
			flex-basis: 100%;
		}
	}
}
#tabs .serverMessage,
#tabs .addPrivateMessage,
#tabs .message {
	&.whisper:after {
		right: unset;
		left: -10px;
		top: -1px;
		font-size: 9px;
		line-height: 1;
		width: initial;
		padding: 1px;
		height: min-content;
	}
}
#tabs .serverMessage[data-private-cam='true']:after,
#userMenu[data-private-cam='true'] [data-action='viewWebcam'].fa:after {
	content: ' \\f023';
	font-family: 'Font Awesome 5 Free';
	color: red;
	font-weight: 900;
}
#tabs .serverMessage[data-private-cam='true']:after {
	margin-left: 20px;
}
#tabs .webcamOpened .watchCam {
	position: relative;
}
.jsPanel .userAvatar {
	--avatarSize: 22px;
	cursor: pointer;
	padding: 0;
	border-radius: var(--avatarSize);
	height: var(--avatarSize);
	width: var(--avatarSize);
	margin-top: -1px;
	object-fit: cover;
}
.jsPanel-btn-close {
	line-height: 1;
}
.headerPrivateBtn {
	display: none;
}
.jsPanel[data-status='undefined'] .jsPanel-title {
	text-decoration-color: orange;
	text-decoration-line: underline;
	text-decoration-style: solid;
	text-decoration-thickness: 2px;
}
.jsPanel[data-status='undefined'] button:where([data-status-value='++']),
.jsPanel[data-status='--'] button:where([data-status-value='++']),
.jsPanel[data-status='-'] button:where([data-status-value='++']),
.jsPanel[data-status='+'] button:where([data-status-value='--']),
.jsPanel[data-status='++'] button:where([data-status-value='--']) {
	display: none;
}
.jsPanel:where([data-rotation=''], [data-rotation='0']) video {
	rotate: 0deg;
}
.jsPanel[data-rotation='90'] video {
	rotate: 90deg;
}
.jsPanel[data-rotation='180'] video {
	rotate: 180deg;
}
.jsPanel[data-rotation='270'] video {
	rotate: 270deg;
}
.jsPanel[data-rotation='90'] video,
.jsPanel[data-rotation='270'] video {
	width: 85% !important;
	margin-left: 7.5%;
}
.slide_block {
	width: 14px;
}
#usersContainer {
	width: 240px;
	&.leftLayout #slide_block {
		top: 50px;
		left: 31px;
		z-index: 101;
		height: 50px;
	}
	> ul.nav.nav-pills > li > a {
		padding: 0.2rem;
		height: auto !important;
	}
}
#myWebcamContainer {
	> .btn-group {
		padding-block: 0;
	}
	.btn {
		text-wrap: inherit;
		line-height: 1 !important;
	}
	#myAudioVideoCheckBoxContainer {
		position: absolute;
		top: 0;
		right: 0;
		text-wrap: nowrap;
	}
	#myAudioVideoCheckBoxContainer .btn {
		min-height: 1rem;
	}
	#myWebcamDiv>button {
		transform: translateY(-15px) scaleY(.25);
		transition: transform .2s ease-out;
	}
	#myWebcamDiv>div {
		transform: translateY(-7px) scaleY(.25);
		transition: transform .2s ease-out;
	}
	&:hover #myWebcamDiv>button,
	&:hover #myWebcamDiv>div {
		transform: initial;
	}
}
#userList .userItem {
	border-bottom: none;
	margin-left: 1px;
}
#userList .userLabel {
	top: 0;
	margin-left: 2px;
	padding-left: 8px;
	border-top-left-radius: 9px;
	border-bottom-left-radius: 9px;
	.userSince {
		left: unset;
	}
}
.webcamBtn {
	padding-inline: 1rem;
	margin: 0;
	border-top-left-radius: 0;
	border-bottom-left-radius: 0;
}
.webcamBtn i.lock {
	&.fa-unlock {
		display: none;
	}
	&.fa-lock {
		text-shadow: -2px 2px 0 white;
		right: -25px;
		top: 1px;
	}
	position: absolute;
	left: -11px;
	top: 5px;
	font-size: 16px;
}
.webcamBtn i.fa-volume-down {
	padding-inline: 4px;
}
.eye-icon .fa-eye.isWatching {
	color: gold !important;
	position: absolute;
	right: 42px;
	bottom: 5px;
	text-shadow: -1px -1px 1px goldenrod;
}
#userList .userItem:has(.webcamBtn.visible i.lock.fa-lock) {
	--lock-color: rgba(205, 0, 0, 0.33);
	--v-padding: 0px;
	.userLabel {
		box-shadow: inset 3px 0 1px 2px var(--lock-color);
	}
	.webcamBtn.visible {
		box-shadow: inset -3px 0 1px 2px var(--lock-color);
		border: none;
	}
}
.jsPanel {
	box-shadow: none;
	border-color: #888 !important;
}
#roomsBtn {
	line-height: 1 !important;
	padding-left: 6px !important;
	padding-right: 3px !important;
	margin-right: 0px !important;
}
.header-btn-wrap {
	margin: 0 3px;
	padding-right: 0px;
}
.header-custom-btns span {
	padding-right: 10px;
}
#myAvatar {
	padding-inline: 0;
	.myUsername {
	display: none;
}}
#bbw_header_controls {
	position: relative;
	display: flex;
	align-items: center;
	margin-left: 6px;
	white-space: nowrap;
	button,
	input {
		margin: 0;
		padding-inline: 0.5rem;
		line-height: 1.275;
	}
	input[type=number] {
		padding-inline: 0;
	}
	label {
		margin: 0;
	}
}
#header {
	padding-right: 0 !important;
	padding-left: 0 !important;
	height: 30px !important;
	.status {
		top: -8px;
		left: -6px;
	}
}
#tabsAndFooter,
#footer {
	width: max(280px, min(50%, calc(100% - var(--bbw-cams-width, 1107px))));
}
#tabsAndFooter {
	resize: horizontal;
	overflow: hidden;
	min-width: 200px;
	max-width: 80%;
}
#footer {
	position: relative;
	width: 100%;
}
video.mobile.mobile {
	max-width: 100% !important;
	max-height: 100% !important;
}
.jsPanel-headerbar {
	min-height: 18px;
}
.jsPanel-titlebar {
	min-height: 16px;
}
.jsPanel-titlebar h3 {
	margin-block: 1px;
}
`;

// file://./src/betterbw/styles/topLevel.css?raw
const tl_css = `
iframe {
	display: block;
}
`;

// file://./src/betterbw/betterbw.user.ts

async function topLevelStyles() {
	GM.addStyle(tl_css);
}
function hacks() {
	chatHTML5.myUser.mutedUsers = chatHTML5.myUser.mutedUsers || "";
	const original_updateNumberUsersDisplay = chatHTML5.updateNumberUsersDisplay;
	chatHTML5.updateNumberUsersDisplay = debounce(original_updateNumberUsersDisplay);
	batchUserListRebuilds();
	setupCamCleanup();
	lightenTimers();
	applySiteOptions();
}
async function main() {
	hacks();
	GM.addStyle(static_css);
	sweepExpiredCooldowns();
	setupResponsiveLayout();
	observeChatNav();
	const roomSelected = waitToBe("#roomsModal", ["aria-hidden"], el => "false" === el.getAttribute("aria-hidden")).then(() => waitToBe("#roomsModal"));
	closeCamsUntil(roomSelected);
	observePanels();
	setupCamsMoveAway();
	setupDragToSwap();
	await roomSelected;
	setRoomSelected();
	await setupTools();
	await runAlgo(getSetting("preferredAlgo"));
}

// file://./src/betterbw/index.ts

if (location.pathname.startsWith("/html5-chat/chatroom")) topLevelStyles();
else if (location.pathname.startsWith("//html5-chat/chat2") && "undefined" != typeof chatHTML5) main();