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.2.2
// @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
// @supportURL     https://greasyfork.org/en/scripts/419514-romeo-additions
// ==/UserScript==

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

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 hookDom() {
    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 onElement(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"
    },
    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?"
    },
    commonOptions: {
        de: "Allgemeines",
        en: "Common"
    },
    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"
    },
    display: {
        de: "Anzeige",
        en: "Display"
    },
    distance: {
        de: "Entfernung",
        en: "Distance"
    },
    enhancedFilter: {
        de: "Filter mit neuen Optionen",
        en: "Filter with new options"
    },
    enhancedFilterDesc: {
        de: "Erlaubt den Filter von Radar-Ergebnissen mit zusätzlichen Infos.",
        en: "Allows filters in radar results with additional infos."
    },
    enhancedTiles: {
        de: "Profildetails auf Kacheln",
        en: "Grid stats"
    },
    enhancedTilesDesc: {
        de: "Zeigt folgende Details auf Kacheln. Der Radar nutzt dafür große Benutzerkacheln.",
        en: "Shows the following details on tiles. The radar uses large user tiles for this."
    },
    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 auch lange Profilüberschriften ungekürzt auf Kacheln.",
        en: "Shows even long profile headlines completely on tiles."
    },
    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 entfernen",
        en: "Remove in activities"
    },
    hideActivitiesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in Aktivitäten.",
        en: "Removes hidden users even in the activities."
    },
    hideContacts: {
        de: "In Kontakten entfernen",
        en: "Remove in contacts"
    },
    hideContactsDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in Kontakten.",
        en: "Removes hidden users even in contacts."
    },
    hideLikes: {
        de: "In Bilder-Likes entfernen",
        en: "Remove in picture likes"
    },
    hideLikesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in Likes von Bildern.",
        en: "Removes hidden users even in likes on pictures."
    },
    hideMessages: {
        de: "In Chat entfernen",
        en: "Remove in chat"
    },
    hideMessagesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in der Chatliste.",
        en: "Removes hidden users even in the chat list."
    },
    hideUser: {
        de: "Benutzer ausblenden",
        en: "Hide user"
    },
    hideVisits: {
        de: "In Besuchern entfernen",
        en: "Remove in visitors"
    },
    hideVisitsDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in der Besucherliste.",
        en: "Removes hidden users even in the visitor list."
    },
    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"
    },
    sendEnter: {
        de: "Enter sendet Nachricht",
        en: "Enter sends message"
    },
    sendEnterDesc: {
        de: "Wenn deaktiviert erzeugt Enter einen Absatz und Strg+Enter versendet 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: "Wenn deaktiviert werden Popups wie Standort- oder Fehlermeldungen versteckt.",
        en: "If disabled, popups like GPS or error messages are hidden."
    },
    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: "Kacheln pro Zeile (0 für Standard)",
        en: "Tiles per row (0 for default)"
    },
    typingNotifications: {
        de: "Tippbenachrichtigungen",
        en: "Typing notifications"
    },
    typingNotificationsDesc: {
        de: "Wenn deaktiviert können Empfänger die Eingabe einer Nachricht nicht mehr sehen.",
        en: "If disabled, receivers can no longer see that a message is being composed."
    },
    viewFullImage: {
        de: "Bild anzeigen",
        en: "View full image"
    },
    viewProfile: {
        de: "Profilvorschau anzeigen",
        en: "Show profile preview"
    },
    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 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 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 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 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 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 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);
            }
            /* new tiles */
            #cruise main > ul {
                grid-template-columns: repeat(var(--tile-count), 1fr);
            }
            /* desktop - do not adjust "Discover" .js-strip tile sizes for now */
            .search-results__item:not(.js-strip .search-results__item) {
                padding-bottom: var(--tile-size) !important;
                width: var(--tile-size) !important;
            }
            /* mobile - starts at 768px at which .search-results__item becomes 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;
                }
            }
            `);
        }
    } 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 = {};

function hookFetch() {
    const realFetch = window.fetch;
    window.fetch = async (resource, options) => {
        // Hook send.
        const e = { method: resource.method, url: resource.url };
        if (callHook(fetchHooks, "send", e) && e.cancel) {
            return;
        }
        resource.method = e.method;
        resource.url = e.url;

        // Hook receive.
        const response = await realFetch(resource, options);
        try {
            e.body = await response.clone().json();
        } catch {
            return response; // not JSON or empty reply
        }
        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 hookXhr() {
    // 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)) {
            if (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);
}

// ---- Romeo ----

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 icon as PLUS is faked to enhance tiles */
.js-romeo-badge {
    display: 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;
    }
}
`);

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

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

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

// ---- Context Menu ----

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

#ra_context_ul {
    background: #121212;
    border: 1px solid #000;
    display: none;
    font-size: 0.8rem;
    position: absolute;
    z-index: 10001;
}

.ra_context_li {
    color: #FFF;
    cursor: default;
    padding: 5px 15px 5px 0px;
    white-space: nowrap;
}

.ra_context_li .icon {
    color: #00BDFF;
    margin: 0px 10px 0px 13px;
}

.ra_context_li:hover, .ra_context_li:active {
    background-color: hsla(0,0%,100%,.125);
}

@media screen and (max-width: 767px) {
    #ra_context_bg {
        background: #0000007F;
    }

    #ra_context_ul {
        bottom: 0;
        left: unset !important;
        font-size: 1.2rem;
        top: unset !important;
        position: fixed;
        width: 100%;
    }

    .ra_context_li {
        padding: 20px;
    }
}
`);

let contextMenuBg;
let contextMenuUl;
let contextMenuX;
let contextMenuY;
const contextMenuHandlers = {
    // radar > contact tile
    ".js-contact": el => {
        const username = getUsernameFromHref(el.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideContacts())
                        el.parentNode.style.display = "none";
                }
            }
        );
    },
    // radar > user tile
    ".search-results__item .tile:not(div[class*='tile--loading--'])": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (el.classList.contains("tile--plus")) {
                        el.closest(".search-results__item").style.display = "none";
                    } else {
                        el.style.display = "none";
                    }
                }
            }
        );
    },
    // radar > small user tile
    ".search-results__item .tile[class*='tile--loading--']": el => {
        const small = el.querySelector("div.SMALL");
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImagePreview(getFullImageUrl(getBackgroundImageUrl(small.style.backgroundImage))) }
        );
    },
    // search > user tile
    ".js-results .tile:not(div[class*='tile--loading--'])": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    el.style.display = "none";
                }
            }
        );
    },

    // cruise > visitors, cruise > visited
    "main#visitors > ul > li, main#visited-grid > ul > li": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideVisits())
                        el.style.display = "none";
                }
            }
        );
    },
    // cruise > likes
    "main#likers-list > ul > li": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideLikes())
                        el.style.display = "none";
                }
            }
        );
    },

    // messages > message > sent image
    ".js-chat .reactView img": el => {
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImagePreview(getFullImageUrl(el.src)) }
        );
    },
    // messages > message
    ".js-chat .reactView": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideMessages())
                        el.style.display = "none";
                }
            }
        );
    },

    // contacts > contact
    ".js-contacts .reactView": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideContacts())
                        el.style.display = "none";
                }
            }
        );
    },

    // activities > activity > liked image
    "img.thumbnail": el => {
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImagePreview(getFullImageUrl(el.src)) }
        );
    },
    // activities > activity
    ".js-stream .js-list .listitem": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfilePreview(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideActivities())
                        el.style.display = "none";
                }
            }
        );
    }
};

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

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

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

        // Go through hierarchy of clicked elements.
        for (const el of document.elementsFromPoint(contextMenuX, contextMenuY)) {
            // 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(contextMenuHandlers)) {
                if (el.matches(key)) {
                    log(`opening menu '${key}'`);
                    value(el);
                    e.preventDefault();
                    return;
                }
            }
        }
    });
}

function showContextMenu() {
    contextMenuBg.style.display = "block";

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

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

function hideContextMenu() {
    contextMenuBg.style.display = "none";
    contextMenuUl.style.display = "none";
}

// ---- Lists ----

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

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

// ---- Previews ----

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

let previewLayer;

function createPreview(title) {
    const container = document.querySelector("#spotlight-container");
    previewLayer = addElement(container, `<div class="layer layer--spotlight" style="top:0;z-index:100;">
        <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 hookPreviewHistory() {
    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) {
    const profile = profileCache[username];
    if (!profile) {
        return;
    }

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

    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 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 personal = profile.personal;
    const sexual = profile.sexual;

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

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

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

// ---- Profiles ----

let 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) {
            const name
                = bmi < 16 ? translate("bmiSevereThin")
                    : bmi < 17 ? translate("bmiModerateThin")
                        : bmi < 18.5 ? translate("bmiMildThin")
                            : bmi < 25 ? translate("bmiNormal")
                                : bmi < 30 ? translate("bmiPreObese")
                                    : bmi < 35 ? translate("bmiObese1")
                                        : bmi < 40 ? translate("bmiObese2")
                                            : translate("bmiObese3");
            result += ` / ${name}`;
        }
        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 fetchHandleReactionsCruiseLikes(e) {
    e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideLikes());
}
function xhrHandleTiles(e) {
    // Restore PLUS-visible visitors.
    e.items_limited = e.items_total;

    // Show as "large tiles" to display user details everywhere.
    if (getEnhancedTiles()) {
        for (let item of e.items ?? []) {
            if (item.display) {
                item.display.large_tile = true;
            }
        }
    }

    return e;
}
function xhrHandleProfiles(e) {
    e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, true);
    e.body = xhrHandleTiles(e.body);
}
function xhrHandleVisits(e) {
    e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, getHideVisits());
    e.body = xhrHandleTiles(e.body);
}

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

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 => fetchHandleReactionsCruiseLikes(e));

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

onXhr("load", "GET", "v4/reactions/pictures/basic", e => {
    e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.user_id, getHideLikes());
});

// ---- Filter ----

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

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

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(keyValues) {
    let filter = {};
    for (const [key, value] of keyValues) {
        addRadarFilter(filter, key, value);
    }
    return filter;
}

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

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 xhrApplyRadarFilter(url) {
    if (!getEnhancedFilter()) {
        return url;
    }

    // Decode existing parameters.
    const sep = url.indexOf("?");
    const baseUrl = url.substring(0, sep);
    const query = url.substring(sep + 1);

    let keyValues = [];
    for (const param of query.split("&")) {
        let [key, value] = param.split("=");
        key = decodeURI(key);

        // Ignore pagination and "Explore" views as filters don't fully work in them.
        if (key == "cursor" || key == "length" && value == 48) {
            return url;
        }

        keyValues.push([key, value]);
    }

    // Overwrite with custom parameters.
    let filter = packRadarFilter(keyValues);
    filter = { ...filter, ...radarFilter };

    // Encode new parameters.
    url = `${baseUrl}?`;
    for (const [key, value] of unpackRadarFilter(filter)) {
        url += `${encodeURI(key)}=${value}&`;
    }
    return url.slice(0, -1);
}

onXhr("open", "GET", "v4/hunqz/profiles", e => e.url = xhrApplyRadarFilter(e.url));
onXhr("open", "GET", "v4/profiles", e => e.url = xhrApplyRadarFilter(e.url));
onXhr("open", "GET", "v4/profiles/popular", e => e.url = xhrApplyRadarFilter(e.url));

onXhr("send", "PUT", "v4/settings/interface/bluebird", e => {
    // Changed filter.
    const id = e.body.search_filter.id;
    radarFilter = getSavedRadarFilter(id);
    replaceFilterContainer(document.querySelector(".js-quick-filter").parentNode);
});
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 ----

addCss(`
/* fix jumping fade 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);
}

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

onElement(`div.grid-tile:not(div[class*="tile--loading--"]) > .reactView > a,
    main#visitors > ul > li > a,
    main#visited-grid > ul > li > a`, 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));
    }
});

// ---- Messaging ----

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

onElement(`.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;

addCss(`
/* ensure images are not cropped */
div[role='dialog'] > div > main > ul > li > img {
    /*object-fit: contain;*/
}
`);

onElement(`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 ----

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

onElement(`#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="https://greasyfork.org/en/scripts/419514" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
});

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");
        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 common section.
    const commonOptionsSection = addSection("commonOptions");

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

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

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

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

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

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

    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 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 hideContacts = addCheckbox(hiddenUsersSection, "hideContacts", "hideContactsDesc");
    hideContacts.checked = getHideContacts();
    hideContacts.addEventListener("change", e => setHideContacts(e.target.checked));

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

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

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

    const hideLikes = addCheckbox(hiddenUsersSection, "hideLikes", "hideLikesDesc");
    hideLikes.checked = getHideLikes();
    hideLikes.addEventListener("change", e => setHideLikes(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)
    });
}

// ---- Load ----

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

hookContextMenu();
hookDom();
hookPreviewHistory();
hookFetch();
hookXhr();