Romeo Additions

Enhances GR, especially for non-PLUS users

اعتبارا من 26-11-2024. شاهد أحدث إصدار.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name Romeo Additions
// @name:de Romeo Additions
// @namespace https://greasyfork.org/en/users/723211-ray/
// @version 7.7.0
// @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 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",
	},
	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.",
	},
	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 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 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()
{
	window.fetch = async (request, init) =>
	{
		if (!(request instanceof Request))
			return await realFetch(request, init);

		// Manipulate request.
		const e =
		{
			cancel: false,
			method: request.method,
			url: request.url
		};
		if (request.body)
		{
			try
			{
				e.body = await request.clone().json();
			}
			catch // not JSON, currently not interested
			{
				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.
		if (response.body)
		{
			try
			{
				e.body = await response.clone().json();
			}
			catch // not JSON, currently not interested
			{
				return response;
			}
		}
		e.cancel = false;
		e.status = response.status;
		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 getBackgroundImageUrl(style)
{
	return style.match(/"(.+)"/)[1];
}

function getFullImageUrl(url)
{
	return url.replace(/squarish\/[0-9]*x[0-9]*\//, "");
}

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

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 = getFullImageUrl(getBackgroundImageUrl(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);
}
`);

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

onMenu(".js-chat .reactView img", el =>
{
	// messages > message > sent image
	showMenu([
		menuItem("search", "viewFullImage", () => showImagePreview(getFullImageUrl(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));
});

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

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