Romeo Additions

Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.

Version vom 27.05.2024. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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

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

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

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

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

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

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

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        5.4.3
// @description    Allows to hide users, display their information on tiles, and enhances the Radar.
// @description:de Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.
// @author         -Ray-, Djamana
// @match          *://*.romeo.com/*
// @license        MIT
// @supportURL     https://greasyfork.org/en/scripts/419514-romeo-additions
// ==/UserScript==

// ---- https://github.com/CoeJoder/GM_wrench ----

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 waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals) {
    if (typeof waitOnce === 'undefined') {
        waitOnce = true;
    }
    if (typeof interval === 'undefined') {
        interval = 300;
    }
    if (typeof maxIntervals === 'undefined') {
        maxIntervals = -1;
    }
    var targetNodes = (typeof selectorOrFunction === 'function')
        ? selectorOrFunction()
        : document.querySelectorAll(selectorOrFunction);

    var targetsFound = targetNodes && targetNodes.length > 0;
    if (targetsFound) {
        targetNodes.forEach(function (targetNode) {
            var attrAlreadyFound = 'data-userscript-alreadyFound';
            var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false;
            if (!alreadyFound) {
                var cancelFound = callback(targetNode);
                if (cancelFound) {
                    targetsFound = false;
                }
                else {
                    targetNode.setAttribute(attrAlreadyFound, true);
                }
            }
        });
    }

    if (maxIntervals !== 0 && !(targetsFound && waitOnce)) {
        maxIntervals -= 1;
        setTimeout(function () {
            waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals);
        }, interval);
    }
};

// ---- CSS ----

const css = `
:root {
    --message-line-clamp: 2;
    --system-message-visibility: visible;
    --tile-headline-white-space: nowrap;
}

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

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

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

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

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

/* hide PLUS icon as PLUS is faked to enhance tiles */
.js-romeo-badge {
    display: none;
}

/* hide PLUS message at bottom of visitor grid */
.visitors div[class^="UnlockMoreVisitorsGrid"] {
    display: none;
}

/* hide models on login page */
div[data-testid="desktop-image"] {
    background-image: 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;
    }
}

/* tile classes */
.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);
}

/* profile preview */
#ra_profile_wrapper {
    background-color: black;
    display: grid;
    grid-template-rows: min-content auto;
    height: 100%;
}
#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, 1fr) 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%;
    }
}

/* context menu */
#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;
    }
}

/* settings pane */
#version {
    color: white;
    display: block;
}`;
let styleTile = null;

// ---- Common ----

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

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

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 onElement(selector, callback) {
    waitForKeyElements(selector, callback, false);
}

// ---- 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"
    },
    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"
    },
    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"
    },
    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 lang = document.documentElement.getAttribute("lang") || "en";
    const translations = _strings[key];
    return translations
        ? translations[lang] || 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();

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 setStyleProp(name, value) {
    document.documentElement.style.setProperty(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 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) {
    setStyleProp("--tile-headline-white-space", value ? "unset" : "nowrap");
    save("fullHeadlines", value);
}
function setFullMessages(value) {
    setStyleProp("--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 setSendEnter(value) {
    save("sendEnter", value);
}
function setSystemMessages(value) {
    setStyleProp("--system-message-visibility", value ? "visible" : "collapse");
    save("systemMessages", value);
}
function setTileCount(value) {
    if (value) {
        setStyleProp("--tile-count", value);
        if (!styleTile) {
            styleTile = addCss(`
            :root {
                --tile-count: 0;
                --tile-size: calc(100% / max(1, var(--tile-count)));
            }
            /* 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 (styleTile) {
            styleTile.remove();
            styleTile = 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)));
}

// ---- UI ----

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

function showLayer() {
    const spotlightContainer = document.querySelector("#spotlight-container");
    const layer = addElement(spotlightContainer, `<div class="layer layer--spotlight" style="top:0;z-index:100;"></div>`);
    layer.addEventListener("click", e => {
        if (e.target === layer) {
            layer.remove();
        }
    });
    return layer;
}

function showImageLayer(src) {
    const layer = showLayer();
    const img = addElement(layer, `<img src="${src}"></img>`);
    img.addEventListener("click", e => layer.remove());
}

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

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: () => showProfileLayer(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: () => showProfileLayer(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: () => showImageLayer(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: () => showProfileLayer(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    el.style.display = "none";
                }
            }
        );
    },

    // visitors > user tile
    ".visitors .tile:not(div[class*='tile--loading--'])": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileLayer(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideVisits())
                        el.style.display = "none";
                }
            }
        );
    },

    // messages > message > sent image
    ".js-chat .reactView img": el => {
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImageLayer(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: () => showProfileLayer(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: () => showProfileLayer(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: () => showImageLayer(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: () => showProfileLayer(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)) {
                    //console.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";
}

// ---- XHR ----

function hookXhr() {
    const realOpen = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
        // Manipulate request.
        const verb = getApiVerb(url);
        //console.log(`<< ${method} url=${url}`);
        switch (verb) {
            case "hunqz/profiles":
            case "profiles":
            case "profiles/popular":
                url = xhrApplyRadarFilter(url);
                break;
        }

        // Manipulate reply.
        this.addEventListener("load", () => {
            //console.log(`>> ${method} ${url}`);
            if (method !== "GET") {
                return;
            }
            const verb = getApiVerb(url);
            const isJson = verb !== "stream" && typeof this.response === "string";
            let reply = isJson ? JSON.parse(this.response) : this.response;
            //console.log(reply);

            // Modify interesting data.
            switch (verb) {
                case "contacts":
                    if (reply.cursors) {
                        reply.items = xhrProcessUserItems(reply.items, x => x.profile, getHideContacts());
                    }
                    break;
                case "messages/conversations":
                    reply.items = xhrProcessUserItems(reply.items, x => x.chat_partner, getHideMessages());
                    break;
                case "notifications/activity-stream":
                    reply = xhrProcessUserItems(reply, x => x.partner, getHideActivities());
                    break;
                case "hunqz/profiles":
                case "profiles":
                case "profiles/blocked":
                case "profiles/popular":
                    reply.items = xhrProcessUserItems(reply.items, x => x, true);
                    reply = xhrEnhanceProfilesAndVisits(reply);
                    break;
                case "session":
                    xhrHandleSession(reply);
                    break;
                case "visitors":
                case "visits":
                    reply.items = xhrProcessUserItems(reply.items, x => x, getHideVisits());
                    reply = xhrEnhanceProfilesAndVisits(reply);
                    break;
                case "reactions/pictures/basic":
                    reply.items = xhrProcessUserItems(reply.items, x => x.user_id, getHideLikes());
                    break;
            }

            // Write back possibly modified data.
            Object.defineProperty(this, "responseText", { writable: true });
            this.responseText = isJson ? JSON.stringify(reply) : reply;
        });

        // Forward to client.
        return realOpen.apply(this, arguments);
    };
}

function getApiVerb(url) {
    if (url.includes("/api/stream")) {
        return "stream";
    }
    // Extract verb in "/api/v#/verb?" or "/api/+/verb?".
    const matches = url.match("/api/(v[0-9]|\\+)/(.*)\\?".replaceAll("/", "\\/"));
    if (matches && matches.length === 3) {
        return matches.at(-1);
    }
}

function xhrEnhanceProfilesAndVisits(reply) {
    // Restore PLUS-visible visitors.
    reply.items_limited = reply.items_total;

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

    return reply;
}

function xhrHandleSession(reply) {
    measurementSystem = reply.bb_settings?.interface?.measurement_system ?? measurementSystem;
}

function xhrProcessUserItems(items, userSelector, filter) {
    let newItems = [];
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();

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

    return newItems;
}

// ---- Profiles ----

let profileCache = {};

function cacheProfile(user) {
    // For activities, user is the activity partner.
    if (user.partner) {
        user = user.partner;
    }
    const existing = profileCache[user.name];
    const profile = {
        id: user.id,
        name: user.name,
        headline: user.headline ?? existing?.headline,
        last_login: user.last_login ?? existing?.last_login,
        location: user.location ?? existing?.location,
        online_status: user.online_status ?? existing?.online_status,
        pic: user.preview_pic?.url_token ?? existing?.pic, // not available if no picture
        personal: user.profile?.personal ?? user.personal ?? existing?.personal, // not available in activities
        sexual: user.profile?.sexual ?? user.sexual ?? existing?.sexual // not available in activities
    };
    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 getProfileAgeRange(range, short) {
    if (range) {
        return short
            ? `${range.min ?? 18}-${range.max ?? 99}`
            : translate("ageRangeValue").replace("$from", range.min ?? 18).replace("$to", range.max ?? 99);
    }
}
function getProfileBmi(height, weight, withName) {
    if (height && weight) {
        const bmi = weight / Math.pow(height / 100, 2);
        let result = `${(Math.round(bmi * 10) / 10).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`
            : `${Math.round(height * 3.280839895) / 100} ft`;
    }
}
function getProfileWeight(weight) {
    if (weight) {
        return measurementSystem === "METRIC"
            ? `${weight}kg`
            : `${Math.round(weight * 2.20462262185)}lbs`;
    }
}

function showProfileLayer(username) {
    const layer = showLayer();

    const profile = profileCache[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 addArray(section, key, array) {
        if (!array) {
            return;
        }
        let values = [];
        for (let i = 0; i < array.length; i++) {
            values.push(translate(array[i]));
        }
        if (values.length) {
            add(section, key, values.join(", "));
        }
    }
    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`
            : `${Math.round(distance * 0.006213712) / 10}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(" / "));
        }
    }

    // Create preview popup.
    const pane = addElement(layer, `<div id="ra_profile_wrapper">
        <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="#">
                    <span class="js-back-icon icon icon-back icon-large"></span>
                </a>
                <div class="layer-header__title">
                    <h2>${profile.name}</h2>
                </div>
            </div>
        </div>
        <div class="layout-item p l-hidden-sm">
            <div class="js-title typo-section-navigation">${profile.name}</div>
        </div>
        <div id="ra_profile_content">
            <div id="ra_profile_left">
                <div>${escapeHtml(profile.headline ?? "")}</div>
                <img id="ra_profile_pic" src="/assets/3e6f78fd4e864c6071e6fa65d3d0c679c48b84a95dee9f232a2e86303c4ed5a1.svg"></img>
            </div>
            <div id="ra_profile_right"></div>
        </div>
    </div>`);
    const right = pane.querySelector("#ra_profile_right");

    // Set image.
    if (profile.pic) {
        const img = pane.querySelector("#ra_profile_pic");
        img.src = `/img/usr/${profile.pic}.jpg`;
    }

    // Set profile metadata.
    {
        const section = addSection(right, "metadata");
        addEnum(section, "onlineStatus", profile.online_status);
        add(section, "lastLogin", 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);
    }

    // Set general details.
    const personal = profile.personal;
    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));
    }

    // Set sexual details.
    const sexual = profile.sexual;
    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();
        }
    }

    // Set looking for details.
    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();
        }
    }

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

    layer.querySelector(".js-back").addEventListener("click", e => layer.remove());
}

// ---- Filter ----

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, enable reset filter button, and reload.
    setRadarFilter();
    document.querySelector(".js-clear-all").classList.remove("is-disabled");
    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 bookmark action if not available.
    const save = el.querySelector(".js-filter-actions .js-save");
    if (save && save.classList.contains("is-plus")) {
        save.parentNode.remove();
    }

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

    filter = el.querySelector(".filter");

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

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

// ---- Tiles ----

onElement(`.tile:not(div[class*="tile--loading--"]) > .reactView > a`, a => {
    const tile = a.closest(".tile");
    if (!tile || tile.classList.contains("tile--small")) {
        return;
    }

    // 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 = tile.querySelector(`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));
    }
    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 ----

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

// ---- Sidebar ----

onElement("li.js-settings > div.accordion > ul", 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(`main[aria-label="Settings"] div.reactView--autoHeight > p[class^="MiniText-sc-"]`, el => {
    el.innerHTML += `<a id="version" 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", "ageRange",
        "bodyHair", "bodyType", "ethnicity", "relationship", "analPosition",
        "dick", "saferSex", "dirty", "sm", "fisting"]) {
        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 ----

(function () {
    setSystemMessages(getSystemMessages());
    setFullHeadlines(getFullHeadlines());
    setTileCount(getTileCount());
    setFullMessages(getFullMessages());
    radarFilter = getRadarFilter();
    tileDetails = getTileDetails();

    addCss(css);
    hookContextMenu();
    hookXhr();
})();