Unlocks Paywalls in recon
// ==UserScript== // @name Recon+ // @namespace https://update.greasyfork.org/scripts/582087/Recon%2B.user.js // @version 2.21 // @match https://www.recon.com/* // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect basemaps.cartocdn.com // @grant GM_getResourceText // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://unpkg.com/[email protected]/dist/leaflet.js // @resource LEAFLET_CSS https://unpkg.com/[email protected]/dist/leaflet.css // @run-at document-start // @description Unlocks Paywalls in recon // ==/UserScript== (function(){ // src/state.ts var win = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; var origXHR = win.XMLHttpRequest; var profileDetailsCache = new Map; var profileVersionsMap = new Map; var profileMediaMap = new Map; var state = { lastBlockedRequest: null, lastProfileDetailPageData: null, authHeaders: {}, currentChatProfileId: null, currentChatConversationId: null, cachedModelName: null }; var interestMap = { 1: "Recon Men", 2: "Skinheads", 4: "Leather", 5: "Rubber", 6: "Sports Gear", 7: "Military", 8: "Hoods & Masks", 9: "Muscle", 10: "Punks", 13: "Bikers", 15: "Bondage", 18: "Suits", 19: "Fisting", 20: "Masters & Slaves", 27: "Boots", 30: "Tattoos & Piercings", 31: "Bears", 34: "Fighting", 36: "Feet", 37: "Pups & Handlers", 38: "Smokers", 39: "Gunge", 40: "Trackies", 41: "Underwear", 42: "Chastity", 43: "Watersports", 44: "Impact Play", 45: "Electro", 46: "Sneakers & Socks", 47: "ADBL" }; // src/toast.ts function showToast(message, type = "success") { let container = document.getElementById("custom-toast-container"); if (!container) { container = document.createElement("div"); container.id = "custom-toast-container"; container.style.cssText = `position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;`; document.body.appendChild(container); } const toast = document.createElement("div"); toast.textContent = message; toast.style.cssText = `background: ${type === "error" ? "#cf2a2a" : "#2acf65"}; color: #fff; padding: 12px 24px; border-radius: 4px; font-family: sans-serif; font-weight: bold; font-size: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; transform: translateY(-20px); transition: all 0.3s ease;`; container.appendChild(toast); setTimeout(() => { toast.style.opacity = "1"; toast.style.transform = "translateY(0)"; }, 10); setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateY(-20px)"; setTimeout(() => toast.remove(), 300); }, 3500); } function showProgressIndicator(title) { let container = document.getElementById("custom-toast-container"); if (!container) { container = document.createElement("div"); container.id = "custom-toast-container"; container.style.cssText = `position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;`; document.body.appendChild(container); } const card = document.createElement("div"); card.style.cssText = ` background: rgba(20, 20, 22, 0.95); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: #fff; padding: 16px; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); width: 300px; pointer-events: auto; opacity: 0; transform: translateY(-20px); transition: all 0.3s ease; display: flex; flex-direction: column; gap: 10px; `; const header = document.createElement("div"); header.style.cssText = `display: flex; justify-content: space-between; align-items: center; font-weight: bold;`; const titleEl = document.createElement("span"); titleEl.textContent = title; const pctEl = document.createElement("span"); pctEl.style.cssText = `color: #a855f7; font-variant-numeric: tabular-nums; font-weight: bold;`; pctEl.textContent = "0%"; header.appendChild(titleEl); header.appendChild(pctEl); const msgEl = document.createElement("div"); msgEl.style.cssText = `font-size: 12px; color: rgba(255,255,255,0.7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`; msgEl.textContent = "Initializing..."; const barTrack = document.createElement("div"); barTrack.style.cssText = `background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px; overflow: hidden;`; const barFill = document.createElement("div"); barFill.style.cssText = `width: 0%; height: 100%; background: linear-gradient(90deg, #6366f1, #a855f7); transition: width 0.2s ease; border-radius: 3px;`; barTrack.appendChild(barFill); card.appendChild(header); card.appendChild(msgEl); card.appendChild(barTrack); container.appendChild(card); setTimeout(() => { card.style.opacity = "1"; card.style.transform = "translateY(0)"; }, 10); return { update(percent, message) { const safePercent = Math.min(100, Math.max(0, percent)); pctEl.textContent = `${Math.round(safePercent)}%`; msgEl.textContent = message; barFill.style.width = `${safePercent}%`; if (safePercent >= 100) { barFill.style.background = "linear-gradient(90deg, #2ec4b6, #2acf65)"; pctEl.style.color = "#2acf65"; } }, close(delay = 1000) { setTimeout(() => { card.style.opacity = "0"; card.style.transform = "translateY(-20px)"; setTimeout(() => card.remove(), 300); }, delay); } }; } // src/utils.ts async function fetchImageAsBlob(url) { try { const res = await win.fetch(url); if (res.ok) return await res.blob(); } catch (e) { console.warn(`[Script] Standard fetch failed for ${url}, trying GM_xmlhttpRequest:`, e); } if (typeof GM_xmlhttpRequest !== "undefined") { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`GM_xmlhttpRequest failed with status ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } throw new Error(`Cannot fetch ${url} without CORS or GM_xmlhttpRequest`); } function getJSZip() { if (typeof JSZip !== "undefined") return Promise.resolve(JSZip); if (win.JSZip) return Promise.resolve(win.JSZip); return new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"; script.onload = () => resolve(window.JSZip || win.JSZip); script.onerror = () => reject(new Error("Failed to load JSZip")); document.head.appendChild(script); }); } async function exportProfileData() { const progress = showProgressIndicator("Exporting Profile"); let JSZipLib; try { progress.update(5, "Loading JSZip library..."); JSZipLib = await getJSZip(); } catch (e) { console.error(e); showToast("Failed to load JSZip library.", "error"); progress.close(0); return; } progress.update(10, "Analyzing profile data..."); const data = state.lastProfileDetailPageData; const profileId = data ? data.id : (win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i) || [])[1] || "unknown"; let displayName = ""; if (data) { displayName = data.displayName || data.username || data.name || ""; } if (!displayName) { const h1 = document.querySelector("h1, .profile-header h1, t101-profile-header h1, .member-info h1, .profile-info-container h1"); if (h1 && h1.innerText.trim()) { displayName = h1.innerText.trim(); } else if (document.title) { displayName = document.title.replace(/\s*-\s*Recon\s*$/i, "").trim(); } } if (!displayName) { displayName = "Profile"; } const photoUrls = new Set; document.querySelectorAll(".photo-tile").forEach((t) => { const htmlEl = t; const bgImg = htmlEl.style.backgroundImage; if (bgImg) { const url = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0]; if (url) photoUrls.add(url); } }); document.querySelectorAll(".profile-avatar").forEach((t) => { const htmlEl = t; const bgImg = htmlEl.style.backgroundImage; if (bgImg) { const url = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0]; if (url) photoUrls.add(url); } }); if (data) { const findUrls = (obj) => { if (typeof obj === "string") { if (obj.startsWith("http") && (obj.includes("/photos/") || obj.includes("/images/") || obj.match(/\.(jpg|jpeg|png|webp|gif)/i))) { photoUrls.add(obj.split("?")[0]); } } else if (typeof obj === "object" && obj !== null) { for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { findUrls(obj[key]); } } } }; findUrls(data); } const urlsArray = Array.from(photoUrls); if (urlsArray.length === 0) { showToast("No photos found to export.", "error"); progress.close(0); return; } progress.update(15, `Found ${urlsArray.length} photos. Starting download...`); const zip = new JSZipLib; let infoText = ""; infoText += `=== RECON PROFILE EXPORT === `; infoText += `Export Date: ${new Date().toLocaleString()} `; infoText += `Profile URL: ${win.location.href} `; const interestMap2 = { 1: "Recon Men", 2: "Skinheads", 4: "Leather", 5: "Rubber", 6: "Sports Gear", 7: "Military", 8: "Hoods & Masks", 9: "Muscle", 10: "Punks", 13: "Bikers", 15: "Bondage", 18: "Suits", 19: "Fisting", 20: "Masters & Slaves", 27: "Boots", 30: "Tattoos & Piercings", 31: "Bears", 34: "Fighting", 36: "Feet", 37: "Pups & Handlers", 38: "Smokers", 39: "Gunge", 40: "Trackies", 41: "Underwear", 42: "Chastity", 43: "Watersports", 44: "Impact Play", 45: "Electro", 46: "Sneakers & Socks", 47: "ADBL" }; if (data) { infoText += `Display Name: ${data.displayName || "N/A"} `; infoText += `Username: ${data.username || "N/A"} `; infoText += `Profile ID: ${data.id || "N/A"} `; if (data.location) { infoText += `Location: ID ${data.location.locationId || "N/A"} `; } if (data.lastUpdatedDate) { infoText += `Last Updated: ${data.lastUpdatedDate} `; } if (data.interests && data.interests.length > 0) { infoText += `Interests: ${data.interests.map((id) => interestMap2[id] || `Tag ${id}`).join(", ")} `; } infoText += ` --- PROFILE INFO (RAW KEY-VALUES) --- `; for (const [key, val] of Object.entries(data)) { if (typeof val !== "object" && val !== null) { infoText += `${key}: ${val} `; } } } const bioSection = document.querySelector(".text-container, .bio-container, .member-bio"); if (bioSection) { infoText += ` --- PROFILE BIO --- `; infoText += bioSection.innerText.trim() + ` `; } const infoSections = document.querySelectorAll(".details-container, .stats-container, .profile-details"); if (infoSections.length > 0) { infoText += ` --- PROFILE DETAILS --- `; infoSections.forEach((section) => { const htmlEl = section; infoText += htmlEl.innerText.trim() + ` `; }); } zip.file("profile_info.txt", infoText); if (data) { zip.file("profile_data.json", JSON.stringify(data, null, 4)); } let successCount = 0; let completedCount = 0; const photosFolder = zip.folder("photos"); const downloadPromises = urlsArray.map(async (url, idx) => { try { const blob = await fetchImageAsBlob(url); const ext = url.split(".").pop()?.split(/[?#]/)[0] || "jpg"; photosFolder.file(`photo_${idx + 1}.${ext}`, blob); successCount++; } catch (err) { console.error(`[Script] Failed to download photo from ${url}:`, err); } finally { completedCount++; const percent = 15 + Math.round(completedCount / urlsArray.length * 65); progress.update(percent, `Downloading photos (${completedCount}/${urlsArray.length})...`); } }); await Promise.all(downloadPromises); if (successCount === 0) { showToast("Failed to download any profile photos.", "error"); progress.close(0); return; } progress.update(80, `Zipping ${successCount} photos...`); try { const content = await zip.generateAsync({ type: "blob" }, (metadata) => { const percent = 80 + Math.round(metadata.percent / 100 * 15); progress.update(percent, `Zipping files (${Math.round(metadata.percent)}%)...`); }); progress.update(95, "Saving export file..."); const blobUrl = URL.createObjectURL(content); const a = document.createElement("a"); const safeName = displayName?.replace(/[^a-z0-9_-]/gi, "_").toLowerCase(); a.href = blobUrl; a.download = `${safeName}_${profileId}_export.zip`; a.click(); URL.revokeObjectURL(blobUrl); progress.update(100, `Complete! Saved ${successCount}/${urlsArray.length} photos.`); progress.close(2000); } catch (err) { console.error("[Script] Error creating zip file:", err); showToast("Error generating zip archive.", "error"); progress.close(0); } } function extractPhotoUrls(obj) { const urlsMap = new Map; const findUrls = (val) => { if (!val) return; if (typeof val === "object") { if (typeof val.url === "string" && val.url) { let url = val.url; if (url.startsWith("//")) { url = "https:" + url; } const baseUrl = url.split("?")[0]; if (!urlsMap.has(baseUrl) || url.includes("size=")) { urlsMap.set(baseUrl, url); } } for (const key in val) { if (Object.prototype.hasOwnProperty.call(val, key)) { findUrls(val[key]); } } } else if (typeof val === "string") { if (val.startsWith("http") && (val.includes("/profiles/") || val.includes("/photos/") || val.includes("/images/") || val.includes("/files/") || val.includes("t101api.com") || val.match(/\.(jpg|jpeg|png|webp|gif)/i))) { const baseUrl = val.split("?")[0]; if (!urlsMap.has(baseUrl) || val.includes("size=")) { urlsMap.set(baseUrl, val); } } } }; findUrls(obj); return Array.from(urlsMap.values()); } // src/settings.ts var defaultSettings = { enableXhrInterceptor: true, enableFilterPanel: true, enablePremiumPhotos: true, enableDistanceMap: true, enableShowAsViewed: true, enableAiChatSummary: false, geminiApiKey: "", triangulationProbes: 5, probeOffsetMetres: 5000, customCoordinates: "", enableFakeLocation: false }; var settings = { ...defaultSettings }; function loadSettings() { try { const stored = win.localStorage.getItem("recon_plus_settings"); if (stored) { const parsed = JSON.parse(stored); settings = { ...defaultSettings, ...parsed }; } } catch (e) { console.error("[Recon+] Failed to load settings, using defaults.", e); settings = { ...defaultSettings }; } return settings; } function saveSettings(newSettings) { settings = { ...newSettings }; try { win.localStorage.setItem("recon_plus_settings", JSON.stringify(settings)); } catch (e) { console.error("[Recon+] Failed to save settings.", e); } } function injectSettingsButton() { if (document.getElementById("recon-settings-item")) return; const elements = Array.from(document.querySelectorAll('t101-dropdown-menu-item, a, button, li, .menu-item, [routerlink*="settings"]')); let settingsEl = null; for (const el of elements) { const htmlEl = el; const text = htmlEl.textContent?.trim().toLowerCase() || ""; const href = htmlEl.getAttribute("href") || ""; const routerLink = htmlEl.getAttribute("routerlink") || ""; const innerDiv = htmlEl.querySelector("div"); const innerDivText = innerDiv ? innerDiv.textContent?.trim().toLowerCase() : ""; if (innerDivText === "settings" || text === "settings" || href.endsWith("/settings") || routerLink.includes("settings")) { settingsEl = htmlEl; break; } } if (settingsEl) { const clone = settingsEl.cloneNode(true); clone.id = "recon-settings-item"; clone.removeAttribute("href"); clone.removeAttribute("routerlink"); clone.style.cursor = "pointer"; let textFound = false; const innerSpans = Array.from(clone.querySelectorAll("span, div, .text, .label, .button-text")); for (const span of innerSpans) { if (span.textContent?.trim().toLowerCase() === "settings") { span.textContent = "Recon+ Settings"; textFound = true; break; } } if (!textFound) { clone.textContent = "Recon+ Settings"; } const iconContainer = clone.querySelector("t101-icon, svg"); if (iconContainer) { const faviconUrl = document.querySelector('link[rel*="icon"]')?.href || `${win.location.origin}/favicon.ico`; iconContainer.innerHTML = `<img src="${faviconUrl}" style="width: 24px; height: 24px; object-fit: contain; border-radius: 4px; display: block;" />`; } clone.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const backdrop = document.querySelector(".cdk-overlay-backdrop, .menu-backdrop"); if (backdrop) { backdrop.click(); } openSettingsModal(); }); if (settingsEl.nextSibling) { settingsEl.parentNode.insertBefore(clone, settingsEl.nextSibling); } else { settingsEl.parentNode.appendChild(clone); } } } function openSettingsModal() { if (document.getElementById("recon-settings-overlay")) return; loadSettings(); const overlay = document.createElement("div"); overlay.id = "recon-settings-overlay"; const modal = document.createElement("div"); modal.id = "recon-settings-modal"; const header = document.createElement("div"); header.className = "recon-settings-header"; header.innerHTML = ` <h3 class="recon-settings-title">Recon+ Settings</h3> <span class="recon-settings-close">✕</span> `; modal.appendChild(header); const tabs = document.createElement("div"); tabs.className = "recon-settings-tabs"; tabs.innerHTML = ` <div class="recon-settings-tab active" data-tab="general">General</div> <div class="recon-settings-tab" data-tab="triangulation">Triangulation</div> <div class="recon-settings-tab" data-tab="database">Database</div> `; modal.appendChild(tabs); const content = document.createElement("div"); content.className = "recon-settings-content"; const panelGeneral = document.createElement("div"); panelGeneral.className = "recon-settings-panel active"; panelGeneral.id = "recon-panel-general"; panelGeneral.innerHTML = ` <div class="recon-settings-group"> <div class="recon-settings-group-title">Module Toggles</div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">XHR Request Interception</span> <span class="recon-settings-desc">Intercept network requests to extract profile updates, auth tokens, and location IDs.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-xhr" ${settings.enableXhrInterceptor ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Recon+ Custom Filter Panel</span> <span class="recon-settings-desc">Show the sidebar filter panel to search profiles by bio content and attributes.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-filter" ${settings.enableFilterPanel ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Premium Photo Gallery</span> <span class="recon-settings-desc">Expose all user photos in high resolution without premium paywall blockages.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-photos" ${settings.enablePremiumPhotos ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Approximate Distance Map</span> <span class="recon-settings-desc">Automatically display an approximate area map showing the profile's distance ring.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-map" ${settings.enableDistanceMap ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Profile Utilities ("Show As Viewed")</span> <span class="recon-settings-desc">Render manual view trigger and data export utility buttons on profile pages.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-viewed" ${settings.enableShowAsViewed ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">AI Chat Summarizer</span> <span class="recon-settings-desc">Extract important profile insights, kinks, and meetup arrangements from chat threads.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-chat-summary" ${settings.enableAiChatSummary ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> <div class="recon-settings-row" id="recon-setting-gemini-key-row" style="display: ${settings.enableAiChatSummary ? "flex" : "none"}; flex-direction: column; align-items: stretch; gap: 6px;"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Gemini API Key</span> <span class="recon-settings-desc">Required for chat summarization. Get a free key from Google AI Studio.</span> </div> <div style="position: relative; display: flex; align-items: center; width: 100%;"> <input type="password" id="recon-setting-gemini-key" class="recon-filter-input" style="padding-right: 40px;" placeholder="AIzaSy..." value="${settings.geminiApiKey}"> <span id="recon-toggle-gemini-key-visibility" style="position: absolute; right: 10px; cursor: pointer; color: #adadad; font-size: 16px; user-select: none;">\uD83D\uDC41️</span> </div> </div> </div> `; content.appendChild(panelGeneral); const panelTriangulation = document.createElement("div"); panelTriangulation.className = "recon-settings-panel"; panelTriangulation.id = "recon-panel-triangulation"; panelTriangulation.innerHTML = ` <div class="recon-settings-group"> <div class="recon-settings-group-title">Triangulation Tuning</div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Number of Probe Points</span> <span class="recon-settings-desc">Amount of coordinates searched to locate target. Default: 5 (max 10).</span> </div> <input type="number" id="recon-setting-probes" min="3" max="10" class="recon-filter-input" style="width: 80px;" value="${settings.triangulationProbes}"> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Probe Distance Offset (metres)</span> <span class="recon-settings-desc">Scale in meters of the search grid arm around center. Default: 5000.</span> </div> <input type="number" id="recon-setting-offset" min="500" max="20000" class="recon-filter-input" style="width: 80px;" value="${settings.probeOffsetMetres}"> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Mock Geolocation Center</span> <span class="recon-settings-desc">Override browser GPS coordinate starting point for triangulation. Format: lat, lon (e.g. 51.5074, -0.1278). Keep blank to use your actual GPS location.</span> </div> <input type="text" id="recon-setting-coords" placeholder="e.g. 51.5074, -0.1278" class="recon-filter-input" style="width: 180px;" value="${settings.customCoordinates}"> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Mock GPS Toggle</span> <span class="recon-settings-desc">Force the site and search queries to use the Mock Geolocation Center coordinates above.</span> </div> <label class="recon-switch"> <input type="checkbox" id="recon-setting-fake-location" ${settings.enableFakeLocation ? "checked" : ""}> <span class="recon-slider"></span> </label> </div> </div> `; content.appendChild(panelTriangulation); const panelDatabase = document.createElement("div"); panelDatabase.className = "recon-settings-panel"; panelDatabase.id = "recon-panel-database"; panelDatabase.innerHTML = ` <div class="recon-settings-group"> <div class="recon-settings-group-title">Local IndexedDB Metrics</div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Profiles Cached</span> <span class="recon-settings-desc">Total count of profiles saved locally from views & scans.</span> </div> <span id="recon-db-profile-count" style="font-weight: 800; color: #ff3333;">Loading...</span> </div> <div class="recon-settings-row"> <div class="recon-settings-label-container"> <span class="recon-settings-label">Locations Cached</span> <span class="recon-settings-desc">Total count of locations resolved and saved locally.</span> </div> <span id="recon-db-location-count" style="font-weight: 800; color: #ff3333;">Loading...</span> </div> <div style="margin-top: 10px; display: flex; gap: 10px;"> <button class="recon-settings-btn recon-settings-btn-danger" id="recon-btn-clear-db">Clear Local Database</button> <button class="recon-settings-btn recon-settings-btn-cancel" id="recon-btn-reset-settings">Reset Settings</button> </div> </div> `; content.appendChild(panelDatabase); modal.appendChild(content); const footer = document.createElement("div"); footer.className = "recon-settings-footer"; footer.innerHTML = ` <button class="recon-settings-btn recon-settings-btn-cancel recon-settings-close-action">Cancel</button> <button class="recon-settings-btn recon-settings-btn-save" id="recon-settings-save-btn">Save Changes</button> `; modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); const closeOverlay = () => { overlay.classList.remove("show"); setTimeout(() => overlay.remove(), 250); }; overlay.querySelector(".recon-settings-close")?.addEventListener("click", closeOverlay); overlay.querySelector(".recon-settings-close-action")?.addEventListener("click", closeOverlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeOverlay(); }); const chatSummaryChk = modal.querySelector("#recon-setting-chat-summary"); const keyRow = modal.querySelector("#recon-setting-gemini-key-row"); chatSummaryChk?.addEventListener("change", () => { if (keyRow) { keyRow.style.display = chatSummaryChk.checked ? "flex" : "none"; } }); const toggleVisible = modal.querySelector("#recon-toggle-gemini-key-visibility"); const keyInput = modal.querySelector("#recon-setting-gemini-key"); toggleVisible?.addEventListener("click", () => { if (keyInput) { if (keyInput.type === "password") { keyInput.type = "text"; toggleVisible.textContent = "\uD83D\uDE48"; } else { keyInput.type = "password"; toggleVisible.textContent = "\uD83D\uDC41️"; } } }); const tabEls = modal.querySelectorAll(".recon-settings-tab"); tabEls.forEach((tab) => { tab.addEventListener("click", () => { tabEls.forEach((t) => t.classList.remove("active")); tab.classList.add("active"); const targetTab = tab.getAttribute("data-tab"); modal.querySelectorAll(".recon-settings-panel").forEach((p) => p.classList.remove("active")); modal.querySelector(`#recon-panel-${targetTab}`)?.classList.add("active"); }); }); fetchDBMetrics(); modal.querySelector("#recon-btn-reset-settings")?.addEventListener("click", () => { if (confirm("Are you sure you want to restore settings to defaults?")) { saveSettings(defaultSettings); showToast("Settings restored to defaults!"); closeOverlay(); win.location.reload(); } }); modal.querySelector("#recon-btn-clear-db")?.addEventListener("click", () => { if (confirm("Are you sure you want to delete all cached profiles and locations? This cannot be undone.")) { clearIndexedDB().then(() => { showToast("Local database cleared!"); fetchDBMetrics(); }); } }); modal.querySelector("#recon-settings-save-btn")?.addEventListener("click", () => { const coordsRaw = modal.querySelector("#recon-setting-coords").value.trim(); let coordsParsed = ""; if (coordsRaw) { const parts = coordsRaw.split(","); if (parts.length === 2 && !isNaN(Number(parts[0])) && !isNaN(Number(parts[1]))) { coordsParsed = `${Number(parts[0]).toFixed(6)}, ${Number(parts[1]).toFixed(6)}`; } else { showToast("Invalid custom coordinates format! Use: lat, lon", "error"); return; } } const newSettings = { enableXhrInterceptor: modal.querySelector("#recon-setting-xhr").checked, enableFilterPanel: modal.querySelector("#recon-setting-filter").checked, enablePremiumPhotos: modal.querySelector("#recon-setting-photos").checked, enableDistanceMap: modal.querySelector("#recon-setting-map").checked, enableShowAsViewed: modal.querySelector("#recon-setting-viewed").checked, enableAiChatSummary: modal.querySelector("#recon-setting-chat-summary").checked, geminiApiKey: modal.querySelector("#recon-setting-gemini-key").value.trim(), triangulationProbes: Math.min(10, Math.max(3, Number(modal.querySelector("#recon-setting-probes").value) || 5)), probeOffsetMetres: Math.max(500, Number(modal.querySelector("#recon-setting-offset").value) || 5000), customCoordinates: coordsParsed, enableFakeLocation: modal.querySelector("#recon-setting-fake-location").checked }; saveSettings(newSettings); showToast("Settings saved successfully!"); closeOverlay(); setTimeout(() => win.location.reload(), 300); }); setTimeout(() => overlay.classList.add("show"), 50); } function fetchDBMetrics() { const profileSpan = document.getElementById("recon-db-profile-count"); const locationSpan = document.getElementById("recon-db-location-count"); try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; if (db.objectStoreNames.contains("Profile_Cache") && profileSpan) { const transaction = db.transaction("Profile_Cache", "readonly"); const store = transaction.objectStore("Profile_Cache"); const countReq = store.count(); countReq.onsuccess = () => { if (profileSpan) profileSpan.textContent = String(countReq.result); }; } else if (profileSpan) { profileSpan.textContent = "0 (Store missing)"; } if (db.objectStoreNames.contains("App_Locations") && locationSpan) { const transaction = db.transaction("App_Locations", "readonly"); const store = transaction.objectStore("App_Locations"); const countReq = store.count(); countReq.onsuccess = () => { if (locationSpan) locationSpan.textContent = String(countReq.result); }; } else if (locationSpan) { locationSpan.textContent = "0 (Store missing)"; } db.close(); }; request.onerror = () => { if (profileSpan) profileSpan.textContent = "Error"; if (locationSpan) locationSpan.textContent = "Error"; }; } catch (e) { if (profileSpan) profileSpan.textContent = "Error"; if (locationSpan) locationSpan.textContent = "Error"; } } function clearIndexedDB() { return new Promise((resolve) => { try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; const stores = Array.from(db.objectStoreNames); if (stores.length === 0) { db.close(); resolve(); return; } const transaction = db.transaction(stores, "readwrite"); stores.forEach((storeName) => { transaction.objectStore(storeName).clear(); }); transaction.oncomplete = () => { db.close(); resolve(); }; transaction.onerror = () => { db.close(); resolve(); }; }; request.onerror = () => resolve(); } catch (e) { resolve(); } }); } // src/locate.ts function degToRad(d) { return d * Math.PI / 180; } function radToDeg(r) { return r * 180 / Math.PI; } function offsetLatLon(lat, lon, northMetres, eastMetres) { const R = 6371000; const dLat = northMetres / R; const dLon = eastMetres / (R * Math.cos(degToRad(lat))); return { lat: lat + radToDeg(dLat), lon: lon + radToDeg(dLon) }; } function haversine(lat1, lon1, lat2, lon2) { const R = 6371000; const dLat = degToRad(lat2 - lat1); const dLon = degToRad(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function bearingTo(lat1, lon1, lat2, lon2) { const φ1 = degToRad(lat1), φ2 = degToRad(lat2); const Δ_ = degToRad(lon2 - lon1); const y = Math.sin(Δ_) * Math.cos(φ2); const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δ_); return Math.atan2(y, x); } function destinationPoint(lat, lon, bearingRad, distMetres) { const R = 6371000; const d = distMetres / R; const φ1 = degToRad(lat), λ1 = degToRad(lon); const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d) + Math.cos(φ1) * Math.sin(d) * Math.cos(bearingRad)); const λ2 = λ1 + Math.atan2(Math.sin(bearingRad) * Math.sin(d) * Math.cos(φ1), Math.cos(d) - Math.sin(φ1) * Math.sin(φ2)); return { lat: radToDeg(φ2), lon: radToDeg(λ2) }; } async function searchFromPoint(lat, lon, targetProfileId, myProfileId, age, radiusMetres = 40225) { const ageMin = age ? String(age) : "18"; const ageMax = age ? String(age) : "99"; const params = new URLSearchParams({ activeWithinMinutes: "525600", ageMax, ageMin, culture: "en", isExplore: "true", latitude: lat.toFixed(6), longitude: lon.toFixed(6), myProfileId, radiusMetres: String(radiusMetres), sortProperty: "distance" }); const url = `https://www.recon.com/api/profileSearch/profiles?${params.toString()}`; try { const res = await win.fetch(url, { headers: state.authHeaders }); if (!res.ok) { console.warn(`[Recon+ Locate] fetch failed with status ${res.status}`); return null; } const json = await res.json(); if (!json?.data) return null; const match = json.data.find((p) => p.profileId === targetProfileId); if (!match) return null; return { profileId: match.profileId, distanceMetres: match.distanceMetres }; } catch (e) { console.warn("[Recon+ Locate] fetch error", e); return null; } } function getMyProfileId() { const navLinks = Array.from(document.querySelectorAll("a[href]")); for (const a of navLinks) { const m = a.href.match(/\/profiles\/([a-z0-9-]{36})\/?/i); if (m && !a.href.includes("cruise") && !a.href.includes("bookmarks") && !a.href.includes("connections")) { if (a.querySelector(".profile-avatar") || a.classList.contains("nav-item-right-container") || a.classList.contains("mobile-nav-cell")) { if (a.closest("t101-nav-bar, t101-mobile-nav-bar")) { return m[1]; } } } } for (const a of navLinks) { const m = a.href.match(/\/profiles\/([a-z0-9-]{36})\/?$/i); if (m && a.closest("t101-nav-bar, t101-mobile-nav-bar")) return m[1]; } return null; } function getTargetProfileId() { const urlMatch = win.location.pathname.match(/\/profiles\/([a-f0-9-]{36})/i); if (urlMatch) return urlMatch[1]; if (state.lastProfileDetailPageData?.id) { return state.lastProfileDetailPageData.id; } for (const [id, data] of profileDetailsCache) { if (data.isDetailed) return id; } return null; } function getTargetAge() { const data = state.lastProfileDetailPageData; if (data && typeof data.age === "number" && data.age > 0) { return data.age; } const ageEl = document.querySelector('.age-value, .age, [class*="age-value"], [class*="age_value"]'); if (ageEl && ageEl.textContent) { const m = ageEl.textContent.match(/(\d+)/); if (m) return parseInt(m[1], 10); } const text = document.body.innerText; const match = text.match(/Age\s*:\s*(\d+)/i) || text.match(/(\d+)\s*years?\s*old/i); if (match) return parseInt(match[1], 10); return null; } function getUserPosition() { if (settings.customCoordinates) { const parts = settings.customCoordinates.split(","); if (parts.length === 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { return Promise.resolve({ lat, lon }); } } } return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("Geolocation not supported in this browser")); return; } navigator.geolocation.getCurrentPosition((pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), (err) => reject(new Error(`Geolocation denied: ${err.message}`)), { enableHighAccuracy: false, timeout: 12000, maximumAge: 120000 }); }); } function getProfileDistanceMetres() { const d = state.lastProfileDetailPageData?.distanceMetres; if (typeof d === "number" && d > 0) return d; const distEl = document.querySelector(".distance-value"); const text = distEl ? distEl.textContent : document.body.innerText; const kmMatch = text.match(/(\d[\d,]*\.?\d*)\s*km\b/i); if (kmMatch) { const v = parseFloat(kmMatch[1].replace(/,/g, "")) * 1000; if (v > 0) return v; } const miMatch = text.match(/(\d[\d,]*\.?\d*)\s*mi(?:le)?s?\b/i); if (miMatch) { const v = parseFloat(miMatch[1].replace(/,/g, "")) * 1609.34; if (v > 0) return v; } const ftMatch = text.match(/(\d[\d,]*\.?\d*)\s*ft\b/i); if (ftMatch) { const v = parseFloat(ftMatch[1].replace(/,/g, "")) * 0.3048; if (v > 0) return v; } const mMatch = text.match(/(\d[\d,]+)\s*m\b(?!\s*[il])/i); if (mMatch) { const v = parseFloat(mMatch[1].replace(/,/g, "")); if (v > 0) return v; } return null; } async function triangulate(targetProfileId, myProfileId, centerLat, centerLon, onProgress) { const targetAge = getTargetAge(); const filterLabel = targetAge ? `age ${targetAge}` : "broad search"; onProgress(`Probing origin (${filterLabel})…`); let originHit = await searchFromPoint(centerLat, centerLon, targetProfileId, myProfileId, targetAge); let usedAgeFilter = targetAge; if (!originHit && targetAge !== null) { onProgress("No hit at origin with age filter. Retrying with broad search…"); originHit = await searchFromPoint(centerLat, centerLon, targetProfileId, myProfileId, null); usedAgeFilter = null; } if (!originHit) { onProgress("Profile not found from origin search."); return null; } const apiDist = originHit.distanceMetres; onProgress(`Origin hit! Precise distance: ${Math.round(apiDist)}m`); const probeOffset = settings.probeOffsetMetres || Math.max(1000, Math.min(8000, apiDist * 0.6)); const searchRadius = Math.min(40225, Math.ceil(apiDist + probeOffset + 2000)); const numOffsetProbes = Math.max(2, settings.triangulationProbes - 1); const probeOffsets = []; for (let i = 0;i < numOffsetProbes; i++) { const angle = i * 2 * Math.PI / numOffsetProbes; const name = i === 0 ? "North" : i === 1 ? "East" : i === 2 ? "South" : i === 3 ? "West" : `Probe ${i + 1}`; probeOffsets.push({ name, north: probeOffset * Math.cos(angle), east: probeOffset * Math.sin(angle) }); } const probes = [ { lat: centerLat, lon: centerLon, isHit: true, distanceMetres: apiDist } ]; for (let i = 0;i < probeOffsets.length; i++) { const off = probeOffsets[i]; const { lat, lon } = offsetLatLon(centerLat, centerLon, off.north, off.east); await new Promise((r) => setTimeout(r, 600)); onProgress(`Scanning ${i + 2}/${probeOffsets.length + 1} (${off.name})…`); const hit = await searchFromPoint(lat, lon, targetProfileId, myProfileId, usedAgeFilter, searchRadius); if (hit) { probes.push({ lat, lon, isHit: true, distanceMetres: hit.distanceMetres }); } else { probes.push({ lat, lon, isHit: false, distanceMetres: 0 }); } } onProgress("Resolving location using positive and negative constraints…"); const getPenalty = (lat, lon) => { let penalty = 0; for (const probe of probes) { const distToCandidate = haversine(probe.lat, probe.lon, lat, lon); if (probe.isHit) { penalty += Math.abs(distToCandidate - probe.distanceMetres); } else { if (distToCandidate < searchRadius - 1000) { penalty += (searchRadius - 1000 - distToCandidate) * 5; } } } return penalty; }; let bestLat = centerLat; let bestLon = centerLon; let minPenalty = Infinity; let bestR = apiDist; let bestTheta = 0; for (let rOffset = -1500;rOffset <= 1500; rOffset += 100) { const r = apiDist + rOffset; if (r < 0) continue; for (let deg = 0;deg < 360; deg += 0.5) { const bearingRad = degToRad(deg); const candidate = destinationPoint(centerLat, centerLon, bearingRad, r); const penalty = getPenalty(candidate.lat, candidate.lon); if (penalty < minPenalty) { minPenalty = penalty; bestLat = candidate.lat; bestLon = candidate.lon; bestR = r; bestTheta = deg; } } } for (let rOffset = -150;rOffset <= 150; rOffset += 10) { const r = bestR + rOffset; if (r < 0) continue; for (let degOffset = -1;degOffset <= 1; degOffset += 0.05) { const deg = (bestTheta + degOffset + 360) % 360; const bearingRad = degToRad(deg); const candidate = destinationPoint(centerLat, centerLon, bearingRad, r); const penalty = getPenalty(candidate.lat, candidate.lon); if (penalty < minPenalty) { minPenalty = penalty; bestLat = candidate.lat; bestLon = candidate.lon; } } } const radiusMetres = Math.max(100, Math.min(1e4, minPenalty)); return { lat: bestLat, lon: bestLon, radiusMetres }; } function ensureLeafletCss() { if (document.querySelector("#recon-leaflet-css")) return; try { const gmGet = typeof GM_getResourceText !== "undefined" ? GM_getResourceText : win.GM_getResourceText; const gmAdd = typeof GM_addStyle !== "undefined" ? GM_addStyle : win.GM_addStyle; if (gmGet && gmAdd) { const css = gmGet("LEAFLET_CSS"); if (css) { const style = document.createElement("style"); style.id = "recon-leaflet-css"; style.textContent = css; document.head.appendChild(style); return; } } } catch (_) {} const link = document.createElement("link"); link.id = "recon-leaflet-css"; link.rel = "stylesheet"; link.href = "https://unpkg.com/[email protected]/dist/leaflet.css"; document.head.appendChild(link); } function waitForLeaflet(timeoutMs = 8000) { const check = () => typeof L !== "undefined" && L.map ? L : win.L && win.L.map ? win.L : null; const immediate = check(); if (immediate) return Promise.resolve(immediate); return new Promise((resolve, reject) => { const start = Date.now(); const poll = setInterval(() => { const leaflet = check(); if (leaflet) { clearInterval(poll); resolve(leaflet); } else if (Date.now() - start > timeoutMs) { clearInterval(poll); reject(new Error("Leaflet not available — please reinstall the script to refresh @require cache.")); } }, 150); }); } function renderDualMap(container, L2, primary, secondary) { ensureLeafletCss(); if (container._leafletMap) { try { container._leafletMap.remove(); } catch (_) {} } container.innerHTML = ""; container.style.cssText = ` width: 100%; height: 360px; border-radius: 12px; overflow: hidden; margin-top: 16px; position: relative; box-shadow: 0 4px 24px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.08); `; const MAP_HEIGHT = 360; const mapDiv = document.createElement("div"); mapDiv.style.cssText = `width:100%;height:${MAP_HEIGHT}px;`; container.appendChild(mapDiv); const zoomForRadius = (r) => { if (r < 300) return 15; if (r < 800) return 14; if (r < 2000) return 13; if (r < 5000) return 12; if (r < 15000) return 11; if (r < 40000) return 10; return 8; }; const map = L2.map(mapDiv, { zoomControl: true, attributionControl: true, preferCanvas: false }); container._leafletMap = map; const CspTileLayer = L2.TileLayer.extend({ createTile: function(coords, done) { const tile = document.createElement("img"); tile.alt = ""; tile.setAttribute("role", "presentation"); const url = this.getTileUrl(coords); const gmXhr = typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : win.GM_xmlhttpRequest; if (gmXhr) { gmXhr({ method: "GET", url, responseType: "blob", onload: function(response) { if (response.status === 200 && response.response) { const blobUrl = URL.createObjectURL(response.response); tile.onload = () => { done(null, tile); URL.revokeObjectURL(blobUrl); }; tile.onerror = () => { done(new Error("Tile failed"), tile); URL.revokeObjectURL(blobUrl); }; tile.src = blobUrl; } else { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); } }.bind(this), onerror: function() { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); }.bind(this) }); } else { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); } return tile; } }); new CspTileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>', subdomains: "abcd", maxZoom: 19 }).addTo(map); const addLayer = (layer) => { if (!layer.pinOnly) { L2.circle([layer.lat, layer.lon], { radius: layer.radiusMetres, color: layer.color, fillColor: layer.color, fillOpacity: layer.ringOnly ? 0.06 : 0.18, weight: 2, opacity: layer.ringOnly ? 0.5 : 0.8, dashArray: layer.ringOnly ? "6 4" : undefined }).addTo(map); } const size = layer.ringOnly && !layer.pinOnly ? 10 : 14; const icon = L2.divIcon({ className: "", html: `<div style="width:${size}px;height:${size}px;background:${layer.color};border-radius:50%;border:2px solid #fff;box-shadow:0 0 8px ${layer.color}bb;"></div>`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); L2.marker([layer.lat, layer.lon], { icon }).addTo(map); }; if (secondary) addLayer(secondary); addLayer(primary); let lineDistanceMetres = null; if (secondary) { lineDistanceMetres = haversine(primary.lat, primary.lon, secondary.lat, secondary.lon); L2.polyline([[primary.lat, primary.lon], [secondary.lat, secondary.lon]], { color: "#ffffff", weight: 1.5, opacity: 0.6, dashArray: "5 6" }).addTo(map); const midLat = (primary.lat + secondary.lat) / 2; const midLon = (primary.lon + secondary.lon) / 2; const distLabel = lineDistanceMetres >= 1000 ? `${(lineDistanceMetres / 1000).toFixed(2)} km` : `${Math.round(lineDistanceMetres)} m`; const labelIcon = L2.divIcon({ className: "", html: `<div style="background:rgba(14,14,20,0.88);color:#fff;font-size:10px;font-family:system-ui,sans-serif;font-weight:600;padding:2px 7px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);white-space:nowrap;pointer-events:none;">${distLabel}</div>`, iconAnchor: [30, 10], iconSize: [60, 20] }); L2.marker([midLat, midLon], { icon: labelIcon, interactive: false }).addTo(map); } const boundsFromLayer = (layer) => { const R = 6371000; const r = Math.max(layer.radiusMetres, 200); const dLat = r / R * (180 / Math.PI); const dLon = r / (R * Math.cos(layer.lat * Math.PI / 180)) * (180 / Math.PI); return L2.latLngBounds([layer.lat - dLat, layer.lon - dLon], [layer.lat + dLat, layer.lon + dLon]); }; let bounds = boundsFromLayer(primary); if (secondary) bounds = bounds.extend(boundsFromLayer(secondary)); const legendRows = [ `<div style="display:flex;align-items:center;gap:6px;"> <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${primary.color};"></span> <span style="color:${primary.color};font-weight:700;">${primary.label}</span> </div> <div style="color:#aaa;font-size:11px;margin-left:16px;">${primary.subtitle}</div>` ]; if (secondary) { legendRows.push(`<div style="display:flex;align-items:center;gap:6px;margin-top:6px;"> <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${secondary.color};border:2px solid rgba(255,255,255,0.4);"></span> <span style="color:${secondary.color};font-weight:700;">${secondary.label}</span> </div> <div style="color:#aaa;font-size:11px;margin-left:16px;">${secondary.subtitle}</div>`); } if (lineDistanceMetres !== null) { const distLabel = lineDistanceMetres >= 1000 ? `${(lineDistanceMetres / 1000).toFixed(2)} km` : `${Math.round(lineDistanceMetres)} m`; legendRows.push(`<div style="margin-top:8px;padding-top:7px;border-top:1px solid rgba(255,255,255,0.12);display:flex;align-items:center;gap:6px;"> <span style="color:#fff;font-size:11px;">\uD83D\uDCCF Distance between pins:</span> <span style="color:#fff;font-weight:700;font-size:12px;">${distLabel}</span> </div>`); } const overlay = document.createElement("div"); overlay.style.cssText = ` position: absolute; top: 10px; left: 10px; z-index: 1000; background: rgba(14,14,20,0.93); backdrop-filter: blur(10px); color: #fff; font-family: system-ui, sans-serif; font-size: 12px; padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.12); pointer-events: none; line-height: 1.6; min-width: 180px; `; overlay.innerHTML = legendRows.join(""); container.appendChild(overlay); const doSetView = () => { map.invalidateSize({ animate: false }); map.fitBounds(bounds, { padding: [24, 24], animate: false, maxZoom: zoomForRadius(primary.radiusMetres) }); }; setTimeout(doSetView, 50); setTimeout(doSetView, 300); } function renderLocateMap(container, L2, lat, lon, radiusMetres, label = "\uD83D\uDCCD Estimated Location", subtitle = `±${Math.round(radiusMetres)}m accuracy`, color = "#e32222") { renderDualMap(container, L2, { lat, lon, radiusMetres, color, label, subtitle }); } function injectDistanceMap() { if (!settings.enableDistanceMap) { document.getElementById("recon-locate-map")?.remove(); return; } if (!win.location.pathname.match(/\/profiles\/[^/]+/i)) return; if (document.getElementById("recon-locate-map")) return; const anchor = document.querySelector("t101-profile-extra-info") || document.querySelector(".right-wrapper > t101-profile-extra-info") || document.querySelector(".laptop-background") || document.querySelector(".right-wrapper"); if (!anchor) return; const distMetres = getProfileDistanceMetres(); if (!distMetres || distMetres <= 0) return; const mapContainer = document.createElement("div"); mapContainer.id = "recon-locate-map"; mapContainer.style.cssText = ` width: 100%; margin-top: 20px; border-radius: 12px; overflow: hidden; position: relative; box-shadow: 0 4px 24px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.08); `; anchor.after(mapContainer); Promise.all([waitForLeaflet(), getUserPosition()]).then(([L2, pos]) => { renderLocateMap(mapContainer, L2, pos.lat, pos.lon, distMetres, "\uD83D\uDCCD Approximate Area", `Profile is within ${Math.round(distMetres)}m of your position`, "#3b82f6"); }).catch((err) => { console.warn("[Recon+ Map] Could not render distance map:", err?.message || err); mapContainer.remove(); }); } function injectLocateButton() { injectDistanceMap(); if (!win.location.pathname.match(/\/profiles\/[^/]+/i)) return; if (document.getElementById("recon-locate-btn")) return; let anchor = document.getElementById("custom-export-profile-btn"); if (!anchor) { const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])'); if (cruiseBtn) { anchor = cruiseBtn.closest("button") || cruiseBtn.closest("a") || cruiseBtn; } } if (!anchor) return; const btn = document.createElement("button"); btn.id = "recon-locate-btn"; btn.textContent = "\uD83D\uDD3A Triangulate"; btn.title = "Probe from multiple points to narrow down the actual location"; btn.style.cssText = `background-color: #1a2a1a; color: #4ade80; border: 1px solid #4ade8055; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`; btn.onmouseover = () => btn.style.backgroundColor = "#243324"; btn.onmouseout = () => btn.style.backgroundColor = "#1a2a1a"; if (anchor.id === "custom-export-profile-btn") { anchor.parentNode.insertBefore(btn, anchor.nextSibling); } else { anchor.parentNode.insertBefore(btn, anchor.nextSibling); } btn.addEventListener("click", async () => { btn.disabled = true; btn.textContent = "⏳ Triangulating…"; const setStatus = (msg) => { btn.textContent = `⏳ ${msg}`; }; try { const [L2, pos] = await Promise.all([waitForLeaflet(), getUserPosition()]); const targetId = getTargetProfileId(); const myId = getMyProfileId(); if (!targetId) { showToast("Could not determine target profile ID.", "error"); btn.disabled = false; btn.textContent = "\uD83D\uDD3A Triangulate"; return; } if (!myId) { showToast("Could not determine your profile ID. Make sure you are logged in.", "error"); btn.disabled = false; btn.textContent = "\uD83D\uDD3A Triangulate"; return; } const result = await triangulate(targetId, myId, pos.lat, pos.lon, setStatus); const dist = getProfileDistanceMetres(); document.getElementById("recon-locate-map")?.remove(); const mapContainer = document.createElement("div"); mapContainer.id = "recon-locate-map"; mapContainer.style.cssText = ` width: 100%; margin-top: 20px; border-radius: 12px; overflow: hidden; position: relative; box-shadow: 0 4px 24px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.08); `; const anchor2 = document.querySelector("t101-profile-extra-info") || document.querySelector(".right-wrapper > t101-profile-extra-info") || document.querySelector(".laptop-background") || document.querySelector(".right-wrapper"); if (anchor2) anchor2.after(mapContainer); if (!dist) { showToast("Distance not found on page — cannot plot location.", "error"); mapContainer.remove(); } else if (result) { const bearing = bearingTo(pos.lat, pos.lon, result.lat, result.lon); const userLayer = { lat: pos.lat, lon: pos.lon, radiusMetres: dist, color: "#3b82f6", label: "\uD83D\uDCCD Your Position", subtitle: `~${Math.round(dist)}m reported distance`, ringOnly: true }; const targetLayer = { lat: result.lat, lon: result.lon, radiusMetres: result.radiusMetres, color: "#f97316", label: "\uD83D\uDCCC Estimated Location", subtitle: `±${Math.round(result.radiusMetres)}m accuracy · bearing ${Math.round(radToDeg(bearing + 2 * Math.PI) % 360)}°`, pinOnly: false }; renderDualMap(mapContainer, L2, targetLayer, userLayer); showToast(`Location estimated — ±${Math.round(result.radiusMetres)}m accuracy`, "success"); } else { renderDualMap(mapContainer, L2, { lat: pos.lat, lon: pos.lon, radiusMetres: dist, color: "#f59e0b", label: "\uD83D\uDCCD Distance Ring", subtitle: `Profile within ~${Math.round(dist)}m`, ringOnly: false }); showToast("Triangulation inconclusive — showing distance ring only.", "warning"); } mapContainer.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (e) { showToast(`Error: ${e?.message}`, "error"); } btn.disabled = false; btn.textContent = "\uD83D\uDD3A Triangulate"; }); } function setupGeolocationMock() { if (!win.navigator || !win.navigator.geolocation) return; const origGetCurrentPosition = win.navigator.geolocation.getCurrentPosition; const origWatchPosition = win.navigator.geolocation.watchPosition; win.navigator.geolocation.getCurrentPosition = function(success, error, options) { if (settings.enableFakeLocation && settings.customCoordinates) { const parts = settings.customCoordinates.split(","); if (parts.length === 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { setTimeout(() => { success({ coords: { latitude: lat, longitude: lon, accuracy: 15, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }); }, 0); return; } } } origGetCurrentPosition.call(win.navigator.geolocation, success, error, options); }; win.navigator.geolocation.watchPosition = function(success, error, options) { if (settings.enableFakeLocation && settings.customCoordinates) { const parts = settings.customCoordinates.split(","); if (parts.length === 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { const sendPosition = () => { success({ coords: { latitude: lat, longitude: lon, accuracy: 15, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }); }; setTimeout(sendPosition, 0); const intervalId = setInterval(sendPosition, 1e4); return intervalId; } } } return origWatchPosition.call(win.navigator.geolocation, success, error, options); }; } function openMapPickerModal(currentValue, onSelect) { ensureLeafletCss(); let initialLat = 51.5074; let initialLon = -0.1278; if (currentValue) { const parts = currentValue.split(","); if (parts.length === 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { initialLat = lat; initialLon = lon; } } } const overlay = document.createElement("div"); overlay.id = "recon-map-picker-overlay"; overlay.style.cssText = ` position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background-color: rgba(0, 0, 0, 0.8) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; z-index: 120000 !important; display: flex !important; align-items: center !important; justify-content: center !important; box-sizing: border-box !important; `; const modal = document.createElement("div"); modal.style.cssText = ` background-color: #0b0b0d !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 12px !important; width: 90% !important; max-width: 700px !important; height: 80vh !important; max-height: 550px !important; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8) !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; font-family: 'Raleway', sans-serif; color: #fff !important; box-sizing: border-box !important; `; const header = document.createElement("div"); header.style.cssText = ` padding: 16px 20px !important; border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; display: flex !important; justify-content: space-between !important; align-items: center !important; `; header.innerHTML = ` <h3 style="font-family: 'Barlow', sans-serif !important; font-size: 18px !important; font-weight: 800 !important; margin: 0 !important; text-transform: uppercase; color: #ff3333; letter-spacing: 0.5px;">Select GPS Location</h3> <span id="recon-picker-close" style="color: rgba(255,255,255,0.5); font-size: 20px; cursor: pointer; user-select: none;">✕</span> `; modal.appendChild(header); const mapDiv = document.createElement("div"); mapDiv.style.cssText = ` flex-grow: 1 !important; width: 100% !important; background: #111 !important; position: relative !important; `; modal.appendChild(mapDiv); const footer = document.createElement("div"); footer.style.cssText = ` padding: 14px 20px !important; border-top: 1px solid rgba(255, 255, 255, 0.08) !important; display: flex !important; justify-content: space-between !important; align-items: center !important; background: rgba(0, 0, 0, 0.2) !important; `; const coordDisplay = document.createElement("div"); coordDisplay.style.cssText = `font-size: 13px; color: #adadad; font-family: monospace;`; coordDisplay.textContent = `Selected: ${initialLat.toFixed(6)}, ${initialLon.toFixed(6)}`; const btnContainer = document.createElement("div"); btnContainer.style.cssText = `display: flex; gap: 10px;`; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancel"; cancelBtn.style.cssText = ` background: rgba(255, 255, 255, 0.06); color: #adadad; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 4px; padding: 6px 12px; font-size: 12px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-family: 'Barlow', sans-serif; `; const saveBtn = document.createElement("button"); saveBtn.textContent = "Select Location"; saveBtn.style.cssText = ` background: #ff3333; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 12px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-family: 'Barlow', sans-serif; `; btnContainer.appendChild(cancelBtn); btnContainer.appendChild(saveBtn); footer.appendChild(coordDisplay); footer.appendChild(btnContainer); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); let selectedLat = initialLat; let selectedLon = initialLon; waitForLeaflet().then((L2) => { const map = L2.map(mapDiv, { center: [initialLat, initialLon], zoom: 12, zoomControl: true }); const CspTileLayer = L2.TileLayer.extend({ createTile: function(coords, done) { const tile = document.createElement("img"); tile.alt = ""; tile.setAttribute("role", "presentation"); const url = this.getTileUrl(coords); const gmXhr = typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : win.GM_xmlhttpRequest; if (gmXhr) { gmXhr({ method: "GET", url, responseType: "blob", onload: function(response) { if (response.status === 200 && response.response) { const blobUrl = URL.createObjectURL(response.response); tile.onload = () => { done(null, tile); URL.revokeObjectURL(blobUrl); }; tile.onerror = () => { done(new Error("Tile failed"), tile); URL.revokeObjectURL(blobUrl); }; tile.src = blobUrl; } else { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); } }.bind(this), onerror: function() { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); }.bind(this) }); } else { tile.src = url; L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile)); L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile)); } return tile; } }); new CspTileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { attribution: "© OpenStreetMap, © CARTO", maxZoom: 19 }).addTo(map); const getMarkerIcon = (color) => L2.divIcon({ className: "", html: `<div style="width:14px;height:14px;background:${color};border-radius:50%;border:2px solid #fff;box-shadow:0 0 8px ${color}bb;"></div>`, iconSize: [14, 14], iconAnchor: [7, 7] }); let marker = L2.marker([initialLat, initialLon], { icon: getMarkerIcon("#ff3333") }).addTo(map); map.on("click", (e) => { selectedLat = e.latlng.lat; selectedLon = e.latlng.lng; marker.setLatLng(e.latlng); coordDisplay.textContent = `Selected: ${selectedLat.toFixed(6)}, ${selectedLon.toFixed(6)}`; }); setTimeout(() => map.invalidateSize(), 100); }); const close = () => overlay.remove(); overlay.querySelector("#recon-picker-close")?.addEventListener("click", close); cancelBtn.addEventListener("click", close); saveBtn.addEventListener("click", () => { onSelect(`${selectedLat.toFixed(6)}, ${selectedLon.toFixed(6)}`); close(); }); } function injectQuickGpsPanel() { const tabSwitcher = document.querySelector("t101-profile-navbar-tabs") || document.querySelector(".profile-list-links"); if (!tabSwitcher) return; if (document.getElementById("recon-quick-gps-panel")) return; const panel = document.createElement("div"); panel.id = "recon-quick-gps-panel"; panel.style.cssText = ` background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 12px 16px; margin: 10px 0 16px 0; font-family: 'Raleway', sans-serif; display: flex; align-items: center; gap: 16px; justify-content: space-between; flex-wrap: wrap; width: 100%; box-sizing: border-box; `; const left = document.createElement("div"); left.style.cssText = `display: flex; align-items: center; gap: 12px;`; const iconSpan = document.createElement("span"); iconSpan.textContent = "\uD83D\uDCCD"; iconSpan.style.fontSize = "18px"; const titleDiv = document.createElement("div"); titleDiv.style.cssText = `display: flex; flex-direction: column;`; titleDiv.innerHTML = ` <span style="font-family: 'Barlow', sans-serif; font-size: 14px; font-weight: 800; color: #fff; text-transform: uppercase; letter-spacing: 0.5px;">Mock GPS Location</span> <span style="font-size: 11px; color: #adadad;">Quickly change coordinates to find nearby guys</span> `; left.appendChild(iconSpan); left.appendChild(titleDiv); panel.appendChild(left); const right = document.createElement("div"); right.style.cssText = `display: flex; align-items: center; gap: 12px; flex-wrap: wrap;`; const coordsInput = document.createElement("input"); coordsInput.type = "text"; coordsInput.id = "recon-quick-coords-input"; coordsInput.placeholder = "lat, lon (e.g. 51.5074, -0.1278)"; coordsInput.value = settings.customCoordinates || ""; coordsInput.style.cssText = ` background: rgba(0, 0, 0, 0.25); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: #fff; padding: 6px 12px; font-size: 13px; outline: none; width: 220px; font-family: 'Raleway', sans-serif; `; const mapPickerBtn = document.createElement("button"); mapPickerBtn.innerHTML = "\uD83D\uDDFA️"; mapPickerBtn.title = "Open Map Picker"; mapPickerBtn.style.cssText = ` background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: #fff; padding: 5px 8px; font-size: 16px; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; justify-content: center; flex-shrink: 0; `; mapPickerBtn.onmouseover = () => mapPickerBtn.style.background = "rgba(255, 255, 255, 0.12)"; mapPickerBtn.onmouseout = () => mapPickerBtn.style.background = "rgba(255, 255, 255, 0.06)"; mapPickerBtn.addEventListener("click", (e) => { e.preventDefault(); openMapPickerModal(coordsInput.value, (newCoords) => { coordsInput.value = newCoords; }); }); const switchLabel = document.createElement("label"); switchLabel.className = "recon-switch"; switchLabel.style.cssText = `margin: 0; display: inline-flex; align-items: center; cursor: pointer;`; switchLabel.innerHTML = ` <input type="checkbox" id="recon-quick-fake-toggle" ${settings.enableFakeLocation ? "checked" : ""} style="display: none;"> <span class="recon-slider"></span> `; const applyBtn = document.createElement("button"); applyBtn.textContent = "Apply"; applyBtn.style.cssText = ` background: #ff3333; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 12px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-family: 'Barlow', sans-serif; transition: background 0.2s; `; applyBtn.onmouseover = () => applyBtn.style.background = "#cc0000"; applyBtn.onmouseout = () => applyBtn.style.background = "#ff3333"; right.appendChild(coordsInput); right.appendChild(mapPickerBtn); right.appendChild(switchLabel); right.appendChild(applyBtn); panel.appendChild(right); applyBtn.addEventListener("click", () => { const toggleVal = panel.querySelector("#recon-quick-fake-toggle").checked; const coordsRaw = coordsInput.value.trim(); let coordsParsed = ""; if (coordsRaw) { const parts = coordsRaw.split(","); if (parts.length === 2 && !isNaN(Number(parts[0])) && !isNaN(Number(parts[1]))) { coordsParsed = `${Number(parts[0]).toFixed(6)}, ${Number(parts[1]).toFixed(6)}`; } else { showToast("Invalid coordinates format! Use: lat, lon", "error"); return; } } settings.enableFakeLocation = toggleVal; settings.customCoordinates = coordsParsed; const stored = win.localStorage.getItem("recon_plus_settings"); const parsed = stored ? JSON.parse(stored) : {}; parsed.enableFakeLocation = toggleVal; parsed.customCoordinates = coordsParsed; win.localStorage.setItem("recon_plus_settings", JSON.stringify(parsed)); showToast("Mock GPS updated successfully!"); setTimeout(() => win.location.reload(), 300); }); tabSwitcher.parentNode.insertBefore(panel, tabSwitcher.nextSibling); } // src/lightbox.ts function setupPhotoViewer() { const tiles = document.querySelectorAll(".photo-tile:not([data-lightbox-ready])"); tiles.forEach((tile) => { const htmlTile = tile; htmlTile.setAttribute("data-lightbox-ready", "true"); htmlTile.style.pointerEvents = "auto"; htmlTile.style.cursor = "pointer"; htmlTile.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const container = htmlTile.closest(".member-photos, t101-profile-photos, .gallery-grid, .photo-grid") || document.body; const allTiles = Array.from(container.querySelectorAll(".photo-tile")); const urls = allTiles.map((t) => { const htmlEl = t; const bgImg2 = htmlEl.style.backgroundImage; return bgImg2 ? bgImg2.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0] : null; }).filter((b) => Boolean(b)); const bgImg = htmlTile.style.backgroundImage; if (!bgImg) return; let currentUrl = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0]; let currentIndex = urls.indexOf(currentUrl); if (currentIndex === -1) { currentIndex = 0; urls.unshift(currentUrl); } createFullscreenLightbox(urls, currentIndex); }); }); } function createFullscreenLightbox(urls, startIndex) { if (document.getElementById("custom-photo-lightbox")) return; document.documentElement.style.setProperty("overflow", "hidden", "important"); document.body.style.setProperty("overflow", "hidden", "important"); let currentIndex = startIndex; const overlay = document.createElement("div"); overlay.id = "custom-photo-lightbox"; overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.95); z-index: 100000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.25s ease;`; const img = document.createElement("img"); img.src = urls[currentIndex]; img.style.cssText = `max-width: 90%; max-height: 90%; object-fit: contain; box-shadow: 0 0 20px rgba(0,0,0,0.8); border-radius: 4px; user-select: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);`; const closeBtn = document.createElement("div"); closeBtn.innerHTML = "✕"; closeBtn.style.cssText = `position: absolute; top: 20px; right: 25px; color: #ffffff; font-size: 35px; cursor: pointer; user-select: none; z-index: 100002; transition: transform 0.1s;`; function updateImage(index) { currentIndex = (index + urls.length) % urls.length; img.style.transform = "scale(0.97)"; img.style.opacity = "0"; setTimeout(() => { img.src = urls[currentIndex]; img.onload = () => { img.style.transform = "scale(1)"; img.style.opacity = "1"; }; }, 150); } if (urls.length > 1) { const leftBtn = document.createElement("div"); leftBtn.innerHTML = "❮"; leftBtn.style.cssText = `position: absolute; left: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`; leftBtn.onmouseover = () => { leftBtn.style.background = "rgba(255,255,255,0.2)"; leftBtn.style.transform = "translateY(-50%) scale(1.08)"; }; leftBtn.onmouseout = () => { leftBtn.style.background = "rgba(255,255,255,0.08)"; leftBtn.style.transform = "translateY(-50%) scale(1)"; }; leftBtn.addEventListener("click", (e) => { e.stopPropagation(); updateImage(currentIndex - 1); }); const rightBtn = document.createElement("div"); rightBtn.innerHTML = "❯"; rightBtn.style.cssText = `position: absolute; right: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`; rightBtn.onmouseover = () => { rightBtn.style.background = "rgba(255,255,255,0.2)"; rightBtn.style.transform = "translateY(-50%) scale(1.08)"; }; rightBtn.onmouseout = () => { rightBtn.style.background = "rgba(255,255,255,0.08)"; rightBtn.style.transform = "translateY(-50%) scale(1)"; }; rightBtn.addEventListener("click", (e) => { e.stopPropagation(); updateImage(currentIndex + 1); }); overlay.appendChild(leftBtn); overlay.appendChild(rightBtn); } const handleKeyDown = (e) => { if (e.key === "ArrowLeft" && urls.length > 1) { updateImage(currentIndex - 1); } else if (e.key === "ArrowRight" && urls.length > 1) { updateImage(currentIndex + 1); } else if (e.key === "Escape") { closeLightbox(); } }; const closeLightbox = () => { win.removeEventListener("keydown", handleKeyDown); overlay.style.opacity = "0"; setTimeout(() => { overlay.remove(); document.documentElement.style.removeProperty("overflow"); document.body.style.removeProperty("overflow"); }, 250); }; win.addEventListener("keydown", handleKeyDown); closeBtn.addEventListener("click", closeLightbox); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeLightbox(); }); overlay.appendChild(img); overlay.appendChild(closeBtn); document.body.appendChild(overlay); setTimeout(() => overlay.style.opacity = "1", 10); } // src/dom.ts function paintProfileData(profileId) { const card = document.getElementById(profileId); if (!card || card.dataset.reconInjected) return; const data = profileDetailsCache.get(profileId); const textContainer = card.querySelector(".text-container"); if (!data || !textContainer) return; card.dataset.reconInjected = "true"; if (data.location && data.location.locationId) { let locDiv = document.createElement("div"); locDiv.className = "custom-injected-location"; locDiv.style.cssText = `font-family: 'Raleway', sans-serif; font-size: 13px; font-weight: 700; color: #e32222; margin: 4px 0; text-align: left;`; locDiv.innerHTML = `\uD83D\uDCCD Loading...`; const nameNode = textContainer.querySelector(".name"); if (nameNode) nameNode.after(locDiv); else textContainer.prepend(locDiv); const rawLocId = data.location.locationId; if (isNaN(Number(rawLocId))) { locDiv.innerHTML = `\uD83D\uDCCD ${rawLocId}`; } else { getLocationFromIndexedDB(rawLocId).then((cachedLoc) => { if (cachedLoc) { const locName = cachedLoc.name || cachedLoc.longName || rawLocId; locDiv.innerHTML = `\uD83D\uDCCD ${locName}`; } else { let fetchUrl = data.location.locationUrl || `https://www.recon.com/api/location/locations/${rawLocId}`; if (!fetchUrl.includes("culture=")) { fetchUrl += (fetchUrl.includes("?") ? "&" : "?") + "culture=en"; } win.fetch(fetchUrl, { headers: { ...state.authHeaders } }).then((res) => res.json()).then((locData) => { const name = locData.shortLocationName || locData.name || locData.longLocationName; if (name) { const longName = locData.longLocationName || locData.longName || name; locDiv.innerHTML = `\uD83D\uDCCD ${name}`; saveLocationToIndexedDB(rawLocId, fetchUrl, name, longName); } else { locDiv.innerHTML = `\uD83D\uDCCD Loc ID: ${rawLocId}`; } }).catch(() => { locDiv.innerHTML = `\uD83D\uDCCD Loc ID: ${rawLocId}`; }); } }); } } if (data.interests && data.interests.length > 0) { let intDiv = document.createElement("div"); intDiv.className = "custom-injected-interests"; intDiv.style.cssText = `display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; justify-content: flex-start;`; data.interests.forEach((item) => { let id = item; let text = ""; if (item && typeof item === "object") { id = item.interestId || item.id || item.interest; text = item.name || item.text || item.label; } let span = document.createElement("span"); span.style.cssText = `background-color: rgba(255, 255, 255, 0.2); color: #fff; padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: bold;`; span.textContent = text || interestMap[id] || `Tag: ${id}`; intDiv.appendChild(span); }); textContainer.appendChild(intDiv); } } function paintProfileDetailPage(data) { if (!data || !data.lastUpdatedDate) return; let container = document.querySelector(".profile-header, t101-profile-header, .member-info, .profile-info-container"); if (!container) { container = document.querySelector("h1"); } if (!container) { const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])'); if (cruiseBtn) { container = cruiseBtn.parentElement; } } if (!container) return; if (document.getElementById("custom-profile-last-updated")) return; const dateDiv = document.createElement("div"); dateDiv.id = "custom-profile-last-updated"; dateDiv.style.cssText = ` font-family: 'Raleway', sans-serif; font-size: 14px; font-weight: 700; color: #adadad; margin: 10px 0; display: inline-flex; align-items: center; gap: 6px; `; try { const dateObj = new Date(data.lastUpdatedDate); const formattedDate = dateObj.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); dateDiv.innerHTML = `\uD83D\uDCC5 Profile Updated: <span style="color: #fff; font-weight: 800;">${formattedDate}</span>`; } catch (e) { dateDiv.innerHTML = `\uD83D\uDCC5 Profile Updated: <span style="color: #fff; font-weight: 800;">${data.lastUpdatedDate}</span>`; } if (container.tagName === "H1") { container.after(dateDiv); } else { container.appendChild(dateDiv); } } function injectViewedButton() { const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])'); if (cruiseBtn) { const targetEl = cruiseBtn.closest("button") || cruiseBtn.closest("a") || cruiseBtn; if (settings.enableShowAsViewed) { let viewBtn = document.getElementById("manual-view-btn"); if (!viewBtn) { viewBtn = document.createElement("button"); viewBtn.id = "manual-view-btn"; viewBtn.textContent = "Show As Viewed"; viewBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`; viewBtn.onmouseover = () => viewBtn.style.backgroundColor = "#3d3d3d"; viewBtn.onmouseout = () => viewBtn.style.backgroundColor = "#2b2b2b"; viewBtn.addEventListener("click", () => { if (!state.lastBlockedRequest) return showToast("No profile visit tracker captured yet.", "error"); const xhr = new origXHR; xhr._isManualTrigger = true; xhr.open(state.lastBlockedRequest.method, state.lastBlockedRequest.url); if (state.lastBlockedRequest.headers) { for (const [key, value] of Object.entries(state.lastBlockedRequest.headers)) { xhr.setRequestHeader(key, value); } } xhr.onload = function() { if (this.status >= 200 && this.status < 300) { showToast("Profile marked as viewed successfully!"); } else { let errorMsg = `Failed to contact server: Status ${this.status}`; try { const errData = JSON.parse(this.responseText); if (errData && errData.title) { errorMsg = errData.title; } } catch (e) {} showToast(errorMsg, "error"); } }; xhr.onerror = () => showToast("Failed to contact server.", "error"); xhr.send(state.lastBlockedRequest.body); }); targetEl.parentNode.insertBefore(viewBtn, targetEl.nextSibling); } if (!document.getElementById("custom-export-profile-btn")) { const exportBtn = document.createElement("button"); exportBtn.id = "custom-export-profile-btn"; exportBtn.textContent = "Export Profile"; exportBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`; exportBtn.onmouseover = () => exportBtn.style.backgroundColor = "#3d3d3d"; exportBtn.onmouseout = () => exportBtn.style.backgroundColor = "#2b2b2b"; exportBtn.addEventListener("click", exportProfileData); viewBtn.parentNode.insertBefore(exportBtn, viewBtn.nextSibling); } } injectLocateButton(); } } function paintPremiumGallery(profileId) { if (!settings.enablePremiumPhotos) return; const photos = profileMediaMap.get(profileId); if (!photos || photos.length === 0) return; if (document.getElementById("custom-premium-gallery")) return; let feed = document.querySelector("t101-profile-member-feed, .newsfeed"); let photoGrid = document.querySelector("t101-profile-latest-photos-panel, .latest-photos-block, .photo-tile-container, t101-profile-photos, .member-photos, .gallery-grid, .photo-grid"); let parent = feed ? feed.parentElement : photoGrid ? photoGrid.parentElement : null; if (!parent) { parent = document.querySelector(".profile-header, t101-profile-header, .member-info, .profile-info-container, t101-profile-main, .profile-main-container"); } if (!parent) return; const galleryDiv = document.createElement("div"); galleryDiv.id = "custom-premium-gallery"; galleryDiv.style.cssText = ` margin: 24px 0; padding: 20px; background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; font-family: 'Raleway', sans-serif; clear: both; `; const titleDiv = document.createElement("div"); titleDiv.style.cssText = ` display: flex; align-items: center; gap: 8px; margin-bottom: 16px; `; titleDiv.innerHTML = ` <span style="font-size: 20px;">\uD83D\uDC51</span> <h3 style="margin: 0; font-family: 'Barlow', sans-serif; font-size: 20px; font-weight: 800; background: linear-gradient(90deg, #ff3333, #ff7b00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-transform: uppercase; letter-spacing: 0.5px;">Recon+ Premium Photos</h3> <span style="background: rgba(255, 51, 51, 0.15); border: 1px solid rgba(255, 51, 51, 0.3); color: #ff3333; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; margin-left: auto;">Unlocked</span> `; galleryDiv.appendChild(titleDiv); const grid = document.createElement("div"); grid.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; margin-top: 16px; `; photos.forEach((photoUrl, index) => { let previewUrl = photoUrl; if (previewUrl.includes("size=")) { previewUrl = previewUrl.replace(/size=\d+/, "size=102"); } else { previewUrl += (previewUrl.includes("?") ? "&" : "?") + "size=102"; } if (!previewUrl.includes("deviceTypeId=")) { previewUrl += "&deviceTypeId=1"; } const tile = document.createElement("div"); tile.className = "photo-tile"; tile.style.cssText = ` aspect-ratio: 1; background-image: url('${previewUrl}'); background-size: cover; background-position: center; border-radius: 8px; cursor: pointer; border: 1px solid rgba(255, 255, 255, 0.05); transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s, border-color 0.25s; `; tile.onmouseover = () => { tile.style.transform = "scale(1.05)"; tile.style.boxShadow = "0 8px 16px rgba(0, 0, 0, 0.4)"; tile.style.borderColor = "rgba(255, 255, 255, 0.2)"; }; tile.onmouseout = () => { tile.style.transform = "scale(1)"; tile.style.boxShadow = "none"; tile.style.borderColor = "rgba(255, 255, 255, 0.05)"; }; tile.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const highResUrls = photos.map((u) => u.split("?")[0]); createFullscreenLightbox(highResUrls, index); }); grid.appendChild(tile); }); galleryDiv.appendChild(grid); if (feed) { feed.parentNode.insertBefore(galleryDiv, feed); } else if (photoGrid) { photoGrid.after(galleryDiv); } else { parent.appendChild(galleryDiv); } } // src/db.ts function getLocationFromIndexedDB(locationId) { return new Promise((resolve) => { try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains("App_Locations")) { db.close(); resolve(null); return; } const transaction = db.transaction("App_Locations", "readonly"); const store = transaction.objectStore("App_Locations"); function checkVal(val) { if (val && val.data) { if (val.expiry) { const expiryDate = new Date(val.expiry); if (expiryDate > new Date) { return val.data; } } else { return val.data; } } return null; } const getRequest = store.get(Number(locationId)); getRequest.onsuccess = function() { let result = checkVal(getRequest.result); if (result) { db.close(); resolve(result); } else { const getRequestStr = store.get(String(locationId)); getRequestStr.onsuccess = function() { db.close(); resolve(checkVal(getRequestStr.result)); }; getRequestStr.onerror = function() { db.close(); resolve(null); }; } }; getRequest.onerror = function() { db.close(); resolve(null); }; }; request.onerror = function() { resolve(null); }; } catch (e) { resolve(null); } }); } function saveLocationToIndexedDB(locationId, locationUrl, name, longName) { try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains("App_Locations")) { db.close(); return; } const transaction = db.transaction("App_Locations", "readwrite"); const store = transaction.objectStore("App_Locations"); const expiryDate = new Date; expiryDate.setFullYear(expiryDate.getFullYear() + 1); const value = { data: { locationId: Number(locationId), locationUrl, longName, name }, expiry: expiryDate.toString() }; store.put(value, Number(locationId)); transaction.oncomplete = function() { db.close(); }; }; } catch (e) { console.error("[Script] Error saving location to IndexedDB", e); } } function getFromStore(storeName, profileId) { return new Promise((resolve) => { try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains(storeName)) { db.close(); resolve(null); return; } const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); function checkVal(val) { if (val) { if (val.data) { if (val.expiry) { const expiryDate = new Date(val.expiry); if (expiryDate > new Date) { return val.data; } } else { return val.data; } } return val; } return null; } const dbKey = /^\d+$/.test(String(profileId)) ? Number(profileId) : String(profileId); const getRequest = store.get(dbKey); getRequest.onsuccess = function() { let result = checkVal(getRequest.result); if (result) { db.close(); resolve(result); } else { const fallbackKey = typeof dbKey === "number" ? String(dbKey) : isNaN(Number(dbKey)) ? null : Number(dbKey); if (fallbackKey !== null) { const getRequestFallback = store.get(fallbackKey); getRequestFallback.onsuccess = function() { db.close(); resolve(checkVal(getRequestFallback.result)); }; getRequestFallback.onerror = function() { db.close(); resolve(null); }; } else { db.close(); resolve(null); } } }; getRequest.onerror = function() { db.close(); resolve(null); }; }; request.onerror = function() { resolve(null); }; } catch (e) { resolve(null); } }); } async function getProfileFromIndexedDB(profileId) { try { const [profileCache, profileDetail] = await Promise.all([ getFromStore("Profile_Cache", profileId), getFromStore("Profile_Detail_Cache", profileId) ]); if (!profileCache && !profileDetail) { return null; } return { ...profileCache || {}, ...profileDetail || {} }; } catch (e) { console.error("[Script] Error getting profile from IndexedDB", e); return null; } } function saveProfileToIndexedDB(profileId, data) { try { const request = win.indexedDB.open("RECON_DB"); request.onsuccess = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains("Profile_Cache")) { db.close(); return; } const transaction = db.transaction("Profile_Cache", "readwrite"); const store = transaction.objectStore("Profile_Cache"); const dbKey = /^\d+$/.test(String(profileId)) ? Number(profileId) : String(profileId); const getRequest = store.get(dbKey); getRequest.onsuccess = function() { let existingData = null; const val = getRequest.result; if (val) { existingData = val.data ? val.data : val; } const mergedData = existingData ? { ...existingData, ...data } : data; const expiryDate = new Date; expiryDate.setFullYear(expiryDate.getFullYear() + 1); const value = { data: mergedData, expiry: expiryDate.toString() }; store.put(value, dbKey); }; transaction.oncomplete = function() { db.close(); }; }; } catch (e) { console.error("[Script] Error saving profile to IndexedDB", e); } } function checkAndPaintProfileFromIndexedDB() { const match = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i); if (match) { const profileId = match[1]; const uuidOrNumRegex = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d+)$/i; if (profileId && uuidOrNumRegex.test(profileId)) { getProfileFromIndexedDB(profileId).then((cachedProfile) => { if (cachedProfile) { state.lastProfileDetailPageData = cachedProfile; paintProfileDetailPage(cachedProfile); } }); } } } // src/chatSummary.ts function getCachedMessages(conversationId) { try { const stored = win.localStorage.getItem(`reconplus_chat_${conversationId}`); if (stored) { return JSON.parse(stored); } } catch (e) { console.error("[Recon+] Failed to load cached messages from localStorage", e); } return []; } function saveCachedMessages(conversationId, messages) { try { const existing = getCachedMessages(conversationId); const msgMap = new Map; existing.forEach((m) => { if (m.id) msgMap.set(m.id, m); }); messages.forEach((m) => { if (m.id && m.text) msgMap.set(m.id, m); }); const sorted = Array.from(msgMap.values()).sort((a, b) => { return new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime(); }); win.localStorage.setItem(`reconplus_chat_${conversationId}`, JSON.stringify(sorted)); } catch (e) { console.error("[Recon+] Failed to save cached messages to localStorage", e); } } function handleChatAPIResponse(url, responseText) { try { const convoRegex = /\/api\/messaging\/profiles\/([a-f0-9-]{36})\/conversations\/([a-f0-9-]{36})/i; const match = url.match(convoRegex); if (match) { state.currentChatProfileId = match[1]; state.currentChatConversationId = match[2]; } const data = JSON.parse(responseText); const rawMsgs = data?.data || data?.messages || (Array.isArray(data) ? data : []); if (Array.isArray(rawMsgs) && state.currentChatConversationId) { const currentProfileId = state.currentChatProfileId || state.authHeaders["client-id"] || state.authHeaders["x-client-id"] || "Me"; const formatted = rawMsgs.map((m) => ({ id: m.id || Math.random().toString(36).substring(2), senderProfileId: m.senderProfileId || m.senderId || (m.isOutgoing ? currentProfileId : "Other"), text: m.text || m.body || m.content || "", createdDate: m.createdDate || m.timestamp || new Date().toISOString() })).filter((m) => m.text); saveCachedMessages(state.currentChatConversationId, formatted); } } catch (e) {} } function scrapeChatFromDOM() { const list = []; const messageEls = document.querySelectorAll('t101-message, t101-dm-message, .message, .chat-message, .message-bubble, .msg, [class*="message-item"], .message-row, .chat-bubble, .message-group'); if (messageEls.length > 0) { messageEls.forEach((el) => { const textEl = el.querySelector('.text, .body, .message-text, .message-content, t101-message-body, [class*="content"], [class*="text"]'); const text = (textEl ? textEl.textContent : el.textContent)?.trim() || ""; if (!text) return; let sender = "Other"; const isSent = el.classList.contains("sent") || el.classList.contains("me") || el.classList.contains("outgoing") || el.classList.contains("right") || el.classList.contains("my-content") || el.getAttribute("class")?.includes("right") || el.getAttribute("class")?.includes("me") || el.getAttribute("class")?.includes("sent") || el.getAttribute("class")?.includes("my-") || el.style.justifyContent === "flex-end" || el.style.textAlign === "right"; if (isSent) { sender = "Me"; } else { const senderNameEl = el.querySelector('.sender, .name, .username, .sender-name, [class*="name"]'); if (senderNameEl && senderNameEl.textContent) { sender = senderNameEl.textContent.trim(); } } list.push({ sender, text }); }); } if (list.length === 0) { const container = document.querySelector('t101-message-list, .message-list, .chat-history, .messages, .chat-container, [class*="messages-container"], [class*="conversation-history"]'); if (container) { const items = container.querySelectorAll("p, span, div"); items.forEach((item) => { const text = item.textContent?.trim(); if (text && text.length > 2 && !item.querySelector("p, span, div")) { const isMe = item.closest('[class*="me"], [class*="sent"], [class*="right"], [class*="my-content"]') !== null; list.push({ sender: isMe ? "Me" : "Other", text }); } }); } } return list; } function detectConversationIds() { let conversationId = state.currentChatConversationId; let profileId = state.currentChatProfileId || state.authHeaders["client-id"] || state.authHeaders["x-client-id"] || null; const isMessageUrl = win.location.pathname.includes("/messages") || win.location.pathname.includes("/chat"); if (isMessageUrl) { const uuidMatch = win.location.pathname.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i); if (uuidMatch) { conversationId = uuidMatch[1]; } } if (!conversationId) { const activeLink = document.querySelector('a[href*="/messages/"].active, a[href*="/conversations/"].active, .active a[href*="/messages/"], t101-conversation-list-item.active a, .conversation-item.active a'); if (activeLink) { const href = activeLink.getAttribute("href") || ""; const m = href.match(/\/messages\/(?:chat\/|conversations\/)?([a-f0-9-]{36})/i); if (m) conversationId = m[1]; } } if (!profileId) { const links = Array.from(document.querySelectorAll('a[href*="/profiles/"]')); for (const link of links) { const href = link.getAttribute("href") || ""; const m = href.match(/\/profiles\/([a-f0-9-]{36})/i); if (m) { if (link.querySelector(".profile-avatar") || link.closest(".nav-items-right") || link.classList.contains("nav-item-right-container")) { profileId = m[1]; break; } } } } return { profileId, conversationId }; } async function fetchConversationHistory(profileId, conversationId, take = 100) { const url = `https://www.recon.com/api/messaging/profiles/${profileId}/conversations/${conversationId}/content?culture=en&imageSizes=100,103&take=${take}`; const headers = { ...state.authHeaders }; const response = await win.fetch(url, { headers }); if (!response.ok) { throw new Error(`API Fetch failed: Status ${response.status}`); } const data = await response.json(); const rawMsgs = data?.data || data?.messages || (Array.isArray(data) ? data : []); return rawMsgs.map((m) => ({ id: m.id || Math.random().toString(36).substring(2), senderProfileId: m.senderProfileId || m.senderId || (m.isOutgoing ? profileId : "Other"), text: m.text || m.body || m.content || "", createdDate: m.createdDate || m.timestamp || new Date().toISOString() })).filter((m) => m.text); } async function getFullChatThreadAsync(forceFetch = false) { const { conversationId, profileId } = detectConversationIds(); let cached = []; if (conversationId) { cached = getCachedMessages(conversationId); } if ((cached.length < 5 || forceFetch) && conversationId && profileId && Object.keys(state.authHeaders).length > 0) { try { console.log(`[Recon+] Fetching complete history for conversation: ${conversationId}`); const apiMsgs = await fetchConversationHistory(profileId, conversationId, 100); saveCachedMessages(conversationId, apiMsgs); cached = getCachedMessages(conversationId); } catch (e) { console.error("[Recon+] Failed to fetch history via API", e); } } const thread = cached.map((m) => { const isMe = m.senderProfileId === profileId || m.senderProfileId === "Me" || m.senderProfileId === state.authHeaders["client-id"] || m.senderProfileId === state.authHeaders["x-client-id"]; return { sender: isMe ? "Me" : "Other", text: m.text }; }); const scraped = scrapeChatFromDOM(); scraped.forEach((msg) => { const exists = thread.some((m) => m.text === msg.text); if (!exists) { thread.push(msg); } }); return thread; } async function getCheapestModel(apiKey) { if (state.cachedModelName) { return state.cachedModelName; } try { const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`; const response = await win.fetch(url); if (response.ok) { const data = await response.json(); const modelsList = data.models || []; const eligible = modelsList.filter((m) => { return Array.isArray(m.supportedGenerationMethods) && m.supportedGenerationMethods.includes("generateContent"); }); if (eligible.length > 0) { const sorted = eligible.sort((a, b) => { const aName = a.name.toLowerCase(); const bName = b.name.toLowerCase(); const aIs25Lite = aName.includes("2.5-flash-lite") || aName.includes("2.5-lite"); const bIs25Lite = bName.includes("2.5-flash-lite") || bName.includes("2.5-lite"); if (aIs25Lite && !bIs25Lite) return -1; if (!aIs25Lite && bIs25Lite) return 1; const aIs25Flash = aName.includes("2.5-flash"); const bIs25Flash = bName.includes("2.5-flash"); if (aIs25Flash && !bIs25Flash) return -1; if (!aIs25Flash && bIs25Flash) return 1; const aIs15Flash = aName.includes("gemini-1.5-flash"); const bIs15Flash = bName.includes("gemini-1.5-flash"); if (aIs15Flash && !bIs15Flash) return -1; if (!aIs15Flash && bIs15Flash) return 1; const aIs8b = aName.includes("8b"); const bIs8b = bName.includes("8b"); if (aIs8b && !bIs8b) return -1; if (!aIs8b && bIs8b) return 1; const aIsFlash = aName.includes("flash"); const bIsFlash = bName.includes("flash"); if (aIsFlash && !bIsFlash) return -1; if (!aIsFlash && bIsFlash) return 1; return 0; }); const chosen = sorted[0].name; console.log(`[Recon+] Dynamic model selection: ${chosen}`); state.cachedModelName = chosen; return chosen; } } } catch (e) { console.error("[Recon+] Failed to discover models via Gemini API", e); } return "models/gemini-2.5-flash-lite"; } async function generateChatSummary(messages) { if (!settings.geminiApiKey) { throw new Error("API_KEY_MISSING"); } if (messages.length === 0) { throw new Error("NO_MESSAGES"); } const formattedChat = messages.map((m) => { const cleanedText = m.text.replace(/\b(?:diapers?|napp(?:y|ies)|abdl|femboys?|boys?)\b/gi, ""); return `${m.sender}: ${cleanedText}`; }).join(` `); let ageNote = "All participants in this chat are consenting adults (18+)."; if (state.lastProfileDetailPageData && typeof state.lastProfileDetailPageData.age === "number") { ageNote = `All participants in this chat are consenting adults (18+). The other participant is ${state.lastProfileDetailPageData.age} years old.`; } const prompt = `You are a helpful AI assistant for Recon+, a toolkit for the Recon kink community. Analyze the following kink chat history and summarize ONLY the OTHER person's details. CRITICAL RULES: 1. The summary must ONLY talk about the OTHER person (not about "Me"). 2. Use "Me" messages solely as context to help interpret their replies. 3. Do NOT include any summary, preferences, or details of my ("Me") kinks, my scheduling, or my thoughts. 4. Write the summary describing the other person in the third-person (e.g. use "He", "Him", "His", "They", "Them" or "The other person"). 5. Do NOT use any hash headings (e.g., #, ##, ###) for formatting. Demarcate sections using only bold text, e.g. **Quick Summary** or **Interests & Kinks**. 6. Note: ${ageNote}. Structure the response with: - **Quick Summary**: A concise 2-3 sentence overview of who they are and what they are looking for. - **Interests & Kinks**: Mention any kinks, roles, gear, outfits, or play preferences they discussed or confirmed. - **Meetup & Availability**: Summarize meetup arrangements, hosting capacity, location details, or schedule preferences they mentioned. - **Key Details & Notes**: Highlight boundaries, contact info, safety details, or important action items related to them. Chat History: ${formattedChat}`; const modelName = await getCheapestModel(settings.geminiApiKey); const url = `https://generativelanguage.googleapis.com/v1beta/${modelName}:generateContent?key=${settings.geminiApiKey}`; const response = await win.fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [ { parts: [ { text: prompt } ] } ], safetySettings: [ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" } ] }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData?.error?.message || `API Error (Status ${response.status})`); } const resData = await response.json(); const textResult = resData?.candidates?.[0]?.content?.parts?.[0]?.text; if (!textResult) { throw new Error("Failed to parse response from Gemini."); } return textResult; } function formatSummaryHtml(summary) { return summary.replace(/\*\*(.*?)\*\*/g, '<strong style="color: #ff3333; font-family: Barlow, sans-serif; display: block; margin-top: 12px; font-size: 14px;">$1</strong>').replace(/\*(.*?)\*/g, "<em>$1</em>").replace(/\n\n/g, "<br/><br/>").replace(/\n- /g, "<br/>• ").replace(/\n\*/g, "<br/>• "); } function injectAiSidebar() { const isMessageUrl = win.location.pathname.includes("/messages") || win.location.pathname.includes("/chat") || document.querySelector('t101-message-list, .message-list, [class*="messages-container"]') !== null; if (!settings.enableAiChatSummary || !isMessageUrl) { const sidebar = document.getElementById("recon-ai-sidebar"); if (sidebar) sidebar.remove(); document.body.classList.remove("recon-chat-page"); return; } document.body.classList.add("recon-chat-page"); const isDesktop = win.innerWidth >= 992; if (isDesktop) { let sidebar = document.getElementById("recon-ai-sidebar"); if (!sidebar) { sidebar = document.createElement("div"); sidebar.id = "recon-ai-sidebar"; sidebar.innerHTML = ` <div style="padding: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); flex-shrink: 0;"> <h3 style="margin: 0; font-family: 'Barlow', sans-serif; font-size: 18px; font-weight: 800; background: linear-gradient(90deg, #ff3333, #ff7b00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-transform: uppercase; letter-spacing: 0.5px;">AI Chat Summary</h3> </div> <div id="recon-ai-sidebar-content" style="flex-grow: 1; overflow-y: auto; padding: 16px; font-family: 'Raleway', sans-serif; font-size: 13px; color: #fff;"> <button id="recon-ai-generate-btn" style="width: 100%; background: #ff3333; color: #fff; border: none; border-radius: 6px; padding: 10px 16px; font-size: 13px; font-weight: 800; cursor: pointer; font-family: 'Barlow', sans-serif; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; box-sizing: border-box; display: block;">Generate Summary</button> <div id="recon-ai-summary-result" style="margin-top: 16px;"></div> </div> `; let parent = null; const chatPane = document.querySelector("t101-message-centre-conversation"); if (chatPane && chatPane.parentElement) { parent = chatPane.parentElement; } else { parent = document.querySelector(".conversation-inbox-container"); } if (parent) { parent.appendChild(sidebar); parent.style.setProperty("display", "flex", "important"); parent.style.setProperty("flex-direction", "row", "important"); parent.style.setProperty("flex-wrap", "nowrap", "important"); parent.style.setProperty("align-items", "stretch", "important"); parent.style.setProperty("width", "100%", "important"); } const genBtn = sidebar.querySelector("#recon-ai-generate-btn"); const resultDiv = sidebar.querySelector("#recon-ai-summary-result"); if (genBtn && resultDiv) { genBtn.addEventListener("click", async () => { genBtn.textContent = "Generating..."; genBtn.disabled = true; resultDiv.innerHTML = '<div style="color: #adadad; text-align: center; margin-top: 20px;">Analyzing chat history...</div>'; try { const { conversationId: conversationId2 } = detectConversationIds(); const thread = await getFullChatThreadAsync(false); if (!thread || thread.length === 0) { throw new Error("No messages found"); } const summary = await generateChatSummary(thread); if (conversationId2) { try { win.localStorage.setItem(`reconplus_summary_${conversationId2}`, summary); } catch (e) { console.error("[Recon+] Failed to save summary to localStorage", e); } } let html = formatSummaryHtml(summary); resultDiv.innerHTML = `<div style="line-height: 1.5;">${html}</div>`; genBtn.textContent = "Regenerate Summary"; } catch (err) { resultDiv.innerHTML = `<div style="color: #ef4444; text-align: center; margin-top: 20px;">Error: ${err.message || "Failed to generate summary"}</div>`; genBtn.textContent = "Try Again"; } finally { genBtn.disabled = false; } }); } } const { conversationId } = detectConversationIds(); const currentConvoId = conversationId || ""; if (sidebar.dataset.conversationId !== currentConvoId) { sidebar.dataset.conversationId = currentConvoId; const genBtn = sidebar.querySelector("#recon-ai-generate-btn"); const resultDiv = sidebar.querySelector("#recon-ai-summary-result"); if (genBtn && resultDiv) { let savedSummary = null; if (currentConvoId) { try { savedSummary = win.localStorage.getItem(`reconplus_summary_${currentConvoId}`); } catch (e) { console.error("[Recon+] Failed to read summary from localStorage", e); } } if (savedSummary) { let html = formatSummaryHtml(savedSummary); resultDiv.innerHTML = `<div style="line-height: 1.5;">${html}</div>`; genBtn.textContent = "Regenerate Summary"; } else { resultDiv.innerHTML = ""; genBtn.textContent = "Generate Summary"; } } } } else { const sidebar = document.getElementById("recon-ai-sidebar"); if (sidebar) sidebar.remove(); } } // src/xhr.ts function setupXHRInterceptor() { win.XMLHttpRequest = function() { const xhr = new origXHR; const origOpen = xhr.open; const origSend = xhr.send; const origSetHeader = xhr.setRequestHeader; xhr._headers = {}; xhr.setRequestHeader = function(header, value) { if (this._headers) { this._headers[header] = value; } return origSetHeader.apply(this, arguments); }; xhr.open = function(method, url) { if (typeof url === "string" && url.includes("/api/profileSearch/profiles")) { try { const isRelative = !url.startsWith("http:") && !url.startsWith("https:") && !url.startsWith("//"); const urlObj = new URL(url, win.location.origin || "https://www.recon.com"); let urlChanged = false; if (settings.enableFakeLocation && settings.customCoordinates) { const parts = settings.customCoordinates.split(","); if (parts.length === 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!isNaN(lat) && !isNaN(lon)) { urlObj.searchParams.set("latitude", lat.toFixed(6)); urlObj.searchParams.set("longitude", lon.toFixed(6)); urlChanged = true; console.log(`[Recon+] Overwrote search coordinates with fake location: ${lat.toFixed(6)}, ${lon.toFixed(6)}`); } } } const saved = win.localStorage.getItem("recon_advanced_filters"); if (saved) { const filters = JSON.parse(saved); urlChanged = true; const bodyTypeMap = { athletic: 1, average: 2, large: 3, muscled: 4, slim: 5 }; const bodyHairMap = { average: 1, hairy: 2, none: 3, shaved: 4, some: 5 }; const ethnicityMap = { asian: 1, black: 2, hispanic: 3, latino: 4, "middle eastern": 5, "mixed race": 6, "south asian": 7, white: 8, other: 9 }; const getActiveMinutes = (activeWithin) => { switch (activeWithin) { case "now": return 10; case "hour": return 60; case "day": return 1440; case "week": return 10080; case "any": return 525600; default: return 43200; } }; if (filters.activeWithin) { urlObj.searchParams.set("activeWithinMinutes", String(getActiveMinutes(filters.activeWithin))); } if (filters.onlyPhotos) { urlObj.searchParams.set("hasPhoto", "true"); urlObj.searchParams.set("hasPhotos", "true"); urlObj.searchParams.set("photo", "true"); } else { urlObj.searchParams.delete("hasPhoto"); urlObj.searchParams.delete("hasPhotos"); urlObj.searchParams.delete("photo"); } if (filters.onlyNew) { urlObj.searchParams.set("isNew", "true"); urlObj.searchParams.set("new", "true"); } else { urlObj.searchParams.delete("isNew"); urlObj.searchParams.delete("new"); } if (filters.onlyPremium) { urlObj.searchParams.set("isPremium", "true"); urlObj.searchParams.set("premium", "true"); } else { urlObj.searchParams.delete("isPremium"); urlObj.searchParams.delete("premium"); } if (filters.heightMin) { urlObj.searchParams.set("heightMin", String(filters.heightMin)); urlObj.searchParams.set("minHeight", String(filters.heightMin)); } else { urlObj.searchParams.delete("heightMin"); urlObj.searchParams.delete("minHeight"); } if (filters.heightMax) { urlObj.searchParams.set("heightMax", String(filters.heightMax)); urlObj.searchParams.set("maxHeight", String(filters.heightMax)); } else { urlObj.searchParams.delete("heightMax"); urlObj.searchParams.delete("maxHeight"); } if (filters.bodyTypes && filters.bodyTypes.length > 0) { const ids = filters.bodyTypes.map((t) => bodyTypeMap[t.toLowerCase()]).filter(Boolean).join(","); urlObj.searchParams.set("bodyTypeIds", ids); } else { urlObj.searchParams.set("bodyTypeIds", ""); } if (filters.bodyHairs && filters.bodyHairs.length > 0) { const ids = filters.bodyHairs.map((h) => bodyHairMap[h.toLowerCase()]).filter(Boolean).join(","); urlObj.searchParams.set("bodyHairIds", ids); } else { urlObj.searchParams.set("bodyHairIds", ""); } if (filters.ethnicities && filters.ethnicities.length > 0) { const ids = filters.ethnicities.map((e) => ethnicityMap[e.toLowerCase()]).filter(Boolean).join(","); urlObj.searchParams.set("ethnicityIds", ids); } else { urlObj.searchParams.set("ethnicityIds", ""); } } if (urlChanged) { url = isRelative ? urlObj.pathname + urlObj.search + urlObj.hash : urlObj.toString(); console.log(`[Recon+] Intercepted Search request, updated url: ${url}`); } } catch (e) { console.error("[Recon+] Error patching search parameters:", e); } } this._customUrl = url; if (typeof url === "string" && url.includes("/visitedProfiles/")) { if (!this._isManualTrigger) { this.send = function(body) { state.lastBlockedRequest = { url, method, headers: this._headers || {}, body }; Object.defineProperty(this, "readyState", { value: 4 }); Object.defineProperty(this, "status", { value: 200 }); Object.defineProperty(this, "responseText", { value: '{"success":true}' }); if (this.onreadystatechange) this.onreadystatechange(); if (this.onload) this.onload(); }; return origOpen.apply(this, arguments); } } return origOpen.apply(this, arguments); }; xhr.send = function(body) { if (this._headers) { const authKeys = ["authorization", "x-xsrf-token", "client-id", "x-client-id"]; for (const key of Object.keys(this._headers)) { if (authKeys.includes(key.toLowerCase())) { state.authHeaders[key] = this._headers[key]; } } } this.addEventListener("readystatechange", function() { if (this.readyState === 4 && this._customUrl) { if (this._customUrl.includes("/api/profile/profiles/")) { const match = this._customUrl.match(/\/api\/profile\/profiles\/([a-f0-9-]{36})(?:\?|$)/); if (match) { try { const data = JSON.parse(this.responseText); data.isPremium = true; data.premium = true; data.membershipType = "Premium"; data.membership = { type: "Premium", status: "Active" }; const modifiedText = JSON.stringify(data); Object.defineProperty(this, "responseText", { get: function() { return modifiedText; }, configurable: true }); Object.defineProperty(this, "response", { get: function() { return modifiedText; }, configurable: true }); console.log("[Recon+] Mocked profile summary response to Premium."); } catch (e) {} } } } }); this.addEventListener("load", function() { if (this._customUrl) { if (this._customUrl.includes("/api/profileSearch/profiles")) { console.log(`[Recon+] Intercepted search URL: ${this._customUrl}`); try { const data = JSON.parse(this.responseText); console.log(`[Recon+] Search returned profiles:`, data?.data?.length || 0); if (data && Array.isArray(data.data)) { data.data.forEach((p) => { if (p.profileId && p.profileUrl) { const match = p.profileUrl.match(/\/profiles\/([^/]+)\/(\d+)/); if (match) { const uuid = match[1]; const ver = parseInt(match[2], 10); profileVersionsMap.set(uuid, ver); console.log(`[Recon+] Mapped version: ${uuid} -> ${ver}`); } else { console.log(`[Recon+] Could not parse version from profileUrl: ${p.profileUrl}`); } } }); } } catch (e) { console.error("[Script] Error parsing profileSearch data", e); } } else if (this._customUrl.includes("/api/media/profiles/") && this._customUrl.includes("/fileProperties")) { console.log(`[Recon+] Intercepted fileProperties URL: ${this._customUrl}`); try { const data = JSON.parse(this.responseText); const match = this._customUrl.match(/\/api\/media\/profiles\/([^/]+)\/fileProperties/); if (match && match[1]) { const profileId = match[1]; const urls = extractPhotoUrls(data); console.log(`[Recon+] Captured ${urls.length} media files for profile: ${profileId}`); profileMediaMap.set(profileId, urls); setTimeout(() => paintPremiumGallery(profileId), 50); } } catch (e) { console.error("[Script] Error parsing fileProperties data", e); } } else if (this._customUrl.includes("/api/profile/profiles/")) { console.log(`[Recon+] Intercepted profile URL: ${this._customUrl}`); try { const data = JSON.parse(this.responseText); if (data && data.id) { if (this._customUrl.includes("/detail/")) { console.log(`[Recon+] Intercepted profile detail: ${data.id}, version: ${data.version}`); state.lastProfileDetailPageData = data; const existing = document.getElementById("custom-profile-last-updated"); if (existing) existing.remove(); setTimeout(() => paintProfileDetailPage(data), 50); data.isDetailed = true; const cachedSummary = profileDetailsCache.get(data.id); const merged = cachedSummary ? { ...cachedSummary, ...data } : data; profileDetailsCache.set(data.id, merged); saveProfileToIndexedDB(data.id, merged); } else { console.log(`[Recon+] Intercepted profile summary: ${data.id}`); profileDetailsCache.set(data.id, data); setTimeout(() => paintProfileData(data.id), 50); } } } catch (e) { console.error("[Script] Error parsing profile data", e); } } else if (this._customUrl.includes("/api/message") || this._customUrl.includes("/api/messages") || this._customUrl.includes("/api/conversation") || this._customUrl.includes("/api/chat")) { console.log(`[Recon+] Intercepted chat URL: ${this._customUrl}`); handleChatAPIResponse(this._customUrl, this.responseText); } } }); return origSend.apply(this, arguments); }; return xhr; }; } // src/styles.ts function injectStyles() { const style = document.createElement("style"); style.innerHTML = ` t101-profile-list-filter { display: none !important; } .cdk-overlay-container:has(t101-paywall), t101-paywall { display: none !important; opacity: 0 !important; pointer-events: none !important; } html.cdk-global-scrollblock, body { position: static !important; overflow-y: auto !important; overflow: auto !important; width: auto !important; } .photo-tile { pointer-events: auto !important; cursor: pointer !important; } .text-container .interests-container { display: none !important; } .text-container { height: auto !important; max-height: none !important; padding-bottom: 15px !important; } /* Quick GPS Panel Wrapping */ .profile-panel { display: flex !important; flex-direction: column !important; flex-wrap: wrap !important; } t101-profile-navbar-tabs { display: block !important; width: 100% !important; } #recon-quick-gps-panel { width: 100% !important; max-width: 100% !important; clear: both !important; box-sizing: border-box !important; margin-top: 110px !important; } /* Recon+ Custom Filter Panel */ #recon-filter-panel { background-color: #0b0b0d !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 0px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important; padding: 16px !important; max-height: 70vh !important; display: flex !important; flex-direction: column !important; box-sizing: border-box !important; } .recon-filter-scroll-container { max-height: 55vh !important; overflow-y: auto !important; overflow-x: hidden !important; padding-right: 6px !important; margin: 12px 0 !important; display: block !important; /* Block layout ensures scrolling */ box-sizing: border-box !important; } .recon-filter-scroll-container::-webkit-scrollbar { width: 6px !important; } .recon-filter-scroll-container::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2) !important; border-radius: 3px !important; } .recon-filter-scroll-container::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.12) !important; border-radius: 3px !important; } .recon-filter-scroll-container::-webkit-scrollbar-thumb:hover { background: rgba(255, 51, 51, 0.5) !important; } .recon-apply-btn { background: #ff3333 !important; color: #fff !important; border: none !important; border-radius: 6px !important; padding: 10px 16px !important; font-size: 13px !important; font-weight: 800 !important; cursor: pointer !important; font-family: 'Barlow', sans-serif !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; transition: all 0.2s !important; width: 100% !important; margin-bottom: 8px !important; box-sizing: border-box !important; display: block !important; flex-shrink: 0 !important; } .recon-apply-btn:hover { background: #e62e2e !important; box-shadow: 0 4px 12px rgba(255, 51, 51, 0.3) !important; } .recon-apply-btn:active { transform: scale(0.98) !important; } .recon-filter-section { display: flex !important; flex-direction: column !important; gap: 8px !important; padding-bottom: 12px !important; border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important; margin-bottom: 12px !important; box-sizing: border-box !important; } .recon-filter-section:last-child { border-bottom: none !important; margin-bottom: 0 !important; padding-bottom: 0 !important; } .recon-filter-section-title { font-size: 13px !important; font-weight: 800 !important; color: #adadad !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; font-family: 'Barlow', sans-serif !important; margin-bottom: 4px !important; } .recon-filter-row { display: flex !important; gap: 10px !important; align-items: center !important; width: 100% !important; box-sizing: border-box !important; } .recon-filter-row > * { flex: 1 1 0% !important; min-width: 0 !important; } .recon-chip-container { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; margin-top: 4px !important; } .recon-filter-chip { background: rgba(255, 255, 255, 0.06) !important; color: #adadad !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; padding: 5px 10px !important; border-radius: 20px !important; font-size: 12px !important; font-weight: 700 !important; cursor: pointer !important; user-select: none !important; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; font-family: 'Raleway', sans-serif !important; text-align: center !important; } .recon-filter-chip:hover { background: rgba(255, 255, 255, 0.12) !important; color: #fff !important; border-color: rgba(255, 255, 255, 0.2) !important; } .recon-filter-chip.selected { background: #ff3333 !important; color: #fff !important; border-color: #ff3333 !important; box-shadow: 0 2px 8px rgba(255, 51, 51, 0.4) !important; } .recon-filter-input { width: 100% !important; background: rgba(0, 0, 0, 0.3) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; border-radius: 6px !important; color: #fff !important; padding: 8px 12px !important; font-size: 13px !important; outline: none !important; box-sizing: border-box !important; font-family: 'Raleway', sans-serif !important; transition: border-color 0.2s, background 0.2s !important; } .recon-filter-input:focus { border-color: #ff3333 !important; background: rgba(0, 0, 0, 0.4) !important; } .recon-filter-select { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>"); background-repeat: no-repeat; background-position: right 8px center; background-size: 16px; padding-right: 28px !important; } /* Recon+ Settings Modal */ #recon-settings-overlay { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background-color: rgba(0, 0, 0, 0.8) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; z-index: 110000 !important; display: flex !important; align-items: center !important; justify-content: center !important; opacity: 0 !important; transition: opacity 0.25s ease !important; box-sizing: border-box !important; } #recon-settings-overlay.show { opacity: 1 !important; } #recon-settings-modal { background-color: #0b0b0d !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 12px !important; width: 90% !important; max-width: 600px !important; height: 80vh !important; max-height: 600px !important; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8) !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; font-family: 'Raleway', sans-serif !important; color: #fff !important; transform: scale(0.95) !important; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important; box-sizing: border-box !important; } #recon-settings-overlay.show #recon-settings-modal { transform: scale(1) !important; } .recon-settings-header { padding: 20px 24px !important; border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; display: flex !important; justify-content: space-between !important; align-items: center !important; flex-shrink: 0 !important; } .recon-settings-title { font-family: 'Barlow', sans-serif !important; font-size: 22px !important; font-weight: 800 !important; margin: 0 !important; background: linear-gradient(90deg, #ff3333, #ff7b00) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; } .recon-settings-close { color: rgba(255, 255, 255, 0.5) !important; font-size: 24px !important; cursor: pointer !important; transition: color 0.2s !important; user-select: none !important; line-height: 1 !important; } .recon-settings-close:hover { color: #ff3333 !important; } .recon-settings-tabs { display: flex !important; border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important; background: rgba(255, 255, 255, 0.01) !important; flex-shrink: 0 !important; } .recon-settings-tab { flex: 1 !important; text-align: center !important; padding: 12px 16px !important; cursor: pointer !important; font-family: 'Barlow', sans-serif !important; font-weight: 700 !important; font-size: 14px !important; color: #adadad !important; border-bottom: 2px solid transparent !important; transition: all 0.2s !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; } .recon-settings-tab:hover { color: #fff !important; background: rgba(255, 255, 255, 0.02) !important; } .recon-settings-tab.active { color: #ff3333 !important; border-bottom-color: #ff3333 !important; background: rgba(255, 51, 51, 0.03) !important; } .recon-settings-content { flex-grow: 1 !important; overflow-y: auto !important; padding: 24px !important; box-sizing: border-box !important; } .recon-settings-panel { display: none !important; flex-direction: column !important; gap: 20px !important; } .recon-settings-panel.active { display: flex !important; } .recon-settings-group { display: flex !important; flex-direction: column !important; gap: 12px !important; } .recon-settings-group-title { font-family: 'Barlow', sans-serif !important; font-size: 14px !important; font-weight: 800 !important; color: #ff3333 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; margin-bottom: 4px !important; } .recon-settings-row { display: flex !important; justify-content: space-between !important; align-items: center !important; gap: 16px !important; } .recon-settings-label-container { display: flex !important; flex-direction: column !important; gap: 4px !important; } .recon-settings-label { font-size: 14px !important; font-weight: 700 !important; color: #fff !important; } .recon-settings-desc { font-size: 11px !important; color: #adadad !important; line-height: 1.3 !important; } /* Premium CSS Switch Toggle */ .recon-switch { position: relative !important; display: inline-block !important; width: 46px !important; height: 24px !important; flex-shrink: 0 !important; } .recon-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; } .recon-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: rgba(255, 255, 255, 0.1) !important; transition: .3s !important; border-radius: 24px !important; border: 1px solid rgba(255, 255, 255, 0.05) !important; } .recon-slider:before { position: absolute !important; content: "" !important; height: 16px !important; width: 16px !important; left: 3px !important; bottom: 3px !important; background-color: #adadad !important; transition: .3s !important; border-radius: 50% !important; } .recon-switch input:checked + .recon-slider { background-color: rgba(255, 51, 51, 0.2) !important; border-color: rgba(255, 51, 51, 0.4) !important; } .recon-switch input:checked + .recon-slider:before { transform: translateX(22px) !important; background-color: #ff3333 !important; box-shadow: 0 0 8px rgba(255, 51, 51, 0.6) !important; } .recon-settings-footer { padding: 16px 24px !important; border-top: 1px solid rgba(255, 255, 255, 0.08) !important; display: flex !important; justify-content: flex-end !important; gap: 12px !important; flex-shrink: 0 !important; background: rgba(0, 0, 0, 0.2) !important; } .recon-settings-btn { font-family: 'Barlow', sans-serif !important; font-size: 13px !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; padding: 10px 20px !important; border-radius: 6px !important; cursor: pointer !important; transition: all 0.2s !important; border: none !important; } .recon-settings-btn-save { background: #ff3333 !important; color: #fff !important; } .recon-settings-btn-save:hover { background: #e62e2e !important; box-shadow: 0 4px 12px rgba(255, 51, 51, 0.3) !important; } .recon-settings-btn-cancel { background: rgba(255, 255, 255, 0.06) !important; color: #adadad !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; } .recon-settings-btn-cancel:hover { background: rgba(255, 255, 255, 0.1) !important; color: #fff !important; } .recon-settings-btn-danger { background: rgba(239, 68, 68, 0.1) !important; color: #ef4444 !important; border: 1px solid rgba(239, 68, 68, 0.2) !important; } .recon-settings-btn-danger:hover { background: #ef4444 !important; color: #fff !important; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2) !important; } /* Desktop Chat Full-Width layout and stuck sidebar */ @media (min-width: 992px) { /* Expand main wrappers so they don't constrain the chat size */ body.recon-chat-page .app-container, body.recon-chat-page t101-message-centre, body.recon-chat-page .message-centre-container, body.recon-chat-page .inbox-container.conversation { max-width: 1520px !important; /* 1200px native + 320px sidebar */ width: 100% !important; padding: 0 !important; margin: 0 auto !important; box-sizing: border-box !important; transition: max-width 0.2s ease !important; } /* Ensure the immediate container uses flex row so the 3 columns sit side-by-side */ body.recon-chat-page .conversation-inbox-container { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: stretch !important; width: 100% !important; } body.recon-chat-page t101-message-centre-inbox-list { flex-shrink: 0 !important; } body.recon-chat-page t101-message-centre-conversation { flex: 1 1 0% !important; min-width: 0 !important; } /* The AI Summary sidebar — fits naturally as the 3rd flex child */ #recon-ai-sidebar { position: relative !important; flex: 0 0 320px !important; width: 320px !important; background: rgba(11, 11, 13, 0.95) !important; border-left: 1px solid rgba(255, 255, 255, 0.08) !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; } } `; if (document.head) { document.head.appendChild(style); } else { const docObserver = new MutationObserver(() => { if (document.head) { document.head.appendChild(style); docObserver.disconnect(); } }); docObserver.observe(document.documentElement, { childList: true }); } } // src/filter.ts var advancedFilterState = { keyword: "", activeWithin: "any", onlyPhotos: false, onlyNew: false, onlyPremium: false, heightMin: 0, heightMax: 0, bodyTypes: [], bodyHairs: [], ethnicities: [] }; var savedFilters = win.localStorage.getItem("recon_advanced_filters"); if (savedFilters) { try { const parsed = JSON.parse(savedFilters); Object.assign(advancedFilterState, parsed); } catch (e) { console.error("[Recon+] Error loading saved filters", e); } } var filterState = { fetchQueue: [], activeFetches: 0, queueTotal: 0, queueCompleted: 0 }; var maxConcurrentFetches = 2; var fetchDelayMs = 250; function getActiveCards() { const uuidOrNumRegex = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d+)$/i; return Array.from(document.querySelectorAll("[id]")).filter((el) => uuidOrNumRegex.test(el.id) && (el.querySelector(".text-container") !== null || el.querySelector(".photo-tile") !== null || el.classList.contains("photo-tile"))); } function containsKeyword(obj, keyword) { if (!obj) return false; if (typeof obj === "string") { const val = obj.toLowerCase(); if (val.startsWith("http") || val.includes("/photos/") || val.includes("/images/")) { return false; } return val.includes(keyword); } if (typeof obj === "object") { for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const lowerKey = key.toLowerCase(); if (lowerKey.endsWith("id") || lowerKey.includes("url") || lowerKey.includes("image") || lowerKey.includes("photo") || lowerKey.includes("date") || lowerKey.includes("expiry") || lowerKey === "lat" || lowerKey === "lon" || lowerKey === "latitude" || lowerKey === "longitude" || lowerKey.includes("distance") || lowerKey.includes("status") || lowerKey.includes("culture")) { continue; } if (containsKeyword(obj[key], keyword)) { return true; } } } } return false; } async function applyFilterToCard(cardEl, stateObj) { const profileId = cardEl.id; if (!stateObj.keyword) { cardEl.style.removeProperty("display"); cardEl.style.removeProperty("flex-grow"); cardEl.style.removeProperty("flex-shrink"); cardEl.style.removeProperty("max-width"); return; } let data = profileDetailsCache.get(profileId); if (!data) { data = await getProfileFromIndexedDB(profileId) || undefined; if (data) { profileDetailsCache.set(profileId, data); } } const isDetailed = data && (data.isDetailed || typeof data.longText === "string"); if (isDetailed && data) { if (containsKeyword(data, stateObj.keyword)) { cardEl.style.removeProperty("display"); cardEl.style.setProperty("flex-grow", "0", "important"); cardEl.style.setProperty("flex-shrink", "0", "important"); cardEl.style.setProperty("max-width", "220px", "important"); } else { cardEl.style.setProperty("display", "none", "important"); } } else { cardEl.style.removeProperty("display"); cardEl.style.setProperty("flex-grow", "0", "important"); cardEl.style.setProperty("flex-shrink", "0", "important"); cardEl.style.setProperty("max-width", "220px", "important"); enqueueProfileFetch(profileId); } } function enqueueProfileFetch(profileId) { if (filterState.fetchQueue.includes(profileId)) return; filterState.fetchQueue.push(profileId); filterState.queueTotal = filterState.fetchQueue.length + filterState.queueCompleted; updateFilterStatusUI(); processFetchQueue(); } function getProfilePathFromCard(cardEl) { const profileId = cardEl.id; const links = Array.from(cardEl.querySelectorAll("a")); for (const link of links) { const href = link.getAttribute("href") || ""; const match = href.match(/\/members?\/([^?#]+)/); if (match) { return match[1].replace(/\/$/, ""); } } return profileId; } function processFetchQueue() { if (filterState.fetchQueue.length === 0 || filterState.activeFetches >= maxConcurrentFetches) { return; } if (Object.keys(state.authHeaders).length === 0) { setTimeout(processFetchQueue, 1000); return; } const profileId = filterState.fetchQueue.shift(); if (!profileId) return; filterState.activeFetches++; updateFilterStatusUI(); const card = document.getElementById(profileId); fetchProfileDetail(profileId, card).then((data) => { filterState.activeFetches--; filterState.queueCompleted++; updateFilterStatusUI(); if (data) { data.isDetailed = true; const cachedSummary = profileDetailsCache.get(profileId); const merged = cachedSummary ? { ...cachedSummary, ...data } : data; profileDetailsCache.set(profileId, merged); saveProfileToIndexedDB(profileId, merged); if (card) { applyFilterToCard(card, advancedFilterState); updateFilterStatusUI(); } } setTimeout(processFetchQueue, fetchDelayMs); }).catch((err) => { console.error(`[Recon+] Error fetching profile ${profileId}:`, err); filterState.activeFetches--; filterState.queueCompleted++; updateFilterStatusUI(); setTimeout(processFetchQueue, fetchDelayMs); }); if (filterState.activeFetches < maxConcurrentFetches && filterState.fetchQueue.length > 0) { processFetchQueue(); } } async function fetchProfileDetail(profileId, cardEl) { let data = profileDetailsCache.get(profileId); if (data && (data.isDetailed || typeof data.longText === "string")) { return data; } data = await getProfileFromIndexedDB(profileId) || undefined; if (data && (data.isDetailed || typeof data.longText === "string")) { profileDetailsCache.set(profileId, data); return data; } const path = cardEl ? getProfilePathFromCard(cardEl) : profileId; const parts = path.split("/"); const uuid = parts[0]; let version = parts[1] || null; if (!version) { version = profileVersionsMap.get(uuid) || null; } if (!version) { const summary = profileDetailsCache.get(uuid); if (summary) { if (summary.version) { version = summary.version; } else if (summary.profileUrl) { const match = summary.profileUrl.match(/\/profiles\/([^/]+)\/(\d+)/); if (match) { version = match[2]; } } } } if (!version) { version = 1; } const targetUrl = `/api/profile/profiles/${uuid}/detail/${version}?culture=en`; const res = await win.fetch(targetUrl, { headers: { ...state.authHeaders } }); if (!res.ok) { throw new Error(`Fetch status: ${res.status}`); } const json = await res.json(); return json; } function triggerFilterChange() { win.localStorage.setItem("recon_advanced_filters", JSON.stringify(advancedFilterState)); filterState.fetchQueue = []; filterState.queueTotal = 0; filterState.queueCompleted = 0; const cards = getActiveCards(); cards.forEach((card) => { applyFilterToCard(card, advancedFilterState); }); processFetchQueue(); } function updateFilterStatusUI() { const statusEl = document.getElementById("recon-filter-status"); const badgeEl = document.getElementById("recon-filter-count"); const loadMoreEl = document.getElementById("recon-filter-load-more"); if (!statusEl) return; const cards = getActiveCards(); const total = cards.length; const visible = cards.filter((c) => c.style.display !== "none").length; if (loadMoreEl) { const hasActiveFilter = advancedFilterState.keyword || advancedFilterState.activeWithin !== "any" || advancedFilterState.onlyPhotos || advancedFilterState.onlyNew || advancedFilterState.onlyPremium || advancedFilterState.heightMin || advancedFilterState.heightMax || advancedFilterState.bodyTypes.length > 0 || advancedFilterState.bodyHairs.length > 0 || advancedFilterState.ethnicities.length > 0; loadMoreEl.style.display = hasActiveFilter ? "block" : "none"; } if (Object.keys(state.authHeaders).length === 0) { statusEl.innerHTML = `<span>Waiting for authentication...</span>`; if (badgeEl) badgeEl.style.display = "none"; return; } if (badgeEl) { badgeEl.textContent = `${visible}/${total}`; badgeEl.style.display = "block"; } if (filterState.fetchQueue.length > 0 || filterState.activeFetches > 0) { const pct = filterState.queueTotal > 0 ? Math.round(filterState.queueCompleted / filterState.queueTotal * 100) : 0; statusEl.innerHTML = `<span>Scanning profiles...</span><span style="color: #ff3333; font-weight: bold;">${pct}% (${filterState.queueCompleted}/${filterState.queueTotal})</span>`; } else { statusEl.innerHTML = `<span>Filters active</span><span style="color: #ff3333; font-weight: bold;">Matched: ${visible}</span>`; } } function updatePanelVisibility() { const panel = document.getElementById("recon-filter-panel"); if (!panel) return; const hasCards = getActiveCards().length > 0; panel.style.display = hasCards ? "flex" : "none"; } function triggerLoadMore() { const loadMoreBtn = document.getElementById("recon-filter-load-more"); if (loadMoreBtn) { loadMoreBtn.disabled = true; loadMoreBtn.textContent = "Loading..."; } const cards = getActiveCards(); cards.forEach((card) => { card.style.removeProperty("display"); card.style.removeProperty("flex-grow"); card.style.removeProperty("flex-shrink"); card.style.removeProperty("max-width"); }); win.scrollTo({ top: document.body.scrollHeight, behavior: "instant" }); setTimeout(() => { triggerFilterChange(); if (loadMoreBtn) { loadMoreBtn.disabled = false; loadMoreBtn.textContent = "Load More"; } win.scrollTo({ top: 0, behavior: "smooth" }); }, 1200); } function injectFilterPanel() { if (!settings.enableFilterPanel) { document.getElementById("recon-filter-panel")?.remove(); return; } const myTypeFilter = document.querySelector("t101-profile-list-filter"); if (!myTypeFilter) return; let panel = document.getElementById("recon-filter-panel"); if (panel) { if (panel.parentNode !== myTypeFilter.parentNode) { myTypeFilter.parentNode.insertBefore(panel, myTypeFilter.nextSibling); } return; } panel = document.createElement("div"); panel.id = "recon-filter-panel"; panel.style.cssText = ` position: relative; width: 100%; box-sizing: border-box; color: #fff; display: none; flex-direction: column; `; const titleDiv = document.createElement("div"); titleDiv.style.cssText = ` display: flex; justify-content: space-between; align-items: center; user-select: none; margin-bottom: 8px; `; const titleH3 = document.createElement("h3"); titleH3.textContent = "Recon+ Filter"; titleH3.style.cssText = ` font: 20px/25px Barlow, sans-serif; font-weight: 700; color: #fff; margin: 0; `; const badgeSpan = document.createElement("span"); badgeSpan.id = "recon-filter-count"; badgeSpan.style.cssText = ` display: none; background: #ff3333; color: #fff; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 800; font-family: 'Barlow', sans-serif; `; titleDiv.appendChild(titleH3); titleDiv.appendChild(badgeSpan); panel.appendChild(titleDiv); const scrollContainer = document.createElement("div"); scrollContainer.className = "recon-filter-scroll-container"; const searchSection = document.createElement("div"); searchSection.className = "recon-filter-section"; const searchWrapper = document.createElement("div"); searchWrapper.style.cssText = `position: relative; display: flex; align-items: center; width: 100%;`; const input = document.createElement("input"); input.type = "text"; input.placeholder = "Search bio keywords..."; input.className = "recon-filter-input"; input.style.width = "100%"; input.value = advancedFilterState.keyword; const clearBtn = document.createElement("span"); clearBtn.innerHTML = "✕"; clearBtn.style.cssText = ` position: absolute; right: 12px; cursor: pointer; color: rgba(255, 255, 255, 0.4); font-size: 14px; display: ${input.value ? "block" : "none"}; user-select: none; `; clearBtn.onclick = () => { input.value = ""; clearBtn.style.display = "none"; advancedFilterState.keyword = ""; triggerFilterChange(); }; input.addEventListener("input", () => { clearBtn.style.display = input.value ? "block" : "none"; advancedFilterState.keyword = input.value.trim().toLowerCase(); triggerFilterChange(); }); searchWrapper.appendChild(input); searchWrapper.appendChild(clearBtn); searchSection.appendChild(searchWrapper); const infoText = document.createElement("div"); infoText.style.cssText = ` font-size: 11px; color: #adadad; font-family: 'Raleway', sans-serif; margin-top: 6px; line-height: 1.3; `; infoText.textContent = "Search runs client-side (requires profile loading time)"; searchSection.appendChild(infoText); scrollContainer.appendChild(searchSection); const onlineSection = document.createElement("div"); onlineSection.className = "recon-filter-section"; const onlineTitle = document.createElement("div"); onlineTitle.className = "recon-filter-section-title"; onlineTitle.textContent = "Active Status"; onlineSection.appendChild(onlineTitle); const onlineSelect = document.createElement("select"); onlineSelect.className = "recon-filter-input recon-filter-select"; onlineSelect.style.width = "100%"; const activeOpts = [ { val: "any", label: "Anytime" }, { val: "now", label: "Online Now (last 10m)" }, { val: "hour", label: "Past Hour" }, { val: "day", label: "Past 24 Hours" }, { val: "week", label: "Past Week" } ]; activeOpts.forEach((opt) => { const option = document.createElement("option"); option.value = opt.val; option.textContent = opt.label; option.style.backgroundColor = "#0b0b0d"; if (advancedFilterState.activeWithin === opt.val) { option.selected = true; } onlineSelect.appendChild(option); }); onlineSelect.onchange = () => { advancedFilterState.activeWithin = onlineSelect.value; }; onlineSection.appendChild(onlineSelect); scrollContainer.appendChild(onlineSection); const togglesSection = document.createElement("div"); togglesSection.className = "recon-filter-section"; const togglesTitle = document.createElement("div"); togglesTitle.className = "recon-filter-section-title"; togglesTitle.textContent = "Only Show"; togglesSection.appendChild(togglesTitle); const toggleWrapper = document.createElement("div"); toggleWrapper.style.cssText = `display: flex; flex-direction: column; gap: 8px;`; const createCheckbox = (labelTxt, key) => { const row = document.createElement("label"); row.style.cssText = `display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px; font-family: 'Raleway', sans-serif; color: #adadad;`; const chk = document.createElement("input"); chk.type = "checkbox"; chk.checked = advancedFilterState[key]; chk.style.cssText = `accent-color: #ff3333; width: 16px; height: 16px; cursor: pointer;`; chk.onchange = () => { advancedFilterState[key] = chk.checked; }; const span = document.createElement("span"); span.textContent = labelTxt; row.appendChild(chk); row.appendChild(span); return row; }; toggleWrapper.appendChild(createCheckbox("Guys with Photos", "onlyPhotos")); toggleWrapper.appendChild(createCheckbox("New Guys", "onlyNew")); toggleWrapper.appendChild(createCheckbox("Premium Members", "onlyPremium")); togglesSection.appendChild(toggleWrapper); scrollContainer.appendChild(togglesSection); const heightSection = document.createElement("div"); heightSection.className = "recon-filter-section"; const heightTitle = document.createElement("div"); heightTitle.className = "recon-filter-section-title"; heightTitle.textContent = "Height (cm)"; heightSection.appendChild(heightTitle); const heightRow = document.createElement("div"); heightRow.className = "recon-filter-row"; const minHeightInput = document.createElement("input"); minHeightInput.type = "number"; minHeightInput.placeholder = "Min height"; minHeightInput.className = "recon-filter-input"; minHeightInput.value = advancedFilterState.heightMin ? String(advancedFilterState.heightMin) : ""; minHeightInput.addEventListener("input", () => { advancedFilterState.heightMin = Number(minHeightInput.value) || 0; }); const maxHeightInput = document.createElement("input"); maxHeightInput.type = "number"; maxHeightInput.placeholder = "Max height"; maxHeightInput.className = "recon-filter-input"; maxHeightInput.value = advancedFilterState.heightMax ? String(advancedFilterState.heightMax) : ""; maxHeightInput.addEventListener("input", () => { advancedFilterState.heightMax = Number(maxHeightInput.value) || 0; }); heightRow.appendChild(minHeightInput); heightRow.appendChild(maxHeightInput); heightSection.appendChild(heightRow); scrollContainer.appendChild(heightSection); const createChipSetSection = (titleTxt, items, key) => { const section = document.createElement("div"); section.className = "recon-filter-section"; const title = document.createElement("div"); title.className = "recon-filter-section-title"; title.textContent = titleTxt; section.appendChild(title); const container = document.createElement("div"); container.className = "recon-chip-container"; items.forEach((item) => { const chip = document.createElement("span"); chip.className = "recon-filter-chip"; chip.textContent = item; const isSelected = advancedFilterState[key].includes(item); if (isSelected) chip.classList.add("selected"); chip.onclick = () => { const arr = advancedFilterState[key]; const idx = arr.indexOf(item); if (idx > -1) { arr.splice(idx, 1); chip.classList.remove("selected"); } else { arr.push(item); chip.classList.add("selected"); } }; container.appendChild(chip); }); section.appendChild(container); return section; }; scrollContainer.appendChild(createChipSetSection("Body Type", ["Athletic", "Average", "Large", "Muscled", "Slim"], "bodyTypes")); scrollContainer.appendChild(createChipSetSection("Body Hair", ["Average", "Hairy", "None", "Shaved", "Some"], "bodyHairs")); scrollContainer.appendChild(createChipSetSection("Ethnicity", ["Asian", "Black", "Hispanic", "Latino", "Middle Eastern", "Mixed Race", "South Asian", "White", "Other"], "ethnicities")); panel.appendChild(scrollContainer); const applyBtn = document.createElement("button"); applyBtn.className = "recon-apply-btn"; applyBtn.textContent = "Apply Filters"; applyBtn.onclick = () => { const oldSaved = win.localStorage.getItem("recon_advanced_filters"); let needsReload = false; if (oldSaved) { try { const old = JSON.parse(oldSaved); if (old.activeWithin !== advancedFilterState.activeWithin || old.onlyPhotos !== advancedFilterState.onlyPhotos || old.onlyNew !== advancedFilterState.onlyNew || old.onlyPremium !== advancedFilterState.onlyPremium || old.heightMin !== advancedFilterState.heightMin || old.heightMax !== advancedFilterState.heightMax || JSON.stringify(old.bodyTypes) !== JSON.stringify(advancedFilterState.bodyTypes) || JSON.stringify(old.bodyHairs) !== JSON.stringify(advancedFilterState.bodyHairs) || JSON.stringify(old.ethnicities) !== JSON.stringify(advancedFilterState.ethnicities)) { needsReload = true; } } catch (e) { needsReload = true; } } else { needsReload = true; } win.localStorage.setItem("recon_advanced_filters", JSON.stringify(advancedFilterState)); if (needsReload) { win.location.reload(); } else { triggerFilterChange(); } }; panel.appendChild(applyBtn); const statusDiv = document.createElement("div"); statusDiv.id = "recon-filter-status"; statusDiv.style.cssText = ` font-size: 12px; color: #adadad; font-family: 'Raleway', sans-serif; display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 10px; `; statusDiv.innerHTML = `<span>Status: Ready</span>`; const loadMoreBtn = document.createElement("button"); loadMoreBtn.id = "recon-filter-load-more"; loadMoreBtn.textContent = "Load More"; loadMoreBtn.style.cssText = ` background: rgba(255, 51, 51, 0.15); color: #ff3333; border: 1px solid rgba(255, 51, 51, 0.3); border-radius: 4px; padding: 4px 10px; font-size: 11px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-family: 'Barlow', sans-serif; display: none; transition: all 0.2s; `; loadMoreBtn.onmouseover = () => { loadMoreBtn.style.background = "#ff3333"; loadMoreBtn.style.color = "#fff"; }; loadMoreBtn.onmouseout = () => { loadMoreBtn.style.background = "rgba(255, 51, 51, 0.15)"; loadMoreBtn.style.color = "#ff3333"; }; loadMoreBtn.onclick = triggerLoadMore; statusDiv.appendChild(loadMoreBtn); panel.appendChild(statusDiv); myTypeFilter.parentNode.insertBefore(panel, myTypeFilter.nextSibling); triggerFilterChange(); } // src/index.ts (function() { loadSettings(); setupGeolocationMock(); injectStyles(); if (settings.enableXhrInterceptor) { setupXHRInterceptor(); } checkAndPaintProfileFromIndexedDB(); const observer = new MutationObserver(() => { injectSettingsButton(); injectViewedButton(); setupPhotoViewer(); if (state.lastProfileDetailPageData) { paintProfileDetailPage(state.lastProfileDetailPageData); } const match = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i); if (match && match[1]) { paintPremiumGallery(match[1]); } profileDetailsCache.forEach((data, profileId) => { const card = document.getElementById(profileId); if (card && !card.dataset.reconInjected) paintProfileData(profileId); }); injectFilterPanel(); updatePanelVisibility(); injectDistanceMap(); injectQuickGpsPanel(); injectAiSidebar(); const cards = getActiveCards(); cards.forEach((card) => { if (card.dataset.reconLastFilterKeyword !== advancedFilterState.keyword) { card.dataset.reconLastFilterKeyword = advancedFilterState.keyword; applyFilterToCard(card, advancedFilterState); } }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); if (state.lastProfileDetailPageData) { paintProfileDetailPage(state.lastProfileDetailPageData); } const initMatch = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i); if (initMatch && initMatch[1]) { paintPremiumGallery(initMatch[1]); } })(); })();