您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.4.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, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatTime(str) { const date = new Date(Date.parse(str)); const lang = getLang(); return `${date.toLocaleDateString(lang)} ${date.toLocaleTimeString(lang)}`; } function getLang() { return document.documentElement.getAttribute("lang") || "en"; } function isJson(value) { if (!value || typeof value !== "string") return false; try { JSON.parse(value); return true; } catch { return false; } } function log() { if (GM_info.script.version === "0.0.0") { if (arguments.length > 1 && arguments[1]) arguments[0] += "\n"; console.log(...arguments); } } function round(value, maxDigits = 0) { const f = Math.pow(10, maxDigits); return Math.round(value * f) / f; } // ---- CSS ---- function addCss(css) { let style = document.createElement('style'); if (style.styleSheet) style.styleSheet.cssText = css; else style.appendChild(document.createTextNode(css)); return document.head.appendChild(style); }; function setCssProp(name, value) { document.documentElement.style.setProperty(name, value); } // ---- DOM ---- const domHooks = {}; function initDom() { function tagCall(el, callback) { if (!el.getAttribute("data-ra-hook")) { el.setAttribute("data-ra-hook", true); callback(el); } } const observer = new MutationObserver((mutations, observer) => { for (const mutation of mutations) { for (const el of mutation.addedNodes) { if (el.nodeType === Node.ELEMENT_NODE) { for (const [selector, callback] of Object.entries(domHooks)) { if (el.matches(selector)) { // Trigger for element. tagCall(el, callback); } else { // Trigger for children of attached elements. for (const elChild of el.querySelectorAll(selector)) tagCall(elChild, callback); } } } } } }); observer.observe(document.body, { subtree: true, childList: true }); } function addElement(parent, html) { parent.insertAdjacentHTML("beforeend", html); return parent.lastChild; } function onDom(selector, callback) { // Trigger for existing elements. for (const el of document.querySelectorAll(selector)) callback(el); // Add to observer list. domHooks[selector] = callback; } // ---- Translation ---- const strings = { aboutMe: { de: "Über mich", en: "About Me", }, age: { de: "Alter", en: "Age", }, ageRange: { de: "Altersspanne", en: "Age range", }, ageRangeValue: { de: "Zwischen $from und $to", en: "Between $from and $to", }, analPosition: { en: "Position", }, analPosition_TOP_ONLY: { de: "Nur Aktiv", en: "Top only", }, analPosition_MORE_TOP: { de: "Eher Aktiv", en: "More top", }, analPosition_VERSATILE: { de: "Flexibel", en: "Versatile", }, analPosition_MORE_BOTTOM: { de: "Eher Passiv", en: "More bottom", }, analPosition_BOTTOM_ONLY: { de: "Nur Passiv", en: "Bottom only", }, analPosition_NO: { de: "Kein Anal", en: "No anal", }, beard: { de: "Bart", en: "Beard", }, beard_DESIGNER_STUBBLE: { de: "3-Tage-Bart", en: "Designer stubble", }, beard_FULL_BEARD: { de: "Vollbart", en: "Full beard", }, beard_GOATEE: { en: "Goatee", }, beard_MOUSTACHE: { de: "Schnauzer", en: "Moustache", }, beard_NO_BEARD: { de: "Kein Bart", en: "No beard", }, bedAndBreakfast: { en: "Bed & Breakfast", }, blockUser: { de: "Benutzer blockieren", en: "Block user", }, bmi: { en: "BMI", }, bmiMildThin: { de: "Leichtes Untergewicht", en: "Mildly Thin", }, bmiModerateThin: { de: "Mäßiges Untergewicht", en: "Moderately Thin", }, bmiNormal: { de: "Normal", en: "Normal", }, bmiObese1: { de: "Adipositas I", en: "Obese Class I", }, bmiObese2: { de: "Adipositas II", en: "Obese Class II", }, bmiObese3: { de: "Adipositas III", en: "Obese Class III", }, bmiPreObese: { de: "Präadipositas", en: "Pre-Obese", }, bmiSevereThin: { de: "Starkes Untergewicht", en: "Severely Thin", }, bodyType: { de: "Statur", en: "Body Type", }, bodyType_ATHLETIC: { de: "Athletisch", en: "Athletic", }, bodyType_AVERAGE: { de: "Normal", en: "Average", }, bodyType_BELLY: { de: "Bauch", en: "Belly", }, bodyType_MUSCULAR: { de: "Muskulös", en: "Muscular", }, bodyType_SLIM: { de: "Schlank", en: "Slim", }, bodyType_STOCKY: { de: "Stämmig", en: "Stocky", }, bodyHair: { de: "Körperbehaarung", en: "Body Hair", }, bodyHair_AVERAGE: { de: "Mittel behaart", en: "Hairy", }, bodyHair_LITTLE: { de: "Wenig behaart", en: "Not very hairy", }, bodyHair_SHAVED: { de: "Rasiert", en: "Shaved", }, bodyHair_SMOOTH: { de: "Unbehaart", en: "Smooth", }, bodyHair_VERY_HAIRY: { de: "Stark behaart", en: "Very hairy", }, clearList: { de: "Möchtest du wirklich alle Einträge in der Liste entfernen?", en: "Do you really want to remove all elements from the list?", }, concision: { de: "Beschneidung", en: "Concision", }, concision_CUT: { de: "Beschnitten", en: "Cut", }, concision_UNCUT: { de: "Unbeschnitten", en: "Uncut", }, customRadius: { de: "Benutzerdefinierter Radius", en: "Custom Radius", }, dick: { de: "Schwanz", en: "Dick", }, dick_S: { en: "S", }, dick_M: { en: "M", }, dick_L: { en: "L", }, dick_XL: { en: "XL", }, dick_XXL: { en: "XXL", }, dirty: { en: "Dirty", }, dirty_NO: { de: "Kein Dirty", en: "No dirty", }, dirty_WS_ONLY: { de: "Ja, aber nur NS", en: "WS only", }, dirty_YES: { en: "Dirty", }, display: { de: "Anzeige", en: "Display", }, distance: { de: "Entfernung", en: "Distance", }, enhancedFilter: { de: "Filter mit allen Details", en: "Filter with all details", }, enhancedFilterDesc: { de: "Erlaubt Radar-Ergebnisse nach allen Details zu filtern.", en: "Allows to filter radar results by additional details.", }, enhancedTiles: { de: "Große Kacheln erzwingen", en: "Force big grid", }, enhancedTilesDesc: { de: "Zeigt alle Benutzer in großen Kacheln.", en: "Shows all users in big tiles.", }, ethnicity: { de: "Typ", en: "Ethnicity", }, ethnicity_ARAB: { de: "Araber", en: "Arab", }, ethnicity_ASIAN: { de: "Asiate", en: "Asian", }, ethnicity_BLACK: { de: "Schwarz", en: "Black", }, ethnicity_CAUCASIAN: { de: "Europäer", en: "Caucasian", }, ethnicity_INDIAN: { de: "Inder", en: "Indian", }, ethnicity_LATIN: { de: "Latino", en: "Latin", }, ethnicity_MEDITERRANEAN: { de: "Südländer", en: "Mediterranean", }, ethnicity_MIXED: { en: "Mixed", }, eyeColor: { de: "Augenfarbe", en: "Eye Colour", }, eyeColor_BLUE: { de: "Blau", en: "Blue", }, eyeColor_BROWN: { de: "Braun", en: "Brown", }, eyeColor_GREEN: { de: "Grün", en: "Green", }, eyeColor_GREY: { de: "Grau", en: "Grey", }, eyeColor_OTHER: { de: "Sonstige", en: "Other", }, fetish: { de: "Fetisch", en: "Fetish", }, fetish_BOOTS: { en: "Boots", }, fetish_CROSSDRESSING: { de: "Cross-Dressing", en: "Cross-dressing", }, fetish_DRAG: { de: "Dessous", en: "Lingerie", }, fetish_FORMAL: { de: "Anzug", en: "Formal dress", }, fetish_JEANS: { en: "Jeans", }, fetish_LEATHER: { de: "Leder", en: "Leather", }, fetish_LYCRA: { en: "Lycra", }, fetish_RUBBER: { en: "Rubber", }, fetish_SKATER: { en: "Skater", }, fetish_SKINS: { en: "Skins & Punks", }, fetish_SNEAKERS: { en: "Sneakers & Socks", }, fetish_SPORTS: { de: "Sportsgear", en: "Sports gear", }, fetish_TECHNO: { en: "Raver", }, fetish_UNDERWEAR: { de: "Unterwäsche", en: "Underwear", }, fetish_UNIFORM: { en: "Uniform", }, fetish_WORKER: { de: "Handwerker", en: "Worker", }, filters: { en: "Filters", de: "Filter", }, fisting: { de: "Fisten", en: "Fisting", }, fisting_ACTIVE: { de: "FF Aktiv", en: "FF Active", }, fisting_ACTIVE_PASSIVE: { de: "FF Flexibel", en: "FF Versatile", }, fisting_NO: { de: "Kein FF", en: "No FF", }, fisting_PASSIVE: { de: "FF Passiv", en: "FF Passive", }, fullHeadlines: { de: "Vollständige Überschriften", en: "Full headlines", }, fullHeadlinesDesc: { de: "Zeigt lange Profilüberschriften vollständig.", en: "Shows long profile headlines completely.", }, fullMessages: { de: "Vollständige Nachrichten", en: "Full messages", }, fullMessagesDesc: { de: "Zeigt Nachrichten ungekürzt in der Nachrichtenliste.", en: "Shows messages without truncation in the message list.", }, gender: { de: "Geschlecht", en: "Gender", }, gender_MAN: { de: "Mann", en: "Man", }, gender_TRANS_MAN: { de: "Transmann", en: "Trans man", }, gender_TRANS_WOMAN: { de: "Transfrau", en: "Trans woman", }, gender_NON_BINARY: { de: "Nicht binär", en: "Non-binary", }, gender_OTHER: { de: "Anderes", en: "Other", }, genderOrientation: { de: "Ich bin", en: "I am", }, general: { de: "Allgemein", en: "General", }, hairColor: { de: "Haarfarbe", en: "Hair Colour", }, hairColor_BLACK: { de: "Schwarz", en: "Black", }, hairColor_BLOND: { en: "Blond", }, hairColor_BROWN: { de: "Braune Haare", en: "Brown", }, hairColor_GREY: { de: "Grau", en: "Grey", }, hairColor_LIGHT_BROWN: { de: "Dunkelblond", en: "Light brown", }, hairColor_OTHER: { de: "Sonstige", en: "Other", }, hairColor_RED: { de: "Rot", en: "Red", }, hairLength: { de: "Haarlänge", en: "Hair Length", }, hairLength_AVERAGE: { de: "Normal", en: "Average", }, hairLength_LONG: { de: "Lang", en: "Long", }, hairLength_PUNK: { en: "Punk", }, hairLength_SHAVED: { de: "Rasiert", en: "Shaved", }, hairLength_SHORT: { de: "Kurz", en: "Short", }, height: { de: "Größe", en: "Height", }, hiddenUsers: { de: "Ausgeblendete Benutzer", en: "Hidden users", }, hideActivities: { de: "In Aktivitäten ausblenden", en: "Hide in activities", }, hideContacts: { de: "In Kontakten ausblenden", en: "Hide in contacts", }, hideLikes: { de: "In Bilder-Likes ausblenden", en: "Hide in picture likes", }, hideMessages: { de: "In Chat ausblenden", en: "Hide in chat", }, hideUser: { de: "Benutzer ausblenden", en: "Hide user", }, hideVisits: { de: "In Besuchern ausblenden", en: "Hide in visitors", }, interests: { de: "Interessen", en: "Interests", }, interests_ART: { de: "Kunst", en: "Art", }, interests_BOARDGAME: { de: "Brettspiele", en: "Board games", }, interests_CAR: { de: "Autos", en: "Cars", }, interests_COLLECT: { de: "Sammeln", en: "Collecting", }, interests_COMPUTER: { de: "Computer", en: "Computers", }, interests_COOK: { de: "Kochen", en: "Cooking", }, interests_DANCE: { en: "Dance", }, interests_FILM: { en: "Film & Video", }, interests_FOTO: { de: "Fotografie", en: "Photography", }, interests_GAME: { de: "Computerspiele", en: "Gaming", }, interests_LITERATURE: { de: "Literatur", en: "Literature", }, interests_MODELING: { de: "Modellbau", en: "Model building", }, interests_MOTORBIKE: { de: "Motorrad", en: "Motorbikes", }, interests_MUSIC: { de: "Musik", en: "Music", }, interests_NATURE: { de: "Natur", en: "Nature", }, interests_POLITICS: { de: "Politik", en: "Politics", }, interests_TV: { en: "TV", }, languages: { de: "Sprachen", en: "Languages", }, languages_af: { de: "Afrikaans", en: "Afrikaans", }, languages_ar: { de: "Arabisch", en: "Arabic", }, languages_arm: { de: "Armenisch", en: "Armenian", }, languages_az: { de: "Aserbaidschanisch", en: "Azerbaijani", }, languages_be: { de: "Belarussisch", en: "Belarusian", }, languages_bg: { de: "Bulgarisch", en: "Bulgarian", }, languages_bn: { de: "Bengali", en: "Bengali", }, languages_bs: { de: "Bosnisch", en: "Bosnian", }, languages_bur: { de: "Burmesisch", en: "Burmese", }, languages_ca: { de: "Katalanisch", en: "Catalan", }, languages_ceb: { de: "Cebuano", en: "Cebuano", }, languages_cs: { de: "Tschechisch", en: "Czech", }, languages_da: { de: "Dänisch", en: "Danish", }, languages_de: { de: "Deutsch", en: "German", }, languages_el: { de: "Griechisch", en: "Greek", }, languages_en: { de: "Englisch", en: "English", }, languages_eo: { de: "Esperanto", en: "Esperanto", }, languages_es: { de: "Spanisch", en: "Spanish", }, languages_et: { de: "Estnisch", en: "Estonian", }, languages_eu: { de: "Baskisch", en: "Basque", }, languages_fa: { de: "Persisch", en: "Persian", }, languages_fi: { de: "Finnisch", en: "Finnish", }, languages_fr: { de: "Französisch", en: "French", }, languages_frc: { de: "Kanadisches Französisch", en: "Canadian French", }, languages_gd: { de: "Schottisch-Gälisch", en: "Scottish Gaelic", }, languages_gl: { de: "Galician", en: "Galician", }, languages_gsw: { de: "Schwyzerdütsch", en: "Swiss-German", }, languages_hi: { de: "Hindi", en: "Hindi", }, languages_hr: { de: "Kroatisch", en: "Croatian", }, languages_hu: { de: "Ungarisch", en: "Hungarian", }, languages_id: { de: "Indonesisch", en: "Indonesian", }, languages_is: { de: "Isländisch", en: "Icelandic", }, languages_it: { de: "Italienisch", en: "Italian", }, languages_iw: { de: "Hebräisch", en: "Hebrew", }, languages_ja: { de: "Japanisch", en: "Japanese", }, languages_ka: { de: "Georgisch", en: "Georgian", }, languages_kl: { de: "Grönländisch", en: "Greenlandic (Kalaallisut)", }, languages_km: { de: "Kambodschanisch", en: "Cambodian", }, languages_kn: { de: "Kannada", en: "Kannada", }, languages_ko: { de: "Koreanisch", en: "Korean", }, languages_ku: { de: "Kurdisch", en: "Kurdish", }, languages_la: { de: "Latein", en: "Latin", }, languages_lb: { de: "Luxemburgisch", en: "Luxembourgish", }, languages_lo: { de: "Laotisch", en: "Lao", }, languages_lt: { de: "Litauisch", en: "Lithuanian", }, languages_lv: { de: "Lettisch", en: "Latvian", }, languages_mk: { de: "Mazedonisch", en: "Macedonian", }, languages_ml: { de: "Malayalam", en: "Malayalam", }, languages_mr: { de: "Marathi", en: "Marathi", }, languages_ms: { de: "Malaiisch", en: "Malay", }, languages_mt: { de: "Maltesisch", en: "Maltese", }, languages_nl: { de: "Niederländisch", en: "Dutch", }, languages_no: { de: "Norwegisch", en: "Norwegian", }, languages_oc: { de: "Okzitanisch", en: "Occitan", }, languages_pl: { de: "Polnisch", en: "Polish", }, languages_ps: { de: "Paschtunisch", en: "Pashto", }, languages_pt: { de: "Portugiesisch", en: "Portuguese", }, languages_ro: { de: "Rumänisch", en: "Romanian", }, languages_roh: { de: "Rätoromanisch", en: "Romansch", }, languages_ru: { de: "Russisch", en: "Russian", }, languages_sgn: { de: "Gebärdensprache", en: "Sign language", }, languages_sh: { de: "Serbo-Croatian", en: "Serbo-Croatian", }, languages_sk: { de: "Slowakisch", en: "Slovak", }, languages_sl: { de: "Slowenisch", en: "Slovenian", }, languages_sq: { de: "Albanisch", en: "Albanian", }, languages_sr: { de: "Serbisch", en: "Serbian", }, languages_sv: { de: "Schwedisch", en: "Swedish", }, languages_ta: { de: "Tamil", en: "Tamil", }, languages_te: { de: "Telugu", en: "Telugu", }, languages_th: { de: "Thailändisch", en: "Thai", }, languages_tl: { de: "Tagalog", en: "Tagalog", }, languages_tr: { de: "Türkisch", en: "Turkish", }, languages_uk: { de: "Ukrainisch", en: "Ukrainian", }, languages_us: { de: "US-Englisch", en: "US English", }, languages_vi: { de: "Vietnamesisch", en: "Vietnamese", }, languages_wel: { de: "Walisisch", en: "Welsh", }, languages_wen: { de: "Sorbisch", en: "Sorbian", }, languages_zgh: { de: "Tamazight", en: "Tamazight", }, languages_zh: { de: "Chinesisch", en: "Chinese", }, lastLogin: { de: "Letzter Login", en: "Last Login", }, location: { de: "Ort", en: "Location", }, latLong: { de: "Breitengrad, Längengrad", en: "Latitude, Longitude", }, lookingFor: { de: "Ich suche", en: "Looking For", }, lookingForOther: { de: "Sucht nach", en: "They're Looking For", }, maxAge: { de: "Maximales Alter", en: "Maximal age", }, messages: { de: "Nachrichten", en: "Messages", }, metadata: { de: "Metadaten", en: "Metadata", }, minAge: { de: "Minimales Alter", en: "Minimal age", }, myAge: { de: "Mein Alter", en: "My Age", }, myGender: { de: "Mein Geschlecht", en: "My gender", }, myOrientation: { de: "Meine Orientierung", en: "My orientation", }, new: { de: "Neu", en: "New", }, noEntry: { de: "Keine Angabe", en: "No entry", }, onlineStatus: { en: "Status", }, onlineStatus_DATE: { en: "Date", }, onlineStatus_OFFLINE: { en: "Offline", }, onlineStatus_ONLINE: { en: "Online", }, onlineStatus_SEX: { en: "Now", }, travelersOnly: { de: "Nur Reisende", en: "Travelers only", }, openTo: { de: "Offen für", en: "Open to", }, openTo_FRIENDSHIP: { de: "Freunde", en: "Friends", }, openTo_RELATIONSHIP: { de: "Beziehung", en: "Relationship", }, openTo_SEXDATES: { en: "Sex", }, orientation: { de: "Orientierung", en: "Orientation", }, orientation_BISEXUAL: { de: "Bisexuell", en: "Bisexual", }, orientation_GAY: { en: "Gay", }, orientation_QUEER: { en: "Queer", }, orientation_OTHER: { de: "Andere", en: "Other", }, orientation_STRAIGHT: { de: "Hetero", en: "Straight", }, other: { de: "Sonstige", en: "Other", }, piercings: { en: "Piercings", }, piercings_A_FEW: { de: "Wenige", en: "A few", }, piercings_A_LOT: { de: "Viele", en: "A lot", }, piercings_NO: { de: "Keine Piercings", en: "No piercings", }, profileId: { de: "Profil-ID", en: "Profile ID", }, relationship: { de: "Beziehung", en: "Relationship", }, relationship_MARRIED: { de: "Verheiratet", en: "Married", }, relationship_OPEN: { de: "Offene Partnerschaft", en: "Open", }, relationship_PARTNER: { de: "Verpartnert", en: "Partner", }, relationship_SINGLE: { en: "Single", }, saferSex: { de: "Safer Sex", en: "Safer sex", }, saferSex_ALWAYS: { en: "Safe", }, saferSex_CONDOM: { de: "Kondom", en: "Condom", }, saferSex_NEEDS_DISCUSSION: { de: "Nach Absprache", en: "Let's talk", }, saferSex_PREP: { en: "PrEP", }, saferSex_PREP_AND_CONDOM: { de: "PrEP und Kondom", en: "PrEP and condom", }, saferSex_TASP: { en: "TasP", }, sendEnter: { de: "Enter sendet Nachricht", en: "Enter sends message", }, sendEnterDesc: { de: "Wenn deaktiviert erzeugt Enter einen Absatz und Strg+Enter sendet die Nachricht.", en: "If disabled, Enter creates a new line instead and Ctrl+Enter sends the message.", }, sexual: { de: "Sexuelles", en: "Sexual", }, sm: { de: "SM", en: "S&M", }, sm_NO: { de: "Kein SM", en: "No SM", }, sm_SOFT: { en: "Soft SM", }, sm_YES: { en: "SM", }, smoker: { de: "Raucher", en: "Smoker", }, smoker_NO: { de: "Nein", en: "No", }, smoker_SOCIALLY: { de: "Selten", en: "Socially", }, smoker_YES: { de: "Ja", en: "Yes", }, socialSmoker: { de: "Raucht selten", en: "Social Smoker", }, speakingMyLanguage: { de: "Spricht meine Sprache", en: "Speaking my language", }, systemMessages: { de: "Systemnachrichten", en: "System messages", }, systemMessagesDesc: { de: "Erlaubt Popups wie Standort- oder Fehlermeldungen.", en: "Allows popups like GPS or error messages.", }, tattoos: { en: "Tattoos", }, tattoos_A_FEW: { de: "Wenige", en: "A few", }, tattoos_A_LOT: { de: "Viele", en: "A lot", }, tattoos_NO: { de: "Keine Tattoos", en: "No tattoos", }, tiles: { de: "Benutzerkacheln", en: "User Tiles", }, tileCount: { de: "Kachelspalten (0 für Standard)", en: "Tile columns (0 for default)", }, tileDetails: { de: "Benutzerkachel-Details", en: "User Tile Details", }, typingNotifications: { de: "Tippbenachrichtigungen", en: "Typing notifications", }, typingNotificationsDesc: { de: "Ob Empfänger die Eingabe einer Nachricht sehen können.", en: "Whether receivers can see that a message is being written.", }, viewFullImage: { de: "Bild anzeigen", en: "Preview image", }, viewProfile: { de: "Profilvorschau anzeigen", en: "Preview profile", }, weight: { de: "Gewicht", en: "Weight", } }; function translate(key) { const translations = strings[key]; return translations ? translations[getLang()] || translations.en || "%" + key + "%" : "%" + key + "%"; } function translateEnum(name, key) { return translate(`${name}_${key}`); } // ---- Settings ---- const settingsNs = "RA_SETTINGS:"; let measurementSystem = "METRIC"; let radarFilter = {}; let tileDetails = new Set(); let tileStyle = null; function load(name, fallback) { const value = localStorage.getItem(settingsNs + name); return value === "false" ? false : value ? value : fallback; } function save(name, value) { localStorage.setItem(settingsNs + name, value); } function 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); } /* discover */ section.js-main-stage > main main > section > ul { grid-template-columns: repeat(var(--tile-count), 1fr) !important; } /* radar desktop */ .search-results__item { padding-bottom: var(--tile-size) !important; width: var(--tile-size) !important; } /* radar mobile - starts at 768px where .search-results__item turns inline, requiring to adjust .tile */ @media not screen and (min-width: 768px) { .tile:not(.js-strip .tile):not(.tile--small) { width: var(--tile-size) !important; } } /* visitors */ #cruise main > ul { grid-template-columns: repeat(var(--tile-count), 1fr); } `); } } else { if (tileStyle) { tileStyle.remove(); tileStyle = null; } } save("tileCount", value); } function setTileDetail(key, visible) { if (visible) tileDetails.add(key); else tileDetails.delete(key); save("tileDetails", JSON.stringify(Array.from(tileDetails))); } function setTypingNotifications(value) { save("typingNotifications", value); } function setUserHidden(username, hide) { let hiddenUsers = getHiddenUsers(); if (hide) hiddenUsers.add(username); else hiddenUsers.delete(username); save("hiddenUsers", JSON.stringify(Array.from(hiddenUsers))); } // ---- Fetch/XHR ---- const apiKey = atob("QVM4YnpHSExBOFk5QlhGNzNpRE51UUJIZUVPMFVLamY="); let sessionId; 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 initFetch() { const realFetch = window.fetch; window.fetch = async (resource, options) => { // Hook send. const e = options ? { method: options.method, url: resource } : { 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); } function doFetch(method, url, body, referrer) { const options = { method: method, cache: "no-cache", // sets cache-control credentials: "same-origin", // apparently sets cookie, otherwise try "include" headers: { "x-api-key": apiKey, "x-session-id": sessionId, "x-site": "planetromeo" } }; if (body) { options.headers["content-type"] = "application/json"; options.body = JSON.stringify(body); } if (referrer) { options.referrer = referrer; } return fetch(url, options); } // XHR const xhrHooks = {}; const xhrOpened = {}; function initXhr() { // Hook open. const realOpen = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) { const e = { method: method, url: url }; xhrOpened[this] = e; if (callHook(xhrHooks, "open", e)) { method = e.method; url = e.url; } if (!e.cancel) realOpen.apply(this, arguments); // Hook load. this.addEventListener("load", () => { const json = isJson(this.response); e.body = json ? JSON.parse(this.response) : this.response; e.status = this.status; if (callHook(xhrHooks, "load", e)) { Object.defineProperty(this, "responseText", { writable: true }); this.responseText = json ? JSON.stringify(e.body) : e.body; } if (e.cancel) { this.response = null; this.responseText = null; this.status = 404; } }); }; // Hook send. const realSend = window.XMLHttpRequest.prototype.send; window.XMLHttpRequest.prototype.send = function (body) { const e = xhrOpened[this]; delete xhrOpened[this]; const json = isJson(body); if (body) e.body = json ? JSON.parse(body) : body; if (callHook(xhrHooks, "send", e) && e.body) body = json ? JSON.stringify(e.body) : body; if (!e.cancel) realSend.apply(this, arguments); }; } function onXhr(func, method, route, callback) { addHook(xhrHooks, func, method, route, callback); } // ---- Lists ---- 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(); } // ---- 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 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; } } `); async function blockUser(username) { if (!username) return false; const profileId = profileCache[username].id; if (!profileId) return false; const response = await doFetch("POST", "/api/v4/profiles/blocked", { profile_id: profileId, note: "" }, `https://www.romeo.com/profile/${username}/report`); return response.ok; } function getBackgroundImageUrl(style) { return style.match(/"(.+)"/)[1]; } function getFullImageUrl(url) { return url.replace(/squarish\/[0-9]*x[0-9]*\//, ""); } function getUsernameFromHref(href) { let start = href.indexOf("profile/"); if (start === -1) start = href.indexOf("hunq/"); return href.substring(start).split("/")[1]; } function xhrHandleSession(reply) { // Determine session ID. sessionId = reply.session_id; // Apply settings. const settings = reply.bb_settings; if (settings) { // Determine measurement locale. measurementSystem = settings.interface?.measurement_system ?? measurementSystem; // Determine radar filter, remove deleted ones. const radarFilterId = settings.bluebird?.search_filter?.id; radarFilter = getSavedRadarFilter(radarFilterId); for (const savedFilterId of Object.keys(getSavedRadarFilters())) if (savedFilterId && !reply.data.search_filters.find(x => x.id === savedFilterId)) setSavedRadarFilter(savedFilterId); } // 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)); // ---- Menu ---- addCss(` #ra_context_bg { background: transparent; display: none; height: 100%; position: fixed; width: 100%; z-index: 10000; } #ra_context_ul { background: #232323; border-radius: 1.125rem; box-shadow: rgba(0, 0, 0, 0.32) 0px 0px 2px, rgba(0, 0, 0, 0.24) 0px 0px 1px, rgba(0, 0, 0, 0.16) 0px 0px 5px; display: none; font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif; font-size: 94%; overflow: hidden; position: absolute; z-index: 10001; } .ra_context_li { border-color: transparent; border-left: 2px solid transparent; border-style: solid; border-width: 1px 1px 1px 2px; color: #FFF; cursor: default; padding: 9px 18px 10px 10px; transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1); white-space: nowrap; } .ra_context_li:not(:first-child) { border-top: 1px solid rgba(255, 255, 255, 0.16); } .ra_context_li .icon { margin: 4px; } .ra_context_li:hover { background: #2E2E2E; } @media screen and (max-width: 767px) { #ra_context_bg { background: rgba(0, 0, 0, 0.6); } #ra_context_ul { border-bottom-left-radius: 0; border-bottom-right-radius: 0; bottom: 0; left: unset !important; position: fixed; top: unset !important; width: 100%; } .ra_context_li { padding: 6px; } .ra_context_li .icon { font-size: 1.2rem; margin: 8px; } } `); let menuBg; let menuUl; let menuX; let menuY; const menuHandlers = {}; function initMenu() { // Create context menu canceler. menuBg = addElement(document.body, "<div id='ra_context_bg'></ul>"); menuBg.addEventListener("click", e => hideMenu()); // Create context menu. menuUl = addElement(document.body, "<ul id='ra_context_ul'></ul>"); // Attach to events. addEventListener("contextmenu", e => { menuX = e.clientX; menuY = e.clientY; // Go through hierarchy of clicked elements. for (const el of document.elementsFromPoint(menuX, menuY)) { // Stop when hitting a layer. if (el.classList.contains("layer") || el.classList.contains("layout") || el.classList.contains("ReactModal__Overlay")) break; // Invoke first context handler for this element. for (const [key, value] of Object.entries(menuHandlers)) { if (el.matches(key)) { log(`opening menu '${key}'`); value(el); e.preventDefault(); return; } } } }); } function showMenu() { menuBg.style.display = "block"; menuUl.replaceChildren(); for (const item of arguments) { const li = addElement(menuUl, ` <li class="ra_context_li"> <span class="icon icon-${item.icon}"></span> ${translate(item.text)} </li>`); li.addEventListener("click", e => { hideMenu(); item.onclick(); }); } menuUl.style.display = "block"; const maxX = window.innerWidth - menuUl.offsetWidth; const maxY = window.innerHeight - menuUl.offsetHeight; menuUl.style.left = Math.min(menuX, maxX) + "px"; menuUl.style.top = Math.min(menuY, maxY) + "px"; } function hideMenu() { menuBg.style.display = "none"; menuUl.style.display = "none"; } function onMenu(selector, handler) { menuHandlers[selector] = handler; } function menuItem(icon, text, onclick) { return { icon, text, onclick }; } // ---- 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 initPreviews() { window.addEventListener("popstate", e => { // Restore navigating back to preview. switch (e.state?.ra_preview) { case "image": showImagePreview(e.state.src, false); break; case "profile": showProfilePreview(e.state.username, false); break; } }); window.navigation?.addEventListener("navigate", e => { // Hide preview on any other navigation. previewLayer?.remove(); }); } function showImagePreview(src, pushHistory = true) { if (pushHistory) history.pushState({ ra_preview: "image", src: src }, ""); const content = addElement(createPreview(translate("viewFullImage")), `<div id="ra_image_content"></div>`); addElement(content, `<img id="ra_profile_pic" src="${src}"></img>`); } function showProfilePreview(username, pushHistory = true) { function isEntry(value) { return value && value !== "NO_ENTRY"; } function addSection(el, key) { return addElement(el, ` <details class="ra_profile_details" open> <summary class="ra_profile_summary">${translate(key)}</summary> </details>`); } function add(section, key, value) { if (value) { addElement(section, ` <div class="ra_profile_keyvalue"> <div>${translate(key)}</div> <div>${value}</div> </div>`); } } function addAgeRange(section, range) { if (range) add(section, "ageRange", getProfileAgeRange(range)); } function addArrayEnum(section, key, array) { if (!array) return; let values = []; for (let i = 0; i < array.length; i++) if (isEntry(array[i])) values.push(translateEnum(key, array[i])); if (values.length) add(section, key, values.join(", ")); } function addDistance(section, distance, sensor) { let text = measurementSystem === "METRIC" ? `${distance / 1000} km` : `${round(distance * M2MI, 1)}mi`; if (sensor) text += " (GPS)"; add(section, "distance", text); } function addEnum(section, key, value) { if (isEntry(value)) add(section, key, translateEnum(key, value)); } function addGender(section, genderOrientation) { let values = []; if (isEntry(genderOrientation?.orientation)) values.push(translateEnum("orientation", genderOrientation.orientation)); if (isEntry(genderOrientation?.gender)) values.push(translateEnum("gender", genderOrientation.gender)); if (values.length) add(section, "genderOrientation", values.join(" / ")); } const profile = profileCache[username]; if (!profile) return; const personal = profile.personal; const sexual = profile.sexual; if (pushHistory) history.pushState({ ra_preview: "profile", username: username }, ""); const content = addElement(createPreview(username), `<div id="ra_profile_content"></div>`); const left = addElement(content, `<div id="ra_profile_left"></div>`); const right = addElement(content, `<div id="ra_profile_right"></div>`); addElement(left, `<div>${escapeHtml(profile.headline ?? "")}</div>`); const img = addElement(left, `<img id="ra_profile_pic"></img>`); img.src = profile.pic ? `/img/usr/${profile.pic}.jpg` : "/assets/f8a7712027544ed03920.svg"; const section = addSection(right, "metadata"); addEnum(section, "onlineStatus", profile.online_status); 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 (personal) { const section = addSection(right, "lookingFor"); addArrayEnum(section, "openTo", personal.looking_for); addAgeRange(section, personal.target_age); addArrayEnum(section, "gender", personal.gender_orientation?.looking_for_gender); addArrayEnum(section, "orientation", personal.gender_orientation?.looking_for_orientation); if (!section.querySelectorAll(".ra_profile_keyvalue").length) section.remove(); } if (personal) { const section = addSection(right, "general"); add(section, "age", personal.age); add(section, "height", getProfileHeight(personal.height)); add(section, "weight", getProfileWeight(personal.weight)); add(section, "bmi", getProfileBmi(personal.height, personal.weight, true)); add(section, "bodyType", getProfileEnum("bodyType", personal.body_type)); add(section, "ethnicity", getProfileEnum("ethnicity", personal.ethnicity)); addEnum(section, "hairLength", personal.hair_length); addEnum(section, "hairColor", personal.hair_color); addEnum(section, "beard", personal.beard); addEnum(section, "eyeColor", personal.eye_color); add(section, "bodyHair", getProfileEnum("bodyHair", personal.body_hair)); addGender(section, personal?.gender_orientation); addEnum(section, "smoker", personal.smoker); addEnum(section, "tattoos", personal.tattoo); addEnum(section, "piercings", personal.piercing); addArrayEnum(section, "languages", personal.spoken_languages); add(section, "relationship", getProfileEnum("relationship", personal.relationship)); } if (sexual) { const section = addSection(right, "sexual"); add(section, "analPosition", getProfileEnum("analPosition", sexual.anal_position)); add(section, "dick", getProfileDick(sexual.dick_size, sexual.concision)); addArrayEnum(section, "fetish", sexual.fetish); add(section, "dirty", getProfileEnum("dirty", sexual.dirty_sex)); addEnum(section, "fisting", sexual.fisting); addEnum(section, "sm", sexual.sm); add(section, "saferSex", getProfileEnum("saferSex", sexual.safer_sex)); if (!section.querySelectorAll(".ra_profile_keyvalue").length) section.remove(); } if (profile.personal?.profile_text) { const section = addSection(right, "aboutMe"); addElement(section, `<div id="ra_profile_text">${profile.personal.profile_text}</div>`); } } // ---- Profiles ---- const profileCache = {}; function cacheProfile(profileObject) { const existing = profileCache[profileObject.name]; const profile = { id: profileObject.id, name: profileObject.name, headline: profileObject.headline ?? existing?.headline, last_login: profileObject.last_login ?? existing?.last_login, location: profileObject.location ?? existing?.location, online_status: profileObject.online_status ?? existing?.online_status, pic: profileObject.preview_pic?.url_token ?? existing?.pic, // not available if no picture personal: profileObject.profile?.personal ?? profileObject.personal ?? existing?.personal, // not available in activities sexual: profileObject.profile?.sexual ?? profileObject.sexual ?? existing?.sexual, // not available in activities albums: profileObject.albumsV2 ?? existing?.albumsV2 }; profileCache[profile.name] = profile; return profile; } function filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames) { // Return whether to display the profile. return (!profile.personal || profile.personal.age >= hiddenMinAge && profile.personal.age <= hiddenMaxAge) && !hiddenNames.has(profile.name); } function filterItemsAndCacheProfiles(items, profileSelector, filter) { let newItems = []; const hiddenMaxAge = getHiddenMaxAge(); const hiddenMinAge = getHiddenMinAge(); const hiddenNames = getHiddenUsers(); for (const item of items ?? []) { const profile = cacheProfile(profileSelector(item)); if (!filter || filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames)) newItems.push(item); } return newItems; } function getProfileAgeRange(range, short) { if (range) { const min = range.min ?? "18"; const max = range.max ?? "99"; return short ? `${min}-${max}` : translate("ageRangeValue").replace("$from", min).replace("$to", max); } } function getProfileBmi(height, weight, withName) { if (height && weight) { const bmi = weight / Math.pow(height / 100, 2); let result = `${round(bmi, 1).toFixed(1)}`; if (withName) { for (const [max, key] of Object.entries({ 16: "bmiSevereThin", 17: "bmiModerateThin", 18.5: "bmiMildThin", 25: "bmiNormal", 30: "bmiPreObese", 35: "bmiObese1", 40: "bmiObese2", 99: "bmiObese3", })) { if (bmi < max) return result + ` / ${translate(key)}`; } } return result; } } function getProfileDick(size, concision) { let values = []; if (size && size !== "NO_ENTRY") values.push(translateEnum("dick", size)); if (concision && concision !== "NO_ENTRY") values.push(translateEnum("concision", concision)); if (values.length) return values.join(" - "); } function getProfileEnum(key, value) { if (value && value !== "NO_ENTRY") return translateEnum(key, value); } function getProfileHeight(height) { if (height) { return measurementSystem === "METRIC" ? `${height}cm` : `${round(height * CM2FT, 2)} ft`; } } function getProfileWeight(weight) { if (weight) { return measurementSystem === "METRIC" ? `${weight}kg` : `${round(weight * KG2LBS)}lbs`; } } function fetchHandleReactionsCruiseLikes(e) { e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideLikes()); } function xhrHandleProfiles(e) { e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, true); e.body.items_limited = e.body.items_total; // Remove PLUS ad tile. // Show every user as a large tile. if (getEnhancedTiles()) for (const item of e.body.items ?? []) if (item.display) item.display.large_tile = true; } function xhrHandleVisits(e) { e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, getHideVisits()); e.body.items_limited = e.body.items_total; // Restore PLUS-visible visitors. } onXhr("load", "GET", "v4/contacts", e => { if (e.body.cursors) e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideContacts()); }); onXhr("load", "GET", "v4/messages/conversations", e => { e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.chat_partner, getHideMessages()); }); onXhr("load", "GET", "+/notifications/activity-stream", e => { e.body = filterItemsAndCacheProfiles(e.body, x => x.partner, getHideActivities()); }); 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/profiles", e => xhrHandleProfiles(e)); onFetch("recv", "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; } `); onDom(`.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; if (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 in mobile visitors/visits during load */ div.BIG::before, div.SMALL::before { inset: 60% 0px 0px !important; } /* tile description truncation */ .tile p[class^="SpecialText-"] { white-space: var(--tile-headline-white-space); } .ra_tile_headline { color: rgb(255, 255, 255); font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif; font-size: 0.8125rem; font-weight: 400; line-height: 1.23077; overflow: hidden; text-overflow: ellipsis; text-shadow: rgba(0, 0, 0, 0.32) 0px 1px 1px, rgba(0, 0, 0, 0.42) 1px 1px 1px; white-space: var(--tile-headline-white-space); } .ra_tile_tag_row { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; } .ra_tile_tag { background-color: rgb(46, 46, 46); border-radius: 2px; box-shadow: rgba(0, 0, 0, 0.32) 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 0px 1px, rgba(0, 0, 0, 0.16) 0px 0px 3px; color: rgba(255, 255, 255, 0.87); font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif; font-size: 0.8125rem; font-weight: 400; line-height: 1.23077; padding: 0px 2px; } .ra_tile_tag_new { color: rgb(0, 209, 0); } `); onDom(`div.grid-tile:not(div[class*="tile--loading--"]) > .reactView > a, main#visitors > ul > li > a, main#visited-grid > ul > li > a, section.js-main-stage > main main > section > 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)); } }); onMenu(`section.js-main-stage > main main > section > ul > li, .search-results__item > .tile, .js-results .tile`, el => { // discover / radar / search > tile function hideTile() { if (el.classList.contains("tile--plus")) el.parentNode.style.display = "none"; // big radar tile else el.style.display = "none"; // small / normal tile } let inner; if (inner = el.querySelector("a")) { // full tile const username = getUsernameFromHref(inner.href); showMenu( menuItem("search", "viewProfile", () => showProfilePreview(username)), menuItem("hide-visit", "hideUser", () => { setUserHidden(username, true); hideTile(); }), menuItem("illegal", "blockUser", async () => { if (await blockUser(username)) hideTile(); }), ); } else if (inner = el.querySelector(".BIG") ?? el.querySelector(".SMALL")) { // image-only tile const imageUrl = getFullImageUrl(getBackgroundImageUrl(inner.style.backgroundImage)); showMenu( menuItem("search", "viewFullImage", () => showImagePreview(imageUrl)), ); } }); onMenu(` main#visitors > ul > li, main#visited-grid > ul > li, main#likers-list > ul > li`, el => { // cruise > tiles const a = el.querySelector("a"); const username = getUsernameFromHref(a.href); showMenu( menuItem("search", "viewProfile", () => showProfilePreview(username)), menuItem("hide-visit", "hideUser", () => { setUserHidden(username, true); if (a.closest("main#likers-list") ? getHideLikes() : getHideVisits()) el.style.display = "none"; }), menuItem("illegal", "blockUser", async () => { if (await blockUser(username)) el.style.display = "none"; }), ); }); onMenu("img.thumbnail", el => { // activities > activity > liked image showMenu( menuItem("search", "viewFullImage", () => showImagePreview(getFullImageUrl(el.src))), ); }); onMenu(".js-stream .js-list .listitem", el => { // activities > activity const a = el.querySelector("a"); const username = getUsernameFromHref(a.href); showMenu( menuItem("search", "viewProfile", () => showProfilePreview(username)), menuItem("hide-visit", "hideUser", () => { setUserHidden(username, true); if (getHideActivities()) el.style.display = "none"; }), menuItem("illegal", "blockUser", async () => { if (await blockUser(username)) el.style.display = "none"; }), ); }); // ---- Messaging ---- addCss(` /* message list truncation */ #messenger div[class^="TruncateBlock__Content-sc-"] { -webkit-line-clamp: var(--message-line-clamp); } `); onDom(`.js-send-region.layout-item > div`, el => { el.addEventListener("keydown", e => { // Prevent site event handler from sending message or typing notifications. const enter = e.key === "Enter"; const send = enter && (getSendEnter() || e.ctrlKey); const allow = send || getTypingNotifications() && !enter; if (!allow) e.stopPropagation(); }, true); }); onMenu(".js-chat .reactView", el => { // messages > message const a = el.querySelector("a"); const username = getUsernameFromHref(a.href); showMenu( menuItem("search", "viewProfile", () => showProfilePreview(username)), menuItem("hide-visit", "hideUser", () => { setUserHidden(username, true); if (getHideMessages()) el.style.display = "none"; }), menuItem("illegal", "blockUser", async () => blockUser(username)), ); }); onMenu(".js-chat .reactView img", el => { // messages > message > sent image showMenu( menuItem("search", "viewFullImage", () => showImagePreview(getFullImageUrl(el.src))), ); }); onMenu(".js-contacts .reactView", el => { // contacts > contact const a = el.querySelector("a"); const username = getUsernameFromHref(a.href); showMenu( menuItem("search", "viewProfile", () => showProfilePreview(username)), menuItem("hide-visit", "hideUser", () => { setUserHidden(username, true); if (getHideContacts()) el.style.display = "none"; }), menuItem("illegal", "blockUser", async () => { if (await blockUser(username)) el.style.display = "none"; }), ); }); // ---- Albums ---- let changeProfilePic = false; //addCss(` ///* ensure images are not cropped */ //div[role='dialog'] > div > main > ul > li > img //{ // object-fit: contain; //} //`); onDom(`li#picture_menu_set-as-main-profile-picture`, el => { const button = el.querySelector("button"); // Only allow profile pic being changed when manually clicking this button. button.parentNode.addEventListener("click", e => changeProfilePic = true, true); }); onXhr("send", "PUT", "v4/profiles/me", e => { // Prevent automatic profile picture change when rearranging pictures. if (changeProfilePic) changeProfilePic = false; else if (e.body.preview_pic_id) e.cancel = true; }); // ---- Settings ---- onDom(`li.js-settings > div.accordion > ul.js-list`, el => { // Add extension menu item. const linkClass = el.querySelector("a").className; const link = addElement(el, ` <li> <div> <a class="${linkClass}" href="/me/romeoadditions">${GM_info.script.name}</a> </div> </li>`); link.addEventListener("click", e => { if (link.classList.contains("is-selected")) { link.classList.remove("is-selected"); } else { link.classList.add("is-selected"); setTimeout(() => openSettingsPane(el, link)); // delayed execution to force open panel } }); // Deselect menu item if others are clicked. for (const linkOther of el.querySelectorAll("li")) if (linkOther !== link) linkOther.addEventListener("click", e => link.classList.remove("is-selected")); }); onDom(`#offcanvas-nav > .js-layer-content > main > div.layout > div.reactView--autoHeight > p[class^="MiniText-sc-"]`, el => { el.innerHTML += `<a class="marionette" style="display:block" href="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"); if (desc) { addElement(section, ` <div> <div class="settings__description">${translate(desc)}</div> </div>`); } return input; } function addNumber(section, text, min, max) { return addElement(section, ` <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ] mv-"> <span>${translate(text)}</span> </div> <div class="layout-item [ 6/12--sm ] mv-"> <input class="input input--block" type="number" min="${min}" max="${max}"/> </div> </div>`).querySelector("input"); } function addTagList(section) { return addElement(section, ` <div class="mv js-grid-stats-selector"> <div> <ul class="js-list tags-list tags-list--centered"/> </div> </div>`).querySelector("ul"); } function addTag(ul, tag, text, selected, change) { const li = addElement(ul, ` <li class="tags-list__item"> <a class="js-tag ui-tag ui-tag--removable" href="#"> <span class="ui-tag__label">${text}</span> </a> </li>`); const a = li.querySelector("a"); if (selected) a.classList.add("ui-tag--selected"); li.addEventListener("click", e => { e.preventDefault(); if (a.classList.contains("ui-tag--selected")) { change({ tag: tag, checked: false }); a.classList.remove("ui-tag--selected"); } else { change({ tag: tag, checked: true }); a.classList.add("ui-tag--selected"); } }); } // Add tiles section. const tilesSection = addSection("tiles"); const enhancedFilter = addCheckbox(tilesSection, "enhancedFilter", "enhancedFilterDesc"); enhancedFilter.checked = getEnhancedFilter(); enhancedFilter.addEventListener("change", e => setEnhancedFilter(e.target.checked)); const enhancedTiles = addCheckbox(tilesSection, "enhancedTiles", "enhancedTilesDesc"); enhancedTiles.checked = getEnhancedTiles(); enhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked)); const fullHeadlines = addCheckbox(tilesSection, "fullHeadlines", "fullHeadlinesDesc"); fullHeadlines.checked = getFullHeadlines(); fullHeadlines.addEventListener("change", e => setFullHeadlines(e.target.checked)); const tileCount = addNumber(tilesSection, "tileCount", 0, 10); tileCount.value = getTileCount(); tileCount.addEventListener("change", e => setTileCount(parseInt(e.target.value))); // Add tile details section. const tileDetailsSection = addSection("tileDetails"); const tileDetailsList = addTagList(tileDetailsSection, "tileDetailsList"); for (const tileDetail of ["age", "height", "weight", "bmi", "smoker", "ageRange", "bodyHair", "bodyType", "ethnicity", "relationship", "analPosition", "dick", "saferSex", "dirty", "sm", "fisting", "openTo"]) { addTag(tileDetailsList, tileDetail, translate(tileDetail), tileDetails.has(tileDetail), e => setTileDetail(e.tag, e.checked)); } // Add messages section. const messagesSection = addSection("messages"); const systemMessages = addCheckbox(messagesSection, "systemMessages", "systemMessagesDesc"); systemMessages.checked = getSystemMessages(); systemMessages.addEventListener("change", e => setSystemMessages(e.target.checked)); const fullMessages = addCheckbox(messagesSection, "fullMessages", "fullMessagesDesc"); fullMessages.checked = getFullMessages(); fullMessages.addEventListener("change", e => setFullMessages(e.target.checked)); const typingNotifications = addCheckbox(messagesSection, "typingNotifications", "typingNotificationsDesc"); typingNotifications.checked = getTypingNotifications(); typingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked)); const sendEnter = addCheckbox(messagesSection, "sendEnter", "sendEnterDesc"); sendEnter.checked = getSendEnter(); sendEnter.addEventListener("change", e => setSendEnter(e.target.checked)); // Add hidden users section. const hiddenUsersSection = addSection("hiddenUsers"); const hideContacts = addCheckbox(hiddenUsersSection, "hideContacts"); hideContacts.checked = getHideContacts(); hideContacts.addEventListener("change", e => setHideContacts(e.target.checked)); const hideMessages = addCheckbox(hiddenUsersSection, "hideMessages"); hideMessages.checked = getHideMessages(); hideMessages.addEventListener("change", e => setHideMessages(e.target.checked)); const hideVisits = addCheckbox(hiddenUsersSection, "hideVisits"); hideVisits.checked = getHideVisits(); hideVisits.addEventListener("change", e => setHideVisits(e.target.checked)); const hideActivities = addCheckbox(hiddenUsersSection, "hideActivities"); hideActivities.checked = getHideActivities(); hideActivities.addEventListener("change", e => setHideActivities(e.target.checked)); const hideLikes = addCheckbox(hiddenUsersSection, "hideLikes"); 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(); initMenu(); initDom(); initPreviews(); initFetch(); initXhr();