Romeo Additions

Enhances GR, especially for non-PLUS users

// ==UserScript==
// @name Romeo Additions
// @name:de Romeo Additions
// @namespace https://greasyfork.org/en/users/723211-ray/
// @version 7.9.1
// @description Enhances GR, especially for non-PLUS users
// @description:de Verbessert GR, insbesondere für nicht-PLUS-Benutzer
// @author -Ray-, Djamana
// @match *://*.romeo.com/*
// @license MIT
// @grant none
// @iconURL https://www.romeo.com/assets/favicons/711cd1957a9d865b45974099a6fc413e3bd323fa5fc48d9a964854ad55754ca1/favicon.ico
// @supportURL https://greasyfork.org/en/scripts/419514-romeo-additions
// ==/UserScript==

const CM2FT = 0.03280839895;
const KG2LBS = 2.20462262185;
const M2MI = 0.0006213712;

function decodeUrl(url)
{
	const [base, paramsText] = url.split("?");
	const params = new URLSearchParams(paramsText);
	return [base, params];
}

function encodeUrl(base, params)
{
	return `${base}?${new URLSearchParams(params)}`;
}

function escapeHtml(unsafe)
{
	return unsafe
		.replace(/&/g, "&")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#039;");
}

function formatTime(str)
{
	const date = new Date(Date.parse(str));
	const lang = getLang();
	return `${date.toLocaleDateString(lang)} ${date.toLocaleTimeString(lang)}`;
}

function getLang()
{
	return document.documentElement.getAttribute("lang") || "en";
}

function isJson(value)
{
	if (!value || typeof value !== "string")
		return false;

	try
	{
		JSON.parse(value);
		return true;
	}
	catch
	{
		return false;
	}
}

function log()
{
	if (GM_info.script.version === "0.0.0")
	{
		if (arguments.length > 1 && arguments[1])
			arguments[0] += "\n";
		console.log(...arguments);
	}
}

function round(value, maxDigits = 0)
{
	const f = Math.pow(10, maxDigits);
	return Math.round(value * f) / f;
}

// ---- CSS ----

function addCss(css)
{
	let style = document.createElement('style');
	if (style.styleSheet)
		style.styleSheet.cssText = css;
	else
		style.appendChild(document.createTextNode(css));
	return document.head.appendChild(style);
};

function getCssBackgroundImageUrl(style)
{
	return style.match(/"(.+)"/)[1];
}

function setCssProp(name, value)
{
	document.documentElement.style.setProperty(name, value);
}

// ---- DOM ----

const domHooks = {};

function initDom()
{
	function tagCall(el, callback)
	{
		if (!el.getAttribute("data-ra-hook"))
		{
			el.setAttribute("data-ra-hook", true);
			callback(el);
		}
	}

	const observer = new MutationObserver((mutations, observer) =>
	{
		for (const mutation of mutations)
		{
			for (const el of mutation.addedNodes)
			{
				if (el.nodeType === Node.ELEMENT_NODE)
				{
					for (const [selector, callback] of Object.entries(domHooks))
					{
						if (el.matches(selector))
						{
							// Trigger for element.
							tagCall(el, callback);
						}
						else
						{
							// Trigger for children of attached elements.
							for (const elChild of el.querySelectorAll(selector))
								tagCall(elChild, callback);
						}
					}
				}
			}
		}
	});
	observer.observe(document.body, { subtree: true, childList: true });
}

function addElement(parent, html)
{
	parent.insertAdjacentHTML("beforeend", html);
	return parent.lastChild;
}

function onDom(selector, callback)
{
	// Trigger for existing elements.
	for (const el of document.querySelectorAll(selector))
		callback(el);

	// Add to observer list.
	domHooks[selector] = callback;
}

// ---- Translation ----

const strings =
{
	aboutMe:
	{
		de: "Über mich",
		en: "About Me",
	},
	age:
	{
		de: "Alter",
		en: "Age",
	},
	ageRange:
	{
		de: "Altersspanne",
		en: "Age range",
	},
	ageRangeValue:
	{
		de: "Zwischen $from und $to",
		en: "Between $from and $to",
	},
	analPosition:
	{
		en: "Position",
	},
	analPosition_TOP_ONLY:
	{
		de: "Nur Aktiv",
		en: "Top only",
	},
	analPosition_MORE_TOP:
	{
		de: "Eher Aktiv",
		en: "More top",
	},
	analPosition_VERSATILE:
	{
		de: "Flexibel",
		en: "Versatile",
	},
	analPosition_MORE_BOTTOM:
	{
		de: "Eher Passiv",
		en: "More bottom",
	},
	analPosition_BOTTOM_ONLY:
	{
		de: "Nur Passiv",
		en: "Bottom only",
	},
	analPosition_NO:
	{
		de: "Kein Anal",
		en: "No anal",
	},
	beard:
	{
		de: "Bart",
		en: "Beard",
	},
	beard_DESIGNER_STUBBLE:
	{
		de: "3-Tage-Bart",
		en: "Designer stubble",
	},
	beard_FULL_BEARD:
	{
		de: "Vollbart",
		en: "Full beard",
	},
	beard_GOATEE:
	{
		en: "Goatee",
	},
	beard_MOUSTACHE:
	{
		de: "Schnauzer",
		en: "Moustache",
	},
	beard_NO_BEARD:
	{
		de: "Kein Bart",
		en: "No beard",
	},
	bedAndBreakfast:
	{
		en: "Bed & Breakfast",
	},
	blockUser:
	{
		de: "Benutzer blockieren",
		en: "Block user",
	},
	bmi:
	{
		en: "BMI",
	},
	bmiMildThin:
	{
		de: "Leichtes Untergewicht",
		en: "Mildly Thin",
	},
	bmiModerateThin:
	{
		de: "Mäßiges Untergewicht",
		en: "Moderately Thin",
	},
	bmiNormal:
	{
		de: "Normal",
		en: "Normal",
	},
	bmiObese1:
	{
		de: "Adipositas I",
		en: "Obese Class I",
	},
	bmiObese2:
	{
		de: "Adipositas II",
		en: "Obese Class II",
	},
	bmiObese3:
	{
		de: "Adipositas III",
		en: "Obese Class III",
	},
	bmiPreObese:
	{
		de: "Präadipositas",
		en: "Pre-Obese",
	},
	bmiSevereThin:
	{
		de: "Starkes Untergewicht",
		en: "Severely Thin",
	},
	bodyType:
	{
		de: "Statur",
		en: "Body Type",
	},
	bodyType_ATHLETIC:
	{
		de: "Athletisch",
		en: "Athletic",
	},
	bodyType_AVERAGE:
	{
		de: "Normal",
		en: "Average",
	},
	bodyType_BELLY:
	{
		de: "Bauch",
		en: "Belly",
	},
	bodyType_MUSCULAR:
	{
		de: "Muskulös",
		en: "Muscular",
	},
	bodyType_SLIM:
	{
		de: "Schlank",
		en: "Slim",
	},
	bodyType_STOCKY:
	{
		de: "Stämmig",
		en: "Stocky",
	},
	bodyHair:
	{
		de: "Körperbehaarung",
		en: "Body Hair",
	},
	bodyHair_AVERAGE:
	{
		de: "Mittel behaart",
		en: "Hairy",
	},
	bodyHair_LITTLE:
	{
		de: "Wenig behaart",
		en: "Not very hairy",
	},
	bodyHair_SHAVED:
	{
		de: "Rasiert",
		en: "Shaved",
	},
	bodyHair_SMOOTH:
	{
		de: "Unbehaart",
		en: "Smooth",
	},
	bodyHair_VERY_HAIRY:
	{
		de: "Stark behaart",
		en: "Very hairy",
	},
	clearList:
	{
		de: "Möchtest du wirklich alle Einträge in der Liste entfernen?",
		en: "Do you really want to remove all elements from the list?",
	},
	concision:
	{
		de: "Beschneidung",
		en: "Concision",
	},
	concision_CUT:
	{
		de: "Beschnitten",
		en: "Cut",
	},
	concision_UNCUT:
	{
		de: "Unbeschnitten",
		en: "Uncut",
	},
	customRadius:
	{
		de: "Benutzerdefinierter Radius",
		en: "Custom Radius",
	},
	deleteUnread:
	{
		de: "Ungelesen löschen",
		en: "Delete unread"
	},
	dick:
	{
		de: "Schwanz",
		en: "Dick",
	},
	dick_S:
	{
		en: "S",
	},
	dick_M:
	{
		en: "M",
	},
	dick_L:
	{
		en: "L",
	},
	dick_XL:
	{
		en: "XL",
	},
	dick_XXL:
	{
		en: "XXL",
	},
	dirty:
	{
		en: "Dirty",
	},
	dirty_NO:
	{
		de: "Kein Dirty",
		en: "No dirty",
	},
	dirty_WS_ONLY:
	{
		de: "Ja, aber nur NS",
		en: "WS only",
	},
	dirty_YES:
	{
		en: "Dirty",
	},
	discoverFilter:
	{
		de: "Entdecken filtern",
		en: "Filter Discover",
	},
	discoverFilterDesc:
	{
		de: "Wendet Radar-Filter auf die Entdecken-Seite an (außer Eyecandy).",
		en: "Applies radar filter on the Discover page (except Eyecandy).",
	},
	display:
	{
		de: "Anzeige",
		en: "Display",
	},
	distance:
	{
		de: "Entfernung",
		en: "Distance",
	},
	filter:
	{
		en: "Filter",
	},
	enhancedFilter:
	{
		de: "Erweiterter Filter",
		en: "Extended filter",
	},
	enhancedFilterDesc:
	{
		de: "Erlaubt Radar-Ergebnisse nach allen Details zu filtern.",
		en: "Allows to filter radar results by additional details.",
	},
	enhancedImages:
	{
		de: "Hochauflösende Bilder",
		en: "High-resolution images",
	},
	enhancedImagesDesc:
	{
		de: "Zeigt Kachelbilder in maximaler Auflösung.",
		en: "Shows tile images in maximum resolution.",
	},
	enhancedTiles:
	{
		de: "Große Kacheln erzwingen",
		en: "Force big grid",
	},
	enhancedTilesDesc:
	{
		de: "Zeigt alle Benutzer in großen Kacheln.",
		en: "Shows all users in big tiles.",
	},
	ethnicity:
	{
		de: "Typ",
		en: "Ethnicity",
	},
	ethnicity_ARAB:
	{
		de: "Araber",
		en: "Arab",
	},
	ethnicity_ASIAN:
	{
		de: "Asiate",
		en: "Asian",
	},
	ethnicity_BLACK:
	{
		de: "Schwarz",
		en: "Black",
	},
	ethnicity_CAUCASIAN:
	{
		de: "Europäer",
		en: "Caucasian",
	},
	ethnicity_INDIAN:
	{
		de: "Inder",
		en: "Indian",
	},
	ethnicity_LATIN:
	{
		de: "Latino",
		en: "Latin",
	},
	ethnicity_MEDITERRANEAN:
	{
		de: "Südländer",
		en: "Mediterranean",
	},
	ethnicity_MIXED:
	{
		en: "Mixed",
	},
	eyeColor:
	{
		de: "Augenfarbe",
		en: "Eye Colour",
	},
	eyeColor_BLUE:
	{
		de: "Blau",
		en: "Blue",
	},
	eyeColor_BROWN:
	{
		de: "Braun",
		en: "Brown",
	},
	eyeColor_GREEN:
	{
		de: "Grün",
		en: "Green",
	},
	eyeColor_GREY:
	{
		de: "Grau",
		en: "Grey",
	},
	eyeColor_OTHER:
	{
		de: "Sonstige",
		en: "Other",
	},
	fetish:
	{
		de: "Fetisch",
		en: "Fetish",
	},
	fetish_BOOTS:
	{
		en: "Boots",
	},
	fetish_CROSSDRESSING:
	{
		de: "Cross-Dressing",
		en: "Cross-dressing",
	},
	fetish_DRAG:
	{
		de: "Dessous",
		en: "Lingerie",
	},
	fetish_FORMAL:
	{
		de: "Anzug",
		en: "Formal dress",
	},
	fetish_JEANS:
	{
		en: "Jeans",
	},
	fetish_LEATHER:
	{
		de: "Leder",
		en: "Leather",
	},
	fetish_LYCRA:
	{
		en: "Lycra",
	},
	fetish_RUBBER:
	{
		en: "Rubber",
	},
	fetish_SKATER:
	{
		en: "Skater",
	},
	fetish_SKINS:
	{
		en: "Skins & Punks",
	},
	fetish_SNEAKERS:
	{
		en: "Sneakers & Socks",
	},
	fetish_SPORTS:
	{
		de: "Sportsgear",
		en: "Sports gear",
	},
	fetish_TECHNO:
	{
		en: "Raver",
	},
	fetish_UNDERWEAR:
	{
		de: "Unterwäsche",
		en: "Underwear",
	},
	fetish_UNIFORM:
	{
		en: "Uniform",
	},
	fetish_WORKER:
	{
		de: "Handwerker",
		en: "Worker",
	},
	filters:
	{
		en: "Filters",
		de: "Filter",
	},
	fisting:
	{
		de: "Fisten",
		en: "Fisting",
	},
	fisting_ACTIVE:
	{
		de: "FF Aktiv",
		en: "FF Active",
	},
	fisting_ACTIVE_PASSIVE:
	{
		de: "FF Flexibel",
		en: "FF Versatile",
	},
	fisting_NO:
	{
		de: "Kein FF",
		en: "No FF",
	},
	fisting_PASSIVE:
	{
		de: "FF Passiv",
		en: "FF Passive",
	},
	fullHeadlines:
	{
		de: "Vollständige Überschriften",
		en: "Full headlines",
	},
	fullHeadlinesDesc:
	{
		de: "Zeigt lange Profilüberschriften vollständig.",
		en: "Shows long profile headlines completely.",
	},
	fullMessages:
	{
		de: "Vollständige Nachrichten",
		en: "Full messages",
	},
	fullMessagesDesc:
	{
		de: "Zeigt Nachrichten ungekürzt in der Nachrichtenliste.",
		en: "Shows messages without truncation in the message list.",
	},
	gender:
	{
		de: "Geschlecht",
		en: "Gender",
	},
	gender_MAN:
	{
		de: "Mann",
		en: "Man",
	},
	gender_TRANS_MAN:
	{
		de: "Transmann",
		en: "Trans man",
	},
	gender_TRANS_WOMAN:
	{
		de: "Transfrau",
		en: "Trans woman",
	},
	gender_NON_BINARY:
	{
		de: "Nicht binär",
		en: "Non-binary",
	},
	gender_OTHER:
	{
		de: "Anderes",
		en: "Other",
	},
	genderOrientation:
	{
		de: "Ich bin",
		en: "I am",
	},
	general:
	{
		de: "Allgemein",
		en: "General",
	},
	hairColor:
	{
		de: "Haarfarbe",
		en: "Hair Colour",
	},
	hairColor_BLACK:
	{
		de: "Schwarz",
		en: "Black",
	},
	hairColor_BLOND:
	{
		en: "Blond",
	},
	hairColor_BROWN:
	{
		de: "Braune Haare",
		en: "Brown",
	},
	hairColor_GREY:
	{
		de: "Grau",
		en: "Grey",
	},
	hairColor_LIGHT_BROWN:
	{
		de: "Dunkelblond",
		en: "Light brown",
	},
	hairColor_OTHER:
	{
		de: "Sonstige",
		en: "Other",
	},
	hairColor_RED:
	{
		de: "Rot",
		en: "Red",
	},
	hairLength:
	{
		de: "Haarlänge",
		en: "Hair Length",
	},
	hairLength_AVERAGE:
	{
		de: "Normal",
		en: "Average",
	},
	hairLength_LONG:
	{
		de: "Lang",
		en: "Long",
	},
	hairLength_PUNK:
	{
		en: "Punk",
	},
	hairLength_SHAVED:
	{
		de: "Rasiert",
		en: "Shaved",
	},
	hairLength_SHORT:
	{
		de: "Kurz",
		en: "Short",
	},
	height:
	{
		de: "Größe",
		en: "Height",
	},
	hiddenUsers:
	{
		de: "Ausgeblendete Benutzer",
		en: "Hidden users",
	},
	hideActivities:
	{
		de: "In Aktivitäten ausblenden",
		en: "Hide in activities",
	},
	hideContacts:
	{
		de: "In Kontakten ausblenden",
		en: "Hide in contacts",
	},
	hideFriends:
	{
		de: "In Freunden ausblenden",
		en: "Hide in friends",
	},
	hideLikes:
	{
		de: "In Likes ausblenden",
		en: "Hide in likes",
	},
	hideMessages:
	{
		de: "In Nachrichten ausblenden",
		en: "Hide in messages",
	},
	hideUser:
	{
		de: "Benutzer ausblenden",
		en: "Hide user",
	},
	hideVisits:
	{
		de: "In Besuchern ausblenden",
		en: "Hide in visitors",
	},
	interests:
	{
		de: "Interessen",
		en: "Interests",
	},
	interests_ART:
	{
		de: "Kunst",
		en: "Art",
	},
	interests_BOARDGAME:
	{
		de: "Brettspiele",
		en: "Board games",
	},
	interests_CAR:
	{
		de: "Autos",
		en: "Cars",
	},
	interests_COLLECT:
	{
		de: "Sammeln",
		en: "Collecting",
	},
	interests_COMPUTER:
	{
		de: "Computer",
		en: "Computers",
	},
	interests_COOK:
	{
		de: "Kochen",
		en: "Cooking",
	},
	interests_DANCE:
	{
		en: "Dance",
	},
	interests_FILM:
	{
		en: "Film & Video",
	},
	interests_FOTO:
	{
		de: "Fotografie",
		en: "Photography",
	},
	interests_GAME:
	{
		de: "Computerspiele",
		en: "Gaming",
	},
	interests_LITERATURE:
	{
		de: "Literatur",
		en: "Literature",
	},
	interests_MODELING:
	{
		de: "Modellbau",
		en: "Model building",
	},
	interests_MOTORBIKE:
	{
		de: "Motorrad",
		en: "Motorbikes",
	},
	interests_MUSIC:
	{
		de: "Musik",
		en: "Music",
	},
	interests_NATURE:
	{
		de: "Natur",
		en: "Nature",
	},
	interests_POLITICS:
	{
		de: "Politik",
		en: "Politics",
	},
	interests_TV:
	{
		en: "TV",
	},
	languages:
	{
		de: "Sprachen",
		en: "Languages",
	},
	languages_af:
	{
		de: "Afrikaans",
		en: "Afrikaans",
	},
	languages_ar:
	{
		de: "Arabisch",
		en: "Arabic",
	},
	languages_arm:
	{
		de: "Armenisch",
		en: "Armenian",
	},
	languages_az:
	{
		de: "Aserbaidschanisch",
		en: "Azerbaijani",
	},
	languages_be:
	{
		de: "Belarussisch",
		en: "Belarusian",
	},
	languages_bg:
	{
		de: "Bulgarisch",
		en: "Bulgarian",
	},
	languages_bn:
	{
		de: "Bengali",
		en: "Bengali",
	},
	languages_bs:
	{
		de: "Bosnisch",
		en: "Bosnian",
	},
	languages_bur:
	{
		de: "Burmesisch",
		en: "Burmese",
	},
	languages_ca:
	{
		de: "Katalanisch",
		en: "Catalan",
	},
	languages_ceb:
	{
		de: "Cebuano",
		en: "Cebuano",
	},
	languages_cs:
	{
		de: "Tschechisch",
		en: "Czech",
	},
	languages_da:
	{
		de: "Dänisch",
		en: "Danish",
	},
	languages_de:
	{
		de: "Deutsch",
		en: "German",
	},
	languages_el:
	{
		de: "Griechisch",
		en: "Greek",
	},
	languages_en:
	{
		de: "Englisch",
		en: "English",
	},
	languages_eo:
	{
		de: "Esperanto",
		en: "Esperanto",
	},
	languages_es:
	{
		de: "Spanisch",
		en: "Spanish",
	},
	languages_et:
	{
		de: "Estnisch",
		en: "Estonian",
	},
	languages_eu:
	{
		de: "Baskisch",
		en: "Basque",
	},
	languages_fa:
	{
		de: "Persisch",
		en: "Persian",
	},
	languages_fi:
	{
		de: "Finnisch",
		en: "Finnish",
	},
	languages_fr:
	{
		de: "Französisch",
		en: "French",
	},
	languages_frc:
	{
		de: "Kanadisches Französisch",
		en: "Canadian French",
	},
	languages_gd:
	{
		de: "Schottisch-Gälisch",
		en: "Scottish Gaelic",
	},
	languages_gl:
	{
		de: "Galician",
		en: "Galician",
	},
	languages_gsw:
	{
		de: "Schwyzerdütsch",
		en: "Swiss-German",
	},
	languages_hi:
	{
		de: "Hindi",
		en: "Hindi",
	},
	languages_hr:
	{
		de: "Kroatisch",
		en: "Croatian",
	},
	languages_hu:
	{
		de: "Ungarisch",
		en: "Hungarian",
	},
	languages_id:
	{
		de: "Indonesisch",
		en: "Indonesian",
	},
	languages_is:
	{
		de: "Isländisch",
		en: "Icelandic",
	},
	languages_it:
	{
		de: "Italienisch",
		en: "Italian",
	},
	languages_iw:
	{
		de: "Hebräisch",
		en: "Hebrew",
	},
	languages_ja:
	{
		de: "Japanisch",
		en: "Japanese",
	},
	languages_ka:
	{
		de: "Georgisch",
		en: "Georgian",
	},
	languages_kl:
	{
		de: "Grönländisch",
		en: "Greenlandic (Kalaallisut)",
	},
	languages_km:
	{
		de: "Kambodschanisch",
		en: "Cambodian",
	},
	languages_kn:
	{
		de: "Kannada",
		en: "Kannada",
	},
	languages_ko:
	{
		de: "Koreanisch",
		en: "Korean",
	},
	languages_ku:
	{
		de: "Kurdisch",
		en: "Kurdish",
	},
	languages_la:
	{
		de: "Latein",
		en: "Latin",
	},
	languages_lb:
	{
		de: "Luxemburgisch",
		en: "Luxembourgish",
	},
	languages_lo:
	{
		de: "Laotisch",
		en: "Lao",
	},
	languages_lt:
	{
		de: "Litauisch",
		en: "Lithuanian",
	},
	languages_lv:
	{
		de: "Lettisch",
		en: "Latvian",
	},
	languages_mk:
	{
		de: "Mazedonisch",
		en: "Macedonian",
	},
	languages_ml:
	{
		de: "Malayalam",
		en: "Malayalam",
	},
	languages_mr:
	{
		de: "Marathi",
		en: "Marathi",
	},
	languages_ms:
	{
		de: "Malaiisch",
		en: "Malay",
	},
	languages_mt:
	{
		de: "Maltesisch",
		en: "Maltese",
	},
	languages_nl:
	{
		de: "Niederländisch",
		en: "Dutch",
	},
	languages_no:
	{
		de: "Norwegisch",
		en: "Norwegian",
	},
	languages_oc:
	{
		de: "Okzitanisch",
		en: "Occitan",
	},
	languages_pl:
	{
		de: "Polnisch",
		en: "Polish",
	},
	languages_ps:
	{
		de: "Paschtunisch",
		en: "Pashto",
	},
	languages_pt:
	{
		de: "Portugiesisch",
		en: "Portuguese",
	},
	languages_ro:
	{
		de: "Rumänisch",
		en: "Romanian",
	},
	languages_roh:
	{
		de: "Rätoromanisch",
		en: "Romansch",
	},
	languages_ru:
	{
		de: "Russisch",
		en: "Russian",
	},
	languages_sgn:
	{
		de: "Gebärdensprache",
		en: "Sign language",
	},
	languages_sh:
	{
		de: "Serbo-Croatian",
		en: "Serbo-Croatian",
	},
	languages_sk:
	{
		de: "Slowakisch",
		en: "Slovak",
	},
	languages_sl:
	{
		de: "Slowenisch",
		en: "Slovenian",
	},
	languages_sq:
	{
		de: "Albanisch",
		en: "Albanian",
	},
	languages_sr:
	{
		de: "Serbisch",
		en: "Serbian",
	},
	languages_sv:
	{
		de: "Schwedisch",
		en: "Swedish",
	},
	languages_ta:
	{
		de: "Tamil",
		en: "Tamil",
	},
	languages_te:
	{
		de: "Telugu",
		en: "Telugu",
	},
	languages_th:
	{
		de: "Thailändisch",
		en: "Thai",
	},
	languages_tl:
	{
		de: "Tagalog",
		en: "Tagalog",
	},
	languages_tr:
	{
		de: "Türkisch",
		en: "Turkish",
	},
	languages_uk:
	{
		de: "Ukrainisch",
		en: "Ukrainian",
	},
	languages_us:
	{
		de: "US-Englisch",
		en: "US English",
	},
	languages_vi:
	{
		de: "Vietnamesisch",
		en: "Vietnamese",
	},
	languages_wel:
	{
		de: "Walisisch",
		en: "Welsh",
	},
	languages_wen:
	{
		de: "Sorbisch",
		en: "Sorbian",
	},
	languages_zgh:
	{
		de: "Tamazight",
		en: "Tamazight",
	},
	languages_zh:
	{
		de: "Chinesisch",
		en: "Chinese",
	},
	lastLogin:
	{
		de: "Letzter Login",
		en: "Last Login",
	},
	location:
	{
		de: "Ort",
		en: "Location",
	},
	latLong:
	{
		de: "Breitengrad, Längengrad",
		en: "Latitude, Longitude",
	},
	lookingFor:
	{
		de: "Ich suche",
		en: "Looking For",
	},
	lookingForOther:
	{
		de: "Sucht nach",
		en: "They're Looking For",
	},
	maxAge:
	{
		de: "Maximales Alter",
		en: "Maximal age",
	},
	messages:
	{
		de: "Nachrichten",
		en: "Messages",
	},
	metadata:
	{
		de: "Metadaten",
		en: "Metadata",
	},
	minAge:
	{
		de: "Minimales Alter",
		en: "Minimal age",
	},
	myAge:
	{
		de: "Mein Alter",
		en: "My Age",
	},
	myGender:
	{
		de: "Mein Geschlecht",
		en: "My gender",
	},
	myOrientation:
	{
		de: "Meine Orientierung",
		en: "My orientation",
	},
	new:
	{
		de: "Neu",
		en: "New",
	},
	noEntry:
	{
		de: "Keine Angabe",
		en: "No entry",
	},
	onlineStatus:
	{
		en: "Status",
	},
	onlineStatus_DATE:
	{
		en: "Date",
	},
	onlineStatus_OFFLINE:
	{
		en: "Offline",
	},
	onlineStatus_ONLINE:
	{
		en: "Online",
	},
	onlineStatus_SEX:
	{
		en: "Now",
	},
	travelersOnly:
	{
		de: "Nur Reisende",
		en: "Travelers only",
	},
	openTo:
	{
		de: "Offen für",
		en: "Open to",
	},
	openTo_FRIENDSHIP:
	{
		de: "Freunde",
		en: "Friends",
	},
	openTo_RELATIONSHIP:
	{
		de: "Beziehung",
		en: "Relationship",
	},
	openTo_SEXDATES:
	{
		en: "Sex",
	},
	orientation:
	{
		de: "Orientierung",
		en: "Orientation",
	},
	orientation_BISEXUAL:
	{
		de: "Bisexuell",
		en: "Bisexual",
	},
	orientation_GAY:
	{
		en: "Gay",
	},
	orientation_QUEER:
	{
		en: "Queer",
	},
	orientation_OTHER:
	{
		de: "Andere",
		en: "Other",
	},
	orientation_STRAIGHT:
	{
		de: "Hetero",
		en: "Straight",
	},
	other:
	{
		de: "Sonstige",
		en: "Other",
	},
	piercings:
	{
		en: "Piercings",
	},
	piercings_A_FEW:
	{
		de: "Wenige",
		en: "A few",
	},
	piercings_A_LOT:
	{
		de: "Viele",
		en: "A lot",
	},
	piercings_NO:
	{
		de: "Keine Piercings",
		en: "No piercings",
	},
	profileId:
	{
		de: "Profil-ID",
		en: "Profile ID",
	},
	relationship:
	{
		de: "Beziehung",
		en: "Relationship",
	},
	relationship_MARRIED:
	{
		de: "Verheiratet",
		en: "Married",
	},
	relationship_OPEN:
	{
		de: "Offene Partnerschaft",
		en: "Open",
	},
	relationship_PARTNER:
	{
		de: "Verpartnert",
		en: "Partner",
	},
	relationship_SINGLE:
	{
		en: "Single",
	},
	saferSex:
	{
		de: "Safer Sex",
		en: "Safer sex",
	},
	saferSex_ALWAYS:
	{
		en: "Safe",
	},
	saferSex_CONDOM:
	{
		de: "Kondom",
		en: "Condom",
	},
	saferSex_NEEDS_DISCUSSION:
	{
		de: "Nach Absprache",
		en: "Let's talk",
	},
	saferSex_PREP:
	{
		en: "PrEP",
	},
	saferSex_PREP_AND_CONDOM:
	{
		de: "PrEP und Kondom",
		en: "PrEP and condom",
	},
	saferSex_TASP:
	{
		en: "TasP",
	},
	searchFilter:
	{
		de: "Suche filtern",
		en: "Filter Search",
	},
	searchFilterDesc:
	{
		de: "Wendet Radar-Filter auf die Suchergebnisse an.",
		en: "Applies radar filter on the search results.",
	},
	sendEnter:
	{
		de: "Enter sendet Nachricht",
		en: "Enter sends message",
	},
	sendEnterDesc:
	{
		de: "Wenn deaktiviert erzeugt Enter einen Absatz und Strg+Enter sendet die Nachricht.",
		en: "If disabled, Enter creates a new line instead and Ctrl+Enter sends the message.",
	},
	sexual:
	{
		de: "Sexuelles",
		en: "Sexual",
	},
	sm:
	{
		de: "SM",
		en: "S&M",
	},
	sm_NO:
	{
		de: "Kein SM",
		en: "No SM",
	},
	sm_SOFT:
	{
		en: "Soft SM",
	},
	sm_YES:
	{
		en: "SM",
	},
	smoker:
	{
		de: "Raucher",
		en: "Smoker",
	},
	smoker_NO:
	{
		de: "Nein",
		en: "No",
	},
	smoker_SOCIALLY:
	{
		de: "Selten",
		en: "Socially",
	},
	smoker_YES:
	{
		de: "Ja",
		en: "Yes",
	},
	socialSmoker:
	{
		de: "Raucht selten",
		en: "Social Smoker",
	},
	speakingMyLanguage:
	{
		de: "Spricht meine Sprache",
		en: "Speaking my language",
	},
	systemMessages:
	{
		de: "Systemnachrichten",
		en: "System messages",
	},
	systemMessagesDesc:
	{
		de: "Erlaubt Popups wie Standort- oder Fehlermeldungen.",
		en: "Allows popups like GPS or error messages.",
	},
	tattoos:
	{
		en: "Tattoos",
	},
	tattoos_A_FEW:
	{
		de: "Wenige",
		en: "A few",
	},
	tattoos_A_LOT:
	{
		de: "Viele",
		en: "A lot",
	},
	tattoos_NO:
	{
		de: "Keine Tattoos",
		en: "No tattoos",
	},
	tiles:
	{
		de: "Benutzerkacheln",
		en: "User Tiles",
	},
	tileCount:
	{
		de: "Kachelspalten (0 für Standard)",
		en: "Tile columns (0 for default)",
	},
	typingNotifications:
	{
		de: "Tippbenachrichtigungen",
		en: "Typing notifications",
	},
	typingNotificationsDesc:
	{
		de: "Ob Empfänger die Eingabe einer Nachricht sehen können.",
		en: "Whether receivers can see that a message is being written.",
	},
	viewFullImage:
	{
		de: "Bild anzeigen",
		en: "Preview image",
	},
	viewProfile:
	{
		de: "Profilvorschau anzeigen",
		en: "Preview profile",
	},
	weight:
	{
		de: "Gewicht",
		en: "Weight",
	}
};

function translate(key)
{
	const translations = strings[key];
	return translations
		? translations[getLang()] || translations.en || "%" + key + "%"
		: "%" + key + "%";
}

function translateEnum(name, key)
{
	return translate(`${name}_${key}`);
}

// ---- Settings ----

const settingsNs = "RA_SETTINGS:";
let measurementSystem = "METRIC";
let radarFilter = {};
let tileDetails = new Set();
let tileStyle = null;

function load(name, fallback)
{
	const value = localStorage.getItem(settingsNs + name);
	return value === "false" ? false : value ? value : fallback;
}
function save(name, value)
{
	localStorage.setItem(settingsNs + name, value);
}

function getDiscoverFilter()
{
	return load("discoverFilter", false);
}
function getEnhancedFilter()
{
	return load("enhancedFilter", true);
}
function getEnhancedImages()
{
	return load("enhancedImages", true);
}
function getEnhancedTiles()
{
	return load("enhancedTiles", true);
}
function getFullHeadlines()
{
	return load("fullHeadlines", true);
}
function getFullMessages()
{
	return load("fullMessages", true);
}
function getHiddenMaxAge()
{
	return load("hiddenMaxAge", 99);
}
function getHiddenMinAge()
{
	return load("hiddenMinAge", 18);
}
function getHiddenUsers()
{
	return new Set(JSON.parse(load("hiddenUsers", `[]`)));
}
function getHideActivities()
{
	return load("hideActivities", true);
}
function getHideContacts()
{
	return load("hideContacts", false);
}
function getHideFriends()
{
	return load("hideFriends", true);
}
function getHideLikes()
{
	return load("hideLikes", true);
}
function getHideMessages()
{
	return load("hideMessages", false);
}
function getHideVisits()
{
	return load("hideVisits", true);
}
function getRadarFilter()
{
	return JSON.parse(load("radarFilter", `{}`));
}
function getSavedRadarFilter(id)
{
	return getSavedRadarFilters()[id] ?? getRadarFilter();
}
function getSavedRadarFilters()
{
	return JSON.parse(load("savedRadarFilters", "{}"));
}
function getSearchFilter()
{
	return load("searchFilter", false);
}
function getSendEnter()
{
	return load("sendEnter", true);
}
function getSystemMessages()
{
	return load("systemMessages", true);
}
function getTileCount()
{
	return parseInt(load("tileCount", 0));
}
function getTileDetails()
{
	return new Set(JSON.parse(load("tileDetails", `[ "age", "height", "bodyHair", "bodyType", "relationship", "analPosition" ]`)));
}
function getTypingNotifications()
{
	return load("typingNotifications", true);
}
function setDiscoverFilter(value)
{
	save("discoverFilter", value);
}
function setEnhancedFilter(value)
{
	save("enhancedFilter", value);
}
function setEnhancedImages(value)
{
	save("enhancedImages", value);
}
function setEnhancedTiles(value)
{
	save("enhancedTiles", value);
}
function setFullHeadlines(value)
{
	setCssProp("--tile-headline-white-space", value ? "unset" : "nowrap");
	save("fullHeadlines", value);
}
function setFullMessages(value)
{
	setCssProp("--message-line-clamp", value ? "unset" : "2");
	save("fullMessages", value);
}
function setHiddenMaxAge(value)
{
	save("hiddenMaxAge", value);
}
function setHiddenMinAge(value)
{
	save("hiddenMinAge", value);
}
function setHideActivities(value)
{
	save("hideActivities", value);
}
function setHideContacts(value)
{
	save("hideContacts", value);
}
function setHideFriends(value)
{
	save("hideFriends", value);
}
function setHideLikes(value)
{
	save("hideLikes", value);
}
function setHideMessages(value)
{
	save("hideMessages", value);
}
function setHideVisits(value)
{
	save("hideVisits", value);
}
function setRadarFilter()
{
	save("radarFilter", JSON.stringify(radarFilter));
}
function setSavedRadarFilter(id, value = null)
{
	const filters = JSON.parse(load("savedRadarFilters", "{}"));
	if (value)
		filters[id] = value;
	else
		delete filters[id];
	save("savedRadarFilters", JSON.stringify(filters));
}
function setSearchFilter(value)
{
	save("searchFilter", value);
}
function setSendEnter(value)
{
	save("sendEnter", value);
}
function setSystemMessages(value)
{
	setCssProp("--system-message-visibility", value ? "visible" : "collapse");
	save("systemMessages", value);
}
function setTileCount(value)
{
	if (value)
	{
		setCssProp("--tile-count", value);
		if (!tileStyle)
		{
			tileStyle = addCss(`
			:root
			{
				--tile-count: 0;
				--tile-size: calc(100% / max(1, var(--tile-count)) - 1px);
			}
			/* discover */
			section.js-main-stage > main main > section > ul
			{
				grid-template-columns: repeat(var(--tile-count), 1fr) !important;
			}
			/* radar desktop */
			.search-results__item
			{
				padding-bottom: var(--tile-size) !important;
				width: var(--tile-size) !important;
			}
			/* radar mobile - starts at 768px where .search-results__item turns inline, requiring to adjust .tile */
			@media not screen and (min-width: 768px)
			{
				.tile:not(.js-strip .tile):not(.tile--small)
				{
					width: var(--tile-size) !important;
				}
			}
			/* visitors */
			#cruise main > ul
			{
				grid-template-columns: repeat(var(--tile-count), 1fr);
			}
			`);
		}
	}
	else
	{
		if (tileStyle)
		{
			tileStyle.remove();
			tileStyle = null;
		}
	}
	save("tileCount", value);
}
function setTileDetail(key, visible)
{
	if (visible)
		tileDetails.add(key);
	else
		tileDetails.delete(key);
	save("tileDetails", JSON.stringify(Array.from(tileDetails)));
}
function setTypingNotifications(value)
{
	save("typingNotifications", value);
}
function setUserHidden(username, hide)
{
	let hiddenUsers = getHiddenUsers();
	if (hide)
		hiddenUsers.add(username);
	else
		hiddenUsers.delete(username);
	save("hiddenUsers", JSON.stringify(Array.from(hiddenUsers)));
}

// ---- Fetch/XHR ----

function addHook(hooks, func, method, route, callback)
{
	if (!hooks[func])
		hooks[func] = {};
	if (!hooks[func][method])
		hooks[func][method] = {};
	hooks[func][method][route] = callback;
}

function callHook(hooks, func, e)
{
	// Log XHR.
	if (func !== "open")
	{
		const prefix = func === "load" || func === "recv" ? e.status : "<<<";
		if (e.body)
			log(`${prefix} ${e.method} ${e.url}`, e.body);
		else
			log(`${prefix} ${e.method} ${e.url}`);
	}

	// Only handle success for now.
	if (e.status && (e.status < 200 || e.status > 299))
		return false;

	// Extract route.
	const routeStart = e.url.indexOf("/api/");
	if (routeStart === -1)
		return false;
	const routeEnd = e.url.indexOf("?");
	const route = e.url.substring(routeStart + "/api/".length, routeEnd === -1 ? undefined : routeEnd);

	// Route to matching hook.
	if (hooks[func] && hooks[func][e.method])
	{
		for (const [hookRoute, callback] of Object.entries(hooks[func][e.method]))
		{
			e.args = matchRoute(route, hookRoute);
			if (e.args !== undefined)
			{
				log(`🪝 ${func} ${e.method} ${route}`);
				callback(e);
				return true;
			}
		}
	}
	return false;
}

function matchRoute(route, match)
{
	if (route === match)
		return [];
	const routeParts = route.split("/");
	const matchParts = match.split("/");
	if (routeParts.length !== matchParts.length)
		return;
	const args = [];
	for (let i = 0; i < routeParts.length; ++i)
	{
		if (matchParts[i] === "*")
			args.push(routeParts[i]);
		else if (routeParts[i] !== matchParts[i])
			return;
	}
	return args;
}

// Fetch

const fetchHooks = {};
const realFetch = window.fetch;

function initFetch()
{
	async function getJsonBody(r)
	{
		// Use conversion to arrayBuffer to check if body exists as Firefox does not have a "body" property.
		const buffer = await r.clone().arrayBuffer();
		if (buffer.byteLength)
		{
			try
			{
				return JSON.parse(new TextDecoder().decode(buffer));
			}
			catch
			{
				return null; // not JSON, currently not interested
			}
		}
	}

	window.fetch = async (request, init) =>
	{
		if (!(request instanceof Request))
			return await realFetch(request, init);

		// Manipulate request.
		let e =
		{
			body: await getJsonBody(request),
			cancel: false,
			method: request.method,
			url: request.url,
		};
		if (e.body === null)
			return realFetch(request, init);
		if (callHook(fetchHooks, "send", e) && e.cancel)
			return;

		// Send request and receive response.
		const response = await realFetch(e.url, {
			body: JSON.stringify(e.body),
			cache: request.cache,
			credentials: request.credentials,
			headers: request.headers,
			integrity: request.integrity,
			keepalive: request.keepalive,
			method: e.method,
			mode: request.mode,
			redirect: request.redirect,
			referrer: request.referrer,
			referrerPolicy: request.referrerPolicy,
		});

		// Manipulate response.
		e =
		{
			body: await getJsonBody(response),
			cancel: false,
			url: request.url,
			method: request.method,
			status: response.status,
		};
		if (e.body === null)
			return response;
		if (callHook(fetchHooks, "recv", e) && e.cancel)
		{
			e.body = null;
			e.status = 404;
		}
		return new Response(JSON.stringify(e.body), { headers: response.headers, status: e.status });
	};
}

function onFetch(func, method, route, callback)
{
	addHook(fetchHooks, func, method, route, callback);
}

// XHR

const xhrHooks = {};
const xhrOpened = {};

function initXhr()
{
	// Hook open.
	const realOpen = window.XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function (method, url, async, user, password)
	{
		const e = { method: method, url: url };
		xhrOpened[this] = e;

		if (callHook(xhrHooks, "open", e))
		{
			method = e.method;
			url = e.url;
		}
		if (!e.cancel)
			realOpen.apply(this, arguments);

		// Hook load.
		this.addEventListener("load", () =>
		{
			const json = isJson(this.response);
			e.body = json ? JSON.parse(this.response) : this.response;
			e.status = this.status;

			if (callHook(xhrHooks, "load", e))
			{
				Object.defineProperty(this, "responseText", { writable: true });
				this.responseText = json ? JSON.stringify(e.body) : e.body;
			}
			if (e.cancel)
			{
				this.response = null;
				this.responseText = null;
				this.status = 404;
			}
		});
	};

	// Hook send.
	const realSend = window.XMLHttpRequest.prototype.send;
	window.XMLHttpRequest.prototype.send = function (body)
	{
		const e = xhrOpened[this];
		delete xhrOpened[this];
		const json = isJson(body);
		if (body)
			e.body = json ? JSON.parse(body) : body;

		if (callHook(xhrHooks, "send", e) && e.body)
			body = json ? JSON.stringify(e.body) : body;
		if (!e.cancel)
			realSend.apply(this, arguments);
	};
}

function onXhr(func, method, route, callback)
{
	addHook(xhrHooks, func, method, route, callback);
}

// ---- Lists ----

function createList(parent, { onGet, onName = null, onAdd = null, onRemove = null, onImport = null, onExport = null } = {})
{
	const container = addElement(parent, `<div class="ra_list"></div>`);

	function createButton(icon, text)
	{
		return `
			<a href="#" class="icon-labeled plain-text-link">
				<span class="icon icon-base ${icon}"></span>
				<span class="icon-labeled__label">${text}</span>
			</a>`;
	}

	// Add elements.
	const toolbar = addElement(container, `<div></div>`);
	const ul = addElement(container, `<ul></ul>`);

	// Create toolbar.
	const searchBox = addElement(toolbar, `<input class="input" type="text" placeholder="Search"></input>`);
	searchBox.addEventListener("input", e => updateList());

	function updateList()
	{
		const filter = searchBox.value.toUpperCase();
		ul.replaceChildren();

		const elements = onGet();
		for (const element of elements)
		{
			// Check if filtered away.
			const name = onName ? onName(element) : element;
			if (!name.toUpperCase().includes(filter))
				continue;

			// Create list entry.
			const li = addElement(ul, `<li></li>`);
			if (onRemove)
			{
				const deleteButton = addElement(li, createButton("icon-cross-negative", name));
				deleteButton.addEventListener("click", e =>
				{
					e.preventDefault();
					onRemove(element);
					li.remove();
				});
			}
			else
			{
				addElement(li, `<div>${name}</div>`);
			}
		}
	}

	if (onAdd)
	{
		const addButton = addElement(toolbar, createButton("icon-add-attachment", "Add"));
		addButton.addEventListener("click", e =>
		{
			e.preventDefault();
			onAdd(searchBox.value);
			updateList();
		});
	}

	if (onRemove)
	{
		const clearButton = addElement(toolbar, createButton("icon-trashcan", "Clear"));
		clearButton.addEventListener("click", e =>
		{
			e.preventDefault();
			if (confirm(translate("clearList")))
			{
				const elements = onGet();
				for (const element of elements)
					onRemove(element);
				updateList();
			}
		});
	}

	if (onImport)
	{
		const importButton = addElement(toolbar, createButton("icon-up-arrow", "Import"));
		importButton.addEventListener("click", e =>
		{
			e.preventDefault();
			// TODO: Handle import
			updateList();
		});
	}

	if (onExport)
	{
		const exportButton = addElement(toolbar, createButton("icon-down-arrow", "Export"));
		exportButton.addEventListener("click", e =>
		{
			e.preventDefault();
			// TODO: Handle export
			updateList();
		});
	}

	// Create list.
	updateList();
}

addCss(`
.ra_list
{
	display: flex;
	flex-direction: column;
	height: 250px;
}

.ra_list > div > a
{
	margin: 0 8px;

	display: inline-flex;
}

.ra_list > ul
{
	flex: 1;
	padding: 2px;
	overflow-y: auto;

	display: flex;
	flex-wrap: wrap;
	align-content: flex-start;
}

.ra_list > ul > li
{
	flex: 50%;
	flex-grow: 0;
	padding: 2px;
}
`);

// ---- Romeo ----

const apiKey = atob("QVM4YnpHSExBOFk5QlhGNzNpRE51UUJIZUVPMFVLamY=");
let sessionId;

async function blockUser(username)
{
	if (!username)
		return false;

	const profileId = profileCache[username].id;
	if (!profileId)
		return false;

	const response = await sendFetch("POST", "/api/v4/profiles/blocked", { profile_id: profileId, note: "" },
		`https://www.romeo.com/profile/${username}/report`);
	return response.ok;
}

function getImageUrl(url, size)
{
	const base = url.substring(0, url.indexOf("/img/usr/"));
	const file = url.substring(url.lastIndexOf("/") + 1);
	return size
		? `${base}/img/usr/squarish/${size}x${size}/${file}`
		: `${base}/img/usr/${file}`;
}

function getUsernameFromHref(href)
{
	let start = href.indexOf("profile/");
	if (start === -1)
		start = href.indexOf("hunq/");
	return href.substring(start).split("/")[1];
}

function sendFetch(method, url, body, referrer)
{
	const options =
	{
		method: method,
		cache: "no-cache",
		credentials: "same-origin",
		headers:
		{
			"x-api-key": apiKey,
			"x-session-id": sessionId,
			"x-site": "planetromeo"
		}
	};
	if (body)
	{
		options.headers["content-type"] = "application/json";
		options.body = JSON.stringify(body);
	}
	if (referrer)
		options.referrer = referrer;
	return realFetch(url, options);
}

function sendXhr(method, url)
{
	return new Promise((resolve, reject) =>
	{
		const xhr = new XMLHttpRequest();
		xhr.open(method, url);

		xhr.onload = () =>
		{
			if (xhr.status >= 200 && xhr.status < 300)
				resolve(xhr.response);
			else
				reject({ status: xhr.status, statusText: xhr.statusText });
		};
		xhr.onerror = () => reject({ status: xhr.status, statusText: xhr.statusText });

		xhr.setRequestHeader("x-api-key", apiKey);
		xhr.setRequestHeader("x-session-id", sessionId);

		xhr.send();
	});
}

function xhrHandleSession(reply)
{
	// Determine session ID.
	sessionId = reply.session_id;

	// Apply settings.
	const settings = reply.bb_settings;
	if (settings)
	{
		// Determine measurement locale.
		measurementSystem = settings.interface?.measurement_system ?? measurementSystem;

		// Determine radar filter, remove deleted ones.
		const radarFilterId = settings.bluebird?.search_filter?.id;
		radarFilter = getSavedRadarFilter(radarFilterId);
		for (const savedFilterId of Object.keys(getSavedRadarFilters()))
			if (savedFilterId && !reply.data.search_filters.find(x => x.id === savedFilterId))
				setSavedRadarFilter(savedFilterId);
	}

	// Determine intiial Discover filter.
	const filter = reply.bb_settings?.bluebird?.search_filter
		?? reply.data?.search_filters;
	if (filter)
	{
		radarFilter["filter[personal][age][max]"] = filter.personal.age.max;
		radarFilter["filter[personal][age][min]"] = filter.personal.age.min;
		radarFilter["filter[personal][height][max]"] = filter.personal.height.max;
		radarFilter["filter[personal][height][min]"] = filter.personal.height.min;
		radarFilter["filter[personal][weight][max]"] = filter.personal.weight.max;
		radarFilter["filter[personal][weight][min]"] = filter.personal.weight.min;
		if (!("filter[location][radius]" in radarFilter))
			radarFilter["filter[location][radius]"] = filter.location.radius;
	}

	// Enable client-side PLUS capabilities.
	const caps = reply.data?.capabilities;
	if (caps)
	{
		caps.can_save_unlimited_searches = true; // enables filter bookmarks
		caps.can_set_plus_radar_style = true; // enables Grid Stats selection
	}
}

addCss(`
:root
{
	--message-line-clamp: 2;
	--system-message-visibility: visible;
	--tile-headline-white-space: nowrap;
}

.feedback
{
	visibility: var(--system-message-visibility);
}

/* hide models on login page */
div[data-testid="desktop-image"]
{
	background-image: none;
}

/* hide PLUS message at bottom of visitor grid */
main#visitors > section
{
	display: none;
}

/* prevent mobile photo view on non-mobile displays */
@media screen and (min-width: 768px)
{
	.ReactModal__Overlay.ReactModal__Overlay--after-open > .ReactModal__Content.ReactModal__Content--after-open > main
	{
		background: none;
	}
	.container-button-next, .container-button-prev
	{
		display: flex;
	}
	.swiper-zoom-container > img
	{
		height: initial;
	}
}
`);

onXhr("load", "GET", "v4/session", e => xhrHandleSession(e.body));

// ---- Menu ----

const menuHandlers = {};
let menuBg, menuUl, menuX, menuY;

function initMenu()
{
	// Create context menu canceler.
	menuBg = addElement(document.body, "<div id='ra_context_bg'></ul>");
	menuBg.addEventListener("click", e => hideMenu());

	// Create context menu.
	menuUl = addElement(document.body, "<ul id='ra_context_ul'></ul>");

	// Attach to events.
	addEventListener("contextmenu", e =>
	{
		menuX = e.clientX;
		menuY = e.clientY;

		// Go through hierarchy of clicked elements.
		for (const el of document.elementsFromPoint(menuX, menuY))
		{
			// Stop when hitting a layer.
			if (el.classList.contains("layer")
				|| el.classList.contains("layout")
				|| el.classList.contains("ReactModal__Overlay"))
				break;
			// Invoke first context handler for this element.
			for (const [key, value] of Object.entries(menuHandlers))
			{
				if (el.matches(key))
				{
					log(`opening menu '${key}'`);
					value(el);
					e.preventDefault();
					return;
				}
			}
		}
	});
}

function showMenu(items)
{
	menuBg.style.display = "block";

	menuUl.replaceChildren();
	for (const item of items)
	{
		const li = addElement(menuUl, `
			<li class="ra_context_li">
				<span class="icon icon-${item.icon}"></span>
				${translate(item.text)}
			</li>`);
		li.addEventListener("click", e =>
		{
			hideMenu();
			item.onclick();
		});
	}

	menuUl.style.display = "block";
	const maxX = window.innerWidth - menuUl.offsetWidth;
	const maxY = window.innerHeight - menuUl.offsetHeight;
	menuUl.style.left = Math.min(menuX, maxX) + "px";
	menuUl.style.top = Math.min(menuY, maxY) + "px";
}

function hideMenu()
{
	menuBg.style.display = "none";
	menuUl.style.display = "none";
}

function onMenu(selector, handler)
{
	menuHandlers[selector] = handler;
}

function menuItem(icon, text, onclick)
{
	return { icon, text, onclick };
}

addCss(`
#ra_context_bg
{
	background: transparent;
	display: none;
	height: 100%;
	position: fixed;
	width: 100%;
	z-index: 100000;
}

#ra_context_ul
{
	background: #232323;
	border-radius: 1.125rem;
	box-shadow: rgba(0, 0, 0, 0.32) 0px 0px 2px, rgba(0, 0, 0, 0.24) 0px 0px 1px, rgba(0, 0, 0, 0.16) 0px 0px 5px;
	display: none;
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	font-size: 94%;
	overflow: hidden;
	position: absolute;
	z-index: 100001;
}

.ra_context_li
{
	border-color: transparent;
	border-left: 2px solid transparent;
	border-style: solid;
	border-width: 1px 1px 1px 2px;
	color: #FFF;
	cursor: default;
	padding: 9px 18px 10px 10px;
	transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1);
	white-space: nowrap;
}

.ra_context_li:not(:first-child) {
	border-top: 1px solid rgba(255, 255, 255, 0.16);
}

.ra_context_li .icon
{
	margin: 4px;
}

.ra_context_li:hover
{
	background: #2E2E2E;
}

@media screen and (max-width: 767px)
{
	#ra_context_bg
	{
		background: rgba(0, 0, 0, 0.6);
	}
	#ra_context_ul
	{
		border-bottom-left-radius: 0;
		border-bottom-right-radius: 0;
		bottom: 0;
		left: unset !important;
		position: fixed;
		top: unset !important;
		width: 100%;
	}
	.ra_context_li
	{
		padding: 6px;
	}
	.ra_context_li .icon
	{
		font-size: 1.2rem;
		margin: 8px;
	}
}
`);

// ---- Previews ----

let previewLayer;

function createPreview(title)
{
	const container = document.querySelector("#spotlight-container");
	previewLayer = addElement(container, `
		<div class="layer layer--spotlight" style="top:0;z-index:10000;">
			<div id="ra_preview_inner">
				<div class="js-header layout-item">
					<div class="layer-header layer-header--primary">
						<a class="back-button l-tappable js-back marionette" href="#">
							<span class="js-back-icon icon icon-cross icon-regular"></span>
						</a>
						<div class="layer-header__title js-title typo-section-navigation" style="text-align:center">
							<h2>${title}</h2>
						</div>
					</div>
				</div>
			</div>
		</div>`);

	previewLayer.addEventListener("click", e =>
	{
		if (e.target === previewLayer)
			previewLayer.remove();
	});
	previewLayer.querySelector(".js-back").addEventListener("click", e => previewLayer.remove());

	return previewLayer.querySelector("#ra_preview_inner");
}

function initPreviews()
{
	window.addEventListener("popstate", e =>
	{
		// Restore navigating back to preview.
		switch (e.state?.ra_preview)
		{
			case "image":
				showImagePreview(e.state.src, false);
				break;
			case "profile":
				showProfilePreview(e.state.username, false);
				break;
		}
	});
	window.navigation?.addEventListener("navigate", e =>
	{
		// Hide preview on any other navigation.
		previewLayer?.remove();
	});
}

function showImagePreview(src, pushHistory = true)
{
	if (pushHistory)
		history.pushState({ ra_preview: "image", src: src }, "");

	const content = addElement(createPreview(translate("viewFullImage")), `<div id="ra_image_content"></div>`);
	addElement(content, `<img id="ra_profile_pic" src="${src}"></img>`);
}

function showProfilePreview(username, pushHistory = true)
{
	function isEntry(value)
	{
		return value && value !== "NO_ENTRY";
	}

	function addSection(el, key)
	{
		return addElement(el, `
			<details class="ra_profile_details" open>
				<summary class="ra_profile_summary">${translate(key)}</summary>
			</details>`);
	}

	function add(section, key, value)
	{
		if (value)
		{
			addElement(section, `
				<div class="ra_profile_keyvalue">
					<div>${translate(key)}</div>
					<div>${value}</div>
				</div>`);
		}
	}
	function addAgeRange(section, range)
	{
		if (range)
			add(section, "ageRange", getProfileAgeRange(range));
	}
	function addArrayEnum(section, key, array)
	{
		if (!array)
			return;
		let values = [];
		for (let i = 0; i < array.length; i++)
			if (isEntry(array[i]))
				values.push(translateEnum(key, array[i]));
		if (values.length)
			add(section, key, values.join(", "));
	}
	function addDistance(section, distance, sensor)
	{
		let text = measurementSystem === "METRIC"
			? `${distance / 1000} km`
			: `${round(distance * M2MI, 1)}mi`;
		if (sensor)
			text += " (GPS)";
		add(section, "distance", text);
	}
	function addEnum(section, key, value)
	{
		if (isEntry(value))
			add(section, key, translateEnum(key, value));
	}
	function addGender(section, genderOrientation)
	{
		let values = [];
		if (isEntry(genderOrientation?.orientation))
			values.push(translateEnum("orientation", genderOrientation.orientation));
		if (isEntry(genderOrientation?.gender))
			values.push(translateEnum("gender", genderOrientation.gender));
		if (values.length)
			add(section, "genderOrientation", values.join(" / "));
	}

	const profile = profileCache[username];
	if (!profile)
		return;
	const personal = profile.personal;
	const sexual = profile.sexual;

	if (pushHistory)
		history.pushState({ ra_preview: "profile", username: username }, "");

	const content = addElement(createPreview(username), `<div id="ra_profile_content"></div>`);
	const left = addElement(content, `<div id="ra_profile_left"></div>`);
	const right = addElement(content, `<div id="ra_profile_right"></div>`);

	addElement(left, `<div>${escapeHtml(profile.headline ?? "")}</div>`);

	const img = addElement(left, `<img id="ra_profile_pic"></img>`);
	img.src = profile.pic ? `/img/usr/${profile.pic}.jpg` : "/assets/f8a7712027544ed03920.svg";

	const section = addSection(right, "metadata");
	addEnum(section, "onlineStatus", profile.online_status);
	if (profile.last_login)
		add(section, "lastLogin", formatTime(profile.last_login));
	if (profile.location)
	{
		add(section, "location", `${profile.location.name}, ${profile.location.country}`);
		addDistance(section, profile.location.distance, profile.location.sensor);
	}
	add(section, "profileId", profile.id);

	if (personal)
	{
		const section = addSection(right, "lookingFor");
		addArrayEnum(section, "openTo", personal.looking_for);
		addAgeRange(section, personal.target_age);
		addArrayEnum(section, "gender", personal.gender_orientation?.looking_for_gender);
		addArrayEnum(section, "orientation", personal.gender_orientation?.looking_for_orientation);
		if (!section.querySelectorAll(".ra_profile_keyvalue").length)
			section.remove();
	}

	if (personal)
	{
		const section = addSection(right, "general");
		add(section, "age", personal.age);
		add(section, "height", getProfileHeight(personal.height));
		add(section, "weight", getProfileWeight(personal.weight));
		add(section, "bmi", getProfileBmi(personal.height, personal.weight, true));
		add(section, "bodyType", getProfileEnum("bodyType", personal.body_type));
		add(section, "ethnicity", getProfileEnum("ethnicity", personal.ethnicity));
		addEnum(section, "hairLength", personal.hair_length);
		addEnum(section, "hairColor", personal.hair_color);
		addEnum(section, "beard", personal.beard);
		addEnum(section, "eyeColor", personal.eye_color);
		add(section, "bodyHair", getProfileEnum("bodyHair", personal.body_hair));
		addGender(section, personal?.gender_orientation);
		addEnum(section, "smoker", personal.smoker);
		addEnum(section, "tattoos", personal.tattoo);
		addEnum(section, "piercings", personal.piercing);
		addArrayEnum(section, "languages", personal.spoken_languages);
		add(section, "relationship", getProfileEnum("relationship", personal.relationship));
	}

	if (sexual)
	{
		const section = addSection(right, "sexual");
		add(section, "analPosition", getProfileEnum("analPosition", sexual.anal_position));
		add(section, "dick", getProfileDick(sexual.dick_size, sexual.concision));
		addArrayEnum(section, "fetish", sexual.fetish);
		add(section, "dirty", getProfileEnum("dirty", sexual.dirty_sex));
		addEnum(section, "fisting", sexual.fisting);
		addEnum(section, "sm", sexual.sm);
		add(section, "saferSex", getProfileEnum("saferSex", sexual.safer_sex));
		if (!section.querySelectorAll(".ra_profile_keyvalue").length)
			section.remove();
	}

	if (profile.personal?.profile_text)
	{
		const section = addSection(right, "aboutMe");
		addElement(section, `<div id="ra_profile_text">${profile.personal.profile_text}</div>`);
	}
}

addCss(`
#ra_preview_inner
{
	background-color: black;
	display: grid;
	grid-template-rows: min-content auto;
	height: 100%;
}

#ra_image_content
{
	overflow-y: scroll;
	padding: 16px;
}

#ra_profile_content
{
	display: grid;
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	grid-template-columns: auto 352px;
	overflow-y: scroll;
	word-break: break-word;
}

#ra_profile_left
{
	background: #121212;
	overflow-y: scroll;
	padding: 16px;
}

#ra_profile_right
{
	overflow-y: scroll;
	padding: 16px;
}

.ra_profile_details:not(:first-child)
{
	border-top: 1px solid rgb(46, 46, 46);
	margin-top: 1rem;
}

.ra_profile_summary
{
	padding: 1rem 0;
}

.ra_profile_keyvalue
{
	display: grid;
	gap: 16px;
	grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
}

.ra_profile_keyvalue > :first-child
{
	color: rgba(255, 255, 255, 0.6);
	text-align: right;
}

#ra_profile_text
{
	white-space: pre-line;
}

@media screen and (max-width: 767px)
{
	#ra_profile_content
	{
		grid-template-columns: initial;
		grid-template-rows: auto auto;
	}
	#ra_profile_left
	{
		overflow-y: initial;
	}
	#ra_profile_right
	{
		overflow-y: initial;
	}
	#ra_profile_pic
	{
		width: 100%;
	}
}
`);

// ---- Profiles ----

const profileCache = {};

function cacheProfile(profileObject)
{
	const existing = profileCache[profileObject.name];
	const profile =
	{
		id: profileObject.id,
		name: profileObject.name,
		headline: profileObject.headline ?? existing?.headline,
		last_login: profileObject.last_login ?? existing?.last_login,
		location: profileObject.location ?? existing?.location,
		online_status: profileObject.online_status ?? existing?.online_status,
		pic: profileObject.preview_pic?.url_token ?? existing?.pic, // not available if no picture
		personal: profileObject.profile?.personal ?? profileObject.personal ?? existing?.personal, // not available in activities
		sexual: profileObject.profile?.sexual ?? profileObject.sexual ?? existing?.sexual, // not available in activities
		albums: profileObject.albumsV2 ?? existing?.albumsV2
	};
	profileCache[profile.name] = profile;
	return profile;
}
function filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames)
{
	// Return whether to display the profile.
	return (!profile.personal || profile.personal.age >= hiddenMinAge && profile.personal.age <= hiddenMaxAge)
		&& !hiddenNames.has(profile.name);
}
function filterItemsAndCacheProfiles(items, profileSelector, filter)
{
	let newItems = [];
	const hiddenMaxAge = getHiddenMaxAge();
	const hiddenMinAge = getHiddenMinAge();
	const hiddenNames = getHiddenUsers();

	for (const item of items ?? [])
	{
		const profile = cacheProfile(profileSelector(item));
		if (!filter || filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames))
			newItems.push(item);
	}
	return newItems;
}

function getProfileAgeRange(range, short)
{
	if (range)
	{
		const min = range.min ?? "18";
		const max = range.max ?? "99";
		return short
			? `${min}-${max}`
			: translate("ageRangeValue").replace("$from", min).replace("$to", max);
	}
}
function getProfileBmi(height, weight, withName)
{
	if (height && weight)
	{
		const bmi = weight / Math.pow(height / 100, 2);
		let result = `${round(bmi, 1).toFixed(1)}`;

		if (withName)
		{
			for (const [max, key] of Object.entries({
				16: "bmiSevereThin",
				17: "bmiModerateThin",
				18.5: "bmiMildThin",
				25: "bmiNormal",
				30: "bmiPreObese",
				35: "bmiObese1",
				40: "bmiObese2",
				99: "bmiObese3",
			}))
			{
				if (bmi < max)
					return result + ` / ${translate(key)}`;
			}
		}
		return result;
	}
}
function getProfileDick(size, concision)
{
	let values = [];
	if (size && size !== "NO_ENTRY")
		values.push(translateEnum("dick", size));
	if (concision && concision !== "NO_ENTRY")
		values.push(translateEnum("concision", concision));
	if (values.length)
		return values.join(" - ");
}
function getProfileEnum(key, value)
{
	if (value && value !== "NO_ENTRY")
		return translateEnum(key, value);
}
function getProfileHeight(height)
{
	if (height)
	{
		return measurementSystem === "METRIC"
			? `${height}cm`
			: `${round(height * CM2FT, 2)} ft`;
	}
}
function getProfileWeight(weight)
{
	if (weight)
	{
		return measurementSystem === "METRIC"
			? `${weight}kg`
			: `${round(weight * KG2LBS)}lbs`;
	}
}

function xhrHandleProfiles(e)
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, true);
	e.body.items_limited = e.body.items_total; // Remove PLUS ad tile.

	// Show every user as a large tile.
	if (getEnhancedTiles())
		for (const item of e.body.items ?? [])
			if (item.display)
				item.display.large_tile = true;
}
function xhrHandleVisits(e)
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, getHideVisits());
	e.body.items_limited = e.body.items_total; // Restore PLUS-visible visitors.
}

onXhr("load", "GET", "v4/contacts", e =>
{
	if (e.body.cursors)
		e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideContacts());
});

onXhr("load", "GET", "v4/messages/conversations", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.chat_partner, getHideMessages());
});

onXhr("load", "GET", "+/notifications/activity-stream", e =>
{
	e.body = filterItemsAndCacheProfiles(e.body, x => x.partner, getHideActivities());
});

onFetch("recv", "GET", "v4/profiles", e => xhrHandleProfiles(e));
onFetch("recv", "GET", "v4/profiles/popular", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/hunqz/profiles", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles/list", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles/popular", e => xhrHandleProfiles(e));

onFetch("recv", "GET", "v4/visitors", e => xhrHandleVisits(e));
onFetch("recv", "GET", "v4/visits", e => xhrHandleVisits(e));
onFetch("recv", "GET", "v4/reactions/cruise/likes", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideLikes());
});

onXhr("load", "GET", "v4/messages/*", e => cacheProfile(e.body));
onXhr("load", "GET", "v4/profiles/*", e => cacheProfile(e.body));
onXhr("load", "GET", "v4/profiles/*/full", e => cacheProfile(e.body));
onFetch("recv", "GET", "v4/profiles/*/linked", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, getHideFriends());
});
onXhr("load", "GET", "v4/reactions/pictures/basic", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.user_id, getHideLikes());
});

// ---- Filter ----

function addRadarFilter(filter, key, value)
{
	if (!isMultiRadarFilter(key))
		filter[key] = value;
	else if (key in filter)
		filter[key].push(value);
	else
		filter[key] = [value];
}

function hasRadarFilter(filter, key, value)
{
	return key in filter
		&& (value === undefined || (isMultiRadarFilter(key)
			? filter[key].includes(value)
			: filter[key] === value));
}

function isMultiRadarFilter(key)
{
	return key.endsWith("[]");
}

function removeRadarFilter(filter, key, value)
{
	if (!hasRadarFilter(filter, key, value))
		return;
	if (isMultiRadarFilter(key))
	{
		filter[key] = filter[key].filter(x => x !== value);
		if (!filter[key].length)
			delete filter[key];
	}
	else
	{
		delete filter[key];
	}
}

function refreshFilter()
{
	// Save filter.
	setRadarFilter();
	// Reset filter title, enable filter reset button.
	document.querySelector(`.js-filter-header p[class^="ResponsiveBodyText-sc-"]`).innerHTML = translate("filters");
	document.querySelector(".js-clear-all").classList.remove("is-disabled");
	// Update results.
	document.querySelector("section.js-main-stage div.js-navigation a.is-selected, div.js-nav-item").click();
}

function packRadarFilter(params)
{
	let filter = {};
	for (const [key, value] of params)
		addRadarFilter(filter, key, value);
	return filter;
}

function unpackRadarFilter(filter)
{
	let params = [];
	for (const key in filter)
	{
		if (isMultiRadarFilter(key))
		{
			for (const value of filter[key])
				params.push([key, value]);
		}
		else
		{
			params.push([key, filter[key]]);
		}
	}
	return params;
}

function replaceFilterContainer(el)
{
	if (!getEnhancedFilter())
		return;

	// Remove plus color from bookmark action.
	const save = el.querySelector(".js-filter-actions .js-save");
	save?.classList.remove("is-plus");

	// Remove all filters on reset.
	const clearAll = el.querySelector(".js-filter-actions .js-clear-all");
	if (Object.keys(radarFilter).length)
		clearAll.classList.remove("is-disabled");
	clearAll.addEventListener("click", e =>
	{
		radarFilter = {};
		setRadarFilter();
		// Filter panel is recreated by default handler, recreate selections.
		setTimeout(() => replaceFilterContainer(el));
	});

	// Clear any remaining extended filters.
	filter = el.querySelector(".filter");
	for (const tags of filter.querySelectorAll(".filter__params-tags.js-tags-list"))
		tags.remove();

	// Remove PLUS-Filter ad if no original filters are selected.
	if (filter.querySelector(".js-quick-filter .js-add-params-button.plain-text-link"))
		filter.querySelector(".js-quick-filter .filter__group-more-options").remove();

	// Add custom filters.

	function addSection(text)
	{
		return addElement(filter, `
			<div class="filter__params-tags js-tags-list">
				<h3 class="typo mb-">${translate(text)}</h3>
			</div>`);
	}

	function addSectionList(text)
	{
		const section = addSection(text);
		return addElement(section, `<ul class="js-list tags-list"></ul>`);
	}

	function addSectionListMulti(text, prefix, filterKey, filterValues, hasNoEntry = true)
	{
		const section = addSectionList(text);
		for (const filterValue of filterValues)
			addListTagFilter(section, `${prefix}_${filterValue}`, filterKey, filterValue);
		if (hasNoEntry)
			addListTagFilter(section, "noEntry", filterKey, "NO_ENTRY");
		return section;
	};

	function addListTag(ul, text, selected, change)
	{
		const li = addElement(ul, `
			<li class="tags-list__item">
				<a class="js-tag ui-tag ui-tag--removable txt-truncate">
					<span class="ui-tag__label">${translate(text)}</span>
				</a>
			</li>`);
		const a = li.querySelector("a");
		if (selected)
			a.classList.add("ui-tag--selected");
		li.addEventListener("click", e =>
		{
			e.preventDefault();
			if (a.classList.contains("ui-tag--selected"))
			{
				change(false);
				a.classList.remove("ui-tag--selected");
			}
			else
			{
				change(true);
				a.classList.add("ui-tag--selected");
			}
		});
	}

	function addListTagFilter(ul, text, filterKey, filterValue)
	{
		let selected = hasRadarFilter(radarFilter, filterKey, filterValue);
		return addListTag(ul, text, selected, checked =>
		{
			if (checked)
				addRadarFilter(radarFilter, filterKey, filterValue);
			else
				removeRadarFilter(radarFilter, filterKey, filterValue);
			refreshFilter();
		});
	}

	function addInput(ul)
	{
		return addElement(ul, `
			<div class="filter__group">
				<div class="js-fulltext-input filter__group--fulltext">
					<div class="Container--uQSLs layout layout--v-center">
						<div class="layout-item layout-item--consume">
							<input class="js-input Input--EicBC input" autocorrect="off" autocapitalize="off" spellcheck="false">
						</div>
					</div>
				</div>
			</div>`).querySelector("input");
	}

	addSectionListMulti("lookingForOther", "openTo", "filter[personal][looking_for][]",
		["SEXDATES", "FRIENDSHIP", "RELATIONSHIP"]);

	addSectionListMulti("bodyType", "bodyType", "filter[personal][body_type][]",
		["SLIM", "AVERAGE", "ATHLETIC", "MUSCULAR", "BELLY", "STOCKY"]);
	addSectionListMulti("ethnicity", "ethnicity", "filter[personal][ethnicity][]",
		["CAUCASIAN", "ASIAN", "LATIN", "MEDITERRANEAN", "BLACK", "MIXED", "ARAB", "INDIAN"]);
	addSectionListMulti("hairLength", "hairLength", "filter[personal][hair_length][]",
		["SHAVED", "SHORT", "AVERAGE", "LONG", "PUNK"]);
	addSectionListMulti("hairColor", "hairColor", "filter[personal][hair_color][]",
		["BLOND", "LIGHT_BROWN", "BROWN", "BLACK", "GREY", "OTHER", "RED"]);
	addSectionListMulti("beard", "beard", "filter[personal][beard][]",
		["DESIGNER_STUBBLE", "MOUSTACHE", "GOATEE", "FULL_BEARD", "NO_BEARD"]);
	addSectionListMulti("eyeColor", "eyeColor", "filter[personal][eye_color][]",
		["BLUE", "BROWN", "GREY", "GREEN", "OTHER"]);
	addSectionListMulti("bodyHair", "bodyHair", "filter[personal][body_hair][]",
		["SMOOTH", "SHAVED", "LITTLE", "AVERAGE", "VERY_HAIRY"]);
	addSectionListMulti("gender", "gender", "filter[personal][gender_orientation][gender][]",
		["MAN", "TRANS_MAN", "TRANS_WOMAN", "NON_BINARY", "OTHER"]);
	addSectionListMulti("orientation", "orientation", "filter[personal][gender_orientation][orientation][]",
		["GAY", "BISEXUAL", "QUEER", "STRAIGHT", "OTHER"]);
	addSectionListMulti("smoker", "smoker", "filter[personal][smoker][]",
		["NO", "SOCIALLY", "YES"]);
	addSectionListMulti("tattoos", "tattoos", "filter[personal][tattoo][]",
		["A_FEW", "A_LOT", "NO"]);
	addSectionListMulti("piercings", "piercings", "filter[personal][piercing][]",
		["A_FEW", "A_LOT", "NO"]);
	addSectionListMulti("relationship", "relationship", "filter[personal][relationship][]",
		["SINGLE", "PARTNER", "OPEN", "MARRIED"]);

	addSectionListMulti("analPosition", "analPosition", "filter[sexual][anal_position][]",
		["TOP_ONLY", "MORE_TOP", "VERSATILE", "MORE_BOTTOM", "BOTTOM_ONLY", "NO"]);
	addSectionListMulti("dick", "dick", "filter[sexual][dick_size][]",
		["S", "M", "L", "XL", "XXL"]);
	addSectionListMulti("concision", "concision", "filter[sexual][concision][]",
		["CUT", "UNCUT"]);
	addSectionListMulti("fetish", "fetish", "filter[sexual][fetish][]",
		["LEATHER", "SPORTS", "SKATER", "RUBBER", "UNDERWEAR", "SKINS", "BOOTS", "LYCRA", "UNIFORM", "FORMAL",
			"TECHNO", "SNEAKERS", "JEANS", "DRAG", "WORKER", "CROSSDRESSING"]);
	addSectionListMulti("dirty", "dirty", "filter[sexual][dirty_sex][]",
		["YES", "NO", "WS_ONLY"]);
	addSectionListMulti("fisting", "fisting", "filter[sexual][fisting][]",
		["ACTIVE", "ACTIVE_PASSIVE", "PASSIVE", "NO"]);
	addSectionListMulti("sm", "sm", "filter[sexual][sm][]",
		["YES", "SOFT", "NO"]);
	addSectionListMulti("saferSex", "saferSex", "filter[sexual][safer_sex][]",
		["ALWAYS", "NEEDS_DISCUSSION", "CONDOM", "PREP", "PREP_AND_CONDOM", "TASP"]);

	addSectionListMulti("interests", "interests", "filter[hobby][interests][]",
		["ART", "BOARDGAME", "CAR", "COLLECT", "COMPUTER", "COOK", "DANCE", "FILM", "FOTO", "GAME", "LITERATURE",
			"MODELING", "MOTORBIKE", "MUSIC", "NATURE", "POLITICS", "TV"], false);

	const section = addSectionList("other");

	const coordInput = addInput(section);
	coordInput.type = "text";
	coordInput.placeholder = translate("latLong");
	if ("filter[location][lat]" in radarFilter && "filter[location][long]" in radarFilter)
		coordInput.value = `${radarFilter["filter[location][lat]"]}, ${radarFilter["filter[location][long]"]}`;
	coordInput.addEventListener("change", e =>
	{
		removeRadarFilter(radarFilter, "filter[location][lat]");
		removeRadarFilter(radarFilter, "filter[location][long]");
		const sep = e.target.value.indexOf(", ");
		if (sep !== -1)
		{
			const lat = parseFloat(e.target.value);
			const long = parseFloat(e.target.value.substring(sep + 2));
			if (!isNaN(lat) && !isNaN(long))
			{
				addRadarFilter(radarFilter, "filter[location][lat]", lat.toString());
				addRadarFilter(radarFilter, "filter[location][long]", long.toString());
			}
		}
		refreshFilter();
	});

	const radiusInput = addInput(section);
	radiusInput.type = "text";
	radiusInput.placeholder = translate("customRadius");
	if ("filter[location][radius]" in radarFilter)
	{
		const radius = radarFilter["filter[location][radius]"];
		radiusInput.value = measurementSystem === "METRIC"
			? radius / 1000
			: round(radius * M2MI, 1);
	}
	radiusInput.addEventListener("change", e =>
	{
		removeRadarFilter(radarFilter, "filter[location][radius]");
		if (parseInt(e.target.value))
		{
			const radius = measurementSystem === "METRIC"
				? e.target.value * 1000
				: e.target.value / M2MI;
			addRadarFilter(radarFilter, "filter[location][radius]", radius);
		}
		refreshFilter();
	});

	addListTagFilter(section, "bedAndBreakfast", "filter[bed_and_breakfast_filter]", "ONLY");
	addListTagFilter(section, "travelersOnly", "filter[travellers_filter]", "TRAVELLERS_ONLY");
	addListTagFilter(section, "speakingMyLanguage", "filter[personal][speaks_my_languages]", "true");
}

function xhrApplyFilter(url, discover)
{
	let [baseUrl, params] = decodeUrl(url);
	let filter = packRadarFilter(params);

	if (discover)
	{
		// Discover
		if (!getDiscoverFilter())
			return url;
	}
	else if ("filter[username]" in filter)
	{
		// Search (plain text only, #-prefixed text generates fulltext search).
		if (!getSearchFilter())
			return url;
	}
	else
	{
		// Radar

		// Store Radar-only configurable parameters for Discover page.
		function saveFilter(key)
		{
			if (filter[key])
				radarFilter[key] = filter[key];
		}
		saveFilter("filter[personal][age][max]");
		saveFilter("filter[personal][age][min]");
		saveFilter("filter[personal][height][max]");
		saveFilter("filter[personal][height][min]");
		saveFilter("filter[personal][weight][max]");
		saveFilter("filter[personal][weight][min]");

		if (!getEnhancedFilter())
			return url;
	}

	// Combine with custom parameters.
	filter = { ...filter, ...radarFilter };
	params = unpackRadarFilter(filter);
	return encodeUrl(baseUrl, params);
}

addCss(`
/* enhanced radar filter */
.js-quick-filter
{
	overflow-y: scroll;
}

/* restore bookmark icon color */
.ui-navbar__button--bookmarks .icon.icon-bookmark-outlined
{
	color: #00bdff !important;
}

/* fix height of filter on mobile */
.sidebar .filter-container
{
	height: unset !important;
}
`);

onDom(`.js-quick-filter`, el =>
{
	replaceFilterContainer(el.parentNode);
});

onFetch("send", "GET", "v4/profiles", e => e.url = xhrApplyFilter(e.url, true));
onXhr("open", "GET", "v4/hunqz/profiles", e => e.url = xhrApplyFilter(e.url));
onXhr("open", "GET", "v4/profiles", e => e.url = xhrApplyFilter(e.url));
onXhr("open", "GET", "v4/profiles/popular", e => e.url = xhrApplyFilter(e.url));

onXhr("send", "PUT", "v4/settings/interface/bluebird", e =>
{
	// Changed filter.
	const id = e.body.search_filter.id;
	if (id)
		radarFilter = getSavedRadarFilter(id);

	const quickFilter = document.querySelector(".js-quick-filter")?.parentNode;
	if (quickFilter)
		replaceFilterContainer(quickFilter);
});
onXhr("load", "DELETE", "v4/search/filters/*", e =>
{
	// Deleted filter.
	const id = e.args[0];
	setSavedRadarFilter(id);
});
onXhr("load", "POST", "v4/search/filters", e =>
{
	// Created filter.
	const id = e.body.id;
	setSavedRadarFilter(id, radarFilter);
});

// ---- Tiles ----

const selTileDiscover = `section.js-content main > section > ul > li > a[href^="/profile/"]`; // li
const selTileRadarSmall = `div.js-search-results div.tile > div.reactView > a[href^="/profile/"]`; // div.tile (query first)
const selTileRadarLarge = `div.js-search-results div.tile--plus > div.reactView > a[href^="/profile/"]`; // div.search-results__item
const selTileRadarImage = `div.js-search-results div.tile > div.reactView > div.SMALL`; // div.tile
const selTileVisitors = `main#visitors a[href^="/profile/"]`; // li
const selTileVisited = `main#visited-grid a[href^="/profile/"]`; // li
const selTileLikes = `main#likers-list a[href^="/profile/"]`; // li
const selTileFriends = `section.js-profile-stats li > a[href^="/profile/"]`; // li
const selTileFriendsList = `main#friends-list li > a[href^="/profile/"]`; // li
const selTilePicLikes = `main#liked-by-list a[href^="/profile/"]`; // li
const selTileSearch = `div.js-results a[href^="/profile/"]`; // div.tile
const selTileActivity = `div.js-as-content div.tile a[href^="/profile/"]`; // div.listitem

function createTileMenu(el, username, removeOnHide, removeOnBlock)
{
	return [
		menuItem("search", "viewProfile", () => showProfilePreview(username)),
		menuItem("hide-visit", "hideUser", () =>
		{
			setUserHidden(username, true);
			if (removeOnHide)
				el.style.display = "none";
		}),
		menuItem("illegal", "blockUser", () =>
		{
			blockUser(username);
			if (removeOnBlock)
				el.style.display = "none";
		}),
	];
}

addCss(`
/* fix jumping fade in mobile visitors/visits during load */
div.BIG::before, div.SMALL::before
{
	inset: 60% 0px 0px !important;
}

/* tile description truncation */
.tile p[class^="SpecialText-"]
{
	white-space: var(--tile-headline-white-space);
}

/* 2 friend list tile columns */
section.js-profile-stats ul, main#friends-list ul
{
	grid-template-columns: 1fr 1fr;
}

.ra_tile_headline
{
	color: rgb(255, 255, 255);
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	font-size: 0.8125rem;
	font-weight: 400;
	line-height: 1.23077;
	overflow: hidden;
	text-overflow: ellipsis;
	text-shadow: rgba(0, 0, 0, 0.32) 0px 1px 1px, rgba(0, 0, 0, 0.42) 1px 1px 1px;
	white-space: var(--tile-headline-white-space);
}

.ra_tile_tag_row
{
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem;
	margin-top: 0.25rem;
}

.ra_tile_tag
{
	background-color: rgb(46, 46, 46);
	border-radius: 2px;
	box-shadow: rgba(0, 0, 0, 0.32) 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 0px 1px, rgba(0, 0, 0, 0.16) 0px 0px 3px;
	color: rgba(255, 255, 255, 0.87);
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	font-size: 0.8125rem;
	font-weight: 400;
	line-height: 1.23077;
	padding: 0px 2px;
}

.ra_tile_tag_new
{
	color: rgb(0, 209, 0);
}
`);

onDom([selTileDiscover, selTileRadarSmall, selTileVisitors, selTileVisited, selTileFriends, selTileFriendsList,
	selTilePicLikes, selTileSearch].join(","), a =>
{
	// Find profile cached for this tile.
	const username = getUsernameFromHref(a.href);
	const profile = profileCache[username];
	if (!profile)
		return;

	// Add custom tags and headline.
	let tagRow;
	let tagClasses;
	let tagNewClasses;
	function addTag(text, isNew)
	{
		if (text)
			addElement(tagRow, `<span class="${isNew ? tagNewClasses : tagClasses}">${text}</span>`);
	}

	const inner = a.firstChild;
	if (inner.classList.contains("BIG"))
	{
		// Find existing tag elements and classes.
		const lastTag = a.querySelector(`div:last-child > span[class^="SpecialText-"]:last-child`);
		if (!lastTag)
			return;
		tagRow = lastTag.parentNode;
		tagClasses = lastTag.classList;
		tagNewClasses = tagRow.firstChild.classList;
		// Clear existing tags.
		tagRow.replaceChildren();
	}
	else
	{
		const container = inner.lastChild;
		// Create headline.
		if (profile.headline)
			addElement(container, `<p class="ra_tile_headline">${profile.headline}</p>`);
		// Create tag row.
		tagRow = addElement(container, `<div class="ra_tile_tag_row">`);
		tagClasses = "ra_tile_tag";
		tagNewClasses = tagClasses + " ra_tile_tag_new";
	}

	// Add tags.
	if (tagNewClasses.value !== tagClasses.value)
		addTag(translate("new"), true);
	const personal = profile.personal;
	if (personal)
	{
		if (tileDetails.has("age")) addTag(personal.age);
		if (tileDetails.has("bodyHair")) addTag(getProfileEnum("bodyHair", personal.body_hair));
		if (tileDetails.has("height")) addTag(getProfileHeight(personal.height));
		if (tileDetails.has("weight")) addTag(getProfileWeight(personal.weight));
		if (tileDetails.has("bmi")) addTag(getProfileBmi(personal.height, personal.weight));
		if (tileDetails.has("ageRange")) addTag(getProfileAgeRange(personal.target_age, true));
		if (tileDetails.has("bodyType")) addTag(getProfileEnum("bodyType", personal.body_type));
		if (tileDetails.has("ethnicity")) addTag(getProfileEnum("ethnicity", personal.ethnicity));
		if (tileDetails.has("relationship")) addTag(getProfileEnum("relationship", personal.relationship));
		if (tileDetails.has("smoker"))
		{
			if (personal.smoker === "YES")
				addTag(translate("smoker"));
			else if (personal.smoker === "SOCIALLY")
				addTag(translate("socialSmoker"));
		}
		if (tileDetails.has("openTo") && personal.looking_for && personal.looking_for[0] !== "NO_ENTRY")
		{
			let text = "";
			for (let openTo of personal.looking_for)
				text += translateEnum("openTo", openTo)[0];
			addTag(text);
		}
	}
	const sexual = profile.sexual;
	if (sexual)
	{
		if (tileDetails.has("analPosition")) addTag(getProfileEnum("analPosition", sexual.anal_position));
		if (tileDetails.has("dick")) addTag(getProfileDick(sexual.dick_size, sexual.concision));
		if (tileDetails.has("saferSex")) addTag(getProfileEnum("saferSex", sexual.safer_sex));
		if (tileDetails.has("dirty")) addTag(getProfileEnum("dirty", sexual.dirty_sex));
		if (tileDetails.has("sm")) addTag(getProfileEnum("sm", sexual.sm));
		if (tileDetails.has("fisting")) addTag(getProfileEnum("fisting", sexual.fisting));
	}
});

onDom(`img[src^="/img/usr/squarish/"][src$=".jpg"]`, el =>
{
	if (getEnhancedImages())
	{
		const url = getImageUrl(el.src, 848);
		el.src = url;
	}
});
onDom(`*[style^='background-image: url("/img/usr/squarish/'][style$='.jpg");']`, el =>
{
	if (getEnhancedImages())
	{
		const url = getImageUrl(getCssBackgroundImageUrl(el.style.backgroundImage), 848);
		el.style.backgroundImage = `url("${url}")`;;
	}
});

onMenu(selTileDiscover, a =>
{
	const el = a.closest("li");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, true, true));
});

onMenu(selTileRadarLarge, a =>
{
	const el = a.closest("div.tile--plus").parentNode;
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, true, true));
});

onMenu(selTileRadarSmall, a =>
{
	const el = a.closest("div.tile");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, true, true));
});

onMenu(selTileRadarImage, el =>
{
	const imageUrl = getImageUrl(getCssBackgroundImageUrl(el.style.backgroundImage));
	showMenu([
		menuItem("search", "viewFullImage", () => showImagePreview(imageUrl)),
	]);
});

onMenu([selTileVisitors, selTileVisited].join(","), a =>
{
	const el = a.closest("li");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, getHideVisits(), true));
});

onMenu([selTileFriends, selTileFriendsList].join(","), a =>
{
	const el = a.closest("li");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, getHideFriends(), false));
});

onMenu([selTileLikes, selTilePicLikes].join(","), a =>
{
	const el = a.closest("li");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, getHideLikes(), true));
});

onMenu(selTileSearch, a =>
{
	const el = a.closest("div.tile");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, true, true));
});

onMenu(selTileActivity, a =>
{
	const el = a.closest("div.listitem");
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, getHideActivities(), true));
});

// ---- Messaging ----

addCss(`
/* message list truncation */
#messenger div[class^="TruncateBlock__Content-sc-"]
{
	-webkit-line-clamp: var(--message-line-clamp);
}
`);

onDom(`.js-send-region.layout-item > div`, el =>
{
	el.addEventListener("keydown", e =>
	{
		// Prevent site event handler from sending message or typing notifications.
		const enter = e.key === "Enter";
		const send = enter && (getSendEnter() || e.ctrlKey);
		const allow = send || getTypingNotifications() && !enter;
		if (!allow)
			e.stopPropagation();
	}, true);
});

onMenu(".js-chat .reactView", el =>
{
	// messages > message
	const a = el.querySelector(`a[href^="/profile/"]`);
	const username = getUsernameFromHref(a.href);
	showMenu([
		...createTileMenu(el, username, getHideMessages(), false),
		menuItem("trashcan", "deleteUnread", () =>
		{
			const a = el.querySelector(`a[href^="/messenger/chat/"]`);
			const sep = a.href.lastIndexOf("/");
			const id = parseInt(a.href.substring(sep + 1));
			sendXhr("DELETE", `/api/v4/messages/conversations/${id}`);
			el.remove();
		})
	]);
});

onMenu(".js-chat .reactView img", el =>
{
	// messages > message > sent image
	showMenu([
		menuItem("search", "viewFullImage", () => showImagePreview(getImageUrl(el.src))),
	]);
});

onMenu(".js-contacts .reactView", el =>
{
	// contacts > contact
	const a = el.querySelector(`a[href^="/profile/"]`);
	const username = getUsernameFromHref(a.href);
	showMenu(createTileMenu(el, username, getHideContacts(), false));
});

// ---- Albums ----

let changeProfilePic = false;

onDom(`li#picture_menu_set-as-main-profile-picture`, el =>
{
	const button = el.querySelector("button");

	// Only allow profile pic being changed when manually clicking this button.
	button.parentNode.addEventListener("click", e => changeProfilePic = true, true);
});

onXhr("send", "PUT", "v4/profiles/me", e =>
{
	// Prevent automatic profile picture change when rearranging pictures.
	if (changeProfilePic)
		changeProfilePic = false;
	else if (e.body.preview_pic_id)
		e.cancel = true;
});

// ---- Settings ----

function openSettingsPane(ul, link)
{
	// Open pane.
	const layerContent = document.querySelector("#offcanvas-nav > .js-layer-content");
	layerContent.classList.add("is-open");

	// Create UI.
	const pane = layerContent.querySelector(".js-side-content");
	pane.replaceChildren();
	const p = addElement(pane, `
		<div class="layout layout--vertical layout--consume">
			<div class="layout-item layout-item--consume layout layout--vertical">

				<div class="js-header layout-item l-hidden-md-lg">
					<div class="layer-header layer-header--primary">
						<a class="back-button l-hidden-md-lg l-tappable js-back marionette" href="/me">
							<span class="js-back-icon icon icon-back icon-large"></span>
						</a>
						<div class="layer-header__title">
							<h2>${GM_info.script.name}</h2>
						</div>
					</div>
				</div>
				<div class="layout-item settings__navigation p l-hidden-sm">
					<div class="js-title typo-section-navigation">${GM_info.script.name}</div>
				</div>

				<div class="layout-item layout-item--consume">
					<div class="js-content js-scrollable fit scrollable">
						<div id="ra_settings_p" class="p"></div>
					</div>
				</div>
			</div>
		</div>`).querySelector("#ra_settings_p");

	function addSection(title)
	{
		return addElement(p, `
			<div class="settings__key">
				<div>
					<span>${translate(title)}</span>
				</div>
				<div class="separator separator--alt separator--narrow [ mb ] "></div>
			</div>`);
	}
	function addCheckbox(section, text, desc)
	{
		const input = addElement(section, `
			<div class="layout layout--v-center">
				<div class="layout-item [ 6/12--sm ]">
					<span>${translate(text)}</span>
				</div>
				<div class="layout-item [ 6/12--sm ]">
					<div class="js-toggle-show-headlines pull-right">
						<div>
							<span class="ui-toggle ui-toggle--default ui-toggle--right">
								<input class="ui-toggle__input" type="checkbox" id="ra_${text}">
								<label class="ui-toggle__label" for="ra_${text}" style="touch-action: pan-y; user-select: none; -webkit-user-drag: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></label>
							</span>
						</div>
					</div>
				</div>
			</div>`).querySelector("input");
		if (desc)
		{
			addElement(section, `
				<div>
					<div class="settings__description">${translate(desc)}</div>
				</div>`);
		}
		return input;
	}
	function addNumber(section, text, min, max)
	{
		return addElement(section, `
			<div class="layout layout--v-center">
				<div class="layout-item [ 6/12--sm ] mv-">
					<span>${translate(text)}</span>
				</div>
				<div class="layout-item [ 6/12--sm ] mv-">
					<input class="input input--block" type="number" min="${min}" max="${max}"/>
				</div>
			</div>`).querySelector("input");
	}
	function addTagList(section)
	{
		return addElement(section, `
			<div class="mv js-grid-stats-selector">
				<div>
					<ul class="js-list tags-list tags-list--centered"/>
				</div>
			</div>`).querySelector("ul");
	}
	function addTag(ul, tag, text, selected, change)
	{
		const li = addElement(ul, `
			<li class="tags-list__item">
				<a class="js-tag ui-tag ui-tag--removable" href="#">
					<span class="ui-tag__label">${text}</span>
				</a>
			</li>`);
		const a = li.querySelector("a");
		if (selected)
			a.classList.add("ui-tag--selected");
		li.addEventListener("click", e =>
		{
			e.preventDefault();
			if (a.classList.contains("ui-tag--selected"))
			{
				change({ tag: tag, checked: false });
				a.classList.remove("ui-tag--selected");
			}
			else
			{
				change({ tag: tag, checked: true });
				a.classList.add("ui-tag--selected");
			}
		});
	}

	// Add filter section.
	const filterSection = addSection("filter");

	const enhancedFilter = addCheckbox(filterSection, "enhancedFilter", "enhancedFilterDesc");
	enhancedFilter.checked = getEnhancedFilter();
	enhancedFilter.addEventListener("change", e => setEnhancedFilter(e.target.checked));

	const discoverFilter = addCheckbox(filterSection, "discoverFilter", "discoverFilterDesc");
	discoverFilter.checked = getDiscoverFilter();
	discoverFilter.addEventListener("change", e => setDiscoverFilter(e.target.checked));

	const searchFilter = addCheckbox(filterSection, "searchFilter", "searchFilterDesc");
	searchFilter.checked = getSearchFilter();
	searchFilter.addEventListener("change", e => setSearchFilter(e.target.checked));

	// Add tiles section.
	const tilesSection = addSection("tiles");

	const enhancedTiles = addCheckbox(tilesSection, "enhancedTiles", "enhancedTilesDesc");
	enhancedTiles.checked = getEnhancedTiles();
	enhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked));

	const enhancedImages = addCheckbox(tilesSection, "enhancedImages", "enhancedImagesDesc");
	enhancedImages.checked = getEnhancedImages();
	enhancedImages.addEventListener("change", e => setEnhancedImages(e.target.checked));

	const fullHeadlines = addCheckbox(tilesSection, "fullHeadlines", "fullHeadlinesDesc");
	fullHeadlines.checked = getFullHeadlines();
	fullHeadlines.addEventListener("change", e => setFullHeadlines(e.target.checked));

	const tileCount = addNumber(tilesSection, "tileCount", 0, 10);
	tileCount.value = getTileCount();
	tileCount.addEventListener("change", e => setTileCount(parseInt(e.target.value)));

	const tileDetailsList = addTagList(tilesSection, "tileDetailsList");
	for (const tileDetail of ["age", "height", "weight", "bmi", "smoker", "ageRange",
		"bodyHair", "bodyType", "ethnicity", "relationship", "analPosition",
		"dick", "saferSex", "dirty", "sm", "fisting", "openTo"])
	{
		addTag(tileDetailsList, tileDetail, translate(tileDetail), tileDetails.has(tileDetail), e => setTileDetail(e.tag, e.checked));
	}

	// Add messages section.
	const messagesSection = addSection("messages");

	const systemMessages = addCheckbox(messagesSection, "systemMessages", "systemMessagesDesc");
	systemMessages.checked = getSystemMessages();
	systemMessages.addEventListener("change", e => setSystemMessages(e.target.checked));

	const fullMessages = addCheckbox(messagesSection, "fullMessages", "fullMessagesDesc");
	fullMessages.checked = getFullMessages();
	fullMessages.addEventListener("change", e => setFullMessages(e.target.checked));

	const typingNotifications = addCheckbox(messagesSection, "typingNotifications", "typingNotificationsDesc");
	typingNotifications.checked = getTypingNotifications();
	typingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked));

	const sendEnter = addCheckbox(messagesSection, "sendEnter", "sendEnterDesc");
	sendEnter.checked = getSendEnter();
	sendEnter.addEventListener("change", e => setSendEnter(e.target.checked));

	// Add hidden users section.
	const hiddenUsersSection = addSection("hiddenUsers");

	const hideMessages = addCheckbox(hiddenUsersSection, "hideMessages");
	hideMessages.checked = getHideMessages();
	hideMessages.addEventListener("change", e => setHideMessages(e.target.checked));

	const hideContacts = addCheckbox(hiddenUsersSection, "hideContacts");
	hideContacts.checked = getHideContacts();
	hideContacts.addEventListener("change", e => setHideContacts(e.target.checked));

	const hideVisits = addCheckbox(hiddenUsersSection, "hideVisits");
	hideVisits.checked = getHideVisits();
	hideVisits.addEventListener("change", e => setHideVisits(e.target.checked));

	const hideLikes = addCheckbox(hiddenUsersSection, "hideLikes");
	hideLikes.checked = getHideLikes();
	hideLikes.addEventListener("change", e => setHideLikes(e.target.checked));

	const hideFriends = addCheckbox(hiddenUsersSection, "hideFriends");
	hideFriends.checked = getHideFriends();
	hideFriends.addEventListener("change", e => setHideFriends(e.target.checked));

	const hideActivities = addCheckbox(hiddenUsersSection, "hideActivities");
	hideActivities.checked = getHideActivities();
	hideActivities.addEventListener("change", e => setHideActivities(e.target.checked));

	const inMinAge = addNumber(hiddenUsersSection, "minAge", 18, 99);
	const inMaxAge = addNumber(hiddenUsersSection, "maxAge", 18, 99);
	let minAge = getHiddenMinAge();
	let maxAge = getHiddenMaxAge();
	inMinAge.value = minAge;
	inMaxAge.value = maxAge;
	inMinAge.addEventListener("change", e =>
	{
		minAge = parseInt(e.target.value);
		setHiddenMinAge(minAge);
		if (minAge > maxAge)
		{
			maxAge = minAge;
			setHiddenMaxAge(maxAge);
			inMaxAge.val(maxAge);
		}
	});
	inMaxAge.addEventListener("change", e =>
	{
		maxAge = parseInt(e.target.value);
		setHiddenMaxAge(maxAge);
		if (maxAge < minAge)
		{
			minAge = maxAge;
			setHiddenMinAge(minAge);
			inMinAge.val(minAge);
		}
	});

	createList(hiddenUsersSection, {
		onGet: () => Array.from(getHiddenUsers()).sort(Intl.Collator().compare),
		onAdd: e => setUserHidden(e, true),
		onRemove: e => setUserHidden(e, false)
	});
}

onDom(`li.js-settings > div.accordion > ul.js-list`, el =>
{
	// Add extension menu item.
	const linkClass = el.querySelector("a").className;
	const link = addElement(el, `
		<li>
			<div>
				<a class="${linkClass}" href="/me/romeoadditions">${GM_info.script.name}</a>
			</div>
		</li>`);
	link.addEventListener("click", e =>
	{
		if (link.classList.contains("is-selected"))
		{
			link.classList.remove("is-selected");
		}
		else
		{
			link.classList.add("is-selected");
			setTimeout(() => openSettingsPane(el, link)); // delayed execution to force open panel
		}
	});
	// Deselect menu item if others are clicked.
	for (const linkOther of el.querySelectorAll("li"))
		if (linkOther !== link)
			linkOther.addEventListener("click", e => link.classList.remove("is-selected"));
});

onDom(`#offcanvas-nav > .js-layer-content > main > div.layout > div.reactView--autoHeight > p[class^="MiniText-sc-"]`, el =>
{
	el.innerHTML += `<a class="marionette" style="display:block" href="${GM_info.script.downloadURL}" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
});

// ---- Load ----

setFullHeadlines(getFullHeadlines());
setFullMessages(getFullMessages());
setSystemMessages(getSystemMessages());
setTileCount(getTileCount());
tileDetails = getTileDetails();

initMenu();
initDom();
initPreviews();
initFetch();
initXhr();