Sleazy Fork is available in English.
Beta: Übersichten, Galerie und Rufnummernsuche auf Ladies.de; Formularhilfe für Lusthaus und Snatchlist.
// ==UserScript==
// @name The Ladies Helper
// @namespace http://tampermonkey.net/
// @version 4.4.43
// @description Beta: Übersichten, Galerie und Rufnummernsuche auf Ladies.de; Formularhilfe für Lusthaus und Snatchlist.
// @description:en Beta: listing, gallery and phone research tools for Ladies.de, with Lusthaus and Snatchlist form autofill.
// @match https://www.ladies.de/sex-anzeigen*
// @match https://ladies.de/sex-anzeigen*
// @match https://www.ladies.de/*/sex-anzeigen*
// @match https://ladies.de/*/sex-anzeigen*
// @match https://huren-test-forum.lusthaus.cc/misc.php*
// @match https://snatchlist.com/forums/*/post-thread*
// @match https://www.snatchlist.com/forums/*/post-thread*
// @license MIT
// @grant GM_download
// @grant GM_xmlhttpRequest
// @connect intim.to
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const CONFIG = {
boxId: "ladies-helper-box",
styleId: "ladies-helper-style",
noteId: "lh-autofill-note",
profileBaseUrl: "https://www.ladies.de/Profil/",
ladiesSearchBaseUrl: "https://www.ladies.de/sex-anzeigen?suchtext=",
googleSearchBaseUrl: "https://www.google.com/search?q=",
googleLensBaseUrl: "https://lens.google.com/uploadbyurl?url=",
intimBaseUrl: "https://intim.to/de/tel/",
ladiesForumBaseUrl: "https://www.ladies-forum.de/search?searchJSON=",
snatchlistUrl:
"https://snatchlist.com/forums/escorts-huren-hobbyhuren-erotikmassagen-bdsm.2/post-thread",
lhReportBaseUrl:
"https://huren-test-forum.lusthaus.cc/misc.php?do=form&fid=1",
archiveBaseUrl: "https://archive.is/?run=1&url=",
whatsappTextTemplate: "Hallo {name}, was sind deine Preise?",
};
const LIMITS = {
noteAutoCloseMs: 8000,
};
// https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/MobileDienste/zugeteilteRNB/start.html
const MOBILE_PREFIXES = [
"015019",
"015020",
"01511",
"01512",
"01514",
"01515",
"01516",
"01517",
"015180",
"015181",
"015182",
"015183",
"015184",
"015185",
"015186",
"015187",
"015188",
"015189",
"01520",
"01521",
"01522",
"01523",
"01525",
"01526",
"01529",
"015310",
"015333",
"015410",
"015510",
"015511",
"015550",
"015551",
"015552",
"015553",
"015554",
"015555",
"015556",
"015557",
"015558",
"015559",
"015560",
"015561",
"015562",
"015563",
"015564",
"015565",
"015566",
"015567",
"015568",
"015569",
"015591",
"015630",
"015636",
"015678",
"015679",
"015700",
"015701",
"015702",
"015703",
"015704",
"015706",
"01573",
"01575",
"01577",
"01578",
"01579",
"015888",
"01590",
"0160",
"0162",
"0163",
"0170",
"0171",
"0172",
"0173",
"0174",
"0175",
"0176",
"0177",
"0178",
"0179",
].sort((a, b) => b.length - a.length);
const COUNTRY_CODES = {
1: "USA/Kanada",
7: "Russland/Kasachstan",
20: "Ägypten",
27: "Südafrika",
30: "Griechenland",
31: "Niederlande",
32: "Belgien",
33: "Frankreich",
34: "Spanien",
36: "Ungarn",
39: "Italien",
40: "Rumänien",
41: "Schweiz",
43: "Österreich",
44: "Großbritannien",
45: "Dänemark",
46: "Schweden",
47: "Norwegen",
48: "Polen",
49: "Deutschland",
51: "Peru",
52: "Mexiko",
53: "Kuba",
54: "Argentinien",
55: "Brasilien",
56: "Chile",
57: "Kolumbien",
58: "Venezuela",
60: "Malaysia",
61: "Australien",
62: "Indonesien",
63: "Philippinen",
64: "Neuseeland",
65: "Singapur",
66: "Thailand",
81: "Japan",
82: "Südkorea",
84: "Vietnam",
86: "China",
90: "Türkei",
91: "Indien",
92: "Pakistan",
93: "Afghanistan",
94: "Sri Lanka",
95: "Myanmar",
98: "Iran",
211: "Südsudan",
212: "Marokko",
213: "Algerien",
216: "Tunesien",
218: "Libyen",
220: "Gambia",
221: "Senegal",
222: "Mauretanien",
223: "Mali",
224: "Guinea",
225: "Elfenbeinküste",
226: "Burkina Faso",
227: "Niger",
228: "Togo",
229: "Benin",
230: "Mauritius",
231: "Liberia",
232: "Sierra Leone",
233: "Ghana",
234: "Nigeria",
235: "Tschad",
236: "Zentralafrikanische Republik",
237: "Kamerun",
238: "Kap Verde",
239: "São Tomé und Príncipe",
240: "Äquatorialguinea",
241: "Gabun",
242: "Republik Kongo",
243: "DR Kongo",
244: "Angola",
245: "Guinea-Bissau",
248: "Seychellen",
249: "Sudan",
250: "Ruanda",
251: "Äthiopien",
252: "Somalia",
253: "Dschibuti",
254: "Kenia",
255: "Tansania",
256: "Uganda",
257: "Burundi",
258: "Mosambik",
260: "Sambia",
261: "Madagaskar",
262: "Réunion/Mayotte",
263: "Simbabwe",
264: "Namibia",
265: "Malawi",
266: "Lesotho",
267: "Botswana",
268: "Eswatini",
269: "Komoren",
297: "Aruba",
298: "Färöer",
299: "Grönland",
350: "Gibraltar",
351: "Portugal",
352: "Luxemburg",
353: "Irland",
354: "Island",
355: "Albanien",
356: "Malta",
357: "Zypern",
358: "Finnland",
359: "Bulgarien",
370: "Litauen",
371: "Lettland",
372: "Estland",
373: "Moldau",
374: "Armenien",
375: "Belarus",
376: "Andorra",
377: "Monaco",
378: "San Marino",
380: "Ukraine",
381: "Serbien",
382: "Montenegro",
383: "Kosovo",
385: "Kroatien",
386: "Slowenien",
387: "Bosnien und Herzegowina",
389: "Nordmazedonien",
420: "Tschechien",
421: "Slowakei",
423: "Liechtenstein",
500: "Falklandinseln",
501: "Belize",
502: "Guatemala",
503: "El Salvador",
504: "Honduras",
505: "Nicaragua",
506: "Costa Rica",
507: "Panama",
508: "Saint-Pierre und Miquelon",
509: "Haiti",
590: "Guadeloupe/Saint-Martin",
591: "Bolivien",
592: "Guyana",
593: "Ecuador",
594: "Französisch-Guayana",
595: "Paraguay",
596: "Martinique",
597: "Suriname",
598: "Uruguay",
599: "Karibische Niederlande/Curaçao",
670: "Osttimor",
673: "Brunei",
674: "Nauru",
675: "Papua-Neuguinea",
676: "Tonga",
677: "Salomonen",
678: "Vanuatu",
679: "Fidschi",
680: "Palau",
681: "Wallis und Futuna",
682: "Cookinseln",
683: "Niue",
685: "Samoa",
686: "Kiribati",
687: "Neukaledonien",
688: "Tuvalu",
689: "Französisch-Polynesien",
850: "Nordkorea",
852: "Hongkong",
853: "Macau",
855: "Kambodscha",
856: "Laos",
880: "Bangladesch",
886: "Taiwan",
960: "Malediven",
961: "Libanon",
962: "Jordanien",
963: "Syrien",
964: "Irak",
965: "Kuwait",
966: "Saudi-Arabien",
967: "Jemen",
968: "Oman",
970: "Palästina",
971: "Vereinigte Arabische Emirate",
972: "Israel",
973: "Bahrain",
974: "Katar",
975: "Bhutan",
976: "Mongolei",
977: "Nepal",
992: "Tadschikistan",
993: "Turkmenistan",
994: "Aserbaidschan",
995: "Georgien",
996: "Kirgisistan",
998: "Usbekistan",
};
const STATE_BY_CITY = {
mannheim: "2",
heidelberg: "2",
karlsruhe: "2",
stuttgart: "2",
freiburg: "2",
ulm: "2",
heilbronn: "2",
muenchen: "3",
munich: "3",
nuernberg: "3",
augsburg: "3",
regensburg: "3",
wuerzburg: "3",
berlin: "4",
potsdam: "5",
bremen: "6",
hamburg: "7",
frankfurt: "8",
wiesbaden: "8",
kassel: "8",
darmstadt: "8",
rostock: "9",
schwerin: "9",
hannover: "10",
braunschweig: "10",
osnabrueck: "10",
goettingen: "10",
koeln: "11",
duesseldorf: "11",
dortmund: "11",
essen: "11",
duisburg: "11",
bochum: "11",
bonn: "11",
muenster: "11",
mainz: "12",
ludwigshafen: "12",
koblenz: "12",
trier: "12",
saarbruecken: "13",
dresden: "14",
leipzig: "14",
chemnitz: "14",
magdeburg: "15",
halle: "15",
kiel: "16",
luebeck: "16",
erfurt: "17",
jena: "17",
wien: "20",
salzburg: "23",
innsbruck: "24",
graz: "27",
zuerich: "35",
bern: "36",
basel: "37",
};
function getVisibleGalleryCloseControl() {
const controls = Array.from(
document.querySelectorAll('.icon-modal-close[data-dismiss="modal"]'),
);
const control = controls.reverse().find((element) => {
const style = window.getComputedStyle(element);
return (
element.getClientRects().length > 0 &&
style.display !== "none" &&
style.visibility !== "hidden" &&
style.visibility !== "collapse"
);
});
return control;
}
function closeVisibleGallery() {
// Let the site clean up its backdrop and gallery state.
getVisibleGalleryCloseControl()?.click();
}
function hideLiveCamCards() {
document
.querySelectorAll('.ladies-card a.card-link[href*="/directre/"]')
.forEach((link) => {
link.closest(".ladies-card")?.remove();
});
}
const FORM_SELECTORS = {
name: "#q_1",
profile: "#q_4",
whatsapp: "#q_87",
phone: "#q_6",
city: "#q_9",
state: 'select[name="10"]',
country: 'select[name="11"]',
shortdesc: "#a_12",
report: "#vB_Editor_001_textarea",
wysiwygMode: "#vB_Editor_001_mode",
};
function escapeHtml(value) {
return String(value ?? "").replace(
/[&<>"']/g,
(char) =>
({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[char],
);
}
function normalizeText(value) {
return String(value || "")
.replace(/\u00a0/g, " ")
.replace(/[ \t]+/g, " ")
.replace(/[ \t]*\n[ \t]*/g, "\n")
.replace(/\n{4,}/g, "\n\n\n")
.trim();
}
function normalizeKey(value) {
return String(value || "")
.toLowerCase()
.trim()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, " ")
.trim();
}
function decodeSlug(value) {
return String(value || "")
.replace(/-/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())
.trim();
}
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
function dispatchFieldEvents(element) {
if (!element) return;
["input", "change", "blur"].forEach((type) => {
element.dispatchEvent(new Event(type, { bubbles: true }));
});
}
function setInputValue(selector, value) {
const element = document.querySelector(selector);
if (!element || value == null || value === "") return false;
element.value = value;
dispatchFieldEvents(element);
return true;
}
function setSelectValue(selector, value) {
const element = document.querySelector(selector);
if (!element || value == null || value === "") return false;
const wantedValue = String(value);
const optionExists = [...element.options].some(
(option) => option.value === wantedValue,
);
if (!optionExists) return false;
element.value = wantedValue;
dispatchFieldEvents(element);
return true;
}
const leanGalleryAds = new Map();
const leanGalleryRoots = new WeakSet();
let leanGalleryFrame = null;
function scheduleLeanGallerySync() {
if (!document.documentElement.classList.contains("lh-lean") && !leanGalleryAds.size) return;
if (leanGalleryFrame !== null) return;
leanGalleryFrame = requestAnimationFrame(() => {
leanGalleryFrame = null;
syncLeanGallerySlides();
});
}
function galleryAncestor(element, selector) {
while (element) {
const match = element.closest?.(selector);
if (match) return match;
element = element.getRootNode?.().host;
}
return null;
}
function syncLeanGallerySlides() {
if (!document.documentElement.classList.contains("lh-lean") && !leanGalleryAds.size) return;
if (!getAdvertId() && !leanGalleryAds.size) return;
const sliders = new Set();
const roots = [document];
for (let index = 0; index < roots.length; index++) {
const root = roots[index];
if (!leanGalleryRoots.has(root)) {
leanGalleryRoots.add(root);
const observer = new MutationObserver(scheduleLeanGallerySync);
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["data-advert-slide"],
});
}
root.querySelectorAll("*").forEach((element) => {
if (element.shadowRoot) roots.push(element.shadowRoot);
});
}
if (document.documentElement.classList.contains("lh-lean")) {
roots.forEach((root) =>
root
.querySelectorAll(
'[data-advert-slide], .solads-slide, iframe.sol-frame[aria-label="SolAds"], iframe[src="/api/plutos"]',
)
.forEach((ad) => {
const slide = galleryAncestor(ad, "swiper-slide, .swiper-slide");
if (!slide || leanGalleryAds.has(slide)) return;
const slider = galleryAncestor(slide, "swiper-container, .swiper");
const marker = document.createComment("ladies-helper-ad-slide");
slide.before(marker);
leanGalleryAds.set(slide, marker);
slide.remove();
if (slider) sliders.add(slider);
}),
);
for (const [slide, marker] of leanGalleryAds) {
if (!marker.isConnected) leanGalleryAds.delete(slide);
}
} else {
for (const [slide, marker] of leanGalleryAds) {
if (marker.isConnected) {
const slider = galleryAncestor(
marker.parentElement,
"swiper-container, .swiper",
);
marker.replaceWith(slide);
if (slider) sliders.add(slider);
}
}
leanGalleryAds.clear();
}
sliders.forEach((slider) => {
const swiper = slider.swiper;
if (swiper && !swiper.destroyed) {
swiper.update();
const index = Math.min(
swiper.activeIndex || 0,
Math.max(0, swiper.slides.length - 1),
);
swiper.slideTo(index, 0, false);
}
});
}
function readLeanPreference() {
try {
return localStorage.getItem("ladies-helper-lean") === "true";
} catch {
return false;
}
}
function setLeanMode(enabled) {
document.documentElement.classList.toggle("lh-lean", enabled);
syncLeanGallerySlides();
try {
localStorage.setItem("ladies-helper-lean", String(enabled));
} catch {
/* The toggle still works when browser storage is unavailable. */
}
}
function initLeanMode() {
const style = document.createElement("style");
style.id = "ladies-helper-lean-style";
// CSS also applies to sections inserted later. Removing the class restores
// their original display rules without deleting or recreating page content.
style.textContent = `
html.lh-lean swiper-slide:has([data-advert-slide]),
html.lh-lean swiper-slide:has(.solads-slide),
html.lh-lean .swiper-slide:has([data-advert-slide]),
html.lh-lean .swiper-slide:has(.solads-slide),
html.lh-lean .container.relative.main-navigation,
html.lh-lean nav.navbar.navbar-lds,
html.lh-lean ul.nav.navbar-nav,
html.lh-lean .options-container.top.padded-bottom,
html.lh-lean .card-list > h1:has(.title-adverts-count),
html.lh-lean .card-list > h3,
html.lh-lean div.col-xs-12.untergruppe.text-left,
html.lh-lean #advert-results-found-info,
html.lh-lean footer.hidden-print,
html.lh-lean #azm-stars-promotional-content,
html.lh-lean .page-stripe-azm-stars-promotional-content,
html.lh-lean .azm-stars-promotional-content,
html.lh-lean .container:has(> .row-azm-stars-promotional-content) {
display: none !important;
}
`;
document.head.appendChild(style);
document.documentElement.classList.toggle("lh-lean", readLeanPreference());
syncLeanGallerySlides();
// Newly attached shadow roots do not produce document mutations.
setInterval(() => {
if (document.documentElement.classList.contains("lh-lean"))
scheduleLeanGallerySync();
}, 1000);
}
function addLeanToggle(box) {
const content = box.querySelector(".lh-content");
if (!content) return;
const row = document.createElement("div");
row.className = "lh-section";
const label = document.createElement("label");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "lh-lean-toggle";
checkbox.checked = readLeanPreference();
checkbox.addEventListener("change", () => setLeanMode(checkbox.checked));
label.title = "Menü, Werbung (auch in Galerien) und Seitenfuß ausblenden";
label.append(checkbox, " Schlank");
const loadControls = content
.querySelector(".lh-load-restart")
?.closest(".lh-mini-buttons");
if (loadControls) {
loadControls.style.flexWrap = "wrap";
label.style.marginLeft = "auto";
label.style.whiteSpace = "nowrap";
loadControls.appendChild(label);
} else {
row.style.display = "flex";
row.style.alignItems = "center";
row.style.flexWrap = "wrap";
row.style.gap = "6px 10px";
row.appendChild(label);
const ratingControls = box.querySelector(".lh-intim-controls");
if (ratingControls) row.appendChild(ratingControls);
content.prepend(row);
}
}
function addStyle() {
if (document.getElementById(CONFIG.styleId)) return;
const style = document.createElement("style");
style.id = CONFIG.styleId;
style.textContent = `
#${CONFIG.boxId} {
position: fixed;
top: 16px;
right: 16px;
z-index: 99999999;
width: 460px;
max-width: calc(100vw - 32px);
background: #fff;
color: #000;
border: 2px solid #d00000;
border-radius: 8px;
padding: 9px 10px;
font: 13px/1.35 Arial, sans-serif;
box-shadow: 0 3px 14px rgba(0,0,0,.3);
word-break: break-word;
}
#${CONFIG.boxId} .lh-header {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
}
#${CONFIG.boxId} .lh-header-text {
flex: 1;
min-width: 0;
line-height: 1.3;
}
#${CONFIG.boxId} .lh-number-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 14px;
row-gap: 3px;
margin-top: 3px;
}
#${CONFIG.boxId} .lh-number-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
min-width: 0;
}
@media (max-width: 480px) {
#${CONFIG.boxId} .lh-number-grid {
grid-template-columns: minmax(0, 1fr);
}
}
#${CONFIG.boxId} .lh-number-text {
font-family: monospace;
white-space: nowrap;
}
#${CONFIG.boxId} .lh-number-links {
display: flex;
gap: 4px;
align-items: center;
}
#${CONFIG.boxId} .lh-number-links a {
text-decoration: none;
font-weight: 700;
white-space: nowrap;
}
#${CONFIG.boxId} .lh-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
#${CONFIG.boxId} .lh-label {
font-weight: 700;
}
#${CONFIG.boxId} .lh-content,
#${CONFIG.boxId} .lh-section {
margin-top: 6px;
}
#${CONFIG.boxId} .lh-formats {
user-select: text;
cursor: text;
}
#${CONFIG.boxId} a {
color: #0645ad;
text-decoration: underline;
}
#${CONFIG.boxId} .lh-links {
line-height: 1.55;
}
#${CONFIG.boxId} .lh-separator {
color: #777;
margin: 0 3px;
}
#${CONFIG.boxId} .lh-mini-buttons {
margin-top: 5px;
display: flex;
gap: 5px;
align-items: center;
}
#${CONFIG.boxId} .lh-copy-button,
#${CONFIG.boxId} .lh-help,
#${CONFIG.boxId} .lh-minimize,
#${CONFIG.boxId} .lh-close {
cursor: pointer;
border: 1px solid #999;
border-radius: 4px;
background: #f4f4f4;
color: #000;
font-weight: 700;
line-height: 1;
}
#${CONFIG.boxId} .lh-copy-button {
padding: 3px 6px;
font-size: 11px;
}
#${CONFIG.boxId} .lh-copy-button.lh-copied {
background: #e8ffe8;
border-color: #4a9b4a;
}
#${CONFIG.boxId} .lh-help,
#${CONFIG.boxId} .lh-minimize,
#${CONFIG.boxId} .lh-close {
width: 22px;
height: 21px;
padding: 0;
}
#${CONFIG.boxId} .lh-help {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
text-decoration: none;
}
#${CONFIG.boxId} .lh-close {
font-size: 15px;
}
#${CONFIG.boxId}.lh-collapsed {
width: 460px;
}
#${CONFIG.boxId}.lh-collapsed .lh-content {
display: none;
}
#${CONFIG.boxId} .lh-warning {
color: #b00000;
font-weight: 700;
}
#${CONFIG.noteId} {
position: fixed;
top: 16px;
right: 16px;
z-index: 99999999;
background: #fff;
color: #000;
border: 2px solid #2d7a2d;
border-radius: 8px;
padding: 8px 10px;
font: 13px Arial, sans-serif;
box-shadow: 0 3px 14px rgba(0,0,0,.25);
}
`;
document.head.appendChild(style);
}
async function downloadAllAdvertPictures(imageLinks, control) {
if (control.getAttribute("aria-disabled") === "true") return;
if (typeof GM_download !== "function") {
control.textContent = "Download nicht verfügbar – Skript aktualisieren und Seite neu laden";
return;
}
control.setAttribute("aria-disabled", "true");
let downloaded = 0;
let failed = 0;
try {
for (const [index, image] of imageLinks.entries()) {
control.textContent = `Download ${index + 1} / ${imageLinks.length} …`;
try {
await new Promise((resolve, reject) => {
GM_download({
url: image.imageUrl,
name: image.fileName || `ladies-bild-${index + 1}.jpg`,
saveAs: false,
timeout: 60000,
onload: resolve,
onerror: reject,
ontimeout: () => reject(new Error("Download-Zeitlimit überschritten")),
});
});
downloaded++;
} catch (error) {
failed++;
console.warn("Ladies Helper: Bild-Download fehlgeschlagen", error);
}
}
control.textContent = failed
? `${downloaded} gespeichert, ${failed} fehlgeschlagen – Download-Berechtigungen in Tampermonkey prüfen`
: `${downloaded} Bilder gespeichert – erneut herunterladen`;
} finally {
control.removeAttribute("aria-disabled");
}
}
function getAdvertId() {
const match = window.location.pathname.match(/-(\d+)(?:\/)?$/);
return match ? match[1] : null;
}
function getProfileId() {
const element =
document.querySelector("[data-profile-id]") ||
document.querySelector("div.advert-actions.hidden-print");
return (
element?.getAttribute("data-profile-id") ||
element?.dataset?.profileId ||
null
);
}
function buildSingleLadiesForumUrl(number) {
const searchJson = {
keywords: `"${number}"`,
title_only: 0,
};
return (
CONFIG.ladiesForumBaseUrl + encodeURIComponent(JSON.stringify(searchJson))
);
}
function getRawPhone() {
const telephoneElement = document.querySelector('[itemprop="telephone"]');
if (telephoneElement?.textContent?.trim()) {
return telephoneElement.textContent.trim();
}
const text = document.body?.innerText || "";
const match =
text.match(/(?:\+\d{1,4}|00\d{1,4}|0)\s*[\d][\d\s\-\/().]{5,22}\d/) ||
text.match(/\b\d{7,18}\b/);
return match ? match[0].trim() : null;
}
function getCountryCodeInfoFromDigits(digits) {
const cleanDigits = String(digits || "").replace(/\D/g, "");
for (let length = 3; length >= 1; length -= 1) {
const code = cleanDigits.slice(0, length);
if (COUNTRY_CODES[code]) {
return {
code,
prefix: `+${code}`,
country: COUNTRY_CODES[code],
};
}
}
return null;
}
function normalizeGermanMobile(rawPhone) {
if (!rawPhone) return null;
const digits = String(rawPhone)
.trim()
.replace(/[^\d+]/g, "")
.replace(/^(\+49)/, "0")
.replace(/^(0049)/, "0")
.replace(/^(49)(?=1[5-7])/, "0")
.replace(/\D/g, "");
if (!digits.startsWith("0")) return null;
const prefix = MOBILE_PREFIXES.find((value) => digits.startsWith(value));
if (!prefix) return null;
const rest = digits.slice(prefix.length);
if (rest.length < 5) return null;
const plain = `${prefix}${rest}`;
const main = `${prefix}-${rest}`;
const slash = `${prefix}/${rest}`;
let altMain = "";
let altSlash = "";
if (prefix.length === 5) {
altMain = `${prefix.slice(0, 4)}-${prefix.slice(4)}${rest}`;
altSlash = `${prefix.slice(0, 4)}/${prefix.slice(4)}${rest}`;
}
const plus49 = `+49${plain.slice(1)}`;
const international0049 = `0049${plain.slice(1)}`;
const whatsappNumber = plus49.replace(/[^\d]/g, "");
return {
raw: rawPhone,
main,
plain,
slash,
plus49,
international0049,
whatsappNumber,
all: unique([
main,
altMain,
plain,
slash,
altSlash,
plus49,
international0049,
]),
type: "de_mobile",
isGermanMobile: true,
isGermanLandline: false,
isInternational: true,
countryCodeInfo: {
code: "49",
prefix: "+49",
country: "Deutschland",
},
displayWarning: "",
};
}
function isLikelyGermanLandlineDigits(digits) {
const cleanDigits = String(digits || "").replace(/\D/g, "");
if (!/^0[2-9]\d{5,11}$/.test(cleanDigits)) return false;
if (/^01/.test(cleanDigits)) return false;
if (/^032/.test(cleanDigits)) return false;
if (/^0700/.test(cleanDigits)) return false;
if (/^0800/.test(cleanDigits)) return false;
if (/^0900/.test(cleanDigits)) return false;
return true;
}
function splitGermanLandline(digits) {
const cleanDigits = String(digits || "").replace(/\D/g, "");
if (
cleanDigits.startsWith("030") ||
cleanDigits.startsWith("040") ||
cleanDigits.startsWith("069") ||
cleanDigits.startsWith("089")
) {
return {
areaCode: cleanDigits.slice(0, 3),
subscriber: cleanDigits.slice(3),
};
}
return {
areaCode: cleanDigits.slice(0, 4),
subscriber: cleanDigits.slice(4),
};
}
function normalizeGermanLandline(rawPhone) {
if (!rawPhone) return null;
const raw = normalizeText(rawPhone);
const compact = raw
.replace(/[^\d+]/g, "")
.replace(/^(\+49)/, "0")
.replace(/^(0049)/, "0")
.replace(/^(49)(?=[2-9])/, "0");
const digits = compact.replace(/\D/g, "");
if (!isLikelyGermanLandlineDigits(digits)) return null;
const { areaCode, subscriber } = splitGermanLandline(digits);
if (!areaCode || subscriber.length < 3) return null;
const nationalWithoutZero = digits.slice(1);
const plus49 = `+49${nationalWithoutZero}`;
const international0049 = `0049${nationalWithoutZero}`;
const whatsappNumber = `49${nationalWithoutZero}`;
const main = `${areaCode}-${subscriber}`;
const slash = `${areaCode}/${subscriber}`;
return {
raw,
main,
plain: digits,
slash,
plus49,
international0049,
whatsappNumber,
all: unique([main, digits, slash, plus49, international0049]),
type: "de_landline",
isGermanMobile: false,
isGermanLandline: true,
isInternational: true,
countryCodeInfo: {
code: "49",
prefix: "+49",
country: "Deutschland",
},
displayWarning: "",
};
}
function normalizeAnyPhone(rawPhone) {
if (!rawPhone) return null;
const germanMobile = normalizeGermanMobile(rawPhone);
if (germanMobile) return germanMobile;
const germanLandline = normalizeGermanLandline(rawPhone);
if (germanLandline) return germanLandline;
const raw = normalizeText(rawPhone);
const compact = raw.replace(/[^\d+]/g, "");
if (!compact) return null;
const digits = compact.replace(/\D/g, "");
let plus = "";
let international00 = "";
let main = raw;
let whatsappNumber = digits;
let countryCodeInfo = null;
if (compact.startsWith("+")) {
plus = `+${digits}`;
international00 = `00${digits}`;
main = plus;
whatsappNumber = digits;
countryCodeInfo = getCountryCodeInfoFromDigits(digits);
} else if (compact.startsWith("00")) {
const without00 = digits.replace(/^00/, "");
plus = `+${without00}`;
international00 = `00${without00}`;
main = plus;
whatsappNumber = without00;
countryCodeInfo = getCountryCodeInfoFromDigits(without00);
} else if (digits.startsWith("49") && digits.length >= 8) {
plus = `+${digits}`;
international00 = `00${digits}`;
main = plus;
whatsappNumber = digits;
countryCodeInfo = getCountryCodeInfoFromDigits(digits);
}
const grouped = plus ? plus.replace(/^(\+\d{1,4})(\d+)$/, "$1-$2") : raw;
const countryHint = countryCodeInfo
? ` Ländervorwahl erkannt: ${countryCodeInfo.prefix} ${countryCodeInfo.country}.`
: "";
return {
raw,
main: grouped || main,
plain: digits,
slash: raw,
plus49: plus,
international0049: international00,
whatsappNumber,
all: unique([grouped, plus, international00, digits, raw]),
type: "international_or_unknown",
isGermanMobile: false,
isGermanLandline: false,
isInternational: Boolean(plus),
countryCodeInfo,
displayWarning: `Rufnummer gefunden, aber nicht als deutsche Mobil- oder Festnetznummer erkannt.${countryHint}`,
};
}
function getTitleFromPage() {
const h1Text = document.querySelector("h1")?.textContent?.trim();
if (h1Text) return h1Text.replace(/\s+/g, " ");
return String(document.title || "")
.replace(/\s*[-|].*$/g, "")
.replace(/\s+/g, " ")
.trim();
}
function getLastUrlPartWithoutAdvertId() {
const parts = window.location.pathname.split("/").filter(Boolean);
const lastPart = parts[parts.length - 1] || "";
return lastPart.replace(/-\d+$/, "");
}
function getCityFromUrl() {
const firstToken = getLastUrlPartWithoutAdvertId().split("-")[0] || "";
return decodeSlug(firstToken);
}
function getArtistNameFromPage() {
const selectors = [
"h2.page-heading.kuenstlername",
".page-heading.kuenstlername",
".kuenstlername",
'[class*="kuenstlername"]',
];
for (const selector of selectors) {
const text = normalizeText(document.querySelector(selector)?.textContent);
if (text) return text;
}
return "";
}
function getNameFromUrl() {
const artistName = getArtistNameFromPage();
if (artistName) return artistName;
const tokens = getLastUrlPartWithoutAdvertId().split("-").filter(Boolean);
if (tokens.length <= 1) return getTitleFromPage();
return decodeSlug(tokens.slice(1).join("-")) || getTitleFromPage();
}
function getStateValueForCity(city) {
const key = normalizeKey(city).replace(/\s+/g, "");
return STATE_BY_CITY[key] || "0";
}
function getCountryValueForCity(city) {
const state = getStateValueForCity(city);
if (["20", "21", "22", "23", "24", "25", "26", "27", "28"].includes(state))
return "2";
if (["31", "32"].includes(state)) return "4";
if (["35", "36", "37", "38", "39", "40", "41", "42", "43"].includes(state))
return "3";
return "1";
}
function getAdvertTextWithoutPs() {
const container =
document.querySelector(".row_anzeigentext .einzelansicht-text") ||
document.querySelector(".einzelansicht-text") ||
document.querySelector(".row_anzeigentext");
if (!container) return "";
const clone = container.cloneNode(true);
clone
.querySelectorAll(".text-trans, script, style")
.forEach((element) => element.remove());
clone.querySelectorAll("div").forEach((div) => {
const text = normalizeText(div.textContent);
if (
/^PS\s*:/i.test(text) ||
/Bitte erwähne/i.test(text) ||
/Anzeige von/i.test(text)
) {
div.remove();
}
});
return normalizeText(clone.textContent)
.replace(/PS\s*:\s*Bitte erwähne[\s\S]*$/i, "")
.replace(/Bitte erwähne[\s\S]*$/i, "")
.trim();
}
function getTextWithoutChildren(element, childSelector) {
if (!element) return "";
const clone = element.cloneNode(true);
clone.querySelectorAll(childSelector).forEach((child) => child.remove());
return normalizeText(clone.textContent);
}
function getAttributeTextFromItem(item, label) {
const valueContainer =
item.querySelector(".col-xs-7") ||
item.querySelector(".attribute-value") ||
item.querySelector('[class*="value"]');
const directValue =
normalizeText(valueContainer?.querySelector("span")?.textContent) ||
normalizeText(valueContainer?.querySelector("p")?.textContent) ||
getTextWithoutChildren(valueContainer, "strong, label");
if (directValue) return directValue;
const itemText = normalizeText(item.textContent);
if (!itemText) return "";
return label ? normalizeText(itemText.replace(label, "")) : itemText;
}
function getAttributeListFromPage() {
const values = [];
const seen = new Set();
function addAttribute(label, value) {
const cleanLabel = normalizeText(label).replace(/\s*:\s*$/g, "");
const cleanValue = normalizeText(value).replace(/\s*:\s*/g, ": ");
if (!cleanLabel || !cleanValue) return;
if (/Alle Angaben ohne Gewähr/i.test(`${cleanLabel} ${cleanValue}`))
return;
if (
/Ganzen Text|Drucken|Kontakt|Telefon|WhatsApp|Anzeige melden/i.test(
`${cleanLabel} ${cleanValue}`,
)
)
return;
const line = `${cleanLabel}: ${cleanValue}`;
const key = normalizeKey(line);
if (seen.has(key)) return;
seen.add(key);
values.push(line);
}
document
.querySelectorAll(
".einzelansicht-attributes .attribute-items, .trait-attributes .attribute-items",
)
.forEach((item) => {
const label =
normalizeText(item.querySelector(".hidden-md strong")?.textContent) ||
normalizeText(item.querySelector("strong")?.textContent) ||
normalizeText(item.querySelector("label")?.textContent);
const value = getAttributeTextFromItem(item, label);
addAttribute(label, value);
});
[
".row_attribute li",
".row_attributes li",
".row_merkmale li",
".row_ausstattung li",
".advert-attributes li",
".profile-attributes li",
".attributes li",
".merkmale li",
".attribute-list li",
'[class*="attribute"] li',
'[class*="merkmal"] li',
'[class*="ausstattung"] li',
].forEach((selector) => {
document.querySelectorAll(selector).forEach((element) => {
const label =
normalizeText(element.querySelector("strong")?.textContent) ||
normalizeText(element.querySelector("label")?.textContent);
let value = "";
if (label) {
value =
normalizeText(element.querySelector("span")?.textContent) ||
getTextWithoutChildren(element, "strong, label");
}
if (label && value) {
addAttribute(label, value);
return;
}
const text = normalizeText(element.textContent);
if (!text || text.length > 120) return;
const parts = text.split(/\s*:\s*/);
if (parts.length >= 2) {
addAttribute(parts.shift(), parts.join(": "));
}
});
});
return values;
}
function formatAttributesForBbCode(attributes) {
if (!attributes.length) return "";
return [
"[B]Attribute:[/B]",
...attributes.map((value) => `- ${value}`),
].join("\n");
}
function getAbsoluteUrl(value) {
if (!value) return null;
const cleanValue = String(value).trim();
if (
!cleanValue ||
cleanValue.startsWith("data:") ||
cleanValue.startsWith("blob:") ||
cleanValue.startsWith("javascript:")
) {
return null;
}
try {
return new URL(cleanValue, window.location.href).href;
} catch {
return null;
}
}
function getImageFileName(url) {
try {
const parsed = new URL(url, window.location.href);
return decodeURIComponent(parsed.pathname.split("/").pop() || "");
} catch {
return "";
}
}
function extractUrlsFromSrcset(srcset) {
if (!srcset) return [];
return String(srcset)
.split(",")
.map((part) => part.trim().split(/\s+/)[0])
.filter(Boolean);
}
function extractBackgroundImageUrls(styleValue) {
if (!styleValue) return [];
const urls = [];
const regex = /url\(["']?([^"')]+)["']?\)/gi;
let match;
while ((match = regex.exec(styleValue)) !== null) {
urls.push(match[1]);
}
return urls;
}
function collectPageImageUrls() {
const rawUrls = [];
document.querySelectorAll("img").forEach((img) => {
rawUrls.push(
img.getAttribute("src"),
img.getAttribute("data-src"),
img.getAttribute("data-original"),
img.getAttribute("data-lazy-src"),
img.getAttribute("data-full"),
img.getAttribute("data-image"),
img.currentSrc,
);
rawUrls.push(...extractUrlsFromSrcset(img.getAttribute("srcset")));
rawUrls.push(...extractUrlsFromSrcset(img.getAttribute("data-srcset")));
});
document.querySelectorAll("a[href]").forEach((link) => {
rawUrls.push(link.getAttribute("href"));
});
document.querySelectorAll('[style*="background"]').forEach((element) => {
rawUrls.push(
...extractBackgroundImageUrls(element.getAttribute("style")),
);
});
return rawUrls
.flatMap((value) => {
const text = String(value || "");
const directUrl = getAbsoluteUrl(text);
const embeddedMatches =
text.match(
/https?:\/\/[^"'\s<>]+\.(?:jpe?g|png|webp)(?:\?[^"'\s<>]*)?/gi,
) || [];
const relativeMatches =
text.match(
/[^"'\s<>]+\/\d+-A\d+\.(?:jpe?g|png|webp)(?:\?[^"'\s<>]*)?/gi,
) || [];
return [
directUrl,
...embeddedMatches.map(getAbsoluteUrl),
...relativeMatches.map(getAbsoluteUrl),
];
})
.filter(Boolean);
}
function buildGoogleLensUrl(imageUrl) {
return CONFIG.googleLensBaseUrl + encodeURIComponent(imageUrl);
}
function getAdvertImageLinks(advertId) {
if (!advertId) return [];
const uniqueBySuffix = new Map();
const regex = new RegExp(`^${advertId}-(A\\d+)\\.(jpe?g|png|webp)$`, "i");
collectPageImageUrls().forEach((url) => {
const fileName = getImageFileName(url);
const match = fileName.match(regex);
if (!match) return;
const suffix = match[1].toUpperCase();
const suffixNumber = Number(suffix.replace("A", ""));
if (!uniqueBySuffix.has(suffix)) {
uniqueBySuffix.set(suffix, {
label: suffix,
copyLabel: `Google Lens Bild ${suffix}`,
fileName,
imageUrl: url,
url: buildGoogleLensUrl(url),
suffixNumber,
});
}
});
return [...uniqueBySuffix.values()].sort(
(a, b) => a.suffixNumber - b.suffixNumber,
);
}
function buildGooglePhoneUrl(phone) {
const query = phone.all
.filter(Boolean)
.map((number) => `"${number}"`)
.join(" OR ");
return CONFIG.googleSearchBaseUrl + encodeURIComponent(query);
}
function buildSingleGoogleUrl(number) {
return CONFIG.googleSearchBaseUrl + encodeURIComponent(`"${number}"`);
}
function buildSingleLadiesUrl(number) {
return CONFIG.ladiesSearchBaseUrl + encodeURIComponent(number);
}
function buildSingleIntimUrl(number) {
const digits = String(number).replace(/\D/g, "");
return CONFIG.intimBaseUrl + encodeURIComponent(digits) + "/";
}
function buildIntimUrl(phone) {
return CONFIG.intimBaseUrl + encodeURIComponent(phone.main) + "/";
}
function requestIntimApi(url) {
return new Promise((resolve, reject) => {
const endpoint = new URL(url);
if (endpoint.origin !== "https://intim.to") {
reject(new Error("Ungültige intim.to-Adresse"));
return;
}
endpoint.searchParams.set("format", "api");
GM_xmlhttpRequest({
method: "GET",
url: endpoint.href,
anonymous: true,
timeout: 20000,
onload: (response) => {
try {
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
let data;
try { data = JSON.parse(response.responseText); }
catch { throw new Error("Server liefert kein JSON (möglicherweise eine Schutz- oder Anmeldeseite)"); }
resolve(data);
} catch (error) { reject(error); }
},
onerror: () => reject(new Error("Netzwerkfehler")),
ontimeout: () => reject(new Error("Zeitlimit überschritten")),
});
});
}
function parseIntimProfiles(data, phone) {
if (!Array.isArray(data.profiles) || Number(data.result_total) !== data.profiles.length)
throw new Error("Unvollständige intim.to-Antwort");
const digits = phone.main.replace(/\D/g, "");
const profiles = new Map();
for (const item of data.profiles) {
const url = new URL(item.url_absolute);
const match = url.pathname.match(/^\/de\/tel\/([^/]+)\/([^/]+)\/$/);
if (url.origin !== "https://intim.to" || !match ||
match[1].replace(/\D/g, "") !== digits)
throw new Error("Unerwartetes intim.to-Profil");
url.search = "";
url.hash = "";
profiles.set(url.href, { url: url.href, name: String(item.name || match[2]) });
}
return [...profiles.values()];
}
function parseIntimRating(data, profileUrl) {
const schema = data.schema_org;
if (!schema || new URL(schema.url, "https://intim.to").href !== profileUrl || !schema.rateit_arr)
throw new Error("Bewertung nicht verfügbar");
const rating = schema.rateit_arr;
const count = Number(rating.TOTAL_RATINGS);
const average = Number(rating.AVERAGE_RATING);
if (rating.TOTAL_RATINGS == null || !Number.isInteger(count) || count < 0 ||
(count > 0 && (rating.AVERAGE_RATING == null || !Number.isFinite(average) || average < 0 || average > 5)))
throw new Error("Ungültige Bewertung");
return { count, average: count ? average : null };
}
function readIntimCache() {
try {
const cache = JSON.parse(sessionStorage.getItem("lh-intim-ratings-v1") || "{}");
return Object.fromEntries(Object.entries(cache).filter(([, entry]) =>
entry && Date.now() - entry.time < 30 * 60 * 1000 && Array.isArray(entry.results)));
} catch { return {}; }
}
function initIntimRatings(box, phone) {
const container = box.querySelector(".lh-intim-ratings");
if (!container || !phone) return;
const checkbox = box.querySelector(".lh-intim-auto");
const button = box.querySelector(".lh-intim-load");
try {
checkbox.checked = localStorage.getItem("lh-intim-auto") === "true";
} catch { checkbox.checked = false; }
let running = false;
const load = async (refresh) => {
if (running) return;
running = true;
button.disabled = true;
container.querySelector(".lh-intim-status").textContent = "Bewertungen werden geladen …";
try {
await showIntimRatings(box, phone, refresh);
} finally {
running = false;
button.disabled = false;
button.textContent = "Neu laden";
}
};
button.addEventListener("click", () => load(true));
checkbox.addEventListener("change", () => {
try { localStorage.setItem("lh-intim-auto", String(checkbox.checked)); }
catch { /* Keep the choice for this page when storage is unavailable. */ }
if (checkbox.checked) load(false);
});
if (checkbox.checked) load(false);
}
async function showIntimRatings(box, phone, refresh = false) {
const container = box.querySelector(".lh-intim-ratings");
if (!container || !phone) return;
const status = container.querySelector(".lh-intim-status");
const key = phone.main.replace(/\D/g, "");
const render = (results) => {
status.replaceChildren();
if (!results.length) status.textContent = "Kein Profil zur Nummer gefunden.";
for (const profile of results) {
const row = document.createElement("div");
const link = document.createElement("a");
link.href = profile.url;
link.textContent = profile.name;
link.target = "_blank";
link.rel = "noopener noreferrer";
const label = profile.error ? `Bewertung nicht abrufbar: ${profile.error}` : profile.count
? `★ ${profile.average.toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 2 })} / 5 · ${profile.count} ${profile.count === 1 ? "Bewertung" : "Bewertungen"}`
: "Keine Bewertung";
row.append(link, document.createTextNode(` – ${label}`));
status.appendChild(row);
}
};
try {
const cached = !refresh && readIntimCache()[key];
if (cached) { render(cached.results); return; }
const profiles = parseIntimProfiles(await requestIntimApi(buildIntimUrl(phone)), phone);
const results = [];
for (const profile of profiles) {
if (!box.isConnected) return;
status.textContent = `Bewertungen laden: ${results.length + 1} / ${profiles.length} …`;
try {
results.push({ ...profile, ...parseIntimRating(await requestIntimApi(profile.url), profile.url) });
} catch (error) {
console.warn("Ladies Helper: intim.to-Bewertung", error);
results.push({ ...profile, error: error.message || "Unbekannter Fehler" });
}
}
if (!box.isConnected) return;
render(results);
if (!results.some((profile) => profile.error)) {
try {
const cache = readIntimCache();
delete cache[key];
const entries = Object.entries(cache).slice(-49);
entries.push([key, { time: Date.now(), results }]);
sessionStorage.setItem("lh-intim-ratings-v1", JSON.stringify(Object.fromEntries(entries)));
} catch { /* Lookup still works when storage is unavailable. */ }
}
} catch (error) {
status.textContent = `Abfrage fehlgeschlagen: ${error.message || "Unbekannter Fehler"}`;
console.warn("Ladies Helper: intim.to-Abfrage", error);
}
}
function buildLadiesSearchUrl(phone) {
return CONFIG.ladiesSearchBaseUrl + encodeURIComponent(phone.main);
}
function buildLadiesForumUrl(phone) {
const searchJson = {
keywords: phone.all.join(" OR "),
title_only: 0,
};
return (
CONFIG.ladiesForumBaseUrl + encodeURIComponent(JSON.stringify(searchJson))
);
}
function buildWhatsappText(name) {
const cleanName = normalizeText(name);
if (cleanName) {
return CONFIG.whatsappTextTemplate.replace("{name}", cleanName);
}
return "Hallo, was sind deine Preise?";
}
function buildWhatsappUrl(phone, name = "") {
const text = buildWhatsappText(name);
if (!phone?.whatsappNumber) {
return `https://wa.me/?text=${encodeURIComponent(text)}`;
}
return `https://wa.me/${phone.whatsappNumber}?text=${encodeURIComponent(text)}`;
}
function buildArchiveUrl(profileUrl) {
if (!profileUrl) return null;
return CONFIG.archiveBaseUrl + encodeURIComponent(profileUrl);
}
function getResearchLinks(phone) {
return [
{
label: "Google Rufnummersuche",
shortLabel: "Google",
url: buildGooglePhoneUrl(phone),
},
{ label: "intim.to", shortLabel: "intim.to", url: buildIntimUrl(phone) },
];
}
function buildReportLinksBbCodeText(profileUrl, phone) {
const lines = [];
if (phone) {
getResearchLinks(phone).forEach((link) => {
lines.push(`${link.label}: ${link.url}`);
});
}
return lines.join("\n");
}
function buildPhoneNumbersBbCodeText(phone) {
if (!phone?.all?.length) return "";
const numbers = unique(phone.all)
.map((number) => normalizeText(number))
.filter(Boolean);
if (!numbers.length) return "";
return ["[B]Rufnummern[/B]:", "- " + numbers.join("\n- ")].join("\n");
}
function buildLhReportUrl(prefillData) {
const url = new URL(CONFIG.lhReportBaseUrl);
const payload = new URLSearchParams();
Object.entries(prefillData).forEach(([key, value]) => {
if (value != null && value !== '') payload.set('lh_' + key, value);
});
// The browser retains the fragment; the forum receives only do=form&fid=1.
url.hash = payload.toString();
return url.href;
}
function createTextLink(label, url) {
return `
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">
${escapeHtml(label)}
</a>
`;
}
function createInlineLinkRow(links) {
return links
.map((link) =>
link.imageUrl
? `<a class="lh-image-link" href="${escapeHtml(link.url)}" data-preview-src="${escapeHtml(link.imageUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(link.label)}</a>`
: createTextLink(link.label, link.url),
)
.join('<span class="lh-separator">|</span>');
}
function addImageHoverPreviews(box) {
const preview = document.createElement("img");
preview.alt = "";
preview.hidden = true;
preview.setAttribute("aria-hidden", "true");
Object.assign(preview.style, {
position: "fixed",
width: "180px",
height: "220px",
objectFit: "contain",
background: "#fff",
padding: "4px",
border: "1px solid #999",
borderRadius: "6px",
boxShadow: "0 3px 14px rgba(0,0,0,.3)",
pointerEvents: "none",
zIndex: "2147483647",
maxWidth: "calc(100vw - 24px)",
maxHeight: "calc(100vh - 24px)",
});
box.appendChild(preview);
const hide = () => {
preview.hidden = true;
};
preview.addEventListener("error", hide);
box.querySelectorAll(".lh-image-link").forEach((link) => {
const show = () => {
preview.src = link.dataset.previewSrc;
preview.hidden = false;
const anchor = link.getBoundingClientRect();
const bounds = preview.getBoundingClientRect();
const left = Math.max(
8,
Math.min(anchor.left, window.innerWidth - bounds.width - 8),
);
const below = anchor.bottom + 8;
const top =
below + bounds.height <= window.innerHeight - 8
? below
: Math.max(8, anchor.top - bounds.height - 8);
preview.style.left = left + "px";
preview.style.top = top + "px";
};
link.addEventListener("mouseenter", show);
link.addEventListener("mouseleave", hide);
link.addEventListener("focus", show);
link.addEventListener("blur", hide);
link.addEventListener("click", hide);
});
// Capturing scroll also covers scrolling within the page's panels.
document.addEventListener("scroll", hide, { capture: true, passive: true });
window.addEventListener("resize", hide);
}
function getCopyLinks(profileUrl, phone) {
const links = getResearchLinks(phone).map((link) => ({
label: link.label,
url: link.url,
}));
if (profileUrl) {
links.push({
label: "Ladies Profil",
url: profileUrl,
});
}
return links;
}
function getCopyData(profileUrl, phone) {
return {
numbers: phone.all,
links: getCopyLinks(profileUrl, phone),
};
}
function buildMarkdownText(copyData) {
const lines = [];
lines.push("Rufnummern:");
copyData.numbers.forEach((number) => lines.push(number));
lines.push("");
copyData.links.forEach((link) => {
lines.push(`[${link.label}](${link.url})`);
});
return lines.join("\n");
}
function buildBbCodeText(copyData) {
const lines = [];
lines.push("[B]Rufnummern:[/B]");
copyData.numbers.forEach((number) => lines.push(number));
lines.push("");
copyData.links.forEach((link) => {
lines.push(`[url=${link.url}]${link.label}[/url]`);
});
return lines.join("\n");
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
} catch {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand("copy");
textarea.remove();
}
if (!button) return;
const oldText = button.textContent;
button.textContent = "ok";
button.classList.add("lh-copied");
setTimeout(() => {
button.textContent = oldText;
button.classList.remove("lh-copied");
}, 900);
}
function getWhatsappFieldValue(phone) {
if (!phone) return "";
if (phone.plus49 && phone.plus49.startsWith("+")) return phone.plus49;
if (phone.main && String(phone.main).startsWith("+")) return phone.main;
if (phone.whatsappNumber) return `+${phone.whatsappNumber}`;
return phone.raw || "";
}
function getPrefillData(profileUrl, phone, rawPhoneFromPage = "") {
const city = getCityFromUrl();
const name = getNameFromUrl();
const stateValue = getStateValueForCity(city);
const countryValue = getCountryValueForCity(city);
const advertText = getAdvertTextWithoutPs();
const attributes = getAttributeListFromPage();
const attributesBbCodeText = formatAttributesForBbCode(attributes);
const phoneNumbersBbCodeText = buildPhoneNumbersBbCodeText(phone);
const linksText = phone
? buildReportLinksBbCodeText(profileUrl, phone)
: "";
const whatsappFieldValue = getWhatsappFieldValue(phone);
const shortdesc = normalizeText(advertText);
const reportBlocks = [
advertText ? `[B]Anzeigentext:[/B]\n[QUOTE]${advertText}[/QUOTE]` : "",
attributesBbCodeText,
phoneNumbersBbCodeText,
`[i]Quelle: ${profileUrl}[/i]`,
linksText ? `[B]Recherchelinks:[/B]\n${linksText}` : "",
'[COLOR="Red"]Automatisch vorausgefüllt[/COLOR]',
"[B]Eigener Bericht:[/B]\n\n...",
].filter(Boolean);
const report = reportBlocks.join("\n\n");
return {
name,
profile: profileUrl || "",
whatsapp: whatsappFieldValue,
phone: rawPhoneFromPage || phone?.raw || "",
city,
state: stateValue,
country: countryValue,
source: window.location.href,
adverttext: advertText,
attributes: attributes.join(" | "),
shortdesc,
report: normalizeText(report),
};
}
function buildSnatchlistUrl(data, phone) {
const numbers = unique([data.phone, data.whatsapp, phone?.plain,
phone?.international0049, ...(phone?.all || [])]);
const numberLines = numbers.map(number => number +
' [URL=' + buildSingleGoogleUrl(number) + ']G[/URL]' +
' [URL=' + buildSingleIntimUrl(number) + ']I[/URL]');
const numberBlock = '[B]Rufnummern[/B]:\n' + numberLines.join('\n');
const researchBlock = phone ? '[B]Recherchelinks:[/B]\n' +
getResearchLinks(phone).map(link => '[URL=' + link.url + ']' + link.label + '[/URL]').join('\n') : '';
let report = data.report || '';
const numberPattern = /\[B\]Rufnummern\[\/B\]:[\s\S]*?(?=\n\n|$)/i;
if (numberPattern.test(report)) report = report.replace(numberPattern, () => numberBlock);
else if (numbers.length) report += '\n\n' + numberBlock;
const researchPattern = /\[B\]Recherchelinks:\[\/B\][\s\S]*?(?=\n\n|$)/i;
if (researchBlock) {
if (researchPattern.test(report)) report = report.replace(researchPattern, () => researchBlock);
else report += '\n\n' + researchBlock;
}
data = {...data, numbers: numbers.join('\n'), report};
const url = new URL(CONFIG.snatchlistUrl);
const payload = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (value != null && value !== '') payload.set('sl_' + key, value);
});
url.hash = payload.toString();
return url.href;
}
function snatchlistMessageHtml(report) {
return '<p>' + escapeHtml(report)
.replace(/\[b\]/gi, '<strong>').replace(/\[\/b\]/gi, '</strong>')
.replace(/\[i\]/gi, '<em>').replace(/\[\/i\]/gi, '</em>')
.replace(/\[quote\]/gi, '<blockquote>').replace(/\[\/quote\]/gi, '</blockquote>')
.replace(/\[color(?:=[^\]]*)?\]|\[\/color\]/gi, '')
.replace(/\[url=(https?:\/\/[^\]\s]+)\]([\s\S]*?)\[\/url\]/gi, '<a href="$1">$2</a>')
.replace(/\n/g, '<br>') + '</p>';
}
function setSnatchlistEditor(form, report, force = false) {
const textarea = form.querySelector('textarea.js-editor');
if (!textarea) return false;
const handler = window.XF?.getEditorInContainer?.(textarea);
const rich = form.querySelector('.fr-element[contenteditable="true"]');
const bb = handler?.ed && handler.isBbCodeView() ? handler.ed.bbCode.getTextArea() : null;
if (!rich && !bb) return false;
const current = bb ? bb.value : rich.textContent;
if (!force && String(current || '').replace(/[\s\u200B\uFEFF]/g, '')) return true;
const html = snatchlistMessageHtml(report);
if (handler?.ed && typeof handler.replaceContent === 'function') {
handler.replaceContent(html, report);
}
if (bb) {
bb.value = report;
dispatchFieldEvents(bb);
} else {
// Keep the visible editable surface and submitted textarea synchronized,
// even when the editor handler is not exposed to the userscript.
rich.innerHTML = html;
textarea.value = html;
rich.dispatchEvent(new Event('input', {bubbles: true}));
dispatchFieldEvents(textarea);
handler?.ed?.events?.trigger('contentChanged');
}
return false; // Confirm on the next pass, after editor initialization settles.
}
function addSnatchlistTags(form, data) {
const field = form.querySelector('input[name="tags"]');
const tagify = field && window.XF?.Element?.getHandler?.(field, 'token-input')?.tagify;
if (!tagify) return false;
const candidates = [data.name, data.city,
...(data.numbers || [data.phone, data.whatsapp].filter(Boolean).join('\n')).split('\n')];
const existing = new Set(tagify.value.map(tag => String(tag.value).toLocaleLowerCase()));
const pending = [];
candidates.forEach(value => {
const tag = String(value || '').trim().slice(0, 50);
const key = tag.toLocaleLowerCase();
if (tag.length < 2 || existing.has(key)) return;
existing.add(key);
pending.push(tag);
});
const available = Math.max(0, (tagify.settings.maxTags || 50) - tagify.value.length);
if (pending.length && available) {
tagify.addTags(pending.slice(0, available));
field.value = tagify.getInputValue();
dispatchFieldEvents(field);
}
return true;
}
function initSnatchlistForm() {
const params = new URLSearchParams(location.hash.slice(1));
const data = {};
params.forEach((value, key) => { if (key.startsWith('sl_')) data[key.slice(3)] = value; });
if (!data.name && !data.report) return;
const filled = new WeakSet();
const states = {
'2':'BadenWuerttemberg', '3':'Bayern', '4':'Berlin', '5':'Brandenburg',
'6':'Bremen', '7':'Hamburg', '8':'Hessen', '9':'MecklenburgVorpommern',
'10':'Niedersachsen', '11':'NordrheinWestfalen', '12':'RheinlandPfalz',
'13':'Saarland', '14':'Sachsen', '15':'SachsenAnhalt', '16':'SchleswigHolstein',
'17':'Thueringen', '20':'Wien', '23':'Salzburg', '24':'Tirol', '27':'Steiermark',
'35':'KantonZuerich', '36':'KantonBern', '37':'Basel'
};
const attributes = data.attributes || '';
const height = attributes.match(/Größe:\s*(\d+)\s*cm/i)?.[1] || '';
const bra = attributes.match(/Oberweite:\s*([^|]+)/i)?.[1]?.replace(/\s/g, '') || '';
const values = {
title: [data.name, data.city].filter(Boolean).join(' – ').slice(0, 150),
'custom_fields[name]': data.name,
'custom_fields[description]': data.adverttext,
'custom_fields[sonstige_Anzeigen]': [data.profile, data.source].filter(Boolean).join('\n'),
'custom_fields[phone]': data.numbers || [data.phone, data.whatsapp].filter(Boolean).join('\n'),
'custom_fields[BL]': states[data.state] || '',
'custom_fields[height]': height,
'custom_fields[bra_size]': bra
};
let attempts = 0;
const fill = () => {
const title = document.querySelector('textarea[name="title"]');
const form = title?.closest('form');
if (!form) return false;
const girl = form.querySelector('input[name="custom_fields[category]"][value="10"]');
if (girl && !filled.has(girl)) {
if (!form.querySelector('input[name="custom_fields[category]"]:checked')) {
girl.checked = true;
dispatchFieldEvents(girl);
}
filled.add(girl);
}
let ready = true;
const tagsField = form.querySelector('input[name="tags"]');
if (!tagsField || !filled.has(tagsField)) {
if (addSnatchlistTags(form, data)) filled.add(tagsField);
else ready = false;
}
Object.entries(values).forEach(([name, value]) => {
if (!value) return;
const field = form.querySelector('[name="' + name + '"]');
if (!field) { ready = false; return; }
if (filled.has(field)) return;
// Preserve existing drafts and edits made while the editor initializes.
if (field.value.trim()) { filled.add(field); return; }
if (field.tagName === 'SELECT' && !Array.from(field.options).some(option => option.value === value)) return;
field.value = value;
filled.add(field);
dispatchFieldEvents(field);
});
if (data.report) {
if (!form.querySelector('.lh-snatch-insert')) {
const insert = document.createElement('button');
insert.type = 'button';
insert.className = 'button lh-snatch-insert';
insert.textContent = 'Bericht einsetzen';
insert.title = 'Vorbereiteten Bericht in den Editor einsetzen';
insert.style.marginBottom = '8px';
insert.addEventListener('click', () => {
const rich = form.querySelector('.fr-element[contenteditable="true"]');
const handler = window.XF?.getEditorInContainer?.(form);
const bb = handler?.ed && handler.isBbCodeView() ? handler.ed.bbCode.getTextArea() : null;
const existing = bb ? bb.value : rich?.textContent;
if (String(existing || '').trim() && !window.confirm('Vorhandenen Editorinhalt durch den vorbereiteten Bericht ersetzen?')) return;
setSnatchlistEditor(form, data.report, true);
});
form.querySelector('textarea.js-editor')?.closest('.js-editorWrap')?.before(insert);
if (!insert.isConnected) form.querySelector('textarea.js-editor')?.before(insert);
}
if (!setSnatchlistEditor(form, data.report)) return false;
}
if (!ready) return false;
showLhAutofillNote('Snatchlist Autofill');
return true;
};
if (fill()) return;
const timer = setInterval(() => {
attempts++;
if (fill() || attempts >= 120) clearInterval(timer);
}, 500);
}
function getVisibleLinks(phone, profileUrl, prefillData) {
const links = [
...getResearchLinks(phone),
{
label: "WhatsApp",
shortLabel: "WhatsApp",
url: buildWhatsappUrl(phone, prefillData.name),
},
{
label: "Snatchlist erstellen",
shortLabel: "Snatchlist erstellen",
url: buildSnatchlistUrl(prefillData, phone),
},
{
label: "LH Bericht erstellen",
shortLabel: "LH Bericht erstellen",
url: buildLhReportUrl(prefillData),
},
];
const archiveUrl = buildArchiveUrl(profileUrl);
if (archiveUrl) {
links.push({
label: "Archiv.is",
shortLabel: "Archiv.is",
url: archiveUrl,
});
}
return links;
}
function addProfileNavigation(box) {
const row = document.createElement("div");
row.className = "lh-profile-navigation";
row.style.cssText = "display:flex;align-items:center;gap:4px";
const findSource = (selector) =>
Array.from(document.querySelectorAll(selector)).find(
(element) => !box.contains(element) && element.getAttribute("href"),
);
const entries = [
[".previous-advert", "←", "Vorherige Anzeige (Shift + ←)"],
[".backlink-url", "Übersicht", "Zurück zur Übersicht"],
[".next-advert", "→", "Nächste Anzeige (Shift + →)"],
].map(([selector, text, title]) => {
const link = document.createElement("a");
link.className = "lh-copy-button";
link.textContent = text;
link.title = title;
link.setAttribute("aria-label", title);
if (selector !== ".backlink-url")
link.setAttribute("aria-keyshortcuts", selector === ".previous-advert" ? "Shift+ArrowLeft" : "Shift+ArrowRight");
link.style.cssText =
"display:inline-flex;align-items:center;justify-content:center;text-decoration:none;padding:0 5px;height:21px;box-sizing:border-box;white-space:nowrap";
row.appendChild(link);
const refresh = () => updateLink(selector, link);
// Refresh before the site's pointer/click tracking reads navigation data.
link.addEventListener("pointerdown", refresh);
link.addEventListener("mousedown", refresh);
link.addEventListener("focus", refresh);
link.addEventListener("click", (event) => {
refresh();
if (link.getAttribute("aria-disabled") === "true") {
event.preventDefault();
return;
}
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button > 0) return;
// Use the site's actual navigation control so its own click handlers
// can resolve the next advert and preserve the search context.
const source = findSource(selector);
if (source) {
event.preventDefault();
event.stopPropagation();
source.click();
}
});
return { selector, link };
});
const setAttribute = (element, name, value) => {
if (element.getAttribute(name) !== value)
element.setAttribute(name, value);
};
const updateLink = (selector, link) => {
const source = findSource(selector);
const href = source?.getAttribute("href");
const enabled =
!!href &&
href !== "#" &&
source.getAttribute("aria-disabled") !== "true" &&
!source.closest(".disabled");
// These attributes carry previous/next search context into the next page.
const data = new Map(
Array.from(source?.attributes || [])
.filter((attribute) => attribute.name.startsWith("data-"))
.map((attribute) => [attribute.name, attribute.value]),
);
Array.from(link.attributes).forEach((attribute) => {
if (attribute.name.startsWith("data-") && !data.has(attribute.name))
link.removeAttribute(attribute.name);
});
data.forEach((value, name) => setAttribute(link, name, value));
if (enabled) setAttribute(link, "href", source.href);
else if (link.hasAttribute("href")) link.removeAttribute("href");
setAttribute(link, "aria-disabled", String(!enabled));
link.style.opacity = enabled ? "1" : "0.4";
link.style.cursor = enabled ? "pointer" : "default";
};
box.querySelector(".lh-help").before(row);
const handledKeys = new Set();
const consumeKey = (event) => {
event.preventDefault();
event.stopImmediatePropagation();
};
const onKeyDown = (event) => {
if (!box.isConnected || !event.shiftKey || !["ArrowLeft", "ArrowRight"].includes(event.key)) return;
if (handledKeys.has(event.key)) {
consumeKey(event);
return;
}
if (!box.isConnected || event.defaultPrevented || event.repeat || event.isComposing ||
!event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ||
!["ArrowLeft", "ArrowRight"].includes(event.key)) {
return;
}
const editing = [...event.composedPath(), document.activeElement].some((element) =>
element?.isContentEditable || element?.matches?.("input:not([type='checkbox']):not([type='radio']):not([type='button']):not([type='submit']), textarea, select, [role='textbox'], [role='combobox'], [role='slider'], [role='spinbutton']"));
if (editing || getVisibleGalleryCloseControl()) {
return;
}
const selector = event.key === "ArrowLeft" ? ".previous-advert" : ".next-advert";
const { link } = entries.find((entry) => entry.selector === selector);
updateLink(selector, link);
if (link.getAttribute("aria-disabled") === "true") {
return;
}
handledKeys.add(event.key);
consumeKey(event);
// Finish keyboard dispatch before invoking the same path as a mouse click.
// Site keyboard listeners must not also navigate for this key press.
setTimeout(() => {
if (!box.isConnected) return;
try {
// Include tracking events before activating the placeholder link.
// Omit view: a userscript sandbox's window is not a native Window.
const mouse = { bubbles: true, cancelable: true, composed: true, button: 0 };
if (typeof PointerEvent === "function")
link.dispatchEvent(new PointerEvent("pointerdown", { ...mouse, buttons: 1, pointerId: 1, pointerType: "mouse", isPrimary: true }));
link.dispatchEvent(new MouseEvent("mousedown", { ...mouse, buttons: 1 }));
if (typeof PointerEvent === "function")
link.dispatchEvent(new PointerEvent("pointerup", { ...mouse, buttons: 0, pointerId: 1, pointerType: "mouse", isPrimary: true }));
link.dispatchEvent(new MouseEvent("mouseup", { ...mouse, buttons: 0 }));
link.click();
} catch (error) {
handledKeys.clear();
console.warn("Ladies Helper: Tastenkürzel", error);
}
}, 0);
};
const onKeyUp = (event) => {
if (handledKeys.delete(event.key)) consumeKey(event);
};
const clearHandledKeys = () => handledKeys.clear();
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("keyup", onKeyUp, true);
window.addEventListener("blur", clearHandledKeys);
const cleanup = () => {
observer.disconnect();
window.removeEventListener("keydown", onKeyDown, true);
window.removeEventListener("keyup", onKeyUp, true);
window.removeEventListener("blur", clearHandledKeys);
clearHandledKeys();
};
const update = () => {
if (!box.isConnected) {
cleanup();
return;
}
entries.forEach(({ selector, link }) => updateLink(selector, link));
};
const observer = new MutationObserver((records) => {
if (records.some((record) => !box.contains(record.target))) update();
if (!box.isConnected) cleanup();
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
"href",
"aria-disabled",
"class",
"data-ref-id",
"data-ref-position",
"data-next-previous-id",
],
});
update();
}
function renderBox(profileId, advertId, phone, rawPhone, imageLinks) {
const profileUrl = profileId
? CONFIG.profileBaseUrl + encodeURIComponent(profileId)
: null;
const prefillData = getPrefillData(profileUrl, phone, rawPhone);
const links = phone ? getVisibleLinks(phone, profileUrl, prefillData) : [];
let html = `
<div class="lh-header">
<div class="lh-header-text">
<span class="lh-label">The Ladies Helper – Profil <small style="font-size:10px;font-weight:normal">v4.4.43</small></span>
${
profileUrl
? `
<div><a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener noreferrer">
${escapeHtml(profileUrl)}
</a>${phone ? ` <span class="lh-separator">|</span> ${createTextLink("WhatsApp", links[2].url)}` : ""}</div>
`
: (phone ? `<div>${createTextLink("WhatsApp", links[2].url)}</div>` : '')
}
</div>
<div class="lh-actions">
<button type="button" class="lh-copy-button lh-gallery-close" hidden title="Galerie schließen">Galerie schließen</button>
<a class="lh-help" href="https://sleazyfork.org/de/scripts/602072-the-ladies-helper" target="_blank" rel="noopener noreferrer" title="Hilfe und Updates auf Sleazy Fork" aria-label="Hilfe und Updates auf Sleazy Fork">?</a>
<button type="button" class="lh-minimize" title="minimieren">−</button>
<button type="button" class="lh-close" title="schließen">×</button>
</div>
</div>
<div class="lh-content">
`;
if (phone) {
html += `
<div class="lh-section lh-intim-ratings">
<span class="lh-label">${createTextLink("intim.to – Profile zur Nummer", links[1].url)}</span>
<span class="lh-separator">|</span>
${createTextLink("Supporter werden", "https://intim.to/de/supporter/")}
<div class="lh-intim-controls" style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap">
<label><input type="checkbox" class="lh-intim-auto"> intim.to Bewertung automatisch laden</label>
<button type="button" class="lh-copy-button lh-intim-load" title="intim.to Bewertungen laden oder aktualisieren">Laden</button>
</div>
<div class="lh-intim-status" role="status">Automatisches Laden ist deaktiviert.</div>
</div>
<div class="lh-section">
${
phone.displayWarning
? `<div class="lh-warning">${escapeHtml(phone.displayWarning)}<br>${escapeHtml(phone.raw)}</div>`
: ""
}
${
phone.countryCodeInfo && phone.type !== "international_or_unknown"
? `<div><span class="lh-label">Ländervorwahl:</span> ${escapeHtml(phone.countryCodeInfo.prefix)} ${escapeHtml(phone.countryCodeInfo.country)}</div>`
: ""
}
<div class="lh-formats">
<div><span class="lh-label">Rufnummern:</span> ${createTextLink("Suche alle (Google)", links[0].url)}</div>
<div class="lh-number-grid">
${phone.all
.map(
(number) => `
<div class="lh-number-row">
<span class="lh-number-text">${escapeHtml(number)}</span>
<span class="lh-number-links">
<a href="${escapeHtml(buildSingleGoogleUrl(number))}" target="_blank" rel="noopener noreferrer" title="Google nur diese Nummer">[G]</a>
<a href="${escapeHtml(buildSingleIntimUrl(number))}" target="_blank" rel="noopener noreferrer" title="intim.to nur diese Nummer">[I]</a>
</span>
</div>
`,
)
.join("")}
</div>
</div>
</div>
<div class="lh-section lh-links">
${
imageLinks.length
? `
<div>
<span class="lh-label">Google Lens Bilder:</span>
${createInlineLinkRow(imageLinks)}
</div>
`
: ""
}
<div>
${createTextLink("Snatchlist erstellen", links[3].url)}
<span class="lh-separator">|</span>
${createTextLink("LH Bericht erstellen", links[4].url)}
${
links[5]
? `<span class="lh-separator">|</span>${createTextLink("Archiv.is erstellen", links[5].url)}`
: ""
}
</div>
<div class="lh-mini-buttons">
<button type="button" class="lh-copy-button lh-copy-markdown">markdown</button>
<button type="button" class="lh-copy-button lh-copy-bb">BB</button>
${imageLinks.length ? '<button type="button" class="lh-copy-button lh-download-all-pictures">Download aller Bilder</button>' : ''}
</div>
</div>
`;
} else if (rawPhone) {
html += `
<div class="lh-section lh-warning">
Rufnummer gefunden, aber nicht verarbeitet:<br>
${escapeHtml(rawPhone)}
</div>
`;
}
html += "</div>";
const existingBox = document.getElementById(CONFIG.boxId);
if (existingBox) {
existingBox.remove();
}
const box = document.createElement("div");
box.id = CONFIG.boxId;
box.innerHTML = html;
document.body.appendChild(box);
addLeanToggle(box);
addImageHoverPreviews(box);
addProfileNavigation(box);
initIntimRatings(box, phone);
const galleryCloseButton = box.querySelector(".lh-gallery-close");
galleryCloseButton?.addEventListener("click", closeVisibleGallery);
const updateGalleryCloseButton = () => {
if (!box.isConnected) {
galleryObserver.disconnect();
return;
}
const hidden = !getVisibleGalleryCloseControl();
if (galleryCloseButton.hidden !== hidden)
galleryCloseButton.hidden = hidden;
};
const galleryObserver = new MutationObserver(updateGalleryCloseButton);
galleryObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class", "style", "hidden", "aria-hidden"],
});
updateGalleryCloseButton();
const minimizeButton = box.querySelector(".lh-minimize");
const closeButton = box.querySelector(".lh-close");
closeButton?.addEventListener("click", () => {
box.remove();
});
minimizeButton?.addEventListener("click", () => {
box.classList.toggle("lh-collapsed");
if (box.classList.contains("lh-collapsed")) {
minimizeButton.textContent = "+";
minimizeButton.title = "anzeigen";
} else {
minimizeButton.textContent = "−";
minimizeButton.title = "minimieren";
}
});
if (phone) {
const copyData = getCopyData(profileUrl, phone);
const markdownButton = box.querySelector(".lh-copy-markdown");
const bbButton = box.querySelector(".lh-copy-bb");
markdownButton?.addEventListener("click", () => {
copyToClipboard(buildMarkdownText(copyData), markdownButton);
});
bbButton?.addEventListener("click", () => {
copyToClipboard(buildBbCodeText(copyData), bbButton);
});
}
const downloadAllPicturesLink = box.querySelector(
".lh-download-all-pictures",
);
downloadAllPicturesLink?.addEventListener("click", (event) => {
event.preventDefault();
downloadAllAdvertPictures(imageLinks, downloadAllPicturesLink);
});
console.log("Ladies Helper angezeigt:", {
profileId,
profileUrl,
advertId,
rawPhone,
phone,
prefillData,
imageLinks,
});
}
function showLadiesBox() {
hideLiveCamCards();
if (document.getElementById(CONFIG.boxId)) {
return true;
}
const profileId = getProfileId();
const advertId = getAdvertId();
const rawPhone = getRawPhone();
const phone = normalizeAnyPhone(rawPhone);
const imageLinks = getAdvertImageLinks(advertId);
if (!profileId && !phone && !rawPhone && !advertId) {
console.log(
"Ladies Helper: Keine Profil-ID, keine Anzeigen-ID und keine Rufnummer gefunden.",
);
return false;
}
addStyle();
renderBox(profileId, advertId, phone, rawPhone, imageLinks);
return true;
}
function readLhParams() {
const params = new URLSearchParams(window.location.search);
const fragment = new URLSearchParams(window.location.hash.slice(1));
fragment.forEach((value, key) => {
if (key.startsWith('lh_')) params.set(key, value);
});
return {
name: params.get("lh_name") || "",
profile: params.get("lh_profile") || "",
whatsapp: params.get("lh_whatsapp") || "",
phone: params.get("lh_phone") || "",
city: params.get("lh_city") || "",
state: params.get("lh_state") || "",
country: params.get("lh_country") || "",
shortdesc: params.get("lh_shortdesc") || "",
report: params.get("lh_report") || "",
source: params.get("lh_source") || "",
adverttext: params.get("lh_adverttext") || "",
attributes: params.get("lh_attributes") || "",
};
}
const lhAutofilledFields = new WeakSet();
function fillLhForm() {
const data = readLhParams();
if (!data.name && !data.profile && !data.phone && !data.city) return true;
// A login page can also contain vbform. Identify the actual report form
// by its fields and wait for delayed fields instead of claiming success.
const firstField = document.querySelector(FORM_SELECTORS.name) ||
document.querySelector(FORM_SELECTORS.profile) ||
document.querySelector(FORM_SELECTORS.report);
const form = firstField?.closest('form');
if (!form) return false;
const wysiwygMode = form.querySelector(FORM_SELECTORS.wysiwygMode);
if (wysiwygMode && !lhAutofilledFields.has(wysiwygMode)) {
wysiwygMode.value = "0";
dispatchFieldEvents(wysiwygMode);
lhAutofilledFields.add(wysiwygMode);
}
let ready = true;
let filled = 0;
for (const key of ['name', 'profile', 'whatsapp', 'phone', 'city', 'state', 'country', 'shortdesc', 'report']) {
const value = data[key];
if (value == null || value === '') continue;
const field = form.querySelector(FORM_SELECTORS[key]);
if (!field) { ready = false; continue; }
if (lhAutofilledFields.has(field)) { filled++; continue; }
if (field.tagName === 'SELECT' &&
!Array.from(field.options).some(option => option.value === String(value))) {
ready = false;
continue;
}
field.value = String(value);
// Mark first: event handlers may trigger observers synchronously.
lhAutofilledFields.add(field);
dispatchFieldEvents(field);
filled++;
}
if (!ready || !filled) return false;
showLhAutofillNote();
return true;
}
function showLhAutofillNote(label = "LH Autofill") {
addStyle();
if (document.getElementById(CONFIG.noteId)) return;
const note = document.createElement("div");
note.id = CONFIG.noteId;
note.innerHTML = `
<strong>${escapeHtml(label)}:</strong> Formular vorausgefüllt.<br>
Bitte prüfen und manuell absenden.
<button type="button" id="lh-autofill-note-close" style="margin-left:6px;">×</button>
`;
document.body.appendChild(note);
document
.getElementById("lh-autofill-note-close")
?.addEventListener("click", () => {
note.remove();
});
setTimeout(() => {
note.remove();
}, LIMITS.noteAutoCloseMs);
}
function observeUntil(callback, retryDelays = []) {
let done = false;
let running = false;
let observer = null;
const timers = new Set();
const attempt = () => {
if (done || running) return;
running = true;
try {
if (callback()) {
done = true;
observer?.disconnect();
timers.forEach(timer => clearTimeout(timer));
timers.clear();
}
} finally {
running = false;
}
};
attempt();
if (done) return;
observer = new MutationObserver(attempt);
observer.observe(document.documentElement, {childList: true, subtree: true, attributes: true});
retryDelays.forEach(delay => {
const timer = setTimeout(() => { timers.delete(timer); attempt(); }, delay);
timers.add(timer);
});
}
function getListingRadius(href = location.href) {
const value = new URL(href).searchParams.get("umkreis");
return value !== null && /^\d+$/.test(value) ? value : "10";
}
function createListingLoader(
status,
onStateChange,
onRadiusMismatch = () => {},
) {
// Freeze the search and explicitly include its radius on every request.
const listingUrl = new URL(location.href);
listingUrl.searchParams.set("umkreis", getListingRadius(listingUrl.href));
let total = 1; // The results API supplies the radius-specific page count.
let initialized = false;
const fallbackGroup = document
.querySelector("div.ladies-card a.card-link")
?.closest("div.ladies-card")?.parentElement;
const completed = new Set();
const seen = new Set();
const pagers = Array.from(
document.querySelectorAll(".pagination-container"),
).map((element) => ({
element,
hidden: element.hidden,
display: element.style.display,
}));
let controller = null;
let task = null;
let radiusOffer = null;
const notify = () =>
onStateChange({
running: !!task,
done: initialized && completed.size >= total,
offeredRadius: radiusOffer,
});
const showNavigation = () =>
pagers.forEach(({ element, hidden, display }) => {
element.hidden = hidden;
element.style.display = display;
});
const run = async (signal) => {
try {
for (let page = 1; page <= total; page++) {
if (completed.has(page)) continue;
signal.throwIfAborted();
status.textContent =
"Alle Anzeigen laden: " +
completed.size +
" / " +
total +
" Seiten …";
const url = new URL(listingUrl.href);
url.pathname = "/api" + url.pathname;
url.searchParams.set("page", String(page));
// The site's session keeps the largest searched radius. Public requests
// without session cookies let the selected radius decrease again.
const response = await fetch(url.href, {
credentials: "omit",
cache: "no-store",
signal,
});
if (!response.ok) throw new Error("HTTP " + response.status);
const data = await response.json();
signal.throwIfAborted();
const requestedRadius = Number(
listingUrl.searchParams.get("umkreis"),
);
if (
data.distanceSearched == null ||
Number(data.distanceSearched) !== requestedRadius
) {
const offeredRadius = Number(data.distanceSearched);
if (
data.distanceSearched != null &&
Number.isInteger(offeredRadius) &&
offeredRadius >= 0
) {
radiusOffer = offeredRadius;
onRadiusMismatch(offeredRadius);
}
throw new Error(
"Angefordert: " +
requestedRadius +
" km; Server liefert: " +
data.distanceSearched +
" km. Laden gestoppt.",
);
}
if (
typeof data.content !== "string" ||
!Number.isInteger(data.pagination?.totalPages) ||
data.pagination.currentPage !== page
)
throw new Error("Ungültige Suchantwort.");
total = Math.max(1, data.pagination.totalPages);
const doc = new DOMParser().parseFromString(
data.content,
"text/html",
);
const cards = Array.from(
doc.querySelectorAll("div.ladies-card"),
).filter((card) => card.querySelector("a.card-link[href]"));
if (!cards.length && Number(data.advertCount) !== 0)
throw new Error("Keine Anzeigen auf Seite " + page);
if (!initialized) {
// Discard the initial HTML results: they may use a different radius.
document
.querySelectorAll("div.ladies-card")
.forEach((card) => card.remove());
initialized = true;
const actualRadius = document.querySelector("#actual-umkreis");
if (actualRadius)
actualRadius.textContent =
requestedRadius === 0 ? "ohne" : requestedRadius + " km";
const resultInfo = document.querySelector(
".advert-results-found-info",
);
if (resultInfo && typeof data.advertResultsFoundInfo === "string") {
resultInfo.innerHTML = data.advertResultsFoundInfo;
}
}
for (const card of cards) {
const href = new URL(
card.querySelector("a.card-link[href]").getAttribute("href"),
url.href,
);
if (href.pathname.includes("/directre/") || seen.has(href.pathname))
continue;
seen.add(href.pathname);
const group =
Array.from(document.querySelectorAll(".card-list")).find(
(element) => element.className === card.parentElement.className,
) || fallbackGroup;
const imported = document.importNode(card, true);
imported
.querySelectorAll("script")
.forEach((element) => element.remove());
imported.querySelectorAll("img").forEach((img) => {
const src =
img.getAttribute("data-src") ||
img.getAttribute("data-original") ||
img.getAttribute("src");
if (src) img.src = new URL(src, url.href).href;
img.loading = "lazy";
});
group.appendChild(imported);
}
completed.add(page);
}
hideLiveCamCards();
pagers.forEach(({ element }) => {
element.hidden = true;
element.style.display = "none";
});
status.textContent = "Alle " + total + " Seiten geladen.";
} catch (error) {
showNavigation();
status.textContent =
(signal.aborted ? "Laden gestoppt" : "Laden unterbrochen") +
" (" +
completed.size +
" / " +
total +
" Seiten).";
if (!signal.aborted) {
status.textContent += " " + error.message;
console.warn("Ladies Helper: vollständige Ansicht", error);
}
}
};
const resume = () => {
if (task) return task;
radiusOffer = null;
onRadiusMismatch(null);
controller = new AbortController();
task = Promise.resolve()
.then(() => run(controller.signal))
.finally(() => {
task = null;
controller = null;
notify();
});
notify();
return task;
};
const stop = () => {
controller?.abort();
};
const restart = async () => {
stop();
if (task) await task;
// Keep the current view until a valid replacement page has arrived.
// The first successful response clears the old cards in run().
completed.clear();
seen.clear();
initialized = false;
total = 1;
showNavigation();
return resume();
};
notify();
const acceptRadius = async (value) => {
if (!Number.isInteger(value) || value < 0) return;
stop();
if (task) await task;
listingUrl.searchParams.set("umkreis", String(value));
listingUrl.searchParams.set("page", "1");
listingUrl.hash = "";
history.replaceState(history.state, "", listingUrl.href);
return restart();
};
return { resume, stop, restart, acceptRadius };
}
function buildRadiusUrl(value, href = location.href) {
const url = new URL(href);
url.searchParams.set("umkreis", String(value));
url.searchParams.set("page", "1");
url.hash = "";
return url.href;
}
function readAutoLoadPreference() {
try {
return localStorage.getItem("ladies-helper-auto-load") !== "false";
} catch {
return true;
}
}
function renderRadiusOffer(status, radius, accept) {
status.parentElement.querySelector(".lh-radius-offer")?.remove();
if (!Number.isInteger(radius) || radius < 0) return;
const row = document.createElement("div");
row.className = "lh-radius-offer";
row.style.setProperty("display", "block", "important");
row.style.marginTop = "6px";
const button = document.createElement("button");
button.type = "button";
button.className = "lh-copy-button lh-accept-radius";
button.textContent = radius + " km laden?";
button.title = "Umkreis auf " + radius + " km setzen und laden";
button.setAttribute("aria-label", button.title);
button.style.setProperty("display", "inline-block", "important");
button.addEventListener("click", () => {
button.disabled = true;
accept(radius);
});
row.appendChild(button);
status.after(row);
}
function buildListingViewUrl(listView, href = location.href) {
const url = new URL(href);
url.searchParams.set("ansicht", listView ? "list" : "gallery");
url.searchParams.set("page", "1");
url.hash = "";
return url.href;
}
function showListingBox() {
if (document.getElementById(CONFIG.boxId)) return true;
if (!document.querySelector("div.ladies-card a.card-link")) return false;
addStyle();
const box = document.createElement("div");
box.id = CONFIG.boxId;
const citySlug = location.pathname.split("/").filter(Boolean)[0];
const city = citySlug !== "sex-anzeigen" ? decodeSlug(citySlug) : "";
const radiusValues = [
"0",
"10",
"20",
"30",
"50",
"70",
"100",
"150",
"200",
"300",
];
const currentRadius = getListingRadius();
const requestedView = new URL(location.href).searchParams.get("ansicht");
const listView = requestedView ? requestedView === "list" :
!!document.querySelector(".card-list.azm-type-list");
// Preserve a radius supplied in the URL even if the site adds another option.
if (!radiusValues.includes(currentRadius)) radiusValues.push(currentRadius);
box.innerHTML = `
<div class="lh-header">
<div class="lh-header-text"><span class="lh-label">The Ladies Helper – Übersicht <small style="font-size:10px;font-weight:normal">v4.4.43</small></span>
${city ? "<div>" + escapeHtml(city) + "</div>" : ""}
</div>
<div class="lh-actions">
<a class="lh-help" href="https://sleazyfork.org/de/scripts/602072-the-ladies-helper" target="_blank" rel="noopener noreferrer" title="Hilfe und Updates auf Sleazy Fork" aria-label="Hilfe und Updates auf Sleazy Fork">?</a>
<button type="button" class="lh-minimize" title="minimieren">−</button>
<button type="button" class="lh-close" title="Helper schließen">×</button>
</div>
</div>
<div class="lh-content">
<div class="lh-section">
<label><span class="lh-label">Umkreis:</span>
<select class="lh-radius" aria-label="Umkreis ändern">
${radiusValues
.map(
(value) =>
'<option value="' +
escapeHtml(value) +
'"' +
(value === currentRadius ? " selected" : "") +
">" +
(value === "0" ? "ohne" : escapeHtml(value) + " km") +
"</option>",
)
.join("")}
</select>
</label>
</div>
<div class="lh-section">
<label style="display:inline-flex;align-items:center;gap:7px;cursor:pointer" title="Zwischen Kacheln und Liste wechseln">
<span style="font-weight:${listView ? 'normal' : 'bold'}">Kacheln</span>
<input type="checkbox" role="switch" class="lh-view-toggle" aria-label="Listenansicht" ${listView ? 'checked' : ''}>
<span style="font-weight:${listView ? 'bold' : 'normal'}">Liste</span>
</label>
</div>
<div class="lh-listing-status" role="status">Anzeigen werden geladen …</div>
<div class="lh-section"><label>
<input type="checkbox" class="lh-auto-load" ${readAutoLoadPreference() ? "checked" : ""}>
Alle Seiten automatisch laden
</label></div>
<div class="lh-mini-buttons">
<button type="button" class="lh-copy-button lh-load-stop" title="Laden stoppen" aria-label="Laden stoppen">■ Stop</button>
<button type="button" class="lh-copy-button lh-load-listing" title="Laden starten oder fortsetzen" aria-label="Laden fortsetzen">▶ Fortsetzen</button>
<button type="button" class="lh-copy-button lh-load-restart" title="Alle Seiten von vorne neu laden" aria-label="Alle Seiten neu laden">↻ Neustart</button>
</div>
</div>`;
document.body.appendChild(box);
addLeanToggle(box);
box
.querySelector(".lh-close")
.addEventListener("click", () => box.remove());
const minimize = box.querySelector(".lh-minimize");
minimize.addEventListener("click", () => {
const collapsed = box.classList.toggle("lh-collapsed");
minimize.textContent = collapsed ? "+" : "−";
minimize.title = collapsed ? "anzeigen" : "minimieren";
});
box.querySelector(".lh-radius").addEventListener("change", (event) => {
location.assign(buildRadiusUrl(event.target.value));
});
box.querySelector(".lh-view-toggle").addEventListener("change", (event) => {
location.assign(buildListingViewUrl(event.target.checked));
});
const button = box.querySelector(".lh-load-listing");
const stopButton = box.querySelector(".lh-load-stop");
const restartButton = box.querySelector(".lh-load-restart");
const autoLoad = box.querySelector(".lh-auto-load");
const status = box.querySelector(".lh-listing-status");
let restarting = false;
const loader = createListingLoader(
status,
({ running, done, offeredRadius }) => {
stopButton.hidden = !running;
button.hidden = running;
button.disabled = done;
button.textContent = done
? "✓ Vollständig geladen"
: "▶ Laden / Fortsetzen";
renderRadiusOffer(
status,
!running ? offeredRadius : null,
async (radius) => {
const select = box.querySelector(".lh-radius");
if (
!Array.from(select.options).some(
(option) => option.value === String(radius),
)
) {
const option = document.createElement("option");
option.value = String(radius);
option.textContent = radius + " km";
select.appendChild(option);
}
select.value = String(radius);
await loader.acceptRadius(radius);
},
);
},
);
button.addEventListener("click", () => loader.resume());
stopButton.addEventListener("click", () => loader.stop());
restartButton.addEventListener("click", async () => {
if (restarting) return;
restarting = true;
restartButton.disabled = true;
try {
await loader.restart();
} finally {
restarting = false;
restartButton.disabled = false;
}
});
autoLoad.addEventListener("change", () => {
try {
localStorage.setItem(
"ladies-helper-auto-load",
String(autoLoad.checked),
);
} catch {
/* Keep the choice on this page if storage is unavailable. */
}
if (autoLoad.checked) loader.resume();
else loader.stop();
});
box
.querySelector(".lh-close")
.addEventListener("click", () => loader.stop());
if (autoLoad.checked) loader.resume();
else status.textContent = "Automatisches Laden ist deaktiviert.";
return true;
}
function initLadiesPage() {
if (!getAdvertId()) {
const listingUrl = new URL(location.href);
const radius = listingUrl.searchParams.get("umkreis");
if (radius === null || !/^\d+$/.test(radius)) {
// Reload first so page counts and initial cards also use the 10 km default.
location.replace(buildRadiusUrl("10"));
return;
}
observeUntil(() => {
hideLiveCamCards();
return showListingBox();
}, [1000, 3000, 5000, 8000]);
return;
}
observeUntil(() => {
hideLiveCamCards();
return showLadiesBox();
}, [1000, 3000, 5000, 8000]);
}
function initLhFormPage() {
const current = new URL(location.href);
const legacyKeys = Array.from(current.searchParams.keys()).filter(key => key.startsWith('lh_'));
if (legacyKeys.length) {
const fragment = new URLSearchParams(current.hash.slice(1));
legacyKeys.forEach(key => {
if (!fragment.has(key)) fragment.set(key, current.searchParams.get(key));
current.searchParams.delete(key);
});
current.hash = fragment.toString();
// Reload old links without sending the helper payload to the forum.
location.replace(current.href);
return;
}
observeUntil(() => {
const frame = document.querySelector('frameset frame');
const payload = new URLSearchParams(location.hash.slice(1));
if (frame && payload.has('lh_name') && !payload.has('lh_in_frame')) {
const destination = new URL(CONFIG.lhReportBaseUrl);
payload.set('lh_in_frame', '1');
destination.hash = payload.toString();
frame.src = destination.href;
return true;
}
return fillLhForm();
}, [1000, 3000, 5000]);
}
if (location.hostname.includes("ladies.de")) {
initLeanMode();
initLadiesPage();
}
if (["snatchlist.com", "www.snatchlist.com"].includes(location.hostname)) {
initSnatchlistForm();
}
if (location.hostname === "huren-test-forum.lusthaus.cc") {
initLhFormPage();
}
})();