您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows to hide users, display their information on tiles, and enhances the Radar.
当前为
// ==UserScript== // @name Romeo Additions // @name:de Romeo Additions // @namespace https://greasyfork.org/en/users/723211-ray/ // @version 4.1.0 // @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 // ==/UserScript== // ==== Dependencies ==== // ---- https://github.com/CoeJoder/GM_wrench ---- function addCss(css) { let style = document.createElement('style'); style.type = 'text/css'; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } 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 ==== addCss(` :root { --message-line-clamp: 2; --tile-headline-white-space: nowrap; --tile-size-factor: 0; --tile-size-group-factor: 0; --tile-size-xxlarge: calc(100% / max(1, var(--tile-size-factor) + 1)); --tile-size-xlarge: calc(100% / max(1, var(--tile-size-factor) + 2)); --tile-size-large: calc(100% / max(1, var(--tile-size-factor) + 3)); --tile-size-medium: calc(100% / max(1, var(--tile-size-factor) + 4)); --tile-size-small: calc(100% / max(1, var(--tile-size-factor) + 5)); --tile-size-xsmall: calc(100% / max(1, var(--tile-size-factor) + 6)); --tile-size-xxsmall: calc(100% / max(1, var(--tile-size-factor) + 7)); --tile-size-group-large: calc(100% / max(1, var(--tile-size-group-factor) + 3)); --tile-size-group-medium: calc(100% / max(1, var(--tile-size-group-factor) + 4)); --tile-size-group-small: calc(100% / max(1, var(--tile-size-group-factor) + 5)); } /* responsive tile size overrides (visitors) */ .grouped-tiles-small .tile { width: var(--tile-size-group-large); } @media screen and (min-width: 768px)and (max-width:1023px) { .grouped-tiles-small .tile { width: var(--tile-size-group-medium); } } @media screen and (min-width: 1024px) { .grouped-tiles-small .tile { width: var(--tile-size-group-small); } } @media screen and (min-width: 35rem)and (max-width:48rem) { .grouped-tiles-small .tile { width: var(--tile-size-group-medium); } } /* responsive tile size overrides (radar) */ @media screen and (min-width: 48rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xlarge) !important; width: var(--tile-size-xlarge) !important; } .is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xlarge) !important; width: var(--tile-size-xlarge) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xxlarge) !important; width: var(--tile-size-xxlarge) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xxlarge) !important; width: var(--tile-size-xxlarge) !important; } } @media screen and (min-width: 60rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-large) !important; width: var(--tile-size-large) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xlarge) !important; width: var(--tile-size-xlarge) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xlarge) !important; width: var(--tile-size-xlarge) !important; } } @media screen and (min-width: 80rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-medium) !important; width: var(--tile-size-medium) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-large) !important; width:var(--tile-size-large) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xlarge) !important; width: var(--tile-size-xlarge) !important; } } @media screen and (min-width: 100rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-small) !important; width: var(--tile-size-small) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-medium) !important; width: var(--tile-size-medium) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-large) !important; width: var(--tile-size-large) !important; } } @media screen and (min-width: 120rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xsmall) !important; width: var(--tile-size-xsmall) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-small) !important; width: var(--tile-size-small) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-medium) !important; width: var(--tile-size-medium) !important; } } @media screen and (min-width: 140rem) { .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xxsmall) !important; width: var(--tile-size-xxsmall) !important; } .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-xsmall) !important; width: var(--tile-size-xsmall) !important; } .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item { padding-bottom: var(--tile-size-small) !important; width: var(--tile-size-small) !important; } } /* enhanced radar filter */ .js-quick-filter { overflow: scroll; } /* 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; } /* wider visitor list */ #visits>.layer__container--wider { max-width: 1227px; width: unset; } /* hide PLUS message at bottom of visitor grid */ #visits div[class^="UnlockMoreVisitorsGrid"] { display: none; } /* hide models on login page */ div[data-testid="desktop-image"] { background-image: none; } /* allow clicking tile actions above stamps */ .listitem__highlight--footprint { pointer-events: none; } /* tile action bar */ .tile__bar { background: #121212; border-radius: 5px 0 0 5px; padding: 2px 1px 2px 2px; box-shadow: 0 0 8px 0 #0000007F; right: 0; opacity: 0; position: absolute; top: 50%; transform: translate(100%, -50%); transition: opacity 0.2s ease-out, transform 0.2s ease-out; z-index: 1; } @media screen and (hover:none) { .tile__bar { opacity: 0.5; transform: translate(0%, -50%); } } .tile__bar_action { border-radius: 3px; color: white; display: block; padding: 6px; } .tile__bar_action:hover { background-color: #00A3E4; } .tile__bar_action:active { background-color: #06648B; } .tile:hover .tile__bar { opacity: 0.5; transform: translate(0, -50%); } .tile:hover .tile__bar:hover { opacity: 1; } /* profile preview */ #ra_profile_wrapper { background-color: black; display: grid; font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif; grid-template-columns: auto 352px; height: 100%; 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; } #version { color: white; } `); // ==== Script ==== function addElement(parent, html) { parent.insertAdjacentHTML("beforeend", html); return parent.lastChild; } function onElement(selector, callback) { waitForKeyElements(selector, callback, false); } // ---- Language ---- 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" }, 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" }, 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: "Erweiterter Filter", en: "Enhanced filter" }, enhancedFilterDesc: { de: "Erlaubt die Filterung von Radar-Ergebnissen ohne PLUS und fügt neue Filter hinzu.", en: "Allows filtering of radar results without having PLUS adds new filters." }, enhancedTiles: { de: "Erweiterte Kacheln", en: "Enhanced tiles" }, enhancedTilesDesc: { de: "Zeigt alle Details auf den Kacheln. Im Radar zeigt dies Benutzer mit großen Kacheln.", en: "Shows all user details on tiles. The radar will display users with large 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" }, 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 anzeigen", en: "Show 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 anzeigen", en: "Show 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" }, hiddenUsersList: { de: "Liste ausgeblendeter Benutzernamen", en: "List of hidden user names" }, hideActivities: { de: "Auch Activities verstecken", en: "Also hide activities" }, hideActivitiesDesc: { de: "Versteckt ausgeblendete Benutzer auch im Activity Stream.", en: "Removes hidden users even in the activity stream." }, hideLikes: { de: "Auch Likes auf Bilder verstecken", en: "Also hide likes on pictures" }, hideLikesDesc: { de: "Versteckt ausgeblendete Benutzer auch in der Liste an Likes von Bildern.", en: "Removes hidden users even in the list of likes on pictures." }, hideMessages: { de: "Auch Nachrichten verstecken", en: "Also hide messages" }, hideMessagesDesc: { de: "Versteckt ausgeblendete Benutzer auch in der Nachrichtenliste.", en: "Removes hidden users even in the message list." }, hideUser: { de: "Benutzer ausblenden", en: "Hide user" }, 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" }, 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" }, 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" }, 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" }, 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: "Ich habe einen Partner", 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" }, searchOptions: { de: "Suchoptionen", en: "Search options" }, 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" }, 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" }, tileDetailsList: { de: "Profildetails auf Kacheln anzeigen", en: "Grid Stats" }, tiles: { de: "Benutzerkacheln", en: "User tiles" }, tileSizeFactor: { de: "Extra-Kacheln pro Zeile (Radar)", en: "Extra tiles per row (Radar)" }, tileSizeGroupFactor: { de: "Extra-Kacheln pro Zeile (Besucher)", en: "Extra tiles per row (Visitors)" }, typingNotifications: { de: "Tippbenachrichtigungen", en: "Typing notifications" }, typingNotificationsDesc: { de: "Wenn deaktiviert, können Empfänger nicht mehr sehen, dass eine Nachricht verfasst wird.", en: "If disabled, receivers can no longer see that a message is being composed." }, viewFullImage: { de: "Bild vergrößern", en: "View full image" }, viewProfile: { de: "Profil ohne Besuch anzeigen", en: "View profile without visiting" }, 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 getHideLikes() { return load("hideLikes", true); } function getHideMessages() { return load("hideMessages", false); } function getRadarFilter() { return JSON.parse(load("radarFilter", `{}`)); } function getSendEnter() { return load("sendEnter", true); } function getTileDetails() { return new Set(JSON.parse(load("tileDetails", `[ "age", "height", "bodyHair", "bodyType", "relationship", "analPosition" ]`))); } function getTileSizeFactor() { return load("tileSizeFactor", 0); } function getTileSizeGroupFactor() { return load("tileSizeGroupFactor", 0); } 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 setHideLikes(value) { save("hideLikes", value); } function setHideMessages(value) { save("hideMessages", value); } function setRadarFilter() { save("radarFilter", JSON.stringify(radarFilter)); } function setSendEnter(value) { save("sendEnter", value); } function setTileDetail(key, visible) { if (visible) { tileDetails.add(key); } else { tileDetails.delete(key); } save("tileDetails", JSON.stringify(Array.from(tileDetails))); } function setTileSizeFactor(value) { setStyleProp("--tile-size-factor", value); save("tileSizeFactor", value); } function setTileSizeGroupFactor(value) { setStyleProp("--tile-size-group-factor", value); save("tileSizeGroupFactor", value); } 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))); } // ---- XHR ---- 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 proxyXhr() { const realOpen = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) { // Manipulate request. const verb = getApiVerb(url); switch (verb) { case "hunqz/profiles": case "profiles": url = xhrApplyRadarFilter(url); break; } // Manipulate reply. this.addEventListener("load", () => { //console.log(`[RA] ${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(`[RA] ${verb}`, reply); // Modify interesting data. switch (verb) { 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": 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, true); 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 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; } // ---- Radar ---- 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 && 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 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 all filters on reset. el.querySelector(".js-clear-all").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-add-params-button.plain-text-link")) { filter.querySelector(".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); } // 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(); }); } 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", "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); // TODO: section = addSectionList("other"); // filter[personal][speaks_my_languages] // filter[travellers_filter] // filter[bed_and_breakfast_filter] } 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 continuations and non-fully-filterable views. if (key == "cursor" || key == "length" && value != 96) { 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); } onElement(".filter-container", el => { replaceFilterContainer(el); }); // ---- 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 showProfile(layer, profile) { 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 id="ra_profile_left"> <h1>${profile.name}</h1> <div>${profile.headline}</div> <img id="ra_profile_pic" src="/assets/09114cba6c284f3a673d5f84300ab6b6.svg"></img> </div> <div id="ra_profile_right"></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>`); } } // ---- Tile UI ---- onElement(`.tile:not(div[class*="tile--loading--"]) > .reactView`, el => { const tile = el.closest(".tile"); if (!tile) { return; } // Extract user name. const a = tile.querySelector("a"); let start = a.href.indexOf("profile/"); if (start === -1) { start = a.href.indexOf("hunq/"); } const username = a.href.substring(start).split("/")[1]; // Modify tile. const tileBar = addElement(tile, `<div class="tile__bar"></div>`); addHideUserAction(tileBar, tile, username); addShowProfileAction(tileBar, username); addUserTags(tile, username); }); function addHideUserAction(tileBar, tile, username) { const action = addElement(tileBar, `<a class="tile__bar_action" href="#" title="${translate("hideUser")}"> <span class="icon icon-hide-visit"> </a>`); action.addEventListener("click", e => { e.preventDefault(); setUserHidden(username, true); tile.style.display = "none"; }); } function addShowProfileAction(tileBar, username) { const action = addElement(tileBar, `<a class="tile__bar_action" title="${translate("viewProfile")}"> <span class="icon icon-info"> </a>`); action.addEventListener("click", e => { e.preventDefault(); const spotlightContainer = document.querySelector("#spotlight-container"); const layer = addElement(spotlightContainer, `<div class="layer layer--spotlight" style="top:0;z-index:100;"></div>`); showProfile(layer, profileCache[username]); layer.addEventListener("click", e => { if (e.target === layer) { layer.remove() } }); }); } function addUserTags(tile, username) { const tag = tile.querySelector(`span[class^="SpecialText-"]:last-child`); if (!tag) { return; } // Remove all existing tags. const tags = tag.parentNode; tags.replaceChildren(); // Add user selected tags. function addTag(text) { if (text) { addElement(tags, `<span class="${tag.classList}">${text}</span>`); } } const profile = profileCache[username]; // Add general details. 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)); } // Add sexual details. 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 UI ---- 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 UI ---- function openPane(ul, link) { // Open pane. const layerContent = document.querySelector("#offcanvas-nav > .js-layer-content"); layerContent.classList.add("is-open"); // Replace pane contents. const pane = layerContent.querySelector(".js-side-content"); pane.replaceChildren(); addElement(pane, ` <div class="layout layout--vertical layout--consume"> <div class="layout-item layout-item--consume layout layout--vertical"> <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 class="p"> <div class="settings__key"> <div> <span>${translate("searchOptions")}</span> </div> <div class="separator separator--alt separator--narrow [ mb ] "></div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("enhancedFilter")}</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_enhancedFilter"> <label class="ui-toggle__label" for="ra_enhancedFilter" 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> <div> <div class="settings__description">${translate("enhancedFilterDesc")}</div> </div> </div> <div class="settings__key"> <div> <span>${translate("tiles")}</span> </div> <div class="separator separator--alt separator--narrow [ mb ] "></div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("enhancedTiles")}</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_enhancedTiles"> <label class="ui-toggle__label" for="ra_enhancedTiles" 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> <div> <div class="settings__description">${translate("enhancedTilesDesc")}</div> </div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("fullHeadlines")}</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_fullHeadlines"> <label class="ui-toggle__label" for="ra_fullHeadlines" 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> <div> <div class="settings__description">${translate("fullHeadlinesDesc")}</div> </div> <div class="settings__key"> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("tileSizeFactor")}</span> </div> <div class="layout-item [ 6/12--sm ]"> <input class="input input--block" id="ra_tileSizeFactor" type="number" min="-5" max="5"/> </div> </div> </div> <div class="settings__key"> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("tileSizeGroupFactor")}</span> </div> <div class="layout-item [ 6/12--sm ]"> <input class="input input--block" id="ra_tileSizeGroupFactor" type="number" min="-5" max="5"/> </div> </div> </div> <div class="settings__key"> <div class="mb"> <span>${translate("tileDetailsList")}</span> </div> <div class="js-grid-stats-selector"> <div> <ul class="js-list tags-list tags-list--centered" id="ra_tileDetails"/> </div> </div> </div> </div> <div class="settings__key"> <div> <span>${translate("messages")}</span> </div> <div class="separator separator--alt separator--narrow [ mb ] "></div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("fullMessages")}</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_fullMessages"> <label class="ui-toggle__label" for="ra_fullMessages" 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> <div> <div class="settings__description">${translate("fullMessagesDesc")}</div> </div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("typingNotifications")}</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_typingNotifications"> <label class="ui-toggle__label" for="ra_typingNotifications" 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> <div> <div class="settings__description">${translate("typingNotificationsDesc")}</div> </div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("sendEnter")}</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_sendEnter"> <label class="ui-toggle__label" for="ra_sendEnter" 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> <div> <div class="settings__description">${translate("sendEnterDesc")}</div> </div> </div> <div class="settings__key"> <div> <span>${translate("hiddenUsers")}</span> </div> <div class="separator separator--alt separator--narrow [ mb ] "></div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("hideMessages")}</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_hideMessages"> <label class="ui-toggle__label" for="ra_hideMessages" 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> <div> <div class="settings__description">${translate("hideMessagesDesc")}</div> </div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("hideActivities")}</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_hideActivities"> <label class="ui-toggle__label" for="ra_hideActivities" 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> <div> <div class="settings__description">${translate("hideActivitiesDesc")}</div> </div> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("hideLikes")}</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_hideLikes"> <label class="ui-toggle__label" for="ra_hideLikes" 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> <div> <div class="settings__description">${translate("hideLikesDesc")}</div> </div> <div class="settings__key"> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("minAge")}</span> </div> <div class="layout-item [ 6/12--sm ]"> <input class="input input--block" id="ra_hiddenMinAge" type="number" min="18" max="99"/> </div> </div> </div> <div class="settings__key"> <div class="layout layout--v-center"> <div class="layout-item [ 6/12--sm ]"> <span>${translate("maxAge")}</span> </div> <div class="layout-item [ 6/12--sm ]"> <input class="input input--block" id="ra_hiddenMaxAge" type="number" min="18" max="99"/> </div> </div> </div> <div class="settings__key"> <div class="mb"> <span>${translate("hiddenUsersList")}</span> </div> <div class="js-grid-stats-selector"> <div> <ul class="js-list tags-list tags-list--centered" id="ra_hiddenUsers"/> </div> </div> </div> </div> </div> </div> </div> </div> </div>`); 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"); } }); } // Handle tile settings. const inEnhancedTiles = pane.querySelector("#ra_enhancedTiles"); inEnhancedTiles.checked = getEnhancedTiles(); inEnhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked)); const inFullHeadlines = pane.querySelector("#ra_fullHeadlines"); inFullHeadlines.checked = getFullHeadlines(); inFullHeadlines.addEventListener("change", e => setFullHeadlines(e.target.checked)); const inEnhancedFilter = pane.querySelector("#ra_enhancedFilter"); inEnhancedFilter.checked = getEnhancedFilter(); inEnhancedFilter.addEventListener("change", e => setEnhancedFilter(e.target.checked)); const inTileSizeFactor = pane.querySelector("#ra_tileSizeFactor"); inTileSizeFactor.value = getTileSizeFactor(); inTileSizeFactor.addEventListener("change", e => setTileSizeFactor(parseInt(e.target.value))); const inTileSizeGroupFactor = pane.querySelector("#ra_tileSizeGroupFactor"); inTileSizeGroupFactor.value = getTileSizeGroupFactor(); inTileSizeGroupFactor.addEventListener("change", e => setTileSizeGroupFactor(parseInt(e.target.value))); const inTileDetails = pane.querySelector("#ra_tileDetails"); for (const tileDetail of ["age", "height", "weight", "bmi", "ageRange", "bodyHair", "bodyType", "ethnicity", "relationship", "analPosition", "dick", "saferSex", "dirty", "sm", "fisting"]) { addTag(inTileDetails, tileDetail, translate(tileDetail), tileDetails.has(tileDetail), e => setTileDetail(e.tag, e.checked)); } // Handle message settings. const inFullMessages = pane.querySelector("#ra_fullMessages"); inFullMessages.checked = getFullMessages(); inFullMessages.addEventListener("change", e => setFullMessages(e.target.checked)); const inTypingNotifications = pane.querySelector("#ra_typingNotifications"); inTypingNotifications.checked = getTypingNotifications(); inTypingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked)); const inSendEnter = pane.querySelector("#ra_sendEnter"); inSendEnter.checked = getSendEnter(); inSendEnter.addEventListener("change", e => setSendEnter(e.target.checked)); // Handle hidden interacctions. const inHideMessages = pane.querySelector("#ra_hideMessages"); inHideMessages.checked = getHideMessages(); inHideMessages.addEventListener("change", e => setHideMessages(e.target.checked)); const inHideActivities = pane.querySelector("#ra_hideActivities"); inHideActivities.checked = getHideActivities(); inHideActivities.addEventListener("change", e => setHideActivities(e.target.checked)); const inHideLikes = pane.querySelector("#ra_hideLikes"); inHideLikes.checked = getHideLikes(); inHideLikes.addEventListener("change", e => setHideLikes(e.target.checked)); // Handle hidden age. const inMinAge = pane.querySelector("#ra_hiddenMinAge"); const inMaxAge = pane.querySelector("#ra_hiddenMaxAge"); 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); } }); // Handle hidden user list. const hiddenUsers = pane.querySelector("#ra_hiddenUsers"); const users = Array.from(getHiddenUsers()).sort(Intl.Collator().compare); for (const user of users) { addTag(hiddenUsers, user, user, true, e => setUserHidden(e.tag, e.checked)); }; } 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(() => openPane(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(`div[class^="Version--"]`, el => { el.innerHTML += `<br> <a id="version" href="https://greasyfork.org/en/scripts/419514" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`; }); // ---- On Load ---- (function () { "use strict"; setFullHeadlines(getFullHeadlines()); setTileSizeFactor(getTileSizeFactor()); setFullMessages(getFullMessages()); radarFilter = getRadarFilter(); tileDetails = getTileDetails(); proxyXhr(); })();