Romeo Additions

Enhances GR, especially for non-PLUS users

  1. // ==UserScript==
  2. // @name Romeo Additions
  3. // @name:de Romeo Additions
  4. // @namespace https://greasyfork.org/en/users/723211-ray/
  5. // @version 9.3.0
  6. // @description Enhances GR, especially for non-PLUS users
  7. // @description:de Verbessert GR, insbesondere für nicht-PLUS-Benutzer
  8. // @author -Ray-, Djamana
  9. // @match *://*.romeo.com/*
  10. // @license MIT
  11. // @grant none
  12. // @iconURL https://www.romeo.com/assets/favicons/711cd1957a9d865b45974099a6fc413e3bd323fa5fc48d9a964854ad55754ca1/favicon.ico
  13. // @supportURL https://greasyfork.org/en/scripts/419514
  14. // ==/UserScript==
  15.  
  16. const CM2FT = 0.03280839895;
  17. const KG2LBS = 2.20462262185;
  18. const M2MI = 0.0006213712;
  19.  
  20. function decodeUrl(url)
  21. {
  22. const [path, paramsText] = url.split("?");
  23. const params = new URLSearchParams(paramsText);
  24. return [path, params];
  25. }
  26.  
  27. function encodeUrl(path, params)
  28. {
  29. return `${path}?${new URLSearchParams(params)}`;
  30. }
  31.  
  32. function escapeHtml(unsafe)
  33. {
  34. return unsafe
  35. .replace(/&/g, "&")
  36. .replace(/</g, "&lt;")
  37. .replace(/>/g, "&gt;")
  38. .replace(/"/g, "&quot;")
  39. .replace(/'/g, "&#039;");
  40. }
  41.  
  42. function formatTime(str)
  43. {
  44. const date = new Date(Date.parse(str));
  45. const lang = getLang();
  46. return `${date.toLocaleDateString(lang)} ${date.toLocaleTimeString(lang)}`;
  47. }
  48.  
  49. function formatYearMonth(year, month)
  50. {
  51. const date = new Date(year, month - 1);
  52. const lang = getLang();
  53. return date.toLocaleString(lang, { month: "numeric", year: "numeric" });
  54. }
  55.  
  56. function getLang()
  57. {
  58. return document.documentElement.getAttribute("lang") || "en";
  59. }
  60.  
  61. function isJson(value)
  62. {
  63. if (!value || typeof value !== "string")
  64. return false;
  65.  
  66. try
  67. {
  68. JSON.parse(value);
  69. return true;
  70. }
  71. catch
  72. {
  73. return false;
  74. }
  75. }
  76.  
  77. function round(value, maxDigits = 0)
  78. {
  79. const f = Math.pow(10, maxDigits);
  80. return Math.round(value * f) / f;
  81. }
  82.  
  83. (function (css)
  84. {
  85. css.add = function (css)
  86. {
  87. let style = document.createElement('style');
  88. if (style.styleSheet)
  89. style.styleSheet.cssText = css;
  90. else
  91. style.appendChild(document.createTextNode(css));
  92. return document.head.appendChild(style);
  93. };
  94.  
  95. css.getStyleImageUrl = function (style)
  96. {
  97. return style.match(/"(.+)"/)[1];
  98. };
  99.  
  100. css.setProp = function (name, value)
  101. {
  102. document.documentElement.style.setProperty(name, value);
  103. };
  104. }(window.css ??= {}));
  105.  
  106. (function (dom)
  107. {
  108. const hooks = {};
  109.  
  110. function tagCall(el, callback)
  111. {
  112. if (!el.getAttribute("data-ra-hook"))
  113. {
  114. el.setAttribute("data-ra-hook", true);
  115. callback(el);
  116. }
  117. }
  118.  
  119. dom.add = function (parent, html, prepend)
  120. {
  121. if (prepend)
  122. {
  123. parent.insertAdjacentHTML("afterbegin", html);
  124. return parent.firstChild;
  125. }
  126. else
  127. {
  128. parent.insertAdjacentHTML("beforeend", html);
  129. return parent.lastChild;
  130. }
  131. };
  132.  
  133. dom.on = function (selector, callback)
  134. {
  135. // Trigger for existing elements.
  136. for (const el of document.querySelectorAll(selector))
  137. callback(el);
  138.  
  139. // Add to observer list.
  140. hooks[selector] = callback;
  141. };
  142.  
  143. const observer = new MutationObserver((mutations, observer) =>
  144. {
  145. for (const mutation of mutations)
  146. {
  147. for (const el of mutation.addedNodes)
  148. {
  149. if (el.nodeType === Node.ELEMENT_NODE)
  150. {
  151. for (const [selector, callback] of Object.entries(hooks))
  152. {
  153. if (el.matches(selector))
  154. {
  155. // Trigger for element.
  156. tagCall(el, callback);
  157. }
  158. else
  159. {
  160. // Trigger for children of attached elements.
  161. for (const elChild of el.querySelectorAll(selector))
  162. tagCall(elChild, callback);
  163. }
  164. }
  165. }
  166. }
  167. }
  168. });
  169. observer.observe(document.body, { subtree: true, childList: true });
  170. }(window.dom ??= {}));
  171.  
  172. (function (utm)
  173. {
  174. const K0 = 0.9996;
  175.  
  176. const E = 0.00669438;
  177. const E2 = E * E;
  178. const E3 = E2 * E;
  179. const E_P2 = E / (1.0 - E);
  180.  
  181. const SQRT_E = Math.sqrt(1 - E);
  182. const _E = (1 - SQRT_E) / (1 + SQRT_E);
  183. const _E2 = _E * _E;
  184. const _E3 = _E2 * _E;
  185. const _E4 = _E3 * _E;
  186. const _E5 = _E4 * _E;
  187.  
  188. const M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256);
  189. const M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024);
  190. const M3 = (15 * E2 / 256 + 45 * E3 / 1024);
  191. const M4 = (35 * E3 / 3072);
  192.  
  193. const P2 = (3. / 2 * _E - 27. / 32 * _E3 + 269. / 512 * _E5);
  194. const P3 = (21. / 16 * _E2 - 55. / 32 * _E4);
  195. const P4 = (151. / 96 * _E3 - 417. / 128 * _E5);
  196. const P5 = (1097. / 512 * _E4);
  197.  
  198. const R = 6378137;
  199.  
  200. const ZONE_LETTERS = "CDEFGHJKLMNPQRSTUVWXX";
  201.  
  202. function deg2rad(deg)
  203. {
  204. return deg * 0.017453292519943295;
  205. }
  206.  
  207. function rad2deg(rad)
  208. {
  209. return rad * 57.29577951308232;
  210. }
  211.  
  212. function modAngle(value)
  213. {
  214. return (value + Math.PI) % (2 * Math.PI) - Math.PI;
  215. }
  216.  
  217. function latitudeToZoneLetter(latitude)
  218. {
  219. if (-80 <= latitude && latitude <= 84)
  220. return ZONE_LETTERS[Math.trunc(latitude + 80) >> 3];
  221. }
  222.  
  223. function latlonToZoneNumber(latitude, longitude)
  224. {
  225. if ((56 <= latitude && latitude < 64) && (3 <= longitude && longitude < 12))
  226. return 32;
  227.  
  228. if ((72 <= latitude && latitude <= 84) && longitude >= 0)
  229. {
  230. if (longitude < 9)
  231. return 31;
  232. else if (longitude < 21)
  233. return 33;
  234. else if (longitude < 33)
  235. return 35;
  236. else if (longitude < 42)
  237. return 37;
  238. }
  239.  
  240. return Math.trunc((longitude + 180) / 6) + 1;
  241. }
  242.  
  243. function zoneNumberToCentralLongitude(zoneNumber)
  244. {
  245. return (zoneNumber - 1) * 6 - 180 + 3;
  246. }
  247.  
  248. utm.fromLatlon = function (latitude, longitude, forcedZoneNumber)
  249. {
  250. if (latitude < -80 || latitude > 84)
  251. throw RangeError("latitude must be between 80 deg S and 84 deg N.");
  252. if (longitude < -180 || longitude > 180)
  253. throw RangeError("longitude must be between 180 deg W and 180 deg E.");
  254.  
  255. const latRad = deg2rad(latitude);
  256. const latSin = Math.sin(latRad);
  257. const latCos = Math.cos(latRad);
  258.  
  259. const latTan = latSin / latCos;
  260. const latTan2 = latTan * latTan;
  261. const latTan4 = latTan2 * latTan2;
  262.  
  263. const zoneNumber = forcedZoneNumber ?? latlonToZoneNumber(latitude, longitude);
  264. const zoneLetter = latitudeToZoneLetter(latitude);
  265.  
  266. const lonRad = deg2rad(longitude);
  267. const centralLon = zoneNumberToCentralLongitude(zoneNumber);
  268. const centralLonRad = deg2rad(centralLon);
  269.  
  270. const n = R / Math.sqrt(1 - E * Math.pow(latSin, 2));
  271. const c = E_P2 * Math.pow(latCos, 2);
  272.  
  273. const a = latCos * modAngle(lonRad - centralLonRad);
  274. const a2 = a * a;
  275. const a3 = a2 * a;
  276. const a4 = a3 * a;
  277. const a5 = a4 * a;
  278. const a6 = a5 * a;
  279.  
  280. const m = R * (M1 * latRad -
  281. M2 * Math.sin(2 * latRad) +
  282. M3 * Math.sin(4 * latRad) -
  283. M4 * Math.sin(6 * latRad));
  284.  
  285. const easting = K0 * n * (a +
  286. a3 / 6 * (1 - latTan2 + c) +
  287. a5 / 120 * (5 - 18 * latTan2 + latTan4 + 72 * c - 58 * E_P2)) + 500000;
  288.  
  289. const northing = K0 * (m + n * latTan * (a2 / 2 +
  290. a4 / 24 * (5 - latTan2 + 9 * c + 4 * Math.pow(c, 2)) +
  291. a6 / 720 * (61 - 58 * latTan2 + latTan4 + 600 * c - 330 * E_P2)))
  292. + (latitude < 0 ? 10000000 : 0);
  293.  
  294. return [easting, northing, zoneNumber, zoneLetter];
  295. };
  296.  
  297. utm.toLatlon = function (easting, northing, zoneNumber, zoneLetter)
  298. {
  299. zoneLetter = zoneLetter.toUpperCase();
  300. const northern = zoneLetter >= 'N';
  301.  
  302. const x = easting - 500000;
  303. const y = northern ? northing : northing - 10000000;
  304.  
  305. const m = y / K0;
  306. const mu = m / (R * M1);
  307.  
  308. const pRad = (mu +
  309. P2 * Math.sin(2 * mu) +
  310. P3 * Math.sin(4 * mu) +
  311. P4 * Math.sin(6 * mu) +
  312. P5 * Math.sin(8 * mu));
  313.  
  314. const pSin = Math.sin(pRad);
  315. const pSin2 = pSin * pSin;
  316.  
  317. const pCos = Math.cos(pRad);
  318.  
  319. const pTan = pSin / pCos;
  320. const pTan2 = pTan * pTan;
  321. const pTan4 = pTan2 * pTan2;
  322.  
  323. const epSin = 1 - E * pSin2;
  324. const epSinSqrt = Math.sqrt(1 - E * pSin2);
  325.  
  326. const n = R / epSinSqrt;
  327. const r = (1 - E) / epSin;
  328.  
  329. const c = E_P2 * Math.pow(pCos, 2);
  330. const c2 = c * c;
  331.  
  332. const d = x / (n * K0);
  333. const d2 = d * d;
  334. const d3 = d2 * d;
  335. const d4 = d3 * d;
  336. const d5 = d4 * d;
  337. const d6 = d5 * d;
  338.  
  339. let latitude = (pRad - (pTan / r) *
  340. (d2 / 2 - d4 / 24 * (5 + 3 * pTan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
  341. d6 / 720 * (61 + 90 * pTan2 + 298 * c + 45 * pTan4 - 252 * E_P2 - 3 * c2));
  342.  
  343. let longitude = (d -
  344. d3 / 6 * (1 + 2 * pTan2 + c) +
  345. d5 / 120 * (5 - 2 * c + 28 * pTan2 - 3 * c2 + 8 * E_P2 + 24 * pTan4)) / pCos;
  346. longitude = modAngle(longitude + deg2rad(zoneNumberToCentralLongitude(zoneNumber)));
  347.  
  348. return [rad2deg(latitude), rad2deg(longitude)];
  349. };
  350. }(window.utm ??= {}));
  351.  
  352. (function (net)
  353. {
  354. const hooks = {};
  355. const xhrOpened = {};
  356. const realFetch = window.fetch;
  357. const realOpen = window.XMLHttpRequest.prototype.open;
  358. const realSend = window.XMLHttpRequest.prototype.send;
  359.  
  360. function matchRoute(path, route)
  361. {
  362. const pathParts = path.split("/");
  363. const routeParts = route.split("/");
  364. if (pathParts.length !== routeParts.length)
  365. return;
  366. const args = [];
  367. for (let i = 0; i < pathParts.length; ++i)
  368. {
  369. if (routeParts[i] === "*")
  370. args.push(pathParts[i]);
  371. else if (pathParts[i] !== routeParts[i])
  372. return;
  373. }
  374. return args;
  375. }
  376.  
  377. function callHooks(type, e)
  378. {
  379. // Only handle success for now.
  380. if (e.status && !(e.status >= 200 && e.status < 300))
  381. return false;
  382.  
  383. // Forward to hooking route.
  384. const matches = e.url.match("/api/[^?]*");
  385. if (!matches)
  386. return false;
  387.  
  388. let result = false;
  389. for (const [route, callback] of (hooks[[type, e.method]] ?? []))
  390. {
  391. e.args = matchRoute(matches[0], route);
  392. if (e.args !== undefined)
  393. {
  394. romeo.log(`hooked ${type} ${e.method} ${route}`);
  395. callback(e);
  396. result ||= true;
  397. }
  398. }
  399. return result;
  400. }
  401.  
  402. net.on = function (type, url, callback)
  403. {
  404. const [method, route] = url.split(" ");
  405. (hooks[[type, method]] ??= []).push([route, callback]);
  406. };
  407.  
  408. net.realXhr = function ()
  409. {
  410. const xhr = new XMLHttpRequest();
  411. xhr.open = realOpen;
  412. xhr.send = realSend;
  413. return xhr;
  414. };
  415.  
  416. window.fetch = async function (request, init)
  417. {
  418. async function getJsonBody(r)
  419. {
  420. // Use conversion to arrayBuffer to check if body exists as Firefox does not have a "body" property.
  421. const buffer = await r.clone().arrayBuffer();
  422. if (buffer.byteLength)
  423. {
  424. try
  425. {
  426. return JSON.parse(new TextDecoder().decode(buffer));
  427. }
  428. catch
  429. {
  430. return null; // not JSON, currently not interested
  431. }
  432. }
  433. }
  434.  
  435. // Only support fetch(Request) overload for now.
  436. if (!(request instanceof Request))
  437. return await realFetch(request, init);
  438.  
  439. // Manipulate request.
  440. const eReq =
  441. {
  442. body: await getJsonBody(request),
  443. cancel: false,
  444. method: request.method,
  445. url: request.url,
  446. };
  447. if (eReq.body === null)
  448. return realFetch(request, init);
  449. if (callHooks("fetch:send", eReq) && eReq.cancel)
  450. return;
  451.  
  452. // Send request and receive response.
  453. const response = await realFetch(eReq.url, {
  454. body: JSON.stringify(eReq.body),
  455. cache: request.cache,
  456. credentials: request.credentials,
  457. headers: request.headers,
  458. integrity: request.integrity,
  459. keepalive: request.keepalive,
  460. method: eReq.method,
  461. mode: request.mode,
  462. redirect: request.redirect,
  463. referrer: request.referrer,
  464. referrerPolicy: request.referrerPolicy,
  465. });
  466.  
  467. // Manipulate response.
  468. const eRes =
  469. {
  470. body: await getJsonBody(response),
  471. cancel: false,
  472. method: request.method,
  473. status: response.status,
  474. url: request.url,
  475. };
  476. if (eRes.body === null)
  477. return response;
  478. if (callHooks("fetch:recv", eRes) && eRes.cancel)
  479. {
  480. eRes.body = null;
  481. eRes.status = 404;
  482. }
  483. return new Response(JSON.stringify(eRes.body), { headers: response.headers, status: eRes.status });
  484. };
  485.  
  486. window.XMLHttpRequest.prototype.open = function (method, url, async, user, password)
  487. {
  488. const e = { method: method, url: url };
  489. xhrOpened[this] = e;
  490.  
  491. if (callHooks("xhr:open", e))
  492. {
  493. method = e.method;
  494. url = e.url;
  495. }
  496. if (!e.cancel)
  497. realOpen.apply(this, arguments);
  498.  
  499. // Hook load.
  500. this.addEventListener("load", () =>
  501. {
  502. const json = isJson(this.response);
  503. e.body = json ? JSON.parse(this.response) : this.response;
  504. e.status = this.status;
  505.  
  506. if (callHooks("xhr:load", e))
  507. {
  508. Object.defineProperty(this, "responseText", { writable: true });
  509. this.responseText = json ? JSON.stringify(e.body) : e.body;
  510. }
  511. if (e.cancel)
  512. {
  513. this.response = null;
  514. this.responseText = null;
  515. this.status = 404;
  516. }
  517. });
  518. };
  519.  
  520. window.XMLHttpRequest.prototype.send = function (body)
  521. {
  522. const e = xhrOpened[this];
  523. delete xhrOpened[this];
  524. const json = isJson(body);
  525. if (body)
  526. e.body = json ? JSON.parse(body) : body;
  527.  
  528. if (callHooks("xhr:send", e) && e.body)
  529. body = json ? JSON.stringify(e.body) : body;
  530. if (!e.cancel)
  531. realSend.apply(this, arguments);
  532. };
  533. }(window.net ??= {}));
  534.  
  535. (function (str)
  536. {
  537. str.strings =
  538. {
  539. aboutMe:
  540. {
  541. de: "Über mich",
  542. en: "About Me",
  543. },
  544. age:
  545. {
  546. de: "Alter",
  547. en: "Age",
  548. },
  549. ageRange:
  550. {
  551. de: "Altersspanne",
  552. en: "Age range",
  553. },
  554. ageRangeValue:
  555. {
  556. de: "Zwischen $from und $to",
  557. en: "Between $from and $to",
  558. },
  559. analPosition:
  560. {
  561. en: "Position",
  562. },
  563. analPosition_TOP_ONLY:
  564. {
  565. de: "Nur Aktiv",
  566. en: "Top only",
  567. },
  568. analPosition_MORE_TOP:
  569. {
  570. de: "Eher Aktiv",
  571. en: "More top",
  572. },
  573. analPosition_VERSATILE:
  574. {
  575. de: "Flexibel",
  576. en: "Versatile",
  577. },
  578. analPosition_MORE_BOTTOM:
  579. {
  580. de: "Eher Passiv",
  581. en: "More bottom",
  582. },
  583. analPosition_BOTTOM_ONLY:
  584. {
  585. de: "Nur Passiv",
  586. en: "Bottom only",
  587. },
  588. analPosition_NO:
  589. {
  590. de: "Kein Anal",
  591. en: "No anal",
  592. },
  593. beard:
  594. {
  595. de: "Bart",
  596. en: "Beard",
  597. },
  598. beard_DESIGNER_STUBBLE:
  599. {
  600. de: "3-Tage-Bart",
  601. en: "Designer stubble",
  602. },
  603. beard_FULL_BEARD:
  604. {
  605. de: "Vollbart",
  606. en: "Full beard",
  607. },
  608. beard_GOATEE:
  609. {
  610. en: "Goatee",
  611. },
  612. beard_MOUSTACHE:
  613. {
  614. de: "Schnauzer",
  615. en: "Moustache",
  616. },
  617. beard_NO_BEARD:
  618. {
  619. de: "Kein Bart",
  620. en: "No beard",
  621. },
  622. bedAndBreakfast:
  623. {
  624. en: "Bed & Breakfast",
  625. },
  626. blockUser:
  627. {
  628. de: "Benutzer blockieren",
  629. en: "Block user",
  630. },
  631. bmi:
  632. {
  633. en: "BMI",
  634. },
  635. bmiMildThin:
  636. {
  637. de: "Leichtes Untergewicht",
  638. en: "Mildly Thin",
  639. },
  640. bmiModerateThin:
  641. {
  642. de: "Mäßiges Untergewicht",
  643. en: "Moderately Thin",
  644. },
  645. bmiNormal:
  646. {
  647. de: "Normal",
  648. en: "Normal",
  649. },
  650. bmiObese1:
  651. {
  652. de: "Adipositas I",
  653. en: "Obese Class I",
  654. },
  655. bmiObese2:
  656. {
  657. de: "Adipositas II",
  658. en: "Obese Class II",
  659. },
  660. bmiObese3:
  661. {
  662. de: "Adipositas III",
  663. en: "Obese Class III",
  664. },
  665. bmiPreObese:
  666. {
  667. de: "Präadipositas",
  668. en: "Pre-Obese",
  669. },
  670. bmiSevereThin:
  671. {
  672. de: "Starkes Untergewicht",
  673. en: "Severely Thin",
  674. },
  675. bodyType:
  676. {
  677. de: "Statur",
  678. en: "Body Type",
  679. },
  680. bodyType_ATHLETIC:
  681. {
  682. de: "Athletisch",
  683. en: "Athletic",
  684. },
  685. bodyType_AVERAGE:
  686. {
  687. de: "Normal",
  688. en: "Average",
  689. },
  690. bodyType_BELLY:
  691. {
  692. de: "Bauch",
  693. en: "Belly",
  694. },
  695. bodyType_MUSCULAR:
  696. {
  697. de: "Muskulös",
  698. en: "Muscular",
  699. },
  700. bodyType_SLIM:
  701. {
  702. de: "Schlank",
  703. en: "Slim",
  704. },
  705. bodyType_STOCKY:
  706. {
  707. de: "Stämmig",
  708. en: "Stocky",
  709. },
  710. bodyHair:
  711. {
  712. de: "Körperbehaarung",
  713. en: "Body Hair",
  714. },
  715. bodyHair_AVERAGE:
  716. {
  717. de: "Mittel behaart",
  718. en: "Hairy",
  719. },
  720. bodyHair_LITTLE:
  721. {
  722. de: "Wenig behaart",
  723. en: "Not very hairy",
  724. },
  725. bodyHair_SHAVED:
  726. {
  727. de: "Rasiert",
  728. en: "Shaved",
  729. },
  730. bodyHair_SMOOTH:
  731. {
  732. de: "Unbehaart",
  733. en: "Smooth",
  734. },
  735. bodyHair_VERY_HAIRY:
  736. {
  737. de: "Stark behaart",
  738. en: "Very hairy",
  739. },
  740. chooseLocation:
  741. {
  742. de: "Wähle deinen Standort",
  743. en: "Choose your location",
  744. },
  745. clearList:
  746. {
  747. de: "Möchtest du wirklich alle Einträge in der Liste entfernen?",
  748. en: "Do you really want to remove all elements from the list?",
  749. },
  750. concision:
  751. {
  752. de: "Beschneidung",
  753. en: "Concision",
  754. },
  755. concision_CUT:
  756. {
  757. de: "Beschnitten",
  758. en: "Cut",
  759. },
  760. concision_UNCUT:
  761. {
  762. de: "Unbeschnitten",
  763. en: "Uncut",
  764. },
  765. customRadius:
  766. {
  767. de: "Benutzerdefinierter Radius",
  768. en: "Custom Radius",
  769. },
  770. deleteUnread:
  771. {
  772. de: "Ungelesene löschen",
  773. en: "Delete unread"
  774. },
  775. dick:
  776. {
  777. de: "Schwanz",
  778. en: "Dick",
  779. },
  780. dick_S:
  781. {
  782. en: "S",
  783. },
  784. dick_M:
  785. {
  786. en: "M",
  787. },
  788. dick_L:
  789. {
  790. en: "L",
  791. },
  792. dick_XL:
  793. {
  794. en: "XL",
  795. },
  796. dick_XXL:
  797. {
  798. en: "XXL",
  799. },
  800. dirty:
  801. {
  802. en: "Dirty",
  803. },
  804. dirty_NO:
  805. {
  806. de: "Kein Dirty",
  807. en: "No dirty",
  808. },
  809. dirty_WS_ONLY:
  810. {
  811. de: "Ja, aber nur NS",
  812. en: "WS only",
  813. },
  814. dirty_YES:
  815. {
  816. en: "Dirty",
  817. },
  818. discover:
  819. {
  820. de: "Entdecken-Seite",
  821. en: "Discover page",
  822. },
  823. discoverBanners:
  824. {
  825. de: "Blogeinträge",
  826. en: "Blog posts",
  827. },
  828. discoverBannersDesc:
  829. {
  830. de: "Zeigt Banner oben auf der Entdecken-Seite.",
  831. en: "Displays banners at the top of the Discover page.",
  832. },
  833. discoverFilter:
  834. {
  835. de: "Radarfilter anwenden",
  836. en: "Apply radar filter",
  837. },
  838. discoverFilterDesc:
  839. {
  840. de: "Wendet Radarfilter auf der Entdecken-Seite an.",
  841. en: "Applies radar filter on the Discover page.",
  842. },
  843. discoverGroups:
  844. {
  845. de: "Beliebte Gruppen",
  846. en: "Popular Groups",
  847. },
  848. discoverGroupsDesc:
  849. {
  850. de: "Zeigt Gruppen auf der Entdecken-Seite.",
  851. en: "Displays groups on the Discover page.",
  852. },
  853. distance:
  854. {
  855. de: "Entfernung",
  856. en: "Distance",
  857. },
  858. filter:
  859. {
  860. en: "Filter",
  861. },
  862. enhancedFilter:
  863. {
  864. de: "Erweiterter Filter",
  865. en: "Extended filter",
  866. },
  867. enhancedFilterDesc:
  868. {
  869. de: "Erlaubt Radar-Ergebnisse nach allen Details zu filtern.",
  870. en: "Allows to filter radar results by additional details.",
  871. },
  872. enhancedImages:
  873. {
  874. de: "Hochauflösende Bilder",
  875. en: "High-resolution images",
  876. },
  877. enhancedImagesDesc:
  878. {
  879. de: "Zeigt Kachelbilder in maximaler Auflösung.",
  880. en: "Shows tile images in maximum resolution.",
  881. },
  882. enhancedTiles:
  883. {
  884. de: "Große Kacheln erzwingen",
  885. en: "Force big grid",
  886. },
  887. enhancedTilesDesc:
  888. {
  889. de: "Zeigt alle Benutzer in großen Kacheln.",
  890. en: "Shows all users in big tiles.",
  891. },
  892. ethnicity:
  893. {
  894. de: "Typ",
  895. en: "Ethnicity",
  896. },
  897. ethnicity_ARAB:
  898. {
  899. de: "Araber",
  900. en: "Arab",
  901. },
  902. ethnicity_ASIAN:
  903. {
  904. de: "Asiate",
  905. en: "Asian",
  906. },
  907. ethnicity_BLACK:
  908. {
  909. de: "Schwarz",
  910. en: "Black",
  911. },
  912. ethnicity_CAUCASIAN:
  913. {
  914. de: "Europäer",
  915. en: "Caucasian",
  916. },
  917. ethnicity_INDIAN:
  918. {
  919. de: "Inder",
  920. en: "Indian",
  921. },
  922. ethnicity_LATIN:
  923. {
  924. de: "Latino",
  925. en: "Latin",
  926. },
  927. ethnicity_MEDITERRANEAN:
  928. {
  929. de: "Südländer",
  930. en: "Mediterranean",
  931. },
  932. ethnicity_MIXED:
  933. {
  934. en: "Mixed",
  935. },
  936. eyeColor:
  937. {
  938. de: "Augenfarbe",
  939. en: "Eye Colour",
  940. },
  941. eyeColor_BLUE:
  942. {
  943. de: "Blau",
  944. en: "Blue",
  945. },
  946. eyeColor_BROWN:
  947. {
  948. de: "Braun",
  949. en: "Brown",
  950. },
  951. eyeColor_GREEN:
  952. {
  953. de: "Grün",
  954. en: "Green",
  955. },
  956. eyeColor_GREY:
  957. {
  958. de: "Grau",
  959. en: "Grey",
  960. },
  961. eyeColor_OTHER:
  962. {
  963. de: "Sonstige",
  964. en: "Other",
  965. },
  966. fetish:
  967. {
  968. de: "Fetisch",
  969. en: "Fetish",
  970. },
  971. fetish_BOOTS:
  972. {
  973. en: "Boots",
  974. },
  975. fetish_CROSSDRESSING:
  976. {
  977. de: "Cross-Dressing",
  978. en: "Cross-dressing",
  979. },
  980. fetish_DRAG:
  981. {
  982. de: "Dessous",
  983. en: "Lingerie",
  984. },
  985. fetish_FORMAL:
  986. {
  987. de: "Anzug",
  988. en: "Formal dress",
  989. },
  990. fetish_JEANS:
  991. {
  992. en: "Jeans",
  993. },
  994. fetish_LEATHER:
  995. {
  996. de: "Leder",
  997. en: "Leather",
  998. },
  999. fetish_LYCRA:
  1000. {
  1001. en: "Lycra",
  1002. },
  1003. fetish_RUBBER:
  1004. {
  1005. en: "Rubber",
  1006. },
  1007. fetish_SKATER:
  1008. {
  1009. en: "Skater",
  1010. },
  1011. fetish_SKINS:
  1012. {
  1013. en: "Skins & Punks",
  1014. },
  1015. fetish_SNEAKERS:
  1016. {
  1017. en: "Sneakers & Socks",
  1018. },
  1019. fetish_SPORTS:
  1020. {
  1021. de: "Sportsgear",
  1022. en: "Sports gear",
  1023. },
  1024. fetish_TECHNO:
  1025. {
  1026. en: "Raver",
  1027. },
  1028. fetish_UNDERWEAR:
  1029. {
  1030. de: "Unterwäsche",
  1031. en: "Underwear",
  1032. },
  1033. fetish_UNIFORM:
  1034. {
  1035. en: "Uniform",
  1036. },
  1037. fetish_WORKER:
  1038. {
  1039. de: "Handwerker",
  1040. en: "Worker",
  1041. },
  1042. filters:
  1043. {
  1044. en: "Filters",
  1045. de: "Filter",
  1046. },
  1047. fisting:
  1048. {
  1049. de: "Fisten",
  1050. en: "Fisting",
  1051. },
  1052. fisting_ACTIVE:
  1053. {
  1054. de: "FF Aktiv",
  1055. en: "FF Active",
  1056. },
  1057. fisting_ACTIVE_PASSIVE:
  1058. {
  1059. de: "FF Flexibel",
  1060. en: "FF Versatile",
  1061. },
  1062. fisting_NO:
  1063. {
  1064. de: "Kein FF",
  1065. en: "No FF",
  1066. },
  1067. fisting_PASSIVE:
  1068. {
  1069. de: "FF Passiv",
  1070. en: "FF Passive",
  1071. },
  1072. fullHeadlines:
  1073. {
  1074. de: "Vollständige Überschriften",
  1075. en: "Full headlines",
  1076. },
  1077. fullHeadlinesDesc:
  1078. {
  1079. de: "Zeigt lange Profilüberschriften vollständig.",
  1080. en: "Shows long profile headlines completely.",
  1081. },
  1082. fullMessages:
  1083. {
  1084. de: "Vollständige Nachrichten",
  1085. en: "Full messages",
  1086. },
  1087. fullMessagesDesc:
  1088. {
  1089. de: "Zeigt Nachrichten ungekürzt in der Nachrichtenliste.",
  1090. en: "Shows messages without truncation in the message list.",
  1091. },
  1092. gender:
  1093. {
  1094. de: "Geschlecht",
  1095. en: "Gender",
  1096. },
  1097. gender_MAN:
  1098. {
  1099. de: "Mann",
  1100. en: "Man",
  1101. },
  1102. gender_TRANS_MAN:
  1103. {
  1104. de: "Transmann",
  1105. en: "Trans man",
  1106. },
  1107. gender_TRANS_WOMAN:
  1108. {
  1109. de: "Transfrau",
  1110. en: "Trans woman",
  1111. },
  1112. gender_NON_BINARY:
  1113. {
  1114. de: "Nicht binär",
  1115. en: "Non-binary",
  1116. },
  1117. gender_OTHER:
  1118. {
  1119. de: "Anderes",
  1120. en: "Other",
  1121. },
  1122. genderOrientation:
  1123. {
  1124. de: "Ich bin",
  1125. en: "I am",
  1126. },
  1127. general:
  1128. {
  1129. de: "Allgemein",
  1130. en: "General",
  1131. },
  1132. hairColor:
  1133. {
  1134. de: "Haarfarbe",
  1135. en: "Hair Colour",
  1136. },
  1137. hairColor_BLACK:
  1138. {
  1139. de: "Schwarz",
  1140. en: "Black",
  1141. },
  1142. hairColor_BLOND:
  1143. {
  1144. en: "Blond",
  1145. },
  1146. hairColor_BROWN:
  1147. {
  1148. de: "Braune Haare",
  1149. en: "Brown",
  1150. },
  1151. hairColor_GREY:
  1152. {
  1153. de: "Grau",
  1154. en: "Grey",
  1155. },
  1156. hairColor_LIGHT_BROWN:
  1157. {
  1158. de: "Dunkelblond",
  1159. en: "Light brown",
  1160. },
  1161. hairColor_OTHER:
  1162. {
  1163. de: "Sonstige",
  1164. en: "Other",
  1165. },
  1166. hairColor_RED:
  1167. {
  1168. de: "Rot",
  1169. en: "Red",
  1170. },
  1171. hairLength:
  1172. {
  1173. de: "Haarlänge",
  1174. en: "Hair Length",
  1175. },
  1176. hairLength_AVERAGE:
  1177. {
  1178. de: "Normal",
  1179. en: "Average",
  1180. },
  1181. hairLength_LONG:
  1182. {
  1183. de: "Lang",
  1184. en: "Long",
  1185. },
  1186. hairLength_PUNK:
  1187. {
  1188. en: "Punk",
  1189. },
  1190. hairLength_SHAVED:
  1191. {
  1192. de: "Rasiert",
  1193. en: "Shaved",
  1194. },
  1195. hairLength_SHORT:
  1196. {
  1197. de: "Kurz",
  1198. en: "Short",
  1199. },
  1200. height:
  1201. {
  1202. de: "Größe",
  1203. en: "Height",
  1204. },
  1205. hiddenUsers:
  1206. {
  1207. de: "Ausgeblendete Benutzer",
  1208. en: "Hidden users",
  1209. },
  1210. hideActivities:
  1211. {
  1212. de: "In Aktivitäten ausblenden",
  1213. en: "Hide in activities",
  1214. },
  1215. hideContacts:
  1216. {
  1217. de: "In Kontakten ausblenden",
  1218. en: "Hide in contacts",
  1219. },
  1220. hideFriends:
  1221. {
  1222. de: "In Freunden ausblenden",
  1223. en: "Hide in friends",
  1224. },
  1225. hideLikes:
  1226. {
  1227. de: "In Likes ausblenden",
  1228. en: "Hide in likes",
  1229. },
  1230. hideMessages:
  1231. {
  1232. de: "In Nachrichten ausblenden",
  1233. en: "Hide in messages",
  1234. },
  1235. hideUser:
  1236. {
  1237. de: "Benutzer ausblenden",
  1238. en: "Hide user",
  1239. },
  1240. hideVisits:
  1241. {
  1242. de: "In Besuchern ausblenden",
  1243. en: "Hide in visitors",
  1244. },
  1245. interests:
  1246. {
  1247. de: "Interessen",
  1248. en: "Interests",
  1249. },
  1250. interests_ART:
  1251. {
  1252. de: "Kunst",
  1253. en: "Art",
  1254. },
  1255. interests_BOARDGAME:
  1256. {
  1257. de: "Brettspiele",
  1258. en: "Board games",
  1259. },
  1260. interests_CAR:
  1261. {
  1262. de: "Autos",
  1263. en: "Cars",
  1264. },
  1265. interests_COLLECT:
  1266. {
  1267. de: "Sammeln",
  1268. en: "Collecting",
  1269. },
  1270. interests_COMPUTER:
  1271. {
  1272. de: "Computer",
  1273. en: "Computers",
  1274. },
  1275. interests_COOK:
  1276. {
  1277. de: "Kochen",
  1278. en: "Cooking",
  1279. },
  1280. interests_DANCE:
  1281. {
  1282. en: "Dance",
  1283. },
  1284. interests_FILM:
  1285. {
  1286. en: "Film & Video",
  1287. },
  1288. interests_FOTO:
  1289. {
  1290. de: "Fotografie",
  1291. en: "Photography",
  1292. },
  1293. interests_GAME:
  1294. {
  1295. de: "Computerspiele",
  1296. en: "Gaming",
  1297. },
  1298. interests_LITERATURE:
  1299. {
  1300. de: "Literatur",
  1301. en: "Literature",
  1302. },
  1303. interests_MODELING:
  1304. {
  1305. de: "Modellbau",
  1306. en: "Model building",
  1307. },
  1308. interests_MOTORBIKE:
  1309. {
  1310. de: "Motorrad",
  1311. en: "Motorbikes",
  1312. },
  1313. interests_MUSIC:
  1314. {
  1315. de: "Musik",
  1316. en: "Music",
  1317. },
  1318. interests_NATURE:
  1319. {
  1320. de: "Natur",
  1321. en: "Nature",
  1322. },
  1323. interests_POLITICS:
  1324. {
  1325. de: "Politik",
  1326. en: "Politics",
  1327. },
  1328. interests_TV:
  1329. {
  1330. en: "TV",
  1331. },
  1332. languages:
  1333. {
  1334. de: "Sprachen",
  1335. en: "Languages",
  1336. },
  1337. languages_af:
  1338. {
  1339. de: "Afrikaans",
  1340. en: "Afrikaans",
  1341. },
  1342. languages_ar:
  1343. {
  1344. de: "Arabisch",
  1345. en: "Arabic",
  1346. },
  1347. languages_arm:
  1348. {
  1349. de: "Armenisch",
  1350. en: "Armenian",
  1351. },
  1352. languages_az:
  1353. {
  1354. de: "Aserbaidschanisch",
  1355. en: "Azerbaijani",
  1356. },
  1357. languages_be:
  1358. {
  1359. de: "Belarussisch",
  1360. en: "Belarusian",
  1361. },
  1362. languages_bg:
  1363. {
  1364. de: "Bulgarisch",
  1365. en: "Bulgarian",
  1366. },
  1367. languages_bn:
  1368. {
  1369. de: "Bengali",
  1370. en: "Bengali",
  1371. },
  1372. languages_bs:
  1373. {
  1374. de: "Bosnisch",
  1375. en: "Bosnian",
  1376. },
  1377. languages_bur:
  1378. {
  1379. de: "Burmesisch",
  1380. en: "Burmese",
  1381. },
  1382. languages_ca:
  1383. {
  1384. de: "Katalanisch",
  1385. en: "Catalan",
  1386. },
  1387. languages_ceb:
  1388. {
  1389. de: "Cebuano",
  1390. en: "Cebuano",
  1391. },
  1392. languages_cs:
  1393. {
  1394. de: "Tschechisch",
  1395. en: "Czech",
  1396. },
  1397. languages_da:
  1398. {
  1399. de: "Dänisch",
  1400. en: "Danish",
  1401. },
  1402. languages_de:
  1403. {
  1404. de: "Deutsch",
  1405. en: "German",
  1406. },
  1407. languages_el:
  1408. {
  1409. de: "Griechisch",
  1410. en: "Greek",
  1411. },
  1412. languages_en:
  1413. {
  1414. de: "Englisch",
  1415. en: "English",
  1416. },
  1417. languages_eo:
  1418. {
  1419. de: "Esperanto",
  1420. en: "Esperanto",
  1421. },
  1422. languages_es:
  1423. {
  1424. de: "Spanisch",
  1425. en: "Spanish",
  1426. },
  1427. languages_et:
  1428. {
  1429. de: "Estnisch",
  1430. en: "Estonian",
  1431. },
  1432. languages_eu:
  1433. {
  1434. de: "Baskisch",
  1435. en: "Basque",
  1436. },
  1437. languages_fa:
  1438. {
  1439. de: "Persisch",
  1440. en: "Persian",
  1441. },
  1442. languages_fi:
  1443. {
  1444. de: "Finnisch",
  1445. en: "Finnish",
  1446. },
  1447. languages_fr:
  1448. {
  1449. de: "Französisch",
  1450. en: "French",
  1451. },
  1452. languages_frc:
  1453. {
  1454. de: "Kanadisches Französisch",
  1455. en: "Canadian French",
  1456. },
  1457. languages_gd:
  1458. {
  1459. de: "Schottisch-Gälisch",
  1460. en: "Scottish Gaelic",
  1461. },
  1462. languages_gl:
  1463. {
  1464. de: "Galician",
  1465. en: "Galician",
  1466. },
  1467. languages_gsw:
  1468. {
  1469. de: "Schwyzerdütsch",
  1470. en: "Swiss-German",
  1471. },
  1472. languages_hi:
  1473. {
  1474. de: "Hindi",
  1475. en: "Hindi",
  1476. },
  1477. languages_hr:
  1478. {
  1479. de: "Kroatisch",
  1480. en: "Croatian",
  1481. },
  1482. languages_hu:
  1483. {
  1484. de: "Ungarisch",
  1485. en: "Hungarian",
  1486. },
  1487. languages_id:
  1488. {
  1489. de: "Indonesisch",
  1490. en: "Indonesian",
  1491. },
  1492. languages_is:
  1493. {
  1494. de: "Isländisch",
  1495. en: "Icelandic",
  1496. },
  1497. languages_it:
  1498. {
  1499. de: "Italienisch",
  1500. en: "Italian",
  1501. },
  1502. languages_iw:
  1503. {
  1504. de: "Hebräisch",
  1505. en: "Hebrew",
  1506. },
  1507. languages_ja:
  1508. {
  1509. de: "Japanisch",
  1510. en: "Japanese",
  1511. },
  1512. languages_ka:
  1513. {
  1514. de: "Georgisch",
  1515. en: "Georgian",
  1516. },
  1517. languages_kl:
  1518. {
  1519. de: "Grönländisch",
  1520. en: "Greenlandic (Kalaallisut)",
  1521. },
  1522. languages_km:
  1523. {
  1524. de: "Kambodschanisch",
  1525. en: "Cambodian",
  1526. },
  1527. languages_kn:
  1528. {
  1529. de: "Kannada",
  1530. en: "Kannada",
  1531. },
  1532. languages_ko:
  1533. {
  1534. de: "Koreanisch",
  1535. en: "Korean",
  1536. },
  1537. languages_ku:
  1538. {
  1539. de: "Kurdisch",
  1540. en: "Kurdish",
  1541. },
  1542. languages_la:
  1543. {
  1544. de: "Latein",
  1545. en: "Latin",
  1546. },
  1547. languages_lb:
  1548. {
  1549. de: "Luxemburgisch",
  1550. en: "Luxembourgish",
  1551. },
  1552. languages_lo:
  1553. {
  1554. de: "Laotisch",
  1555. en: "Lao",
  1556. },
  1557. languages_lt:
  1558. {
  1559. de: "Litauisch",
  1560. en: "Lithuanian",
  1561. },
  1562. languages_lv:
  1563. {
  1564. de: "Lettisch",
  1565. en: "Latvian",
  1566. },
  1567. languages_mk:
  1568. {
  1569. de: "Mazedonisch",
  1570. en: "Macedonian",
  1571. },
  1572. languages_ml:
  1573. {
  1574. de: "Malayalam",
  1575. en: "Malayalam",
  1576. },
  1577. languages_mr:
  1578. {
  1579. de: "Marathi",
  1580. en: "Marathi",
  1581. },
  1582. languages_ms:
  1583. {
  1584. de: "Malaiisch",
  1585. en: "Malay",
  1586. },
  1587. languages_mt:
  1588. {
  1589. de: "Maltesisch",
  1590. en: "Maltese",
  1591. },
  1592. languages_nl:
  1593. {
  1594. de: "Niederländisch",
  1595. en: "Dutch",
  1596. },
  1597. languages_no:
  1598. {
  1599. de: "Norwegisch",
  1600. en: "Norwegian",
  1601. },
  1602. languages_oc:
  1603. {
  1604. de: "Okzitanisch",
  1605. en: "Occitan",
  1606. },
  1607. languages_pl:
  1608. {
  1609. de: "Polnisch",
  1610. en: "Polish",
  1611. },
  1612. languages_ps:
  1613. {
  1614. de: "Paschtunisch",
  1615. en: "Pashto",
  1616. },
  1617. languages_pt:
  1618. {
  1619. de: "Portugiesisch",
  1620. en: "Portuguese",
  1621. },
  1622. languages_ro:
  1623. {
  1624. de: "Rumänisch",
  1625. en: "Romanian",
  1626. },
  1627. languages_roh:
  1628. {
  1629. de: "Rätoromanisch",
  1630. en: "Romansch",
  1631. },
  1632. languages_ru:
  1633. {
  1634. de: "Russisch",
  1635. en: "Russian",
  1636. },
  1637. languages_sgn:
  1638. {
  1639. de: "Gebärdensprache",
  1640. en: "Sign language",
  1641. },
  1642. languages_sh:
  1643. {
  1644. de: "Serbo-Croatian",
  1645. en: "Serbo-Croatian",
  1646. },
  1647. languages_sk:
  1648. {
  1649. de: "Slowakisch",
  1650. en: "Slovak",
  1651. },
  1652. languages_sl:
  1653. {
  1654. de: "Slowenisch",
  1655. en: "Slovenian",
  1656. },
  1657. languages_sq:
  1658. {
  1659. de: "Albanisch",
  1660. en: "Albanian",
  1661. },
  1662. languages_sr:
  1663. {
  1664. de: "Serbisch",
  1665. en: "Serbian",
  1666. },
  1667. languages_sv:
  1668. {
  1669. de: "Schwedisch",
  1670. en: "Swedish",
  1671. },
  1672. languages_ta:
  1673. {
  1674. de: "Tamil",
  1675. en: "Tamil",
  1676. },
  1677. languages_te:
  1678. {
  1679. de: "Telugu",
  1680. en: "Telugu",
  1681. },
  1682. languages_th:
  1683. {
  1684. de: "Thailändisch",
  1685. en: "Thai",
  1686. },
  1687. languages_tl:
  1688. {
  1689. de: "Tagalog",
  1690. en: "Tagalog",
  1691. },
  1692. languages_tr:
  1693. {
  1694. de: "Türkisch",
  1695. en: "Turkish",
  1696. },
  1697. languages_uk:
  1698. {
  1699. de: "Ukrainisch",
  1700. en: "Ukrainian",
  1701. },
  1702. languages_us:
  1703. {
  1704. de: "US-Englisch",
  1705. en: "US English",
  1706. },
  1707. languages_vi:
  1708. {
  1709. de: "Vietnamesisch",
  1710. en: "Vietnamese",
  1711. },
  1712. languages_wel:
  1713. {
  1714. de: "Walisisch",
  1715. en: "Welsh",
  1716. },
  1717. languages_wen:
  1718. {
  1719. de: "Sorbisch",
  1720. en: "Sorbian",
  1721. },
  1722. languages_zgh:
  1723. {
  1724. de: "Tamazight",
  1725. en: "Tamazight",
  1726. },
  1727. languages_zh:
  1728. {
  1729. de: "Chinesisch",
  1730. en: "Chinese",
  1731. },
  1732. lastLogin:
  1733. {
  1734. de: "Letzter Login",
  1735. en: "Last Login",
  1736. },
  1737. location:
  1738. {
  1739. de: "Ort",
  1740. en: "Location",
  1741. },
  1742. latLong:
  1743. {
  1744. de: "Breitengrad, Längengrad",
  1745. en: "Latitude, Longitude",
  1746. },
  1747. location:
  1748. {
  1749. de: "Profilstandort",
  1750. en: "Profile location",
  1751. },
  1752. locationFuzz:
  1753. {
  1754. de: "Ungenauer GPS-Standort",
  1755. en: "Fuzzy GPS location",
  1756. },
  1757. locationFuzzDesc:
  1758. {
  1759. de: "Verschleiert GPS-bestimmte Standorte zum Schutz der Privatsphäre.",
  1760. en: "Blurs GPS detected locations to protect privacy.",
  1761. },
  1762. lookingFor:
  1763. {
  1764. de: "Ich suche",
  1765. en: "Looking For",
  1766. },
  1767. lookingForOther:
  1768. {
  1769. de: "Sucht nach",
  1770. en: "They're Looking For",
  1771. },
  1772. maxAge:
  1773. {
  1774. de: "Maximales Alter",
  1775. en: "Maximal age",
  1776. },
  1777. messages:
  1778. {
  1779. de: "Nachrichten",
  1780. en: "Messages",
  1781. },
  1782. metadata:
  1783. {
  1784. de: "Metadaten",
  1785. en: "Metadata",
  1786. },
  1787. minAge:
  1788. {
  1789. de: "Minimales Alter",
  1790. en: "Minimal age",
  1791. },
  1792. myAge:
  1793. {
  1794. de: "Mein Alter",
  1795. en: "My Age",
  1796. },
  1797. myGender:
  1798. {
  1799. de: "Mein Geschlecht",
  1800. en: "My gender",
  1801. },
  1802. myOrientation:
  1803. {
  1804. de: "Meine Orientierung",
  1805. en: "My orientation",
  1806. },
  1807. new:
  1808. {
  1809. de: "Neu",
  1810. en: "New",
  1811. },
  1812. noEntry:
  1813. {
  1814. de: "Keine Angabe",
  1815. en: "No entry",
  1816. },
  1817. onlineStatus:
  1818. {
  1819. en: "Status",
  1820. },
  1821. onlineStatus_DATE:
  1822. {
  1823. en: "Date",
  1824. },
  1825. onlineStatus_OFFLINE:
  1826. {
  1827. en: "Offline",
  1828. },
  1829. onlineStatus_ONLINE:
  1830. {
  1831. en: "Online",
  1832. },
  1833. onlineStatus_SEX:
  1834. {
  1835. en: "Now",
  1836. },
  1837. travelersOnly:
  1838. {
  1839. de: "Nur Reisende",
  1840. en: "Travelers only",
  1841. },
  1842. openTo:
  1843. {
  1844. de: "Offen für",
  1845. en: "Open to",
  1846. },
  1847. openTo_FRIENDSHIP:
  1848. {
  1849. de: "Freunde",
  1850. en: "Friends",
  1851. },
  1852. openTo_RELATIONSHIP:
  1853. {
  1854. de: "Beziehung",
  1855. en: "Relationship",
  1856. },
  1857. openTo_SEXDATES:
  1858. {
  1859. en: "Sex",
  1860. },
  1861. orientation:
  1862. {
  1863. de: "Orientierung",
  1864. en: "Orientation",
  1865. },
  1866. orientation_BISEXUAL:
  1867. {
  1868. de: "Bisexuell",
  1869. en: "Bisexual",
  1870. },
  1871. orientation_GAY:
  1872. {
  1873. en: "Gay",
  1874. },
  1875. orientation_QUEER:
  1876. {
  1877. en: "Queer",
  1878. },
  1879. orientation_OTHER:
  1880. {
  1881. de: "Andere",
  1882. en: "Other",
  1883. },
  1884. orientation_STRAIGHT:
  1885. {
  1886. de: "Hetero",
  1887. en: "Straight",
  1888. },
  1889. other:
  1890. {
  1891. de: "Sonstige",
  1892. en: "Other",
  1893. },
  1894. piercings:
  1895. {
  1896. en: "Piercings",
  1897. },
  1898. piercings_A_FEW:
  1899. {
  1900. de: "Wenige",
  1901. en: "A few",
  1902. },
  1903. piercings_A_LOT:
  1904. {
  1905. de: "Viele",
  1906. en: "A lot",
  1907. },
  1908. piercings_NO:
  1909. {
  1910. de: "Keine Piercings",
  1911. en: "No piercings",
  1912. },
  1913. profileId:
  1914. {
  1915. de: "Profil-ID",
  1916. en: "Profile ID",
  1917. },
  1918. relationship:
  1919. {
  1920. de: "Beziehung",
  1921. en: "Relationship",
  1922. },
  1923. relationship_MARRIED:
  1924. {
  1925. de: "Verheiratet",
  1926. en: "Married",
  1927. },
  1928. relationship_OPEN:
  1929. {
  1930. de: "Offene Partnerschaft",
  1931. en: "Open",
  1932. },
  1933. relationship_PARTNER:
  1934. {
  1935. de: "Verpartnert",
  1936. en: "Partner",
  1937. },
  1938. relationship_SINGLE:
  1939. {
  1940. en: "Single",
  1941. },
  1942. saferSex:
  1943. {
  1944. de: "Safer Sex",
  1945. en: "Safer sex",
  1946. },
  1947. saferSex_ALWAYS:
  1948. {
  1949. en: "Safe",
  1950. },
  1951. saferSex_CONDOM:
  1952. {
  1953. de: "Kondom",
  1954. en: "Condom",
  1955. },
  1956. saferSex_NEEDS_DISCUSSION:
  1957. {
  1958. de: "Nach Absprache",
  1959. en: "Let's talk",
  1960. },
  1961. saferSex_PREP:
  1962. {
  1963. en: "PrEP",
  1964. },
  1965. saferSex_PREP_AND_CONDOM:
  1966. {
  1967. de: "PrEP und Kondom",
  1968. en: "PrEP and condom",
  1969. },
  1970. saferSex_TASP:
  1971. {
  1972. en: "TasP",
  1973. },
  1974. searchFilter:
  1975. {
  1976. de: "Suche filtern",
  1977. en: "Filter Search",
  1978. },
  1979. searchFilterDesc:
  1980. {
  1981. de: "Wendet Radarfilter auf die Suchergebnisse an.",
  1982. en: "Applies radar filter on the search results.",
  1983. },
  1984. sendEnter:
  1985. {
  1986. de: "Enter sendet Nachricht",
  1987. en: "Enter sends message",
  1988. },
  1989. sendEnterDesc:
  1990. {
  1991. de: "Wenn deaktiviert erzeugt Enter einen Absatz und Strg+Enter sendet die Nachricht.",
  1992. en: "If disabled, Enter creates a new line instead and Ctrl+Enter sends the message.",
  1993. },
  1994. sexual:
  1995. {
  1996. de: "Sexuelles",
  1997. en: "Sexual",
  1998. },
  1999. sm:
  2000. {
  2001. de: "SM",
  2002. en: "S&M",
  2003. },
  2004. sm_NO:
  2005. {
  2006. de: "Kein SM",
  2007. en: "No SM",
  2008. },
  2009. sm_SOFT:
  2010. {
  2011. en: "Soft SM",
  2012. },
  2013. sm_YES:
  2014. {
  2015. en: "SM",
  2016. },
  2017. smoker:
  2018. {
  2019. de: "Raucher",
  2020. en: "Smoker",
  2021. },
  2022. smoker_NO:
  2023. {
  2024. de: "Nein",
  2025. en: "No",
  2026. },
  2027. smoker_SOCIALLY:
  2028. {
  2029. de: "Selten",
  2030. en: "Socially",
  2031. },
  2032. smoker_YES:
  2033. {
  2034. de: "Ja",
  2035. en: "Yes",
  2036. },
  2037. socialSmoker:
  2038. {
  2039. de: "Raucht selten",
  2040. en: "Social Smoker",
  2041. },
  2042. speakingMyLanguage:
  2043. {
  2044. de: "Spricht meine Sprache",
  2045. en: "Speaking my language",
  2046. },
  2047. systemMessages:
  2048. {
  2049. de: "Systemnachrichten",
  2050. en: "System messages",
  2051. },
  2052. systemMessagesDesc:
  2053. {
  2054. de: "Erlaubt Popups wie Standort- oder Fehlermeldungen.",
  2055. en: "Allows popups like GPS or error messages.",
  2056. },
  2057. tattoos:
  2058. {
  2059. en: "Tattoos",
  2060. },
  2061. tattoos_A_FEW:
  2062. {
  2063. de: "Wenige",
  2064. en: "A few",
  2065. },
  2066. tattoos_A_LOT:
  2067. {
  2068. de: "Viele",
  2069. en: "A lot",
  2070. },
  2071. tattoos_NO:
  2072. {
  2073. de: "Keine Tattoos",
  2074. en: "No tattoos",
  2075. },
  2076. tiles:
  2077. {
  2078. de: "Benutzerkacheln",
  2079. en: "User tiles",
  2080. },
  2081. tileCount:
  2082. {
  2083. de: "Kachelspalten (0 für Standard)",
  2084. en: "Tile columns (0 for default)",
  2085. },
  2086. typingNotifications:
  2087. {
  2088. de: "Tippbenachrichtigungen",
  2089. en: "Typing notifications",
  2090. },
  2091. typingNotificationsDesc:
  2092. {
  2093. de: "Ob Empfänger die Eingabe einer Nachricht sehen können.",
  2094. en: "Whether receivers can see that a message is being written.",
  2095. },
  2096. viewFullImage:
  2097. {
  2098. de: "Bild anzeigen",
  2099. en: "Preview image",
  2100. },
  2101. viewProfile:
  2102. {
  2103. de: "Profilvorschau anzeigen",
  2104. en: "Preview profile",
  2105. },
  2106. weight:
  2107. {
  2108. de: "Gewicht",
  2109. en: "Weight",
  2110. },
  2111. };
  2112.  
  2113. str.get = function (key)
  2114. {
  2115. const translations = str.strings[key];
  2116. return translations
  2117. ? translations[getLang()] || translations.en || "%" + key + "%"
  2118. : "%" + key + "%";
  2119. };
  2120.  
  2121. str.getEnum = function (name, key)
  2122. {
  2123. return str.get(`${name}_${key}`);
  2124. };
  2125. }(window.str ??= {}));
  2126.  
  2127. (function (list)
  2128. {
  2129. list.create = function (parent, { onGet, onName = null, onAdd = null, onRemove = null, onImport = null, onExport = null } = {})
  2130. {
  2131. const container = dom.add(parent, `<div class="ra_list"></div>`);
  2132.  
  2133. function createButton(icon, text)
  2134. {
  2135. return `
  2136. <a href="#" class="icon-labeled plain-text-link">
  2137. <span class="icon icon-base ${icon}"></span>
  2138. <span class="icon-labeled__label">${text}</span>
  2139. </a>`;
  2140. }
  2141.  
  2142. // Add elements.
  2143. const toolbar = dom.add(container, `<div></div>`);
  2144. const ul = dom.add(container, `<ul></ul>`);
  2145.  
  2146. // Create toolbar.
  2147. const searchBox = dom.add(toolbar, `<input class="input" type="text" placeholder="Search"></input>`);
  2148. searchBox.addEventListener("input", e => updateList());
  2149.  
  2150. function updateList()
  2151. {
  2152. const filter = searchBox.value.toUpperCase();
  2153. ul.replaceChildren();
  2154.  
  2155. const elements = onGet();
  2156. for (const element of elements)
  2157. {
  2158. // Check if filtered away.
  2159. const name = onName ? onName(element) : element;
  2160. if (!name.toUpperCase().includes(filter))
  2161. continue;
  2162.  
  2163. // Create list entry.
  2164. const li = dom.add(ul, `<li></li>`);
  2165. if (onRemove)
  2166. {
  2167. const deleteButton = dom.add(li, createButton("icon-cross-negative", name));
  2168. deleteButton.addEventListener("click", e =>
  2169. {
  2170. e.preventDefault();
  2171. onRemove(element);
  2172. li.remove();
  2173. });
  2174. }
  2175. else
  2176. {
  2177. dom.add(li, `<div>${name}</div>`);
  2178. }
  2179. }
  2180. }
  2181.  
  2182. if (onAdd)
  2183. {
  2184. const addButton = dom.add(toolbar, createButton("icon-add-attachment", "Add"));
  2185. addButton.addEventListener("click", e =>
  2186. {
  2187. e.preventDefault();
  2188. onAdd(searchBox.value);
  2189. updateList();
  2190. });
  2191. }
  2192.  
  2193. if (onRemove)
  2194. {
  2195. const clearButton = dom.add(toolbar, createButton("icon-trashcan", "Clear"));
  2196. clearButton.addEventListener("click", e =>
  2197. {
  2198. e.preventDefault();
  2199. if (confirm(str.get("clearList")))
  2200. {
  2201. const elements = onGet();
  2202. for (const element of elements)
  2203. onRemove(element);
  2204. updateList();
  2205. }
  2206. });
  2207. }
  2208.  
  2209. if (onImport)
  2210. {
  2211. const importButton = dom.add(toolbar, createButton("icon-up-arrow", "Import"));
  2212. importButton.addEventListener("click", e =>
  2213. {
  2214. e.preventDefault();
  2215. // TODO: Handle import
  2216. updateList();
  2217. });
  2218. }
  2219.  
  2220. if (onExport)
  2221. {
  2222. const exportButton = dom.add(toolbar, createButton("icon-down-arrow", "Export"));
  2223. exportButton.addEventListener("click", e =>
  2224. {
  2225. e.preventDefault();
  2226. // TODO: Handle export
  2227. updateList();
  2228. });
  2229. }
  2230.  
  2231. // Create list.
  2232. updateList();
  2233. };
  2234.  
  2235. css.add(`
  2236. .ra_list
  2237. {
  2238. display: flex;
  2239. flex-direction: column;
  2240. height: 250px;
  2241. }
  2242.  
  2243. .ra_list > div > a
  2244. {
  2245. margin: 0 8px;
  2246.  
  2247. display: inline-flex;
  2248. }
  2249.  
  2250. .ra_list > ul
  2251. {
  2252. flex: 1;
  2253. padding: 2px;
  2254. overflow-y: auto;
  2255.  
  2256. display: flex;
  2257. flex-wrap: wrap;
  2258. align-content: flex-start;
  2259. }
  2260.  
  2261. .ra_list > ul > li
  2262. {
  2263. flex: 50%;
  2264. flex-grow: 0;
  2265. padding: 2px;
  2266. }
  2267. `);
  2268. }(window.list ??= {}));
  2269.  
  2270. (function (cfg)
  2271. {
  2272. const namespace = "RA_SETTINGS:";
  2273. cfg.radarFilter = {};
  2274. let tileStyle = null;
  2275.  
  2276. function load(name, fallback)
  2277. {
  2278. const value = localStorage.getItem(namespace + name);
  2279. return value === "false" ? false : value ? value : fallback;
  2280. }
  2281. function save(name, value)
  2282. {
  2283. localStorage.setItem(namespace + name, value);
  2284. }
  2285.  
  2286. function getRadarFilter()
  2287. {
  2288. return JSON.parse(load("radarFilter", `{}`));
  2289. }
  2290.  
  2291. cfg.measurementSystem = "METRIC";
  2292. cfg.tileDetails = new Set(JSON.parse(load("tileDetails",
  2293. `[ "age", "height", "bodyHair", "bodyType", "relationship", "analPosition" ]`)));
  2294.  
  2295. cfg.getDiscoverBanners = function ()
  2296. {
  2297. return load("discoverBanners", true);
  2298. };
  2299. cfg.getDiscoverFilter = function ()
  2300. {
  2301. return load("discoverFilter", false);
  2302. };
  2303. cfg.getDiscoverGroups = function ()
  2304. {
  2305. return load("discoverGroups", true);
  2306. };
  2307. cfg.getEnhancedFilter = function ()
  2308. {
  2309. return load("enhancedFilter", true);
  2310. };
  2311. cfg.getEnhancedImages = function ()
  2312. {
  2313. return load("enhancedImages", true);
  2314. };
  2315. cfg.getEnhancedTiles = function ()
  2316. {
  2317. return load("enhancedTiles", true);
  2318. };
  2319. cfg.getFullHeadlines = function ()
  2320. {
  2321. return load("fullHeadlines", true);
  2322. };
  2323. cfg.getFullMessages = function ()
  2324. {
  2325. return load("fullMessages", true);
  2326. };
  2327. cfg.getHiddenMaxAge = function ()
  2328. {
  2329. return load("hiddenMaxAge", 99);
  2330. };
  2331. cfg.getHiddenMinAge = function ()
  2332. {
  2333. return load("hiddenMinAge", 18);
  2334. };
  2335. cfg.getHiddenUsers = function ()
  2336. {
  2337. return new Set(JSON.parse(load("hiddenUsers", `[]`)));
  2338. };
  2339. cfg.getHideActivities = function ()
  2340. {
  2341. return load("hideActivities", true);
  2342. };
  2343. cfg.getHideContacts = function ()
  2344. {
  2345. return load("hideContacts", false);
  2346. };
  2347. cfg.getHideFriends = function ()
  2348. {
  2349. return load("hideFriends", true);
  2350. };
  2351. cfg.getHideLikes = function ()
  2352. {
  2353. return load("hideLikes", true);
  2354. };
  2355. cfg.getHideMessages = function ()
  2356. {
  2357. return load("hideMessages", false);
  2358. };
  2359. cfg.getHideVisits = function ()
  2360. {
  2361. return load("hideVisits", true);
  2362. };
  2363. cfg.getLocationFuzz = function ()
  2364. {
  2365. return load("locationFuzz", false);
  2366. };
  2367. cfg.getSavedRadarFilter = function (id)
  2368. {
  2369. return cfg.getSavedRadarFilters()[id] ?? getRadarFilter();
  2370. };
  2371. cfg.getSavedRadarFilters = function ()
  2372. {
  2373. return JSON.parse(load("savedRadarFilters", "{}"));
  2374. };
  2375. cfg.getSearchFilter = function ()
  2376. {
  2377. return load("searchFilter", false);
  2378. };
  2379. cfg.getSendEnter = function ()
  2380. {
  2381. return load("sendEnter", true);
  2382. };
  2383. cfg.getSystemMessages = function ()
  2384. {
  2385. return load("systemMessages", true);
  2386. };
  2387. cfg.getTileCount = function ()
  2388. {
  2389. return parseInt(load("tileCount", 0));
  2390. };
  2391. cfg.getTypingNotifications = function ()
  2392. {
  2393. return load("typingNotifications", true);
  2394. };
  2395. cfg.setDiscoverBanners = function (value)
  2396. {
  2397. save("discoverBanners", value);
  2398. };
  2399. cfg.setDiscoverFilter = function (value)
  2400. {
  2401. save("discoverFilter", value);
  2402. };
  2403. cfg.setDiscoverGroups = function (value)
  2404. {
  2405. save("discoverGroups", value);
  2406. };
  2407. cfg.setEnhancedFilter = function (value)
  2408. {
  2409. save("enhancedFilter", value);
  2410. };
  2411. cfg.setEnhancedImages = function (value)
  2412. {
  2413. save("enhancedImages", value);
  2414. };
  2415. cfg.setEnhancedTiles = function (value)
  2416. {
  2417. save("enhancedTiles", value);
  2418. };
  2419. cfg.setFullHeadlines = function (value)
  2420. {
  2421. css.setProp("--tile-headline-white-space", value ? "unset" : "nowrap");
  2422. save("fullHeadlines", value);
  2423. };
  2424. cfg.setFullMessages = function (value)
  2425. {
  2426. css.setProp("--message-line-clamp", value ? "unset" : "2");
  2427. save("fullMessages", value);
  2428. };
  2429. cfg.setHiddenMaxAge = function (value)
  2430. {
  2431. save("hiddenMaxAge", value);
  2432. };
  2433. cfg.setHiddenMinAge = function (value)
  2434. {
  2435. save("hiddenMinAge", value);
  2436. };
  2437. cfg.setHideActivities = function (value)
  2438. {
  2439. save("hideActivities", value);
  2440. };
  2441. cfg.setHideContacts = function (value)
  2442. {
  2443. save("hideContacts", value);
  2444. };
  2445. cfg.setHideFriends = function (value)
  2446. {
  2447. save("hideFriends", value);
  2448. };
  2449. cfg.setHideLikes = function (value)
  2450. {
  2451. save("hideLikes", value);
  2452. };
  2453. cfg.setHideMessages = function (value)
  2454. {
  2455. save("hideMessages", value);
  2456. };
  2457. cfg.setHideVisits = function (value)
  2458. {
  2459. save("hideVisits", value);
  2460. };
  2461. cfg.setLocationFuzz = function (value)
  2462. {
  2463. save("locationFuzz", value);
  2464. };
  2465. cfg.setRadarFilter = function ()
  2466. {
  2467. save("radarFilter", JSON.stringify(cfg.radarFilter));
  2468. };
  2469. cfg.setSavedRadarFilter = function (id, value = null)
  2470. {
  2471. const filters = JSON.parse(load("savedRadarFilters", "{}"));
  2472. if (value)
  2473. filters[id] = value;
  2474. else
  2475. delete filters[id];
  2476. save("savedRadarFilters", JSON.stringify(filters));
  2477. };
  2478. cfg.setSearchFilter = function (value)
  2479. {
  2480. save("searchFilter", value);
  2481. };
  2482. cfg.setSendEnter = function (value)
  2483. {
  2484. save("sendEnter", value);
  2485. };
  2486. cfg.setSystemMessages = function (value)
  2487. {
  2488. css.setProp("--system-message-visibility", value ? "visible" : "collapse");
  2489. save("systemMessages", value);
  2490. };
  2491. cfg.setTileCount = function (value)
  2492. {
  2493. if (value)
  2494. {
  2495. css.setProp("--tile-count", value);
  2496. if (!tileStyle)
  2497. {
  2498. tileStyle = css.add(`
  2499. :root
  2500. {
  2501. --tile-count: 0;
  2502. --tile-size: calc(100% / max(1, var(--tile-count)) - 1px);
  2503. }
  2504. /* discover */
  2505. section.js-main-stage > main main > section > ul
  2506. {
  2507. grid-template-columns: repeat(var(--tile-count), 1fr) !important;
  2508. }
  2509. /* radar desktop */
  2510. .search-results__item
  2511. {
  2512. padding-bottom: var(--tile-size) !important;
  2513. width: var(--tile-size) !important;
  2514. }
  2515. /* radar mobile - starts at 768px where .search-results__item turns inline, requiring to adjust .tile */
  2516. @media not screen and (min-width: 768px)
  2517. {
  2518. .tile:not(.js-strip .tile):not(.tile--small)
  2519. {
  2520. width: var(--tile-size) !important;
  2521. }
  2522. }
  2523. /* visitors */
  2524. #cruise main > ul
  2525. {
  2526. grid-template-columns: repeat(var(--tile-count), 1fr);
  2527. }
  2528. `);
  2529. }
  2530. }
  2531. else
  2532. {
  2533. tileStyle?.remove();
  2534. tileStyle = null;
  2535. }
  2536. save("tileCount", value);
  2537. };
  2538. cfg.setTileDetail = function (key, visible)
  2539. {
  2540. if (visible)
  2541. cfg.tileDetails.add(key);
  2542. else
  2543. cfg.tileDetails.delete(key);
  2544. save("tileDetails", JSON.stringify(Array.from(cfg.tileDetails)));
  2545. };
  2546. cfg.setTypingNotifications = function (value)
  2547. {
  2548. save("typingNotifications", value);
  2549. };
  2550. cfg.setUserHidden = function (username, hide)
  2551. {
  2552. let hiddenUsers = cfg.getHiddenUsers();
  2553. if (hide)
  2554. hiddenUsers.add(username);
  2555. else
  2556. hiddenUsers.delete(username);
  2557. save("hiddenUsers", JSON.stringify(Array.from(hiddenUsers)));
  2558. };
  2559.  
  2560. cfg.setFullHeadlines(cfg.getFullHeadlines());
  2561. cfg.setFullMessages(cfg.getFullMessages());
  2562. cfg.setSystemMessages(cfg.getSystemMessages());
  2563. cfg.setTileCount(cfg.getTileCount());
  2564. }(window.cfg ??= {}));
  2565.  
  2566. (function (romeo)
  2567. {
  2568. const apiKey = atob("QVM4YnpHSExBOFk5QlhGNzNpRE51UUJIZUVPMFVLamY=");
  2569. let sessionId;
  2570.  
  2571. romeo.debug = function ()
  2572. {
  2573. return GM_info.script.version === "0.0.0";
  2574. };
  2575.  
  2576. romeo.getImageUrl = function (url, size)
  2577. {
  2578. const base = url.substring(0, url.indexOf("/img/usr/"));
  2579. const file = url.substring(url.lastIndexOf("/") + 1);
  2580. return size
  2581. ? `${base}/img/usr/squarish/${size}x${size}/${file}`
  2582. : `${base}/img/usr/${file}`;
  2583. };
  2584.  
  2585. romeo.getUsernameFromHref = function (href)
  2586. {
  2587. let start = href.indexOf("profile/");
  2588. if (start === -1)
  2589. start = href.indexOf("hunq/");
  2590. return href.substring(start).split("/")[1];
  2591. };
  2592.  
  2593. romeo.iterItems = async function* (url, body)
  2594. {
  2595. let cursor;
  2596. do
  2597. {
  2598. const response = JSON.parse(await romeo.sendXhr(url, cursor ? { cursor: cursor } : body));
  2599. cursor = response.cursors?.after;
  2600. for (item of response.items)
  2601. yield item;
  2602. } while (cursor);
  2603. };
  2604.  
  2605. romeo.jsonToParams = function (json)
  2606. {
  2607. const params = [];
  2608.  
  2609. function add(parentName, json)
  2610. {
  2611. if (Array.isArray(json))
  2612. {
  2613. const name = parentName + "[]";
  2614. for (const item of json)
  2615. params.push([name, item]);
  2616. }
  2617. else if (typeof (json) === "object")
  2618. {
  2619. for (const name in json)
  2620. add(parentName + "[" + name + "]", json[name]);
  2621. }
  2622. else
  2623. {
  2624. params.push([parentName, json]);
  2625. }
  2626. }
  2627.  
  2628. for (const name in json)
  2629. add(name, json[name]);
  2630.  
  2631. return params;
  2632. };
  2633.  
  2634. romeo.log = function ()
  2635. {
  2636. if (romeo.debug())
  2637. console.log(...arguments);
  2638. };
  2639.  
  2640. romeo.paramsToJson = function (params)
  2641. {
  2642. const json = {};
  2643.  
  2644. for (const [param, value] of params)
  2645. {
  2646. const array = param.endsWith("[]");
  2647. const names = param.split(/[\[\]]+/);
  2648. let end = names.length;
  2649. if (end > 1)
  2650. --end;
  2651.  
  2652. let parent = json;
  2653. for (let i = 0; i < end; ++i)
  2654. {
  2655. const name = names[i];
  2656. if (i !== end - 1)
  2657. parent = parent[name] ??= {};
  2658. else if (array)
  2659. (parent[name] ||= []).push(value);
  2660. else
  2661. parent[name] = value;
  2662. }
  2663. }
  2664.  
  2665. return json;
  2666. };
  2667.  
  2668. romeo.sendFetch = function (url, body)
  2669. {
  2670. let [method, route] = url.split(" ");
  2671. const options =
  2672. {
  2673. method: method,
  2674. cache: "no-cache",
  2675. credentials: "same-origin",
  2676. headers:
  2677. {
  2678. "x-api-key": apiKey,
  2679. "x-session-id": sessionId,
  2680. },
  2681. };
  2682. if (body)
  2683. {
  2684. if (method === "GET")
  2685. {
  2686. route = encodeUrl(route, romeo.jsonToParams(body));
  2687. }
  2688. else
  2689. {
  2690. options.headers["content-type"] = "application/json";
  2691. options.body = JSON.stringify(body);
  2692. }
  2693. }
  2694. return fetch(route, options);
  2695. };
  2696.  
  2697. romeo.sendXhr = function (url, body)
  2698. {
  2699. let [method, route] = url.split(" ");
  2700. return new Promise((resolve, reject) =>
  2701. {
  2702. const xhr = net.realXhr();
  2703. if (method === "GET" && body)
  2704. route = encodeUrl(route, romeo.jsonToParams(body));
  2705. xhr.open(method, route);
  2706.  
  2707. xhr.onload = () =>
  2708. {
  2709. if (xhr.status >= 200 && xhr.status < 300)
  2710. resolve(xhr.response);
  2711. else
  2712. reject({ status: xhr.status, statusText: xhr.statusText });
  2713. };
  2714. xhr.onerror = () => reject({ status: xhr.status, statusText: xhr.statusText });
  2715.  
  2716. xhr.setRequestHeader("x-api-key", apiKey);
  2717. xhr.setRequestHeader("x-session-id", sessionId);
  2718.  
  2719. if (method !== "GET" && body)
  2720. {
  2721. xhr.setRequestHeader("Content-Type", "application/json");
  2722. xhr.send(JSON.stringify(body));
  2723. }
  2724. else
  2725. {
  2726. xhr.send();
  2727. }
  2728. });
  2729. };
  2730.  
  2731. css.add(`
  2732. :root
  2733. {
  2734. --message-line-clamp: 2;
  2735. --system-message-visibility: visible;
  2736. --tile-headline-white-space: nowrap;
  2737. }
  2738.  
  2739. /* hide system popup messages if enabled */
  2740. .feedback
  2741. {
  2742. visibility: var(--system-message-visibility);
  2743. }
  2744.  
  2745. /* hide PLUS message at bottom of visitor grid */
  2746. main#visitors > section
  2747. {
  2748. display: none;
  2749. }
  2750. `);
  2751.  
  2752. net.on("xhr:load", "GET /api/v4/session", e =>
  2753. {
  2754. // Determine session ID.
  2755. sessionId = e.body.session_id;
  2756.  
  2757. // Apply settings.
  2758. const settings = e.body.bb_settings;
  2759. if (settings)
  2760. {
  2761. // Determine measurement locale.
  2762. measurementSystem = settings.interface?.measurement_system ?? measurementSystem;
  2763.  
  2764. // Determine radar filter, remove deleted ones.
  2765. const radarFilterId = settings.bluebird?.search_filter?.id;
  2766. cfg.radarFilter = cfg.getSavedRadarFilter(radarFilterId);
  2767. for (const savedFilterId of Object.keys(cfg.getSavedRadarFilters()))
  2768. if (savedFilterId && !e.body.data.search_filters.find(x => x.id === savedFilterId))
  2769. cfg.setSavedRadarFilter(savedFilterId);
  2770. }
  2771.  
  2772. // Determine initial Discover filter.
  2773. const filter = e.body.bb_settings?.bluebird?.search_filter
  2774. ?? e.body.data?.search_filters;
  2775. if (filter)
  2776. {
  2777. cfg.radarFilter["filter[personal][age][max]"] = filter.personal.age.max;
  2778. cfg.radarFilter["filter[personal][age][min]"] = filter.personal.age.min;
  2779. cfg.radarFilter["filter[personal][height][max]"] = filter.personal.height.max;
  2780. cfg.radarFilter["filter[personal][height][min]"] = filter.personal.height.min;
  2781. cfg.radarFilter["filter[personal][weight][max]"] = filter.personal.weight.max;
  2782. cfg.radarFilter["filter[personal][weight][min]"] = filter.personal.weight.min;
  2783. }
  2784.  
  2785. // Enable client-side PLUS capabilities.
  2786. const caps = e.body.data?.capabilities;
  2787. if (caps)
  2788. {
  2789. caps.can_save_unlimited_searches = true; // enables filter bookmarks
  2790. caps.can_set_plus_radar_style = true; // enables Grid Stats selection
  2791. }
  2792.  
  2793. // Retrieve current user location.
  2794. if (e.body.data?.profile_location)
  2795. {
  2796. romeo.userLat = e.body.data.profile_location.lat;
  2797. romeo.userLon = e.body.data.profile_location.long;
  2798. }
  2799. });
  2800. }(window.romeo ??= {}));
  2801.  
  2802. (function (menu)
  2803. {
  2804. const menuHandlers = {};
  2805. let menuBg, menuUl, menuX, menuY;
  2806.  
  2807. menu.on = function (selector, handler)
  2808. {
  2809. (menuHandlers[selector] ??= []).push(handler);
  2810. };
  2811.  
  2812. menu.item = function (icon, text, onclick)
  2813. {
  2814. return { icon, text, onclick };
  2815. };
  2816.  
  2817. function show(items)
  2818. {
  2819. if (!items)
  2820. return;
  2821.  
  2822. menuBg.style.display = "block";
  2823. menuUl.replaceChildren();
  2824. for (const item of items)
  2825. {
  2826. const li = dom.add(menuUl, `
  2827. <li class="ra_context_li">
  2828. <span class="icon icon-${item.icon}"></span>
  2829. ${str.get(item.text)}
  2830. </li>`);
  2831. li.addEventListener("click", e =>
  2832. {
  2833. hide();
  2834. item.onclick();
  2835. });
  2836. }
  2837.  
  2838. menuUl.style.display = "block";
  2839. const maxX = window.innerWidth - menuUl.offsetWidth;
  2840. const maxY = window.innerHeight - menuUl.offsetHeight;
  2841. menuUl.style.left = Math.min(menuX, maxX) + "px";
  2842. menuUl.style.top = Math.min(menuY, maxY) + "px";
  2843. };
  2844.  
  2845. function hide()
  2846. {
  2847. menuBg.style.display = "none";
  2848. menuUl.style.display = "none";
  2849. }
  2850.  
  2851. css.add(`
  2852. #ra_context_bg
  2853. {
  2854. background: transparent;
  2855. display: none;
  2856. height: 100%;
  2857. position: fixed;
  2858. width: 100%;
  2859. z-index: 100000;
  2860. }
  2861.  
  2862. #ra_context_ul
  2863. {
  2864. background: #232323;
  2865. border-radius: 1.125rem;
  2866. 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;
  2867. display: none;
  2868. font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
  2869. font-size: 94%;
  2870. overflow: hidden;
  2871. position: absolute;
  2872. z-index: 100001;
  2873. }
  2874.  
  2875. .ra_context_li
  2876. {
  2877. border-color: transparent;
  2878. border-left: 2px solid transparent;
  2879. border-style: solid;
  2880. border-width: 1px 1px 1px 2px;
  2881. color: #FFF;
  2882. cursor: default;
  2883. padding: 9px 18px 10px 10px;
  2884. transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1);
  2885. white-space: nowrap;
  2886. }
  2887.  
  2888. .ra_context_li:not(:first-child) {
  2889. border-top: 1px solid rgba(255, 255, 255, 0.16);
  2890. }
  2891.  
  2892. .ra_context_li .icon
  2893. {
  2894. margin: 4px;
  2895. }
  2896.  
  2897. .ra_context_li:hover
  2898. {
  2899. background: #2E2E2E;
  2900. }
  2901.  
  2902. @media screen and (max-width: 767px)
  2903. {
  2904. #ra_context_bg
  2905. {
  2906. background: rgba(0, 0, 0, 0.6);
  2907. }
  2908. #ra_context_ul
  2909. {
  2910. border-bottom-left-radius: 0;
  2911. border-bottom-right-radius: 0;
  2912. bottom: 0;
  2913. left: unset !important;
  2914. position: fixed;
  2915. top: unset !important;
  2916. width: 100%;
  2917. }
  2918. .ra_context_li
  2919. {
  2920. padding: 6px;
  2921. }
  2922. .ra_context_li .icon
  2923. {
  2924. font-size: 1.2rem;
  2925. margin: 8px;
  2926. }
  2927. }
  2928. `);
  2929.  
  2930. // Create context menu canceler.
  2931. menuBg = dom.add(document.body, "<div id='ra_context_bg'></ul>");
  2932. menuBg.addEventListener("click", e => hide());
  2933.  
  2934. // Create context menu.
  2935. menuUl = dom.add(document.body, "<ul id='ra_context_ul'></ul>");
  2936.  
  2937. // Attach to events.
  2938. addEventListener("contextmenu", e =>
  2939. {
  2940. menuX = e.clientX;
  2941. menuY = e.clientY;
  2942.  
  2943. // Go through hierarchy of clicked elements.
  2944. for (const el of document.elementsFromPoint(menuX, menuY))
  2945. {
  2946. // Stop when hitting a layer.
  2947. if (el.classList.contains("layer")
  2948. || el.classList.contains("layout")
  2949. || el.classList.contains("ReactModal__Overlay"))
  2950. break;
  2951. // Invoke first context handler for this element.
  2952. for (const [key, handlers] of Object.entries(menuHandlers))
  2953. {
  2954. if (el.matches(key))
  2955. {
  2956. romeo.log(`opening menu '${key}'`);
  2957.  
  2958. const items = [];
  2959. for (const handler of handlers)
  2960. items.push(...handler(el));
  2961. show(items);
  2962.  
  2963. e.preventDefault();
  2964. return;
  2965. }
  2966. }
  2967. }
  2968. });
  2969. }(window.menu ??= {}));
  2970.  
  2971. // ---- Previews ----
  2972.  
  2973. let previewLayer;
  2974.  
  2975. function createPreview(title)
  2976. {
  2977. const container = document.querySelector("#spotlight-container");
  2978. previewLayer = dom.add(container, `
  2979. <div class="layer layer--spotlight" style="top:0;z-index:10000;">
  2980. <div id="ra_preview_inner">
  2981. <div class="js-header layout-item">
  2982. <div class="layer-header layer-header--primary">
  2983. <a class="back-button l-tappable js-back marionette" href="#">
  2984. <span class="js-back-icon icon icon-cross icon-regular"></span>
  2985. </a>
  2986. <div class="layer-header__title js-title typo-section-navigation" style="text-align:center">
  2987. <h2>${title}</h2>
  2988. </div>
  2989. </div>
  2990. </div>
  2991. </div>
  2992. </div>`);
  2993.  
  2994. previewLayer.addEventListener("click", e =>
  2995. {
  2996. if (e.target === previewLayer)
  2997. previewLayer.remove();
  2998. });
  2999. previewLayer.querySelector(".js-back").addEventListener("click", e => previewLayer.remove());
  3000.  
  3001. return previewLayer.querySelector("#ra_preview_inner");
  3002. }
  3003.  
  3004. function initPreviews()
  3005. {
  3006. window.addEventListener("popstate", e =>
  3007. {
  3008. // Restore navigating back to preview.
  3009. switch (e.state?.ra_preview)
  3010. {
  3011. case "image":
  3012. showImagePreview(e.state.src, false);
  3013. break;
  3014. case "profile":
  3015. showProfilePreview(e.state.username, false);
  3016. break;
  3017. }
  3018. });
  3019. window.navigation?.addEventListener("navigate", e =>
  3020. {
  3021. // Hide preview on any other navigation.
  3022. previewLayer?.remove();
  3023. });
  3024. }
  3025.  
  3026. function showImagePreview(src, pushHistory = true)
  3027. {
  3028. if (pushHistory)
  3029. history.pushState({ ra_preview: "image", src: src }, "");
  3030.  
  3031. const monthYear = getPicMonthYear(src);
  3032.  
  3033. const content = dom.add(createPreview(str.get("viewFullImage")), `<div id="ra_image_content"></div>`);
  3034. dom.add(content, `<img id="ra_profile_pic" src="${src}"></img><br />${monthYear}`);
  3035. }
  3036.  
  3037. function showProfilePreview(username, pushHistory = true)
  3038. {
  3039. function isEntry(value)
  3040. {
  3041. return value && value !== "NO_ENTRY";
  3042. }
  3043.  
  3044. function addSection(el, key)
  3045. {
  3046. return dom.add(el, `
  3047. <details class="ra_profile_details" open>
  3048. <summary class="ra_profile_summary">${str.get(key)}</summary>
  3049. </details>`);
  3050. }
  3051.  
  3052. function add(section, key, value)
  3053. {
  3054. if (value)
  3055. {
  3056. dom.add(section, `
  3057. <div class="ra_profile_keyvalue">
  3058. <div>${str.get(key)}</div>
  3059. <div>${value}</div>
  3060. </div>`);
  3061. }
  3062. }
  3063. function addAgeRange(section, range)
  3064. {
  3065. if (range)
  3066. add(section, "ageRange", getProfileAgeRange(range));
  3067. }
  3068. function addArrayEnum(section, key, array)
  3069. {
  3070. if (!array)
  3071. return;
  3072. let values = [];
  3073. for (let i = 0; i < array.length; i++)
  3074. if (isEntry(array[i]))
  3075. values.push(str.getEnum(key, array[i]));
  3076. if (values.length)
  3077. add(section, key, values.join(", "));
  3078. }
  3079. function addDistance(section, distance, sensor)
  3080. {
  3081. let text = measurementSystem === "METRIC"
  3082. ? `${distance / 1000} km`
  3083. : `${round(distance * M2MI, 1)}mi`;
  3084. if (sensor)
  3085. text += " (GPS)";
  3086. add(section, "distance", text);
  3087. }
  3088. function addEnum(section, key, value)
  3089. {
  3090. if (isEntry(value))
  3091. add(section, key, str.getEnum(key, value));
  3092. }
  3093. function addGender(section, genderOrientation)
  3094. {
  3095. let values = [];
  3096. if (isEntry(genderOrientation?.orientation))
  3097. values.push(str.getEnum("orientation", genderOrientation.orientation));
  3098. if (isEntry(genderOrientation?.gender))
  3099. values.push(str.getEnum("gender", genderOrientation.gender));
  3100. if (values.length)
  3101. add(section, "genderOrientation", values.join(" / "));
  3102. }
  3103.  
  3104. const profile = profileCache[username];
  3105. if (!profile)
  3106. return;
  3107. const personal = profile.personal;
  3108. const sexual = profile.sexual;
  3109.  
  3110. if (pushHistory)
  3111. history.pushState({ ra_preview: "profile", username: username }, "");
  3112.  
  3113. const content = dom.add(createPreview(username), `<div id="ra_profile_content"></div>`);
  3114. const left = dom.add(content, `<div id="ra_profile_left"></div>`);
  3115. const right = dom.add(content, `<div id="ra_profile_right"></div>`);
  3116.  
  3117. dom.add(left, `<div>${escapeHtml(profile.headline ?? "")}</div>`);
  3118.  
  3119. const img = dom.add(left, `<img id="ra_profile_pic"></img>`);
  3120. if (profile.pic)
  3121. {
  3122. img.src = `/img/usr/${profile.pic}.jpg`;
  3123. dom.add(left, "<br />" + getPicMonthYear(profile.pic));
  3124. }
  3125. else
  3126. {
  3127. img.src = "/assets/f8a7712027544ed03920.svg";
  3128. }
  3129.  
  3130. const section = addSection(right, "metadata");
  3131. addEnum(section, "onlineStatus", profile.online_status);
  3132. if (profile.last_login)
  3133. add(section, "lastLogin", formatTime(profile.last_login));
  3134. if (profile.location)
  3135. {
  3136. add(section, "location", `${profile.location.name}, ${profile.location.country}`);
  3137. addDistance(section, profile.location.distance, profile.location.sensor);
  3138. }
  3139. add(section, "profileId", profile.id);
  3140.  
  3141. if (personal)
  3142. {
  3143. const section = addSection(right, "lookingFor");
  3144. addArrayEnum(section, "openTo", personal.looking_for);
  3145. addAgeRange(section, personal.target_age);
  3146. addArrayEnum(section, "gender", personal.gender_orientation?.looking_for_gender);
  3147. addArrayEnum(section, "orientation", personal.gender_orientation?.looking_for_orientation);
  3148. if (!section.querySelectorAll(".ra_profile_keyvalue").length)
  3149. section.remove();
  3150. }
  3151.  
  3152. if (personal)
  3153. {
  3154. const section = addSection(right, "general");
  3155. add(section, "age", personal.age);
  3156. add(section, "height", getProfileHeight(personal.height));
  3157. add(section, "weight", getProfileWeight(personal.weight));
  3158. add(section, "bmi", getProfileBmi(personal.height, personal.weight, true));
  3159. add(section, "bodyType", getProfileEnum("bodyType", personal.body_type));
  3160. add(section, "ethnicity", getProfileEnum("ethnicity", personal.ethnicity));
  3161. addEnum(section, "hairLength", personal.hair_length);
  3162. addEnum(section, "hairColor", personal.hair_color);
  3163. addEnum(section, "beard", personal.beard);
  3164. addEnum(section, "eyeColor", personal.eye_color);
  3165. add(section, "bodyHair", getProfileEnum("bodyHair", personal.body_hair));
  3166. addGender(section, personal?.gender_orientation);
  3167. addEnum(section, "smoker", personal.smoker);
  3168. addEnum(section, "tattoos", personal.tattoo);
  3169. addEnum(section, "piercings", personal.piercing);
  3170. addArrayEnum(section, "languages", personal.spoken_languages);
  3171. add(section, "relationship", getProfileEnum("relationship", personal.relationship));
  3172. }
  3173.  
  3174. if (sexual)
  3175. {
  3176. const section = addSection(right, "sexual");
  3177. add(section, "analPosition", getProfileEnum("analPosition", sexual.anal_position));
  3178. add(section, "dick", getProfileDick(sexual.dick_size, sexual.concision));
  3179. addArrayEnum(section, "fetish", sexual.fetish);
  3180. add(section, "dirty", getProfileEnum("dirty", sexual.dirty_sex));
  3181. addEnum(section, "fisting", sexual.fisting);
  3182. addEnum(section, "sm", sexual.sm);
  3183. add(section, "saferSex", getProfileEnum("saferSex", sexual.safer_sex));
  3184. if (!section.querySelectorAll(".ra_profile_keyvalue").length)
  3185. section.remove();
  3186. }
  3187.  
  3188. if (profile.personal?.profile_text)
  3189. {
  3190. const section = addSection(right, "aboutMe");
  3191. dom.add(section, `<div id="ra_profile_text">${profile.personal.profile_text}</div>`);
  3192. }
  3193. }
  3194.  
  3195. css.add(`
  3196. #ra_preview_inner
  3197. {
  3198. background-color: black;
  3199. display: grid;
  3200. grid-template-rows: min-content auto;
  3201. height: 100%;
  3202. }
  3203.  
  3204. #ra_image_content
  3205. {
  3206. overflow-y: scroll;
  3207. padding: 16px;
  3208. }
  3209.  
  3210. #ra_profile_content
  3211. {
  3212. display: grid;
  3213. font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
  3214. grid-template-columns: auto 352px;
  3215. overflow-y: scroll;
  3216. word-break: break-word;
  3217. }
  3218.  
  3219. #ra_profile_left
  3220. {
  3221. background: #121212;
  3222. overflow-y: scroll;
  3223. padding: 16px;
  3224. }
  3225.  
  3226. #ra_profile_right
  3227. {
  3228. overflow-y: scroll;
  3229. padding: 16px;
  3230. }
  3231.  
  3232. .ra_profile_details:not(:first-child)
  3233. {
  3234. border-top: 1px solid rgb(46, 46, 46);
  3235. margin-top: 1rem;
  3236. }
  3237.  
  3238. .ra_profile_summary
  3239. {
  3240. padding: 1rem 0;
  3241. }
  3242.  
  3243. .ra_profile_keyvalue
  3244. {
  3245. display: grid;
  3246. gap: 16px;
  3247. grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
  3248. }
  3249.  
  3250. .ra_profile_keyvalue > :first-child
  3251. {
  3252. color: rgba(255, 255, 255, 0.6);
  3253. text-align: right;
  3254. }
  3255.  
  3256. #ra_profile_text
  3257. {
  3258. white-space: pre-line;
  3259. }
  3260.  
  3261. @media screen and (max-width: 767px)
  3262. {
  3263. #ra_profile_content
  3264. {
  3265. grid-template-columns: initial;
  3266. grid-template-rows: auto auto;
  3267. }
  3268. #ra_profile_left
  3269. {
  3270. overflow-y: initial;
  3271. }
  3272. #ra_profile_right
  3273. {
  3274. overflow-y: initial;
  3275. }
  3276. #ra_profile_pic
  3277. {
  3278. width: 100%;
  3279. }
  3280. }
  3281. `);
  3282.  
  3283. // ---- Profiles ----
  3284.  
  3285. const profileCache = {};
  3286.  
  3287. function cacheProfile(profileObject)
  3288. {
  3289. const existing = profileCache[profileObject.name];
  3290. const profile =
  3291. {
  3292. id: profileObject.id,
  3293. name: profileObject.name,
  3294. headline: profileObject.headline ?? existing?.headline,
  3295. last_login: profileObject.last_login ?? existing?.last_login,
  3296. location: profileObject.location ?? existing?.location,
  3297. online_status: profileObject.online_status ?? existing?.online_status,
  3298. pic: profileObject.preview_pic?.url_token ?? existing?.pic, // not available if no picture
  3299. personal: profileObject.profile?.personal ?? profileObject.personal ?? existing?.personal, // not available in activities
  3300. sexual: profileObject.profile?.sexual ?? profileObject.sexual ?? existing?.sexual, // not available in activities
  3301. albums: profileObject.albumsV2 ?? existing?.albumsV2
  3302. };
  3303. profileCache[profile.name] = profile;
  3304. return profile;
  3305. }
  3306. function filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames)
  3307. {
  3308. // Return whether to display the profile.
  3309. return (!profile.personal || profile.personal.age >= hiddenMinAge && profile.personal.age <= hiddenMaxAge)
  3310. && !hiddenNames.has(profile.name);
  3311. }
  3312. function filterItemsAndCacheProfiles(items, profileSelector, filter)
  3313. {
  3314. let newItems = [];
  3315. const hiddenMaxAge = cfg.getHiddenMaxAge();
  3316. const hiddenMinAge = cfg.getHiddenMinAge();
  3317. const hiddenNames = cfg.getHiddenUsers();
  3318.  
  3319. for (const item of items ?? [])
  3320. {
  3321. const profile = cacheProfile(profileSelector(item));
  3322. if (!filter || filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames))
  3323. newItems.push(item);
  3324. }
  3325. return newItems;
  3326. }
  3327.  
  3328. function getProfileAgeRange(range, short)
  3329. {
  3330. if (range)
  3331. {
  3332. const min = range.min ?? "18";
  3333. const max = range.max ?? "99";
  3334. return short
  3335. ? `${min}-${max}`
  3336. : str.get("ageRangeValue").replace("$from", min).replace("$to", max);
  3337. }
  3338. }
  3339. function getProfileBmi(height, weight, withName)
  3340. {
  3341. if (height && weight)
  3342. {
  3343. const bmi = weight / Math.pow(height / 100, 2);
  3344. let result = `${round(bmi, 1).toFixed(1)}`;
  3345.  
  3346. if (withName)
  3347. {
  3348. for (const [max, key] of Object.entries({
  3349. 16: "bmiSevereThin",
  3350. 17: "bmiModerateThin",
  3351. 18.5: "bmiMildThin",
  3352. 25: "bmiNormal",
  3353. 30: "bmiPreObese",
  3354. 35: "bmiObese1",
  3355. 40: "bmiObese2",
  3356. 99: "bmiObese3",
  3357. }))
  3358. {
  3359. if (bmi < max)
  3360. return result + ` / ${str.get(key)}`;
  3361. }
  3362. }
  3363. return result;
  3364. }
  3365. }
  3366. function getProfileDick(size, concision)
  3367. {
  3368. let values = [];
  3369. if (size && size !== "NO_ENTRY")
  3370. values.push(str.getEnum("dick", size));
  3371. if (concision && concision !== "NO_ENTRY")
  3372. values.push(str.getEnum("concision", concision));
  3373. if (values.length)
  3374. return values.join(" - ");
  3375. }
  3376. function getProfileEnum(key, value)
  3377. {
  3378. if (value && value !== "NO_ENTRY")
  3379. return str.getEnum(key, value);
  3380. }
  3381. function getProfileHeight(height)
  3382. {
  3383. if (height)
  3384. {
  3385. return measurementSystem === "METRIC"
  3386. ? `${height}cm`
  3387. : `${round(height * CM2FT, 2)} ft`;
  3388. }
  3389. }
  3390. function getProfileWeight(weight)
  3391. {
  3392. if (weight)
  3393. {
  3394. return measurementSystem === "METRIC"
  3395. ? `${weight}kg`
  3396. : `${round(weight * KG2LBS)}lbs`;
  3397. }
  3398. }
  3399.  
  3400. function xhrHandleProfiles(e)
  3401. {
  3402. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, true);
  3403. e.body.items_limited = e.body.items_total; // Remove PLUS ad tile.
  3404.  
  3405. // Show every user as a large tile.
  3406. if (cfg.getEnhancedTiles())
  3407. for (const item of e.body.items ?? [])
  3408. if (item.display)
  3409. item.display.large_tile = true;
  3410. }
  3411. function xhrHandleVisits(e)
  3412. {
  3413. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, cfg.getHideVisits());
  3414. e.body.items_limited = e.body.items_total; // Restore PLUS-visible visitors.
  3415. }
  3416.  
  3417. net.on("xhr:load", "GET /api/v4/contacts", e =>
  3418. {
  3419. if (e.body.cursors)
  3420. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, cfg.getHideContacts());
  3421. });
  3422.  
  3423. net.on("xhr:load", "GET /api/v4/messages/conversations", e =>
  3424. {
  3425. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.chat_partner, cfg.getHideMessages());
  3426. });
  3427.  
  3428. net.on("xhr:load", "GET /api/+/notifications/activity-stream", e =>
  3429. {
  3430. e.body = filterItemsAndCacheProfiles(e.body, x => x.partner, cfg.getHideActivities());
  3431. });
  3432.  
  3433. net.on("fetch:recv", "GET /api/v4/profiles", e => xhrHandleProfiles(e));
  3434. net.on("fetch:recv", "GET /api/v4/profiles/popular", e => xhrHandleProfiles(e));
  3435. net.on("xhr:load", "GET /api/v4/hunqz/profiles", e => xhrHandleProfiles(e));
  3436. net.on("xhr:load", "GET /api/v4/profiles", e => xhrHandleProfiles(e));
  3437. net.on("xhr:load", "GET /api/v4/profiles/list", e => xhrHandleProfiles(e));
  3438. net.on("xhr:load", "GET /api/v4/profiles/popular", e => xhrHandleProfiles(e));
  3439.  
  3440. net.on("fetch:recv", "GET /api/v4/visitors", e => xhrHandleVisits(e));
  3441. net.on("fetch:recv", "GET /api/v4/visits", e => xhrHandleVisits(e));
  3442. net.on("fetch:recv", "GET /api/v4/reactions/cruise/likes", e =>
  3443. {
  3444. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, cfg.getHideLikes());
  3445. });
  3446.  
  3447. net.on("xhr:load", "GET /api/v4/messages/*", e => cacheProfile(e.body));
  3448. net.on("xhr:load", "GET /api/v4/profiles/*", e => cacheProfile(e.body));
  3449. net.on("xhr:load", "GET /api/v4/profiles/*/full", e => cacheProfile(e.body));
  3450. net.on("fetch:recv", "GET /api/v4/profiles/*/linked", e =>
  3451. {
  3452. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, cfg.getHideFriends());
  3453. });
  3454. net.on("fetch:load", "GET /api/v4/reactions/pictures/basic", e =>
  3455. {
  3456. e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.user_id, cfg.getHideLikes());
  3457. });
  3458.  
  3459. // ---- Filter ----
  3460.  
  3461. function addRadarFilter(filter, key, value)
  3462. {
  3463. if (!isMultiRadarFilter(key))
  3464. filter[key] = value;
  3465. else if (key in filter)
  3466. filter[key].push(value);
  3467. else
  3468. filter[key] = [value];
  3469. }
  3470.  
  3471. function hasRadarFilter(filter, key, value)
  3472. {
  3473. return key in filter
  3474. && (value === undefined || (isMultiRadarFilter(key)
  3475. ? filter[key].includes(value)
  3476. : filter[key] === value));
  3477. }
  3478.  
  3479. function isMultiRadarFilter(key)
  3480. {
  3481. return key.endsWith("[]");
  3482. }
  3483.  
  3484. function removeRadarFilter(filter, key, value)
  3485. {
  3486. if (!hasRadarFilter(filter, key, value))
  3487. return;
  3488. if (isMultiRadarFilter(key))
  3489. {
  3490. filter[key] = filter[key].filter(x => x !== value);
  3491. if (!filter[key].length)
  3492. delete filter[key];
  3493. }
  3494. else
  3495. {
  3496. delete filter[key];
  3497. }
  3498. }
  3499.  
  3500. function refreshFilter()
  3501. {
  3502. // Save filter.
  3503. cfg.setRadarFilter();
  3504. // Reset filter title, enable filter reset button.
  3505. document.querySelector(`.js-filter-header p[class^="ResponsiveBodyText-sc-"]`).innerHTML = str.get("filters");
  3506. document.querySelector(".js-clear-all").classList.remove("is-disabled");
  3507. // Update results.
  3508. document.querySelector("section.js-main-stage div.js-navigation a.is-selected, div.js-nav-item").click();
  3509. }
  3510.  
  3511. function packRadarFilter(params)
  3512. {
  3513. let filter = {};
  3514. for (const [key, value] of params)
  3515. addRadarFilter(filter, key, value);
  3516. return filter;
  3517. }
  3518.  
  3519. function unpackRadarFilter(filter)
  3520. {
  3521. let params = [];
  3522. for (const key in filter)
  3523. {
  3524. if (isMultiRadarFilter(key))
  3525. {
  3526. for (const value of filter[key])
  3527. params.push([key, value]);
  3528. }
  3529. else
  3530. {
  3531. params.push([key, filter[key]]);
  3532. }
  3533. }
  3534. return params;
  3535. }
  3536.  
  3537. function replaceFilterContainer(el)
  3538. {
  3539. if (!cfg.getEnhancedFilter())
  3540. return;
  3541.  
  3542. // Remove plus color from bookmark action.
  3543. const save = el.querySelector(".js-filter-actions .js-save");
  3544. save?.classList.remove("is-plus");
  3545.  
  3546. // Remove all filters on reset.
  3547. const clearAll = el.querySelector(".js-filter-actions .js-clear-all");
  3548. if (Object.keys(cfg.radarFilter).length)
  3549. clearAll.classList.remove("is-disabled");
  3550. clearAll.addEventListener("click", e =>
  3551. {
  3552. cfg.radarFilter = {};
  3553. cfg.setRadarFilter();
  3554. // Filter panel is recreated by default handler, recreate selections.
  3555. setTimeout(() => replaceFilterContainer(el));
  3556. });
  3557.  
  3558. // Clear any remaining extended filters.
  3559. filter = el.querySelector(".filter");
  3560. for (const tags of filter.querySelectorAll(".filter__params-tags.js-tags-list"))
  3561. tags.remove();
  3562.  
  3563. // Remove PLUS-Filter ad if no original filters are selected.
  3564. if (filter.querySelector(".js-quick-filter .js-add-params-button.plain-text-link"))
  3565. filter.querySelector(".js-quick-filter .filter__group-more-options").remove();
  3566.  
  3567. // Add custom filters.
  3568.  
  3569. function addSection(text)
  3570. {
  3571. return dom.add(filter, `
  3572. <div class="filter__params-tags js-tags-list">
  3573. <h3 class="typo mb-">${str.get(text)}</h3>
  3574. </div>`);
  3575. }
  3576.  
  3577. function addSectionList(text)
  3578. {
  3579. const section = addSection(text);
  3580. return dom.add(section, `<ul class="js-list tags-list"></ul>`);
  3581. }
  3582.  
  3583. function addSectionListMulti(text, prefix, filterKey, filterValues, hasNoEntry = true)
  3584. {
  3585. const section = addSectionList(text);
  3586. for (const filterValue of filterValues)
  3587. addListTagFilter(section, `${prefix}_${filterValue}`, filterKey, filterValue);
  3588. if (hasNoEntry)
  3589. addListTagFilter(section, "noEntry", filterKey, "NO_ENTRY");
  3590. return section;
  3591. };
  3592.  
  3593. function addListTag(ul, text, selected, change)
  3594. {
  3595. const li = dom.add(ul, `
  3596. <li class="tags-list__item">
  3597. <a class="js-tag ui-tag ui-tag--removable txt-truncate">
  3598. <span class="ui-tag__label">${str.get(text)}</span>
  3599. </a>
  3600. </li>`);
  3601. const a = li.querySelector("a");
  3602. if (selected)
  3603. a.classList.add("ui-tag--selected");
  3604. li.addEventListener("click", e =>
  3605. {
  3606. e.preventDefault();
  3607. if (a.classList.contains("ui-tag--selected"))
  3608. {
  3609. change(false);
  3610. a.classList.remove("ui-tag--selected");
  3611. }
  3612. else
  3613. {
  3614. change(true);
  3615. a.classList.add("ui-tag--selected");
  3616. }
  3617. });
  3618. }
  3619.  
  3620. function addListTagFilter(ul, text, filterKey, filterValue)
  3621. {
  3622. let selected = hasRadarFilter(cfg.radarFilter, filterKey, filterValue);
  3623. return addListTag(ul, text, selected, checked =>
  3624. {
  3625. if (checked)
  3626. addRadarFilter(cfg.radarFilter, filterKey, filterValue);
  3627. else
  3628. removeRadarFilter(cfg.radarFilter, filterKey, filterValue);
  3629. refreshFilter();
  3630. });
  3631. }
  3632.  
  3633. function addInput(ul)
  3634. {
  3635. return dom.add(ul, `
  3636. <div class="filter__group">
  3637. <div class="js-fulltext-input filter__group--fulltext">
  3638. <div class="Container--uQSLs layout layout--v-center">
  3639. <div class="layout-item layout-item--consume">
  3640. <input class="js-input Input--EicBC input" autocorrect="off" autocapitalize="off" spellcheck="false">
  3641. </div>
  3642. </div>
  3643. </div>
  3644. </div>`).querySelector("input");
  3645. }
  3646.  
  3647. addSectionListMulti("lookingForOther", "openTo", "filter[personal][looking_for][]",
  3648. ["SEXDATES", "FRIENDSHIP", "RELATIONSHIP"]);
  3649.  
  3650. addSectionListMulti("bodyType", "bodyType", "filter[personal][body_type][]",
  3651. ["SLIM", "AVERAGE", "ATHLETIC", "MUSCULAR", "BELLY", "STOCKY"]);
  3652. addSectionListMulti("ethnicity", "ethnicity", "filter[personal][ethnicity][]",
  3653. ["CAUCASIAN", "ASIAN", "LATIN", "MEDITERRANEAN", "BLACK", "MIXED", "ARAB", "INDIAN"]);
  3654. addSectionListMulti("hairLength", "hairLength", "filter[personal][hair_length][]",
  3655. ["SHAVED", "SHORT", "AVERAGE", "LONG", "PUNK"]);
  3656. addSectionListMulti("hairColor", "hairColor", "filter[personal][hair_color][]",
  3657. ["BLOND", "LIGHT_BROWN", "BROWN", "BLACK", "GREY", "OTHER", "RED"]);
  3658. addSectionListMulti("beard", "beard", "filter[personal][beard][]",
  3659. ["DESIGNER_STUBBLE", "MOUSTACHE", "GOATEE", "FULL_BEARD", "NO_BEARD"]);
  3660. addSectionListMulti("eyeColor", "eyeColor", "filter[personal][eye_color][]",
  3661. ["BLUE", "BROWN", "GREY", "GREEN", "OTHER"]);
  3662. addSectionListMulti("bodyHair", "bodyHair", "filter[personal][body_hair][]",
  3663. ["SMOOTH", "SHAVED", "LITTLE", "AVERAGE", "VERY_HAIRY"]);
  3664. addSectionListMulti("gender", "gender", "filter[personal][gender_orientation][gender][]",
  3665. ["MAN", "TRANS_MAN", "TRANS_WOMAN", "NON_BINARY", "OTHER"]);
  3666. addSectionListMulti("orientation", "orientation", "filter[personal][gender_orientation][orientation][]",
  3667. ["GAY", "BISEXUAL", "QUEER", "STRAIGHT", "OTHER"]);
  3668. addSectionListMulti("smoker", "smoker", "filter[personal][smoker][]",
  3669. ["NO", "SOCIALLY", "YES"]);
  3670. addSectionListMulti("tattoos", "tattoos", "filter[personal][tattoo][]",
  3671. ["A_FEW", "A_LOT", "NO"]);
  3672. addSectionListMulti("piercings", "piercings", "filter[personal][piercing][]",
  3673. ["A_FEW", "A_LOT", "NO"]);
  3674. addSectionListMulti("relationship", "relationship", "filter[personal][relationship][]",
  3675. ["SINGLE", "PARTNER", "OPEN", "MARRIED"]);
  3676.  
  3677. addSectionListMulti("analPosition", "analPosition", "filter[sexual][anal_position][]",
  3678. ["TOP_ONLY", "MORE_TOP", "VERSATILE", "MORE_BOTTOM", "BOTTOM_ONLY", "NO"]);
  3679. addSectionListMulti("dick", "dick", "filter[sexual][dick_size][]",
  3680. ["S", "M", "L", "XL", "XXL"]);
  3681. addSectionListMulti("concision", "concision", "filter[sexual][concision][]",
  3682. ["CUT", "UNCUT"]);
  3683. addSectionListMulti("fetish", "fetish", "filter[sexual][fetish][]",
  3684. ["LEATHER", "SPORTS", "SKATER", "RUBBER", "UNDERWEAR", "SKINS", "BOOTS", "LYCRA", "UNIFORM", "FORMAL",
  3685. "TECHNO", "SNEAKERS", "JEANS", "DRAG", "WORKER", "CROSSDRESSING"]);
  3686. addSectionListMulti("dirty", "dirty", "filter[sexual][dirty_sex][]",
  3687. ["YES", "NO", "WS_ONLY"]);
  3688. addSectionListMulti("fisting", "fisting", "filter[sexual][fisting][]",
  3689. ["ACTIVE", "ACTIVE_PASSIVE", "PASSIVE", "NO"]);
  3690. addSectionListMulti("sm", "sm", "filter[sexual][sm][]",
  3691. ["YES", "SOFT", "NO"]);
  3692. addSectionListMulti("saferSex", "saferSex", "filter[sexual][safer_sex][]",
  3693. ["ALWAYS", "NEEDS_DISCUSSION", "CONDOM", "PREP", "PREP_AND_CONDOM", "TASP"]);
  3694.  
  3695. addSectionListMulti("interests", "interests", "filter[hobby][interests][]",
  3696. ["ART", "BOARDGAME", "CAR", "COLLECT", "COMPUTER", "COOK", "DANCE", "FILM", "FOTO", "GAME", "LITERATURE",
  3697. "MODELING", "MOTORBIKE", "MUSIC", "NATURE", "POLITICS", "TV"], false);
  3698.  
  3699. const section = addSectionList("other");
  3700.  
  3701. const coordInput = addInput(section);
  3702. coordInput.type = "text";
  3703. coordInput.placeholder = str.get("latLong");
  3704. if ("filter[location][lat]" in cfg.radarFilter && "filter[location][long]" in cfg.radarFilter)
  3705. coordInput.value = `${cfg.radarFilter["filter[location][lat]"]}, ${cfg.radarFilter["filter[location][long]"]}`;
  3706. coordInput.addEventListener("change", e =>
  3707. {
  3708. removeRadarFilter(cfg.radarFilter, "filter[location][lat]");
  3709. removeRadarFilter(cfg.radarFilter, "filter[location][long]");
  3710. const sep = e.target.value.indexOf(", ");
  3711. if (sep !== -1)
  3712. {
  3713. const lat = parseFloat(e.target.value);
  3714. const long = parseFloat(e.target.value.substring(sep + 2));
  3715. if (!isNaN(lat) && !isNaN(long))
  3716. {
  3717. addRadarFilter(cfg.radarFilter, "filter[location][lat]", lat.toString());
  3718. addRadarFilter(cfg.radarFilter, "filter[location][long]", long.toString());
  3719. }
  3720. }
  3721. refreshFilter();
  3722. });
  3723.  
  3724. const radiusInput = addInput(section);
  3725. radiusInput.type = "text";
  3726. radiusInput.placeholder = str.get("customRadius");
  3727. if ("filter[location][radius]" in cfg.radarFilter)
  3728. {
  3729. const radius = cfg.radarFilter["filter[location][radius]"];
  3730. radiusInput.value = measurementSystem === "METRIC"
  3731. ? radius / 1000
  3732. : round(radius * M2MI, 1);
  3733. }
  3734. radiusInput.addEventListener("change", e =>
  3735. {
  3736. removeRadarFilter(cfg.radarFilter, "filter[location][radius]");
  3737. if (parseInt(e.target.value))
  3738. {
  3739. const radius = measurementSystem === "METRIC"
  3740. ? e.target.value * 1000
  3741. : e.target.value / M2MI;
  3742. addRadarFilter(cfg.radarFilter, "filter[location][radius]", radius);
  3743. }
  3744. refreshFilter();
  3745. });
  3746.  
  3747. addListTagFilter(section, "bedAndBreakfast", "filter[bed_and_breakfast_filter]", "ONLY");
  3748. addListTagFilter(section, "travelersOnly", "filter[travellers_filter]", "TRAVELLERS_ONLY");
  3749. addListTagFilter(section, "speakingMyLanguage", "filter[personal][speaks_my_languages]", "true");
  3750. }
  3751.  
  3752. function xhrApplyFilter(url, discover)
  3753. {
  3754. let [path, params] = decodeUrl(url);
  3755. let filter = packRadarFilter(params);
  3756.  
  3757. if (discover)
  3758. {
  3759. // Discover
  3760. if (!cfg.getDiscoverFilter())
  3761. return url;
  3762. }
  3763. else if ("filter[username]" in filter)
  3764. {
  3765. // Search (plain text only, #-prefixed text generates fulltext search).
  3766. if (!cfg.getSearchFilter())
  3767. return url;
  3768. }
  3769. else
  3770. {
  3771. // Radar
  3772.  
  3773. // Store Radar-only configurable parameters for Discover page.
  3774. function saveFilter(key)
  3775. {
  3776. if (filter[key])
  3777. cfg.radarFilter[key] = filter[key];
  3778. }
  3779. saveFilter("filter[personal][age][max]");
  3780. saveFilter("filter[personal][age][min]");
  3781. saveFilter("filter[personal][height][max]");
  3782. saveFilter("filter[personal][height][min]");
  3783. saveFilter("filter[personal][weight][max]");
  3784. saveFilter("filter[personal][weight][min]");
  3785.  
  3786. if (!cfg.getEnhancedFilter())
  3787. return url;
  3788. }
  3789.  
  3790. // Combine with custom parameters.
  3791. filter = { ...filter, ...cfg.radarFilter };
  3792. params = unpackRadarFilter(filter);
  3793. return encodeUrl(path, params);
  3794. }
  3795.  
  3796. css.add(`
  3797. /* enhanced radar filter */
  3798. .js-quick-filter
  3799. {
  3800. overflow-y: scroll;
  3801. }
  3802.  
  3803. /* restore bookmark icon color */
  3804. .ui-navbar__button--bookmarks .icon.icon-bookmark-outlined
  3805. {
  3806. color: #00bdff !important;
  3807. }
  3808.  
  3809. /* fix height of filter on mobile */
  3810. .sidebar .filter-container
  3811. {
  3812. height: unset !important;
  3813. }
  3814. `);
  3815.  
  3816. dom.on(`.js-quick-filter`, el =>
  3817. {
  3818. replaceFilterContainer(el.parentNode);
  3819. });
  3820.  
  3821. net.on("fetch:send", "GET /api/v4/profiles", e => e.url = xhrApplyFilter(e.url, true));
  3822. net.on("xhr:open", "GET /api/v4/hunqz/profiles", e => e.url = xhrApplyFilter(e.url));
  3823. net.on("xhr:open", "GET /api/v4/profiles", e => e.url = xhrApplyFilter(e.url));
  3824. net.on("xhr:open", "GET /api/v4/profiles/popular", e => e.url = xhrApplyFilter(e.url));
  3825.  
  3826. net.on("xhr:send", "PUT /api/v4/settings/interface/bluebird", e =>
  3827. {
  3828. // Changed filter.
  3829. const id = e.body.search_filter.id;
  3830. if (id)
  3831. cfg.radarFilter = cfg.getSavedRadarFilter(id);
  3832.  
  3833. const quickFilter = document.querySelector(".js-quick-filter")?.parentNode;
  3834. if (quickFilter)
  3835. replaceFilterContainer(quickFilter);
  3836. });
  3837. net.on("xhr:load", "DELETE /api/v4/search/filters/*", e =>
  3838. {
  3839. // Deleted filter.
  3840. const id = e.args[0];
  3841. cfg.setSavedRadarFilter(id);
  3842. });
  3843. net.on("xhr:load", "POST /api/v4/search/filters", e =>
  3844. {
  3845. // Created filter.
  3846. const id = e.body.id;
  3847. cfg.setSavedRadarFilter(id, cfg.radarFilter);
  3848. });
  3849.  
  3850. // ---- Tiles ----
  3851.  
  3852. const selTileDiscover = `section.js-content main > section > ul > li > a[href^="/profile/"]`; // li
  3853. const selTileRadarSmall = `div.js-search-results div.tile > div.reactView > a[href^="/profile/"]`; // div.tile (query first)
  3854. const selTileRadarLarge = `div.js-search-results div.tile--plus > div.reactView > a[href^="/profile/"]`; // div.search-results__item
  3855. const selTileRadarImage = `div.js-search-results div.tile > div.reactView > div.SMALL`; // div.tile
  3856. const selTileVisitors = `main#visitors a[href^="/profile/"]`; // li
  3857. const selTileVisited = `main#visited-grid a[href^="/profile/"]`; // li
  3858. const selTileLikes = `main#likers-list a[href^="/profile/"]`; // li
  3859. const selTileFriends = `section.js-profile-stats li > a[href^="/profile/"]`; // li
  3860. const selTileFriendsList = `main#friends-list li > a[href^="/profile/"]`; // li
  3861. const selTilePicLikes = `main#liked-by-list a[href^="/profile/"]`; // li
  3862. const selTileSearch = `div.js-results a[href^="/profile/"]`; // div.tile
  3863. const selTileActivity = `div.js-as-content div.tile a[href^="/profile/"]`; // div.listitem
  3864.  
  3865. function createTileMenu(el, username, removeOnHide, removeOnBlock)
  3866. {
  3867. return [
  3868. menu.item("search", "viewProfile", () => showProfilePreview(username)),
  3869. menu.item("hide-visit", "hideUser", () =>
  3870. {
  3871. cfg.setUserHidden(username, true);
  3872. if (removeOnHide)
  3873. el.style.display = "none";
  3874. }),
  3875. menu.item("illegal", "blockUser", () =>
  3876. {
  3877. const profileId = profileCache[username].id;
  3878. if (!profileId)
  3879. return false;
  3880.  
  3881. romeo.sendFetch("POST /api/v4/profiles/blocked", { profile_id: profileId, note: "" });
  3882. if (removeOnBlock)
  3883. el.style.display = "none";
  3884. }),
  3885. ];
  3886. }
  3887.  
  3888. css.add(`
  3889. /* fix jumping fade in mobile visitors/visits during load */
  3890. div.BIG::before, div.SMALL::before
  3891. {
  3892. inset: 60% 0px 0px !important;
  3893. }
  3894.  
  3895. /* tile description truncation */
  3896. .tile p[class^="SpecialText-"]
  3897. {
  3898. white-space: var(--tile-headline-white-space);
  3899. }
  3900.  
  3901. /* 2 friend list tile columns */
  3902. section.js-profile-stats ul, main#friends-list ul
  3903. {
  3904. grid-template-columns: 1fr 1fr;
  3905. }
  3906.  
  3907. .ra_tile_headline
  3908. {
  3909. color: rgb(255, 255, 255);
  3910. font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
  3911. font-size: 0.8125rem;
  3912. font-weight: 400;
  3913. line-height: 1.23077;
  3914. overflow: hidden;
  3915. text-overflow: ellipsis;
  3916. text-shadow: rgba(0, 0, 0, 0.32) 0px 1px 1px, rgba(0, 0, 0, 0.42) 1px 1px 1px;
  3917. white-space: var(--tile-headline-white-space);
  3918. }
  3919.  
  3920. .ra_tile_tag_row
  3921. {
  3922. display: flex;
  3923. flex-wrap: wrap;
  3924. gap: 0.25rem;
  3925. margin-top: 0.25rem;
  3926. }
  3927.  
  3928. .ra_tile_tag
  3929. {
  3930. background-color: rgb(46, 46, 46);
  3931. border-radius: 2px;
  3932. 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;
  3933. color: rgba(255, 255, 255, 0.87);
  3934. font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
  3935. font-size: 0.8125rem;
  3936. font-weight: 400;
  3937. line-height: 1.23077;
  3938. padding: 0px 2px;
  3939. }
  3940.  
  3941. .ra_tile_tag_new
  3942. {
  3943. color: rgb(0, 209, 0);
  3944. }
  3945. `);
  3946.  
  3947. dom.on([selTileDiscover, selTileRadarSmall, selTileVisitors, selTileVisited, selTileFriends, selTileFriendsList,
  3948. selTilePicLikes, selTileSearch].join(","), a =>
  3949. {
  3950. // Find profile cached for this tile.
  3951. const username = romeo.getUsernameFromHref(a.href);
  3952. const profile = profileCache[username];
  3953. if (!profile)
  3954. return;
  3955.  
  3956. // Add custom tags and headline.
  3957. let tagRow;
  3958. let tagClasses;
  3959. let tagNewClasses;
  3960. function addTag(text, isNew)
  3961. {
  3962. if (text)
  3963. dom.add(tagRow, `<span class="${isNew ? tagNewClasses : tagClasses}">${text}</span>`);
  3964. }
  3965.  
  3966. const inner = a.firstChild;
  3967. if (inner.classList.contains("BIG"))
  3968. {
  3969. // Find existing tag elements and classes.
  3970. const lastTag = a.querySelector(`div:last-child > span[class^="SpecialText-"]:last-child`);
  3971. if (!lastTag)
  3972. return;
  3973. tagRow = lastTag.parentNode;
  3974. tagClasses = lastTag.classList;
  3975. tagNewClasses = tagRow.firstChild.classList;
  3976. // Clear existing tags.
  3977. tagRow.replaceChildren();
  3978. }
  3979. else
  3980. {
  3981. const container = inner.lastChild;
  3982. // Create headline.
  3983. if (profile.headline)
  3984. dom.add(container, `<p class="ra_tile_headline">${profile.headline}</p>`);
  3985. // Create tag row.
  3986. tagRow = dom.add(container, `<div class="ra_tile_tag_row">`);
  3987. tagClasses = "ra_tile_tag";
  3988. tagNewClasses = tagClasses + " ra_tile_tag_new";
  3989. }
  3990.  
  3991. // Add tags.
  3992. if (tagNewClasses.value !== tagClasses.value)
  3993. addTag(str.get("new"), true);
  3994. const personal = profile.personal;
  3995. if (personal)
  3996. {
  3997. if (cfg.tileDetails.has("age")) addTag(personal.age);
  3998. if (cfg.tileDetails.has("bodyHair")) addTag(getProfileEnum("bodyHair", personal.body_hair));
  3999. if (cfg.tileDetails.has("height")) addTag(getProfileHeight(personal.height));
  4000. if (cfg.tileDetails.has("weight")) addTag(getProfileWeight(personal.weight));
  4001. if (cfg.tileDetails.has("bmi")) addTag(getProfileBmi(personal.height, personal.weight));
  4002. if (cfg.tileDetails.has("ageRange")) addTag(getProfileAgeRange(personal.target_age, true));
  4003. if (cfg.tileDetails.has("bodyType")) addTag(getProfileEnum("bodyType", personal.body_type));
  4004. if (cfg.tileDetails.has("ethnicity")) addTag(getProfileEnum("ethnicity", personal.ethnicity));
  4005. if (cfg.tileDetails.has("relationship")) addTag(getProfileEnum("relationship", personal.relationship));
  4006. if (cfg.tileDetails.has("smoker"))
  4007. {
  4008. if (personal.smoker === "YES")
  4009. addTag(str.get("smoker"));
  4010. else if (personal.smoker === "SOCIALLY")
  4011. addTag(str.get("socialSmoker"));
  4012. }
  4013. if (cfg.tileDetails.has("openTo") && personal.looking_for && personal.looking_for[0] !== "NO_ENTRY")
  4014. {
  4015. let text = "";
  4016. for (let openTo of personal.looking_for)
  4017. text += str.getEnum("openTo", openTo)[0];
  4018. addTag(text);
  4019. }
  4020. }
  4021. const sexual = profile.sexual;
  4022. if (sexual)
  4023. {
  4024. if (cfg.tileDetails.has("analPosition")) addTag(getProfileEnum("analPosition", sexual.anal_position));
  4025. if (cfg.tileDetails.has("dick")) addTag(getProfileDick(sexual.dick_size, sexual.concision));
  4026. if (cfg.tileDetails.has("saferSex")) addTag(getProfileEnum("saferSex", sexual.safer_sex));
  4027. if (cfg.tileDetails.has("dirty")) addTag(getProfileEnum("dirty", sexual.dirty_sex));
  4028. if (cfg.tileDetails.has("sm")) addTag(getProfileEnum("sm", sexual.sm));
  4029. if (cfg.tileDetails.has("fisting")) addTag(getProfileEnum("fisting", sexual.fisting));
  4030. }
  4031. });
  4032.  
  4033. dom.on(`img[src^="/img/usr/squarish/"][src$=".jpg"]`, el =>
  4034. {
  4035. if (cfg.getEnhancedImages())
  4036. {
  4037. const url = romeo.getImageUrl(el.src, 848);
  4038. el.src = url;
  4039. }
  4040. });
  4041.  
  4042. dom.on(`*[style^='background-image: url("/img/usr/squarish/'][style$='.jpg");']`, el =>
  4043. {
  4044. if (cfg.getEnhancedImages())
  4045. {
  4046. const url = romeo.getImageUrl(css.getStyleImageUrl(el.style.backgroundImage), 848);
  4047. el.style.backgroundImage = `url("${url}")`;;
  4048. }
  4049. });
  4050.  
  4051. menu.on(selTileDiscover, a =>
  4052. {
  4053. const el = a.closest("li");
  4054. const username = romeo.getUsernameFromHref(a.href);
  4055. return createTileMenu(el, username, true, true);
  4056. });
  4057.  
  4058. menu.on(selTileRadarLarge, a =>
  4059. {
  4060. const el = a.closest("div.tile--plus").parentNode;
  4061. const username = romeo.getUsernameFromHref(a.href);
  4062. return createTileMenu(el, username, true, true);
  4063. });
  4064.  
  4065. menu.on(selTileRadarSmall, a =>
  4066. {
  4067. const el = a.closest("div.tile");
  4068. const username = romeo.getUsernameFromHref(a.href);
  4069. return createTileMenu(el, username, true, true);
  4070. });
  4071.  
  4072. menu.on(selTileRadarImage, el =>
  4073. {
  4074. const imageUrl = romeo.getImageUrl(css.getStyleImageUrl(el.style.backgroundImage));
  4075. return [
  4076. menu.item("search", "viewFullImage", () => showImagePreview(imageUrl)),
  4077. ];
  4078. });
  4079.  
  4080. menu.on([selTileVisitors, selTileVisited].join(","), a =>
  4081. {
  4082. const el = a.closest("li");
  4083. const username = romeo.getUsernameFromHref(a.href);
  4084. return createTileMenu(el, username, cfg.getHideVisits(), true);
  4085. });
  4086.  
  4087. menu.on([selTileFriends, selTileFriendsList].join(","), a =>
  4088. {
  4089. const el = a.closest("li");
  4090. const username = romeo.getUsernameFromHref(a.href);
  4091. return createTileMenu(el, username, cfg.getHideFriends(), false);
  4092. });
  4093.  
  4094. menu.on([selTileLikes, selTilePicLikes].join(","), a =>
  4095. {
  4096. const el = a.closest("li");
  4097. const username = romeo.getUsernameFromHref(a.href);
  4098. return createTileMenu(el, username, cfg.getHideLikes(), true);
  4099. });
  4100.  
  4101. menu.on(selTileSearch, a =>
  4102. {
  4103. const el = a.closest("div.tile");
  4104. const username = romeo.getUsernameFromHref(a.href);
  4105. return createTileMenu(el, username, true, true);
  4106. });
  4107.  
  4108. menu.on(selTileActivity, a =>
  4109. {
  4110. const el = a.closest("div.listitem");
  4111. const username = romeo.getUsernameFromHref(a.href);
  4112. return createTileMenu(el, username, cfg.getHideActivities(), true);
  4113. });
  4114.  
  4115. // ---- Messaging ----
  4116.  
  4117. css.add(`
  4118. /* message list truncation */
  4119. #messenger div[class^="TruncateBlock__Content-sc-"]
  4120. {
  4121. -webkit-line-clamp: var(--message-line-clamp);
  4122. }
  4123. `);
  4124.  
  4125. dom.on(`.js-send-region.layout-item > div`, el =>
  4126. {
  4127. el.addEventListener("keydown", e =>
  4128. {
  4129. // Prevent site event handler from sending message or typing notifications.
  4130. const enter = e.key === "Enter";
  4131. const send = enter && (cfg.getSendEnter() || e.ctrlKey);
  4132. const allow = send || cfg.getTypingNotifications() && !enter;
  4133. if (!allow)
  4134. e.stopPropagation();
  4135. }, true);
  4136. });
  4137.  
  4138. menu.on(".js-chat .reactView", el =>
  4139. {
  4140. // messages > message
  4141. const a = el.querySelector(`a[href^="/profile/"]`);
  4142. const username = romeo.getUsernameFromHref(a.href);
  4143. return [
  4144. ...createTileMenu(el, username, cfg.getHideMessages(), false),
  4145. menu.item("trashcan", "deleteUnread", async () =>
  4146. {
  4147. const partnerId = profileCache[username].id;
  4148.  
  4149. // Retrieve and delete all unread messages.
  4150. const deletes = [];
  4151. let firstRead;
  4152. for await (const item of romeo.iterItems("GET /api/v4/messages", { filter: { partner_id: partnerId } }))
  4153. {
  4154. if (item.folder === "RECEIVED" && item.unread)
  4155. {
  4156. deletes.push(romeo.sendXhr("DELETE /api/v4/messages/" + item.id));
  4157. }
  4158. else
  4159. {
  4160. firstRead = item;
  4161. break;
  4162. }
  4163. }
  4164. await Promise.allSettled(deletes);
  4165.  
  4166. // Show last read message or remove chat if none.
  4167. if (firstRead)
  4168. {
  4169. const textEl = el.querySelector(`p[class^="BaseText-sc-"]`);
  4170. textEl.style.color = "rgba(255, 255, 255, 0.6)";
  4171. textEl.innerHTML = firstRead.text;
  4172. const newEl = el.querySelector(`a > div p[class^="SpecialText-sc-"]`);
  4173. newEl?.parentNode.remove();
  4174. }
  4175. else
  4176. {
  4177. el.remove();
  4178. }
  4179. })
  4180. ];
  4181. });
  4182.  
  4183. menu.on(".js-chat .reactView img", el =>
  4184. {
  4185. // messages > message > sent image
  4186. return [
  4187. menu.item("search", "viewFullImage", () => showImagePreview(romeo.getImageUrl(el.src))),
  4188. ];
  4189. });
  4190.  
  4191. menu.on(".js-contacts .reactView", el =>
  4192. {
  4193. // contacts > contact
  4194. const a = el.querySelector(`a[href^="/profile/"]`);
  4195. const username = romeo.getUsernameFromHref(a.href);
  4196. return createTileMenu(el, username, cfg.getHideContacts(), false);
  4197. });
  4198.  
  4199. // ---- Albums ----
  4200.  
  4201. let changeProfilePic = false;
  4202. const picMonthIdStart = [
  4203. 0x00000000, 0x00000000, 0x000076F1, 0x0000C0C2, 0x000113AC, 0x000193C7, 0x00022ADB, 0x0002E59B, 0x0003C274, 0x0004BDDA, 0x0005CB8F, 0x0006F792, // 2003
  4204. 0x00083008, 0x0009E166, 0x000BC399, 0x000DC70D, 0x000FCAD1, 0x001219F5, 0x0014E794, 0x00180A61, 0x001B7A83, 0x001EEAAA, 0x0022A968, 0x0026AAF2, // 2004
  4205. 0x002B6A0F, 0x00313036, 0x00367804, 0x003C4AE9, 0x00421A3C, 0x0048A2B6, 0x004F0BA9, 0x00563A9C, 0x005D4FF9, 0x00642853, 0x006BC702, 0x0072DC7C, // 2005
  4206. 0x007A1D79, 0x0082636E, 0x0089C239, 0x00921763, 0x009AE5E4, 0x00A3F682, 0x00ACFDC3, 0x00B72E9E, 0x00C1E7A3, 0x00CB8A2A, 0x00D5C06A, 0x00DF62F9, // 2006
  4207. 0x00E909CB, 0x00F428F1, 0x00FE1934, 0x0108EC16, 0x01141330, 0x011FFB48, 0x012B9597, 0x0137A3D7, 0x01436C46, 0x014EBA63, 0x015AEA4F, 0x01665B6D, // 2007
  4208. 0x01721DF4, 0x017ECDD9, 0x018A872F, 0x0197CF93, 0x01A3FCA7, 0x01B1BB65, 0x01BF7FCA, 0x01CE211F, 0x01DD38F7, 0x01EB50F3, 0x01FA24CC, 0x02086E83, // 2008
  4209. 0x0216E0EA, 0x0226DE08, 0x0234EDA5, 0x02552D86, 0x02677F82, 0x027AAE57, 0x028D8BA1, 0x02A17765, 0x02B5E176, 0x02C9ADB5, 0x02DD8AA7, 0x02F0DFE7, // 2009
  4210. 0x0304F979, 0x031B4142, 0x032E6143, 0x0342C960, 0x0356CD19, 0x036CB4D8, 0x0381A103, 0x039883D7, 0x03B0829F, 0x03C679E3, 0x03DD820B, 0x03F432A3, // 2010
  4211. 0x040AF127, 0x0423DDBB, 0x0439C102, 0x045272F4, 0x046B187C, 0x048570E5, 0x04A07AC1, 0x04BCDEC4, 0x04D9B4FA, 0x04F52906, 0x05120AF3, 0x052DF05D, // 2011
  4212. 0x054A17B7, 0x0568A266, 0x0584C8A7, 0x05A2542C, 0x05C0B41B, 0x05DF82C6, 0x05FE3605, 0x061E623C, 0x063F4240, 0x065E2C95, 0x067E22B7, 0x069DBC58, // 2012
  4213. 0x06C0432B, 0x06E65935, 0x0707D372, 0x072DF9EB, 0x0751CA91, 0x077860FB, 0x079C0F3E, 0x07C1E771, 0x07E86366, 0x080DA8CE, 0x0831EE79, 0x08558D00, // 2013
  4214. 0x087B94B3, 0x08A21C54, 0x08C4F55C, 0x08EAC6FE, 0x09119CAA, 0x0939B66C, 0x0960B60A, 0x098A9925, 0x09B76F87, 0x09E01456, 0x0A0906C0, 0x0A305FC4, // 2014
  4215. 0x0A80E9B2, 0x0ADAF553, 0x0B2947CE, 0x0B7D3B5D, 0x0BCF0D62, 0x0C24958C, 0x0C755B5B, 0x0CCD15D3, 0x0D23C304, 0x0D7376DC, 0x0DC5B8FB, 0x0E14171B, // 2015
  4216. 0x0E6631A2, 0x0EBDF2C6, 0x0F0CE2EC, 0x0F607BF5, 0x0FB37151, 0x100AAF7E, 0x105E79ED, 0x10B8BBBF, 0x1115E3C6, 0x116AE5AC, 0x11BE74A4, 0x12120F48, // 2016
  4217. 0x1267B822, 0x12C42272, 0x1315847B, 0x136E4BE0, 0x13C5FDBD, 0x141F2328, 0x147B0F3E, 0x14DDB823, 0x1541CB7A, 0x159E4A36, 0x1601FFF2, 0x16657140, // 2017
  4218. 0x16CB8A49, 0x1734ECE1, 0x1792BE94, 0x17FA4016, 0x185EC78C, 0x18C964A2, 0x193230D2, 0x19A225BD, 0x1A12BD1E, 0x1A79217A, 0x1AE1E760, 0x1B4790FD, // 2018
  4219. 0x1BB4F607, 0x1C23CE64, 0x1C84B186, 0x1CF2F646, 0x1D61D7F8, 0x1DDB56D8, 0x1E59A281, 0x1EDF03D3, 0x1F66D255, 0x1FE26F82, 0x205EC456, 0x20D2BDED, // 2019
  4220. 0x21484431, 0x21C24BA6, 0x2230C76F, 0x22A6C5D6, 0x2312EBBD, 0x23844C83, 0x23F45E75, 0x246D38C1, 0x24EB9338, 0x255DC81A, 0x25D30C39, 0x264389A4, // 2020
  4221. 0x26B9CB56, 0x2730F2C2, 0x279824A7, 0x28072E59, 0x2870B46C, 0x28E20CEE, 0x294EA8DA, 0x29BDFADB, 0x2A2F2276, 0x2A9872C3, 0x2B026A37, 0x2B6628B7, // 2021
  4222. 0x2BCD3A33, 0x2C3819E9, 0x2C95C80F, 0x2CF66474, 0x2D57AFA7, 0x2DC0DF59, 0x2E28345D, 0x2E94DF63, 0x2F014A14, 0x2F63046C, 0x2FC3E3BE, 0x301FADA4, // 2022
  4223. 0x307F54F1, 0x30E474F9, 0x313BDC54, 0x319A0106, 0x31F9240F, 0x3259674F, 0x32B90EF5, 0x3320EB2E, 0x3386B446, 0x33E585DA, 0x34460066, 0x34A0E6FC, // 2023
  4224. 0x34FFEBB4, 0x35602515, 0x35B6161A, 0x360FC04F, 0x3669CD5F, 0x36CA77E2, 0x37290915, 0x378E4751, 0x37F576A3, 0x385137A2, 0x38AE3822, 0x39094260, // 2024
  4225. 0x39675D6B, 0x39C57877, 0x3A1A781F, 0x3A7872C9, 0x3AD384B3, 0x3B319FBF, 0x3B8CB1AA, 0x3BEACCB5, 0x3C48E7C1, 0x3CA3F9AC, 0x3D023519, 0x3D5D4703, // 2025
  4226. ];
  4227.  
  4228. function getPicMonthYear(url)
  4229. {
  4230. const token = url.substr(url.lastIndexOf("/") + 1, 8);
  4231. const id = parseInt(token, 16);
  4232.  
  4233. let i;
  4234. for (i = picMonthIdStart.length - 1; id < picMonthIdStart[i]; --i);
  4235. const year = 2003 + Math.trunc(i / 12);
  4236. const month = 1 + i % 12;
  4237. const future = i === picMonthIdStart.length;
  4238.  
  4239. return (future ? ">" : "") + formatYearMonth(year, month);
  4240. }
  4241.  
  4242. css.add(`
  4243. .ra_albumview_like
  4244. {
  4245. font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
  4246. }
  4247. `);
  4248.  
  4249. dom.on(`[role='dialog'] > div > main > ul > li button[class^="TertiaryButton__Element-sc-"]`, el =>
  4250. {
  4251. const img = el.closest("li").querySelector("img");
  4252. const monthYear = getPicMonthYear(img.src);
  4253. dom.add(el.parentNode, `<p class="ra_albumview_like">${monthYear}</p>`, true);
  4254. });
  4255.  
  4256. dom.on(`div.ReactModal__Content main div > img[src^="/img/usr/original/"]`, img =>
  4257. {
  4258. const p = img.closest("main").querySelector(`p[class^="BaseText-sc-"]`);
  4259. const monthYear = getPicMonthYear(img.src);
  4260. p.innerHTML = monthYear + "<br />" + p.innerHTML;
  4261. });
  4262.  
  4263. dom.on(`li#picture_menu_set-as-main-profile-picture`, el =>
  4264. {
  4265. const button = el.querySelector("button");
  4266.  
  4267. // Only allow profile pic being changed when manually clicking this button.
  4268. button.parentNode.addEventListener("click", e => changeProfilePic = true, true);
  4269. });
  4270.  
  4271. net.on("xhr:send", "PUT /api/v4/profiles/me", e =>
  4272. {
  4273. // Prevent automatic profile picture change when rearranging pictures.
  4274. if (changeProfilePic)
  4275. changeProfilePic = false;
  4276. else if (e.body.preview_pic_id)
  4277. e.cancel = true;
  4278. });
  4279.  
  4280. // ---- Discover ----
  4281.  
  4282. net.on("fetch:recv", "GET /api/content/bluebird/startpages", e =>
  4283. {
  4284. if (!cfg.getDiscoverBanners())
  4285. for (const item of e.body)
  4286. if (item.blogposts)
  4287. item.blogposts = [];
  4288. });
  4289.  
  4290. net.on("fetch:recv", "GET /api/v4/groups", e =>
  4291. {
  4292. if (!cfg.getDiscoverGroups() && e.url.includes("seed=popular-"))
  4293. {
  4294. e.body.cursors = {};
  4295. e.body.items = [];
  4296. e.body.items_total = 0;
  4297. }
  4298. });
  4299.  
  4300. // ---- Location ----
  4301.  
  4302. css.add(`
  4303. div.js-side-content div.js-restriction[class*="Info--"]
  4304. {
  4305. display: none;
  4306. }
  4307.  
  4308. div.js-side-content div.layer-actionbar > button.js-apply.is-disabled
  4309. {
  4310. filter: initial;
  4311. opacity: initial;
  4312. pointer-events: initial;
  4313. }
  4314. `);
  4315.  
  4316. dom.on(`div.js-side-content button[class^="SecondaryButton__Element-sc-"]`, button =>
  4317. {
  4318. button.innerHTML = str.get("chooseLocation");
  4319. });
  4320.  
  4321. net.on("xhr:send", "PUT /api/v4/locations/profile", e =>
  4322. {
  4323. function fuzz()
  4324. {
  4325. const MIN = 123; const MAX = 321;
  4326. const rnd = Math.random() * (MAX - MIN) + MIN;
  4327. return Math.random() > 0.5 ? -rnd : rnd;
  4328. }
  4329.  
  4330. if (e.body.sensor && cfg.getLocationFuzz())
  4331. {
  4332. const [east, north, zn, zl] = utm.fromLatlon(e.body.lat, e.body.long);
  4333. [e.body.lat, e.body.long] = utm.toLatlon(east + fuzz(), north + fuzz(), zn, zl);
  4334. };
  4335. });
  4336.  
  4337. // ---- Settings ----
  4338.  
  4339. function openSettingsPane()
  4340. {
  4341. // Open pane.
  4342. const layerContent = document.querySelector("#offcanvas-nav > .js-layer-content");
  4343. layerContent.classList.add("is-open");
  4344.  
  4345. // Create UI.
  4346. const pane = layerContent.querySelector(".js-side-content");
  4347. pane.replaceChildren();
  4348. const p = dom.add(pane, `
  4349. <div class="layout layout--vertical layout--consume">
  4350. <div class="layout-item layout-item--consume layout layout--vertical">
  4351.  
  4352. <div class="js-header layout-item l-hidden-md-lg">
  4353. <div class="layer-header layer-header--primary">
  4354. <a class="back-button l-hidden-md-lg l-tappable js-back marionette" href="/me">
  4355. <span class="js-back-icon icon icon-back icon-large"></span>
  4356. </a>
  4357. <div class="layer-header__title">
  4358. <h2>${GM_info.script.name}</h2>
  4359. </div>
  4360. </div>
  4361. </div>
  4362. <div class="layout-item settings__navigation p l-hidden-sm">
  4363. <div class="js-title typo-section-navigation">${GM_info.script.name}</div>
  4364. </div>
  4365.  
  4366. <div class="layout-item layout-item--consume">
  4367. <div class="js-content js-scrollable fit scrollable">
  4368. <div id="ra_settings_p" class="p"></div>
  4369. </div>
  4370. </div>
  4371. </div>
  4372. </div>`).querySelector("#ra_settings_p");
  4373.  
  4374. function addSection(title)
  4375. {
  4376. return dom.add(p, `
  4377. <div class="settings__key">
  4378. <div>
  4379. <span>${str.get(title)}</span>
  4380. </div>
  4381. <div class="separator separator--alt separator--narrow [ mb ] "></div>
  4382. </div>`);
  4383. }
  4384. function addCheckbox(section, text, desc)
  4385. {
  4386. const input = dom.add(section, `
  4387. <div class="layout layout--v-center">
  4388. <div class="layout-item [ 6/12--sm ]">
  4389. <span>${str.get(text)}</span>
  4390. </div>
  4391. <div class="layout-item [ 6/12--sm ]">
  4392. <div class="js-toggle-show-headlines pull-right">
  4393. <div>
  4394. <span class="ui-toggle ui-toggle--default ui-toggle--right">
  4395. <input class="ui-toggle__input" type="checkbox" id="ra_${text}">
  4396. <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>
  4397. </span>
  4398. </div>
  4399. </div>
  4400. </div>
  4401. </div>`).querySelector("input");
  4402. if (desc)
  4403. {
  4404. dom.add(section, `
  4405. <div>
  4406. <div class="settings__description">${str.get(desc)}</div>
  4407. </div>`);
  4408. }
  4409. return input;
  4410. }
  4411. function addNumber(section, text, min, max)
  4412. {
  4413. return dom.add(section, `
  4414. <div class="layout layout--v-center">
  4415. <div class="layout-item [ 6/12--sm ] mv-">
  4416. <span>${str.get(text)}</span>
  4417. </div>
  4418. <div class="layout-item [ 6/12--sm ] mv-">
  4419. <input class="input input--block" type="number" min="${min}" max="${max}"/>
  4420. </div>
  4421. </div>`).querySelector("input");
  4422. }
  4423. function addTagList(section)
  4424. {
  4425. return dom.add(section, `
  4426. <div class="mv js-grid-stats-selector">
  4427. <div>
  4428. <ul class="js-list tags-list tags-list--centered"/>
  4429. </div>
  4430. </div>`).querySelector("ul");
  4431. }
  4432. function addTag(ul, tag, text, selected, change)
  4433. {
  4434. const li = dom.add(ul, `
  4435. <li class="tags-list__item">
  4436. <a class="js-tag ui-tag ui-tag--removable" href="#">
  4437. <span class="ui-tag__label">${text}</span>
  4438. </a>
  4439. </li>`);
  4440. const a = li.querySelector("a");
  4441. if (selected)
  4442. a.classList.add("ui-tag--selected");
  4443. li.addEventListener("click", e =>
  4444. {
  4445. e.preventDefault();
  4446. if (a.classList.contains("ui-tag--selected"))
  4447. {
  4448. change({ tag: tag, checked: false });
  4449. a.classList.remove("ui-tag--selected");
  4450. }
  4451. else
  4452. {
  4453. change({ tag: tag, checked: true });
  4454. a.classList.add("ui-tag--selected");
  4455. }
  4456. });
  4457. }
  4458.  
  4459. // Add debug section.
  4460. if (romeo.debug())
  4461. {
  4462. const debugSection = addSection("debug");
  4463.  
  4464. const btUnblockAll = dom.add(debugSection, `<button type="button">Unblock all</button>`);
  4465. btUnblockAll.addEventListener("click", async e =>
  4466. {
  4467. const ids = [];
  4468. for await (const item of romeo.iterItems("GET /api/v4/profiles/blocked", { length: 100 }))
  4469. ids.push(item.id);
  4470. for (const id of ids)
  4471. await romeo.sendXhr("DELETE /api/v4/contacts/" + id);
  4472. });
  4473. }
  4474.  
  4475. // Add general section.
  4476. const generalSection = addSection("general");
  4477.  
  4478. const locationFuzz = addCheckbox(generalSection, "locationFuzz", "locationFuzzDesc");
  4479. locationFuzz.checked = cfg.getLocationFuzz();
  4480. locationFuzz.addEventListener("change", e => cfg.setLocationFuzz(e.target.checked));
  4481.  
  4482. const systemMessages = addCheckbox(generalSection, "systemMessages", "systemMessagesDesc");
  4483. systemMessages.checked = cfg.getSystemMessages();
  4484. systemMessages.addEventListener("change", e => setSystemMessages(e.target.checked));
  4485.  
  4486. // Add discover section.
  4487. const discoverSection = addSection("discover");
  4488.  
  4489. const discoverBanners = addCheckbox(discoverSection, "discoverBanners", "discoverBannersDesc");
  4490. discoverBanners.checked = cfg.getDiscoverBanners();
  4491. discoverBanners.addEventListener("change", e => cfg.setDiscoverBanners(e.target.checked));
  4492.  
  4493. const discoverFilter = addCheckbox(discoverSection, "discoverFilter", "discoverFilterDesc");
  4494. discoverFilter.checked = cfg.getDiscoverFilter();
  4495. discoverFilter.addEventListener("change", e => cfg.setDiscoverFilter(e.target.checked));
  4496.  
  4497. const discoverGroups = addCheckbox(discoverSection, "discoverGroups", "discoverGroupsDesc");
  4498. discoverGroups.checked = cfg.getDiscoverGroups();
  4499. discoverGroups.addEventListener("change", e => cfg.setDiscoverGroups(e.target.checked));
  4500.  
  4501. // Add filter section.
  4502. const filterSection = addSection("filter");
  4503.  
  4504. const enhancedFilter = addCheckbox(filterSection, "enhancedFilter", "enhancedFilterDesc");
  4505. enhancedFilter.checked = cfg.getEnhancedFilter();
  4506. enhancedFilter.addEventListener("change", e => cfg.setEnhancedFilter(e.target.checked));
  4507.  
  4508. const searchFilter = addCheckbox(filterSection, "searchFilter", "searchFilterDesc");
  4509. searchFilter.checked = cfg.getSearchFilter();
  4510. searchFilter.addEventListener("change", e => cfg.setSearchFilter(e.target.checked));
  4511.  
  4512. // Add tiles section.
  4513. const tilesSection = addSection("tiles");
  4514.  
  4515. const enhancedTiles = addCheckbox(tilesSection, "enhancedTiles", "enhancedTilesDesc");
  4516. enhancedTiles.checked = cfg.getEnhancedTiles();
  4517. enhancedTiles.addEventListener("change", e => cfg.setEnhancedTiles(e.target.checked));
  4518.  
  4519. const enhancedImages = addCheckbox(tilesSection, "enhancedImages", "enhancedImagesDesc");
  4520. enhancedImages.checked = cfg.getEnhancedImages();
  4521. enhancedImages.addEventListener("change", e => cfg.setEnhancedImages(e.target.checked));
  4522.  
  4523. const fullHeadlines = addCheckbox(tilesSection, "fullHeadlines", "fullHeadlinesDesc");
  4524. fullHeadlines.checked = cfg.getFullHeadlines();
  4525. fullHeadlines.addEventListener("change", e => cfg.setFullHeadlines(e.target.checked));
  4526.  
  4527. const tileCount = addNumber(tilesSection, "tileCount", 0, 10);
  4528. tileCount.value = cfg.getTileCount();
  4529. tileCount.addEventListener("change", e => cfg.setTileCount(parseInt(e.target.value)));
  4530.  
  4531. const tileDetailsList = addTagList(tilesSection, "tileDetailsList");
  4532. for (const tileDetail of ["age", "height", "weight", "bmi", "smoker", "ageRange",
  4533. "bodyHair", "bodyType", "ethnicity", "relationship", "analPosition",
  4534. "dick", "saferSex", "dirty", "sm", "fisting", "openTo"])
  4535. {
  4536. addTag(tileDetailsList, tileDetail, str.get(tileDetail), cfg.tileDetails.has(tileDetail), e => cfg.setTileDetail(e.tag, e.checked));
  4537. }
  4538.  
  4539. // Add messages section.
  4540. const messagesSection = addSection("messages");
  4541.  
  4542. const fullMessages = addCheckbox(messagesSection, "fullMessages", "fullMessagesDesc");
  4543. fullMessages.checked = cfg.getFullMessages();
  4544. fullMessages.addEventListener("change", e => cfg.setFullMessages(e.target.checked));
  4545.  
  4546. const typingNotifications = addCheckbox(messagesSection, "typingNotifications", "typingNotificationsDesc");
  4547. typingNotifications.checked = cfg.getTypingNotifications();
  4548. typingNotifications.addEventListener("change", e => cfg.setTypingNotifications(e.target.checked));
  4549.  
  4550. const sendEnter = addCheckbox(messagesSection, "sendEnter", "sendEnterDesc");
  4551. sendEnter.checked = cfg.getSendEnter();
  4552. sendEnter.addEventListener("change", e => cfg.setSendEnter(e.target.checked));
  4553.  
  4554. // Add hidden users section.
  4555. const hiddenUsersSection = addSection("hiddenUsers");
  4556.  
  4557. const hideMessages = addCheckbox(hiddenUsersSection, "hideMessages");
  4558. hideMessages.checked = cfg.getHideMessages();
  4559. hideMessages.addEventListener("change", e => cfg.setHideMessages(e.target.checked));
  4560.  
  4561. const hideContacts = addCheckbox(hiddenUsersSection, "hideContacts");
  4562. hideContacts.checked = cfg.getHideContacts();
  4563. hideContacts.addEventListener("change", e => cfg.setHideContacts(e.target.checked));
  4564.  
  4565. const hideVisits = addCheckbox(hiddenUsersSection, "hideVisits");
  4566. hideVisits.checked = cfg.getHideVisits();
  4567. hideVisits.addEventListener("change", e => cfg.setHideVisits(e.target.checked));
  4568.  
  4569. const hideLikes = addCheckbox(hiddenUsersSection, "hideLikes");
  4570. hideLikes.checked = cfg.getHideLikes();
  4571. hideLikes.addEventListener("change", e => cfg.setHideLikes(e.target.checked));
  4572.  
  4573. const hideFriends = addCheckbox(hiddenUsersSection, "hideFriends");
  4574. hideFriends.checked = cfg.getHideFriends();
  4575. hideFriends.addEventListener("change", e => cfg.setHideFriends(e.target.checked));
  4576.  
  4577. const hideActivities = addCheckbox(hiddenUsersSection, "hideActivities");
  4578. hideActivities.checked = cfg.getHideActivities();
  4579. hideActivities.addEventListener("change", e => cfg.setHideActivities(e.target.checked));
  4580.  
  4581. const inMinAge = addNumber(hiddenUsersSection, "minAge", 18, 99);
  4582. const inMaxAge = addNumber(hiddenUsersSection, "maxAge", 18, 99);
  4583. let minAge = cfg.getHiddenMinAge();
  4584. let maxAge = cfg.getHiddenMaxAge();
  4585. inMinAge.value = minAge;
  4586. inMaxAge.value = maxAge;
  4587. inMinAge.addEventListener("change", e =>
  4588. {
  4589. minAge = parseInt(e.target.value);
  4590. cfg.setHiddenMinAge(minAge);
  4591. if (minAge > maxAge)
  4592. {
  4593. maxAge = minAge;
  4594. cfg.setHiddenMaxAge(maxAge);
  4595. inMaxAge.val(maxAge);
  4596. }
  4597. });
  4598. inMaxAge.addEventListener("change", e =>
  4599. {
  4600. maxAge = parseInt(e.target.value);
  4601. cfg.setHiddenMaxAge(maxAge);
  4602. if (maxAge < minAge)
  4603. {
  4604. minAge = maxAge;
  4605. cfg.setHiddenMinAge(minAge);
  4606. inMinAge.val(minAge);
  4607. }
  4608. });
  4609.  
  4610. list.create(hiddenUsersSection, {
  4611. onGet: () => Array.from(cfg.getHiddenUsers()).sort(Intl.Collator().compare),
  4612. onAdd: e => cfg.setUserHidden(e, true),
  4613. onRemove: e => cfg.setUserHidden(e, false)
  4614. });
  4615. }
  4616.  
  4617. dom.on(`li.js-settings > div.accordion > ul.js-list`, el =>
  4618. {
  4619. // Add extension menu item.
  4620. const linkClass = el.querySelector("a").className;
  4621. const link = dom.add(el, `
  4622. <li>
  4623. <div>
  4624. <a class="${linkClass}" href="/me/romeoadditions">${GM_info.script.name}</a>
  4625. </div>
  4626. </li>`);
  4627. link.addEventListener("click", e =>
  4628. {
  4629. if (link.classList.contains("is-selected"))
  4630. {
  4631. link.classList.remove("is-selected");
  4632. }
  4633. else
  4634. {
  4635. link.classList.add("is-selected");
  4636. setTimeout(() => openSettingsPane()); // delayed execution to force open panel
  4637. }
  4638. });
  4639. // Deselect menu item if others are clicked.
  4640. for (const linkOther of el.querySelectorAll("li"))
  4641. if (linkOther !== link)
  4642. linkOther.addEventListener("click", e => link.classList.remove("is-selected"));
  4643. });
  4644.  
  4645. dom.on(`#offcanvas-nav > .js-layer-content > main > div.layout > div.reactView--autoHeight > p[class^="MiniText-sc-"]`, el =>
  4646. {
  4647. el.innerHTML += `<a class="marionette" style="display:block" href="${GM_info.script.downloadURL}" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
  4648. });
  4649.  
  4650. // ---- Init ----
  4651.  
  4652. initPreviews();
  4653. for (const ext of (window.romeoExts ??= []))
  4654. ext();