Unlocks Paywalls in recon
// ==UserScript== // @name Recon+ // @namespace https://update.greasyfork.org/scripts/582087/Recon%2B.user.js // @version 2.1 // @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: {} }; 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/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) { 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: "40225", 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() { 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; } var PROBE_OFFSET_METRES = 5000; async function triangulate(targetProfileId, myProfileId, centerLat, centerLon, onProgress) { const probeOffsets = [ { name: "origin", north: 0, east: 0 }, { name: "North", north: PROBE_OFFSET_METRES, east: 0 }, { name: "South", north: -PROBE_OFFSET_METRES, east: 0 }, { name: "East", north: 0, east: PROBE_OFFSET_METRES }, { name: "West", north: 0, east: -PROBE_OFFSET_METRES } ]; const targetAge = getTargetAge(); let probes = []; const runScan = async (ageFilter) => { probes = []; for (let i = 0;i < probeOffsets.length; i++) { const off = probeOffsets[i]; const { lat, lon } = offsetLatLon(centerLat, centerLon, off.north, off.east); const filterLabel = ageFilter ? `age ${ageFilter}` : "broad search"; onProgress(`Scanning ${i + 1}/${probeOffsets.length} (${off.name}, ${filterLabel})…`); const hit = await searchFromPoint(lat, lon, targetProfileId, myProfileId, ageFilter); if (hit) { probes.push({ lat, lon, isHit: true, distanceMetres: hit.distanceMetres }); } else { probes.push({ lat, lon, isHit: false, distanceMetres: 0 }); } await new Promise((r) => setTimeout(r, 600)); } }; await runScan(targetAge); let hits = probes.filter((p) => p.isHit); if (hits.length === 0 && targetAge !== null) { onProgress("No hits with age filter. Retrying with broad age search…"); await runScan(null); hits = probes.filter((p) => p.isHit); } if (hits.length === 0) { onProgress("Profile not found in any probe search."); return null; } let targetDist = getProfileDistanceMetres(); if (!targetDist || targetDist <= 0) { const firstHit = hits[0]; const distFromCenterToProbe = haversine(centerLat, centerLon, firstHit.lat, firstHit.lon); targetDist = firstHit.distanceMetres + distFromCenterToProbe; } onProgress("Resolving location using positive and negative constraints…"); let bestLat = centerLat; let bestLon = centerLon; let minPenalty = Infinity; for (let deg = 0;deg < 360; deg++) { const bearingRad = degToRad(deg); const candidate = destinationPoint(centerLat, centerLon, bearingRad, targetDist); let penalty = 0; for (const probe of probes) { const distToCandidate = haversine(probe.lat, probe.lon, candidate.lat, candidate.lon); if (probe.isHit) { penalty += Math.abs(distToCandidate - probe.distanceMetres); } else { const maxRadius = 40225; if (distToCandidate < maxRadius - 1000) { penalty += (maxRadius - 1000 - distToCandidate) * 5; } } } if (penalty < minPenalty) { minPenalty = penalty; bestLat = candidate.lat; bestLon = candidate.lon; } } const radiusMetres = Math.max(200, 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 (!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; const exportBtn = document.getElementById("custom-export-profile-btn"); if (!exportBtn) 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"; exportBtn.parentNode.insertBefore(btn, exportBtn.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 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) anchor.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 targetPoint = destinationPoint(pos.lat, pos.lon, bearing, dist); 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: targetPoint.lat, lon: targetPoint.lon, radiusMetres: 0, color: "#f97316", label: "\uD83D\uDCCC Estimated Location", subtitle: `On ring edge · bearing ${Math.round(radToDeg(bearing + 2 * Math.PI) % 360)}°`, pinOnly: true }; renderDualMap(mapContainer, L2, targetLayer, userLayer); showToast(`Location estimated on ring edge — ${Math.round(dist)}m from you`, "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"; }); } // 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; 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) { 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/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) { 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("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); } } } }); return origSend.apply(this, arguments); }; return xhr; }; } // src/styles.ts function injectStyles() { const style = document.createElement("style"); style.innerHTML = ` .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; } `; 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 filterState = { currentSearchKeyword: "", 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; } const matched = val.includes(keyword); if (matched) { console.log(`[Recon+] containsKeyword: Found match in string "${val}" for keyword "${keyword}"`); } return matched; } 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, keyword) { const profileId = cardEl.id; console.log(`[Recon+] applyFilterToCard: card=${profileId}, keyword="${keyword}"`); if (!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); console.log(`[Recon+] Memory cache check for ${profileId}:`, data ? "Found" : "Not Found"); if (!data) { data = await getProfileFromIndexedDB(profileId) || undefined; console.log(`[Recon+] IndexedDB cache check for ${profileId}:`, data ? "Found" : "Not Found"); if (data) { profileDetailsCache.set(profileId, data); } } const isDetailed = data && (data.isDetailed || typeof data.longText === "string"); console.log(`[Recon+] ${profileId} isDetailed = ${isDetailed}`); if (isDetailed && data) { const matches = containsKeyword(data, keyword); console.log(`[Recon+] Profile ${profileId} matches keyword "${keyword}"? ${matches}`); if (matches) { 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"); cardEl.style.removeProperty("flex-grow"); cardEl.style.removeProperty("flex-shrink"); cardEl.style.removeProperty("max-width"); } updateFilterStatusUI(); } else { console.log(`[Recon+] Profile ${profileId} details not available yet. Enqueueing fetch...`); 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 && filterState.currentSearchKeyword) { const matches = containsKeyword(merged, filterState.currentSearchKeyword); if (matches) { card.style.removeProperty("display"); card.style.setProperty("flex-grow", "0", "important"); card.style.setProperty("flex-shrink", "0", "important"); card.style.setProperty("max-width", "220px", "important"); } else { card.style.setProperty("display", "none", "important"); card.style.removeProperty("flex-grow"); card.style.removeProperty("flex-shrink"); card.style.removeProperty("max-width"); } 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) { console.log(`[Recon+] fetchProfileDetail starting for ${profileId}`); let data = profileDetailsCache.get(profileId); if (data && (data.isDetailed || typeof data.longText === "string")) { console.log(`[Recon+] Found detailed profile in memory for ${profileId}`); return data; } data = await getProfileFromIndexedDB(profileId) || undefined; if (data && (data.isDetailed || typeof data.longText === "string")) { console.log(`[Recon+] Found detailed profile in IndexedDB for ${profileId}`); profileDetailsCache.set(profileId, data); return data; } const path = cardEl ? getProfilePathFromCard(cardEl) : profileId; console.log(`[Recon+] Path for ${profileId} parsed as: "${path}"`); const parts = path.split("/"); const uuid = parts[0]; let version = parts[1] || null; if (!version) { version = profileVersionsMap.get(uuid) || null; if (version) console.log(`[Recon+] Version for ${uuid} found in profileVersionsMap: ${version}`); } if (!version) { const summary = profileDetailsCache.get(uuid); if (summary) { if (summary.version) { version = summary.version; console.log(`[Recon+] Version for ${uuid} found in summary.version: ${version}`); } else if (summary.profileUrl) { const match = summary.profileUrl.match(/\/profiles\/([^/]+)\/(\d+)/); if (match) { version = match[2]; console.log(`[Recon+] Version for ${uuid} found in summary.profileUrl: ${version}`); } } } } if (!version) { console.warn(`[Recon+] Version not found for profile ${uuid}, defaulting to 1`); version = 1; } const targetUrl = `/api/profile/profiles/${uuid}/detail/${version}?culture=en`; console.log(`[Recon+] Fetching detail for ${uuid} from: ${targetUrl}`); const res = await win.fetch(targetUrl, { headers: { ...state.authHeaders } }); if (!res.ok) { console.error(`[Recon+] Fetch failed: Status ${res.status} for ${targetUrl}`); throw new Error(`Fetch status: ${res.status}`); } const json = await res.json(); console.log(`[Recon+] Fetch success for ${uuid}:`, json); return json; } function triggerFilterChange(val) { filterState.currentSearchKeyword = val.trim().toLowerCase(); filterState.fetchQueue = []; filterState.queueTotal = 0; filterState.queueCompleted = 0; const cards = getActiveCards(); if (!filterState.currentSearchKeyword) { cards.forEach((card) => { card.style.removeProperty("display"); card.style.removeProperty("flex-grow"); card.style.removeProperty("flex-shrink"); card.style.removeProperty("max-width"); delete card.dataset.reconLastFilterKeyword; }); updateFilterStatusUI(); return; } cards.forEach((card) => { card.dataset.reconLastFilterKeyword = filterState.currentSearchKeyword; applyFilterToCard(card, filterState.currentSearchKeyword); }); 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) { loadMoreEl.style.display = filterState.currentSearchKeyword ? "block" : "none"; } if (Object.keys(state.authHeaders).length === 0) { statusEl.innerHTML = `<span>Waiting for authentication...</span>`; if (badgeEl) badgeEl.style.display = "none"; return; } if (!filterState.currentSearchKeyword) { statusEl.innerHTML = `<span>Status: Filter inactive</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>Scan complete</span><span style="color: #2acf65; font-weight: bold;">Matched: ${visible}</span>`; } } function updatePanelVisibility() { const panel = document.getElementById("recon-filter-panel"); if (!panel) return; const hasCards = getActiveCards().length > 0; if (hasCards) { panel.style.display = "flex"; } else { panel.style.display = "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(() => { if (filterState.currentSearchKeyword) { triggerFilterChange(filterState.currentSearchKeyword); } if (loadMoreBtn) { loadMoreBtn.disabled = false; loadMoreBtn.textContent = "Load More"; } win.scrollTo({ top: 0, behavior: "smooth" }); }, 1200); } function injectFilterPanel() { const myTypeFilter = document.querySelector("t101-profile-my-type-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; background-color: #ffffff26; padding: 25px 15px 15px; width: 100%; box-sizing: border-box; color: #fff; display: none; flex-direction: column; gap: 15px; font-family: 'Barlow', sans-serif; border-bottom: 1px solid rgba(255, 255, 255, 0.25); `; const titleDiv = document.createElement("div"); titleDiv.style.cssText = ` display: flex; justify-content: space-between; align-items: center; user-select: none; `; 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; letter-spacing: 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 inputWrapper = document.createElement("div"); inputWrapper.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.style.cssText = ` background: rgba(0, 0, 0, 0.25); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: #fff; padding: 10px 32px 10px 12px; font-size: 14px; outline: none; width: 100%; box-sizing: border-box; font-family: 'Raleway', sans-serif; transition: border-color 0.2s, background 0.2s; `; input.onfocus = () => { input.style.borderColor = "#ff3333"; input.style.background = "rgba(0, 0, 0, 0.35)"; }; input.onblur = () => { input.style.borderColor = "rgba(255, 255, 255, 0.15)"; input.style.background = "rgba(0, 0, 0, 0.25)"; }; 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: none; user-select: none; transition: color 0.2s; `; clearBtn.onmouseover = () => clearBtn.style.color = "#fff"; clearBtn.onmouseout = () => clearBtn.style.color = "rgba(255, 255, 255, 0.4)"; clearBtn.onclick = () => { input.value = ""; clearBtn.style.display = "none"; triggerFilterChange(""); }; input.addEventListener("input", () => { clearBtn.style.display = input.value ? "block" : "none"; triggerFilterChange(input.value); }); inputWrapper.appendChild(input); inputWrapper.appendChild(clearBtn); panel.appendChild(inputWrapper); 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%; `; 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); } // src/index.ts (function() { injectStyles(); setupXHRInterceptor(); checkAndPaintProfileFromIndexedDB(); const observer = new MutationObserver(() => { 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(); if (filterState.currentSearchKeyword) { const cards = getActiveCards(); cards.forEach((card) => { if (card.dataset.reconLastFilterKeyword !== filterState.currentSearchKeyword) { card.dataset.reconLastFilterKeyword = filterState.currentSearchKeyword; applyFilterToCard(card, filterState.currentSearchKeyword); } }); } }); 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]); } })(); })();