Unlocks Paywalls in recon
// ==UserScript==
// @name Recon+
// @namespace https://update.greasyfork.org/scripts/582087/Recon%2B.user.js
// @version 2.0
// @match https://www.recon.com/*
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @run-at document-start
// @description Unlocks Paywalls in recon
// ==/UserScript==
(function () {
'use strict';
const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const profileDetailsCache = new Map();
const profileVersionsMap = new Map();
let lastBlockedRequest = null;
let lastProfileDetailPageData = null;
const authHeaders = {};
// Complete dictionary for interest IDs
const 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"
};
// -------------------------------------------------------------------------
// 1. CSS INJECTION (Hiding Angular's original elements)
// -------------------------------------------------------------------------
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 });
}
// -------------------------------------------------------------------------
// 2. TOAST SYSTEM
// -------------------------------------------------------------------------
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);
}
// -------------------------------------------------------------------------
// 3. XHR INTERCEPTOR
// -------------------------------------------------------------------------
const origXHR = win.XMLHttpRequest;
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) {
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) {
lastBlockedRequest = {
url: url,
method: method,
headers: this._headers,
body: 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 () {
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())) {
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/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}`);
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;
};
// Helper to query location from IndexedDB (RECON_DB -> App_Locations)
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;
}
// Try querying as number first
const getRequest = store.get(Number(locationId));
getRequest.onsuccess = function () {
let result = checkVal(getRequest.result);
if (result) {
db.close();
resolve(result);
} else {
// Try querying as string
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);
}
});
}
// Helper to save location to IndexedDB (RECON_DB -> App_Locations)
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); // 1 year expiry
const value = {
data: {
locationId: Number(locationId),
locationUrl: locationUrl,
longName: longName,
name: 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);
}
}
// Helper to query profile from IndexedDB (RECON_DB -> Profile_Cache)
function getProfileFromIndexedDB(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("Profile_Cache")) {
db.close();
resolve(null);
return;
}
const transaction = db.transaction("Profile_Cache", "readonly");
const store = transaction.objectStore("Profile_Cache");
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 {
// Fallback to get as opposite type if keys are mixed
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);
}
});
}
// Helper to save profile to IndexedDB (RECON_DB -> Profile_Cache)
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;
}
// Merge details
const mergedData = existingData ? { ...existingData, ...data } : data;
const expiryDate = new Date();
expiryDate.setFullYear(expiryDate.getFullYear() + 1); // 1 year expiry
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\/([^/]+)/);
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) {
lastProfileDetailPageData = cachedProfile;
paintProfileDetailPage(cachedProfile);
}
});
}
}
}
// -------------------------------------------------------------------------
// 4. DOM INJECTION ENGINE
// -------------------------------------------------------------------------
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";
// A. Inject Location (Using IndexedDB with Fetch fallback)
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 = `📍 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 = `📍 ${rawLocId}`;
} else {
getLocationFromIndexedDB(rawLocId).then(cachedLoc => {
if (cachedLoc) {
const locName = cachedLoc.name || cachedLoc.longName || rawLocId;
locDiv.innerHTML = `📍 ${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: {
...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 = `📍 ${name}`;
saveLocationToIndexedDB(rawLocId, fetchUrl, name, longName);
} else {
locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`;
}
}).catch(() => { locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`; });
}
});
}
}
// B. Inject Full Interests Array
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 = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${formattedDate}</span>`;
} catch (e) {
dateDiv.innerHTML = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${data.lastUpdatedDate}</span>`;
}
if (container.tagName === 'H1') {
container.after(dateDiv);
} else {
container.appendChild(dateDiv);
}
}
// -------------------------------------------------------------------------
// 5. RESTORED FEATURES (Buttons & Lightbox)
// -------------------------------------------------------------------------
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: 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() {
showToast('Preparing profile export...', 'success');
let JSZipLib;
try {
JSZipLib = await getJSZip();
} catch (e) {
console.error(e);
showToast('Failed to load JSZip library.', 'error');
return;
}
const data = lastProfileDetailPageData;
const profileId = data ? data.id : (win.location.pathname.match(/\/members\/([^/]+)/) || [])[1] || 'unknown';
const displayName = data ? (data.displayName || data.username) : (document.title || 'Profile');
// 1. Gather all unique photo URLs
const photoUrls = new Set();
// From DOM photo-tiles
document.querySelectorAll('.photo-tile').forEach(t => {
const bgImg = t.style.backgroundImage;
if (bgImg) {
const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
if (url) photoUrls.add(url);
}
});
// From DOM profile avatars
document.querySelectorAll('.profile-avatar').forEach(t => {
const bgImg = t.style.backgroundImage;
if (bgImg) {
const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
if (url) photoUrls.add(url);
}
});
// From lastProfileDetailPageData recursively
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');
return;
}
showToast(`Found ${urlsArray.length} photos. Starting download...`, 'success');
const zip = new JSZipLib();
// 2. Add profile_info.txt
let infoText = '';
infoText += `=== RECON PROFILE EXPORT ===\n`;
infoText += `Export Date: ${new Date().toLocaleString()}\n`;
infoText += `Profile URL: ${win.location.href}\n\n`;
if (data) {
infoText += `Display Name: ${data.displayName || 'N/A'}\n`;
infoText += `Username: ${data.username || 'N/A'}\n`;
infoText += `Profile ID: ${data.id || 'N/A'}\n`;
if (data.location) {
infoText += `Location: ID ${data.location.locationId || 'N/A'}\n`;
}
if (data.lastUpdatedDate) {
infoText += `Last Updated: ${data.lastUpdatedDate}\n`;
}
if (data.interests && data.interests.length > 0) {
infoText += `Interests: ${data.interests.map(id => interestMap[id] || `Tag ${id}`).join(', ')}\n`;
}
infoText += `\n--- PROFILE INFO (RAW KEY-VALUES) ---\n`;
for (const [key, val] of Object.entries(data)) {
if (typeof val !== 'object' && val !== null) {
infoText += `${key}: ${val}\n`;
}
}
}
// Gather bio and details text from DOM
const bioSection = document.querySelector('.text-container, .bio-container, .member-bio');
if (bioSection) {
infoText += `\n--- PROFILE BIO ---\n`;
infoText += bioSection.innerText.trim() + `\n`;
}
const infoSections = document.querySelectorAll('.details-container, .stats-container, .profile-details');
if (infoSections.length > 0) {
infoText += `\n--- PROFILE DETAILS ---\n`;
infoSections.forEach(section => {
infoText += section.innerText.trim() + `\n\n`;
});
}
zip.file("profile_info.txt", infoText);
// 3. Add raw JSON if present
if (data) {
zip.file("profile_data.json", JSON.stringify(data, null, 4));
}
// 4. Download and add photos
let successCount = 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);
}
});
await Promise.all(downloadPromises);
if (successCount === 0) {
showToast('Failed to download any profile photos.', 'error');
return;
}
showToast(`Zipping ${successCount} photos...`, 'success');
try {
const content = await zip.generateAsync({ type: 'blob' });
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);
showToast(`Export complete! Saved ${successCount}/${urlsArray.length} photos.`, 'success');
} catch (err) {
console.error('[Script] Error creating zip file:', err);
showToast('Error generating zip archive.', 'error');
}
}
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;
// Inject Viewed Button
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 (!lastBlockedRequest) return showToast('No profile visit tracker captured yet.', 'error');
const xhr = new origXHR();
xhr._isManualTrigger = true;
xhr.open(lastBlockedRequest.method, lastBlockedRequest.url);
if (lastBlockedRequest.headers) {
for (const [key, value] of Object.entries(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(lastBlockedRequest.body);
});
targetEl.parentNode.insertBefore(viewBtn, targetEl.nextSibling);
}
// Inject Export Button
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);
}
}
}
function setupPhotoViewer() {
const tiles = document.querySelectorAll('.photo-tile:not([data-lightbox-ready])');
tiles.forEach(tile => {
tile.setAttribute('data-lightbox-ready', 'true');
tile.style.pointerEvents = 'auto'; tile.style.cursor = 'pointer';
tile.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
// Get all photo tiles in the same gallery container, or globally on the page if not grouped
const container = tile.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 bgImg = t.style.backgroundImage;
return bgImg ? bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0] : null;
}).filter(Boolean);
const bgImg = tile.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);
}
// -------------------------------------------------------------------------
// 5.5. DESCRIPTION KEYWORD FILTER
// -------------------------------------------------------------------------
let currentSearchKeyword = '';
let fetchQueue = [];
let activeFetches = 0;
let queueTotal = 0;
let queueCompleted = 0;
const maxConcurrentFetches = 2;
const 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') || el.querySelector('.photo-tile') || el.classList.contains('photo-tile')));
}
function containsKeyword(obj, keyword) {
if (!obj) return false;
if (typeof obj === 'string') {
const val = obj.toLowerCase();
// Skip URLs and image paths to avoid matching on file names/extensions
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();
// Skip technical keys
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;
}
// Check memory cache
let data = profileDetailsCache.get(profileId);
console.log(`[Recon+] Memory cache check for ${profileId}:`, data ? "Found" : "Not Found");
// Check IndexedDB cache
if (!data) {
data = await getProfileFromIndexedDB(profileId);
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) {
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...`);
// Keep visible until fetched
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 (fetchQueue.includes(profileId)) return;
fetchQueue.push(profileId);
queueTotal = fetchQueue.length + 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(/\/$/, ''); // Remove trailing slash
}
}
return profileId;
}
function processFetchQueue() {
if (fetchQueue.length === 0 || activeFetches >= maxConcurrentFetches) {
return;
}
if (Object.keys(authHeaders).length === 0) {
setTimeout(processFetchQueue, 1000);
return;
}
const profileId = fetchQueue.shift();
activeFetches++;
updateFilterStatusUI();
const card = document.getElementById(profileId);
fetchProfileDetail(profileId, card).then(data => {
activeFetches--;
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 && currentSearchKeyword) {
const matches = containsKeyword(merged, 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);
activeFetches--;
queueCompleted++;
updateFilterStatusUI();
setTimeout(processFetchQueue, fetchDelayMs);
});
if (activeFetches < maxConcurrentFetches && 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);
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;
// Try profileVersionsMap
if (!version) {
version = profileVersionsMap.get(uuid);
if (version) console.log(`[Recon+] Version for ${uuid} found in profileVersionsMap: ${version}`);
}
// Try summary object
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}`);
}
}
}
}
// Default version if not found
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: {
...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) {
currentSearchKeyword = val.trim().toLowerCase();
fetchQueue = [];
queueTotal = 0;
queueCompleted = 0;
const cards = getActiveCards();
if (!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 = currentSearchKeyword;
applyFilterToCard(card, 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 = currentSearchKeyword ? 'block' : 'none';
}
if (Object.keys(authHeaders).length === 0) {
statusEl.innerHTML = `<span>Waiting for authentication...</span>`;
if (badgeEl) badgeEl.style.display = 'none';
return;
}
if (!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 (fetchQueue.length > 0 || activeFetches > 0) {
const pct = queueTotal > 0 ? Math.round((queueCompleted / queueTotal) * 100) : 0;
statusEl.innerHTML = `<span>Scanning profiles...</span><span style="color: #ff3333; font-weight: bold;">${pct}% (${queueCompleted}/${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 (currentSearchKeyword) {
triggerFilterChange(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);
}
// -------------------------------------------------------------------------
// 6. OBSERVER LIFECYCLE
// -------------------------------------------------------------------------
const observer = new MutationObserver(() => {
injectViewedButton();
setupPhotoViewer();
if (lastProfileDetailPageData) {
paintProfileDetailPage(lastProfileDetailPageData);
}
profileDetailsCache.forEach((data, profileId) => {
const card = document.getElementById(profileId);
if (card && !card.dataset.reconInjected) paintProfileData(profileId);
});
// Recon+ Keyword Filter hooks
injectFilterPanel();
updatePanelVisibility();
if (currentSearchKeyword) {
const cards = getActiveCards();
cards.forEach(card => {
if (card.dataset.reconLastFilterKeyword !== currentSearchKeyword) {
card.dataset.reconLastFilterKeyword = currentSearchKeyword;
applyFilterToCard(card, currentSearchKeyword);
}
});
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// Handle initial state if page is loaded
if (lastProfileDetailPageData) {
paintProfileDetailPage(lastProfileDetailPageData);
}
})();