Sleazy Fork is available in English.
Multi-archive search tool with modern dashboard design + Import/Export + Caching + History
// ==UserScript==
// @name Cam ARNA
// @namespace http://tampermonkey.net/
// @version 2.4.1
// @description Multi-archive search tool with modern dashboard design + Import/Export + Caching + History
// @author user006-ui
// @license MIT
// @match https://*.stripchat.com/*
// @match https://*.chaturbate.com/*
// @match https://chaturbate.com/*
// @match https://*.bongacams.com/*
// @match https://bongacams.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_openInTab
// @connect archivebate.com
// @connect showcamrips.com
// @connect camshowrecordings.com
// @connect camwh.com
// @connect topcamvideos.com
// @connect lovecamporn.com
// @connect camwhores.tv
// @connect bestcam.tv
// @connect xhomealone.com
// @connect stream-leak.com
// @connect mfcamhub.com
// @connect camshowrecord.net
// @connect camwhoresbay.com
// @connect camsave1.com
// @connect onscreens.me
// @connect livecamrips.to
// @connect cumcams.cc
// @connect allmy.cam
// @connect livecamsrip.com
// @connect stripchat.com
// @connect chaturbate.com
// @connect bongacams.com
// @connect camgirlfinder.net
// @connect nrtool.to
// @connect camsrip.com
// @connect camcaps.tv
// @connect curbate.tv
// @connect rec-tube.com
// @connect camshaip.com
// @connect camsclips.net
// @connect motherless.com
// @connect webpussi.com
// ==/UserScript==
(function() {
'use strict';
// --- Configuration & Utils ---
const Config = {
version: '2.4.1',
cacheTTL: 30 * 60 * 1000, // 30 minutes
maxHistory: 10,
colors: {
bg: '#0f172a',
surface: '#1e293b',
border: '#334155',
primary: '#6366f1',
accent: '#818cf8',
text: '#f8fafc',
textMuted: '#94a3b8',
success: '#10b981',
error: '#ef4444'
}
};
const Utils = {
escapeRegex: (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
isValidUsername: (str) => /^[a-zA-Z0-9_\-]{3,50}$/.test(str),
openSafe: (url) => {
if (typeof GM_openInTab === 'function') {
GM_openInTab(url, { active: true, insert: true, setParent: true });
} else {
window.open(url, '_blank', 'noopener,noreferrer');
}
},
getCurrentPlatform: () => {
const h = window.location.host;
if (h.includes('chaturbate')) return 'CB';
if (h.includes('stripchat')) return 'SC';
if (h.includes('bongacams')) return 'BC';
return 'Other';
},
crypto: {
secret: 'CAM_ARNA_SALT_v3_MODERN',
encrypt: (text) => {
if (!text) return '';
try { return CryptoJS.AES.encrypt(text, Utils.crypto.secret).toString(); } catch (e) { return ''; }
},
decrypt: (ciphertext) => {
if (!ciphertext) return '';
try {
const bytes = CryptoJS.AES.decrypt(ciphertext, Utils.crypto.secret);
return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) { return ''; }
}
}
};
const Storage = {
get: (key, defaultValue) => GM_getValue(key, defaultValue),
set: (key, value) => GM_setValue(key, value)
};
// --- Sites Configuration ---
const archiveSites = [
{ name: 'Archivebate', url: 'https://archivebate.com/profile/{username}', domain: 'archivebate.com' },
{ name: 'Showcamrips', url: 'https://showcamrips.com/model/en/{username}', domain: 'showcamrips.com' },
{ name: 'CamRecordings', url: 'https://www.camshowrecordings.com/model/{username}', domain: 'camshowrecordings.com' },
{ name: 'CamWH', url: 'https://camwh.com/tags/{username}/', domain: 'camwh.com' },
{ name: 'TopCam', url: 'https://www.topcamvideos.com/showall/?search={username}', domain: 'topcamvideos.com' },
{ name: 'LoveCam', url: 'https://lovecamporn.com/showall/?search={username}', domain: 'lovecamporn.com' },
{ name: 'Camwhores.tv', url: 'https://www.camwhores.tv/search/{username}/', domain: 'camwhores.tv' },
{ name: 'Bestcam', url: 'https://bestcam.tv/model/{username}', domain: 'bestcam.tv' },
{ name: 'XHome', url: 'https://xhomealone.com/tags/{username}/', domain: 'xhomealone.com' },
{ name: 'StreamLeak', url: 'https://stream-leak.com/models/{username}/', domain: 'stream-leak.com' },
{ name: 'MFCamHub', url: 'https://mfcamhub.com/models/{username}/', domain: 'mfcamhub.com' },
{ name: 'CamRecord', url: 'https://camshowrecord.net/video/list?page=1&model={username}', domain: 'camshowrecord.net' },
{ name: 'CW Bay', url: 'https://www.camwhoresbay.com/search/{username}/', domain: 'camwhoresbay.com' },
{ name: 'CamSave', url: 'https://www.camsave1.com/?search={username}&women=true', domain: 'camsave1.com' },
{ name: 'OnScreens', url: 'https://www.onscreens.me/m/{username}', domain: 'onscreens.me' },
{ name: 'LiveCamRips', url: 'https://livecamrips.to/search/{username}/1', domain: 'livecamrips.to' },
{ name: 'CumCams', url: 'https://cumcams.cc/performer/{username}', domain: 'cumcams.cc' },
{ name: 'AllMyCam', url: 'https://allmy.cam/search/{username}/', domain: 'allmy.cam' },
{ name: 'LCRip', url: 'https://www.livecamsrip.com/{username}/profile', domain: 'livecamsrip.com' },
{ name: 'CamsRip', url: 'https://camsrip.com/{username}/profile', domain: 'camsrip.com' },
{ name: 'CamCaps', url: 'https://camcaps.tv/search/videos/{username}', domain: 'camcaps.tv' },
{ name: 'Curbate', url: 'https://curbate.tv/search?q={username}', domain: 'curbate.tv' },
{ name: 'RecTube', url: 'https://rec-tube.com/search/{username}/', domain: 'rec-tube.com' },
{ name: 'CamShaip', url: 'https://camshaip.com/search/{username}/', domain: 'camshaip.com' },
{ name: 'CamsClips', url: 'https://www.camsclips.net/search/{username}/', domain: 'camsclips.net' },
{ name: 'Motherless', url: 'https://motherless.com/term/{username}', domain: 'motherless.com' },
{ name: 'WebPussi', url: 'https://www.webpussi.com/search/{username}/', domain: 'webpussi.com' }
];
function getFaviconUrl(domain) {
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
}
// --- Page Checker Logic ---
const PageChecker = {
checkPage: function(url) {
return new Promise((resolve) => {
try {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 10000,
onload: (res) => {
try { resolve(this.analyze(res, url)); }
catch(e) { resolve(false); }
},
onerror: () => resolve(false),
ontimeout: () => resolve(false)
});
} catch(e) {
resolve(false);
}
});
},
analyze: function(response, url) {
try {
if (!response || response.status === 404 || response.status >= 500) return false;
const text = response.responseText;
if (!text || typeof text !== 'string') return false;
const lowerText = text.toLowerCase();
const titleMatch = lowerText.match(/<title[^>]*>(.*?)<\/title>/i);
const title = titleMatch ? titleMatch[1] : '';
if (url.includes('livecamrips.to')) {
const noResultPatterns = ['no records found', 'no models found', 'no results', '0 models found'];
if (noResultPatterns.some(p => lowerText.includes(p))) return false;
if (!text.includes('class="video"') && !text.includes('model-card')) return false;
}
if (url.includes('cumcams.cc')) {
const notFoundPatterns = [ /<h1[^>]*>404<\/h1>/i, /performer\s*not\s*found/i ];
if (notFoundPatterns.some(p => p.test(text))) return false;
if (!text.includes('profile-info') && !text.includes('class="performer"')) return false;
}
if (url.includes('allmy.cam')) {
if (!text.includes('class="video-card"')) return false;
}
if (url.includes('showcamrips')) {
if (text.includes('data:image/png;base64')) return false;
}
if (url.includes('camshowrecordings.com')) {
if (!text.includes('class="h1modelpage"')) return false;
}
if (url.includes('camwhores.tv')) {
if (/no\s*videos?\s*found|0\s*videos/i.test(lowerText)) return false;
}
if (['not found', '404', 'error'].some(term => title.includes(term))) return false;
const genericNotFound = [/no\s*videos?\s*found/i, /no\s*results?\s*found/i, /does\s*not\s*exist/i, /0\s*results?/i];
if (genericNotFound.some(p => p.test(lowerText))) return false;
return true;
} catch(e) {
return false; // Error parsing DOM or text = not found
}
}
};
// --- Styling ---
function injectStyles() {
GM_addStyle(`
:root {
--ca-bg: ${Config.colors.bg};
--ca-surf: ${Config.colors.surface};
--ca-border: ${Config.colors.border};
--ca-prim: ${Config.colors.primary};
--ca-acc: ${Config.colors.accent};
--ca-text: ${Config.colors.text};
--ca-muted: ${Config.colors.textMuted};
--ca-success: ${Config.colors.success};
--ca-error: ${Config.colors.error};
}
.ca-fab {
position: fixed; bottom: 20px; right: 20px;
width: 50px; height: 50px; border-radius: 50%;
background: var(--ca-prim); color: white; border: none;
cursor: pointer; z-index: 9999; font-size: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: flex; align-items: center; justify-content: center;
}
.ca-badge {
position: absolute; top: -5px; right: -5px;
background: var(--ca-error); color: white;
font-size: 11px; font-weight: bold;
padding: 2px 6px; border-radius: 10px;
display: none; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.ca-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 10000; font-family: sans-serif;
}
.ca-panel {
background: var(--ca-bg); width: 450px; max-height: 80vh;
border-radius: 12px; border: 1px solid var(--ca-border);
display: flex; flex-direction: column; overflow: hidden; color: var(--ca-text);
}
.ca-head { padding: 16px; border-bottom: 1px solid var(--ca-border); display: flex; justify-content: space-between; align-items: center; }
.ca-brand { font-weight: bold; font-size: 18px; display: flex; align-items: center; gap: 8px; }
.ca-tabs { display: flex; background: var(--ca-surf); padding: 4px; gap: 4px; flex-wrap: wrap; }
.ca-tab { flex: 1; padding: 8px; border: none; background: none; color: var(--ca-muted); cursor: pointer; border-radius: 6px; font-size: 13px; }
.ca-tab.active { background: var(--ca-prim); color: white; }
.ca-body { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; flex: 1; }
.ca-search-box { position: relative; margin-bottom: 16px; }
.ca-input { width: 100%; padding: 10px 10px 10px 35px; background: var(--ca-surf); border: 1px solid var(--ca-border); border-radius: 8px; color: white; box-sizing: border-box; }
.ca-icon { position: absolute; left: 10px; top: 10px; width: 18px; color: var(--ca-muted); }
.ca-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.ca-item { background: var(--ca-surf); padding: 10px; border-radius: 8px; border: 1px solid var(--ca-border); display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; position: relative; }
.ca-item:hover { border-color: var(--ca-prim); }
.ca-item img { width: 16px; height: 16px; }
.ca-item-name { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ca-status { width: 8px; height: 8px; border-radius: 50%; margin-left: auto; }
.checking .ca-status { background: orange; animation: pulse 1s infinite; }
.found { border-color: var(--ca-success) !important; }
.found .ca-status { background: var(--ca-success); }
.not-found { opacity: 0.5; filter: grayscale(1); }
.not-found .ca-status { background: var(--ca-error); }
.ca-toast { position: fixed; bottom: 80px; right: 20px; background: var(--ca-prim); padding: 8px 16px; border-radius: 4px; color: white; z-index: 10001; }
.ca-close { background: none; border: none; color: var(--ca-muted); cursor: pointer; font-size: 18px; }
.ca-btn-small { padding: 4px 8px; background: var(--ca-surf); border: 1px solid var(--ca-border); border-radius: 4px; color: var(--ca-text); cursor: pointer; font-size: 12px; }
.ca-btn-small:hover { background: var(--ca-prim); border-color: var(--ca-prim); }
.ca-tag { font-size: 10px; background: var(--ca-border); padding: 2px 6px; border-radius: 10px; color: var(--ca-text); }
.ca-history-item { padding: 8px; border-bottom: 1px solid var(--ca-border); display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.ca-history-item:hover { background: var(--ca-surf); }
.ca-site-toggle { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid var(--ca-border); }
@keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
`);
}
// --- Main UI Logic ---
const UI = {
isOpen: false,
cacheManager: {
get: (username) => {
try {
let cache = Storage.get('ca_cache', {});
let entry = cache[username];
if (entry && Date.now() < entry.expires) return entry.results;
} catch(e) {}
return null;
},
set: (username, resultsMap) => {
try {
let cache = Storage.get('ca_cache', {});
const now = Date.now();
for (let k in cache) { if (now > cache[k].expires) delete cache[k]; }
cache[username] = { expires: now + Config.cacheTTL, results: resultsMap };
Storage.set('ca_cache', cache);
} catch(e) {}
}
},
toggle: function() {
if (this.isOpen) {
document.querySelector('.ca-overlay')?.remove();
this.isOpen = false;
} else {
this.render();
this.isOpen = true;
const activeUser = document.getElementById('ca-user').value.trim();
if (activeUser) document.getElementById('ca-user').select();
}
},
render: function() {
const overlay = document.createElement('div');
overlay.className = 'ca-overlay';
overlay.onclick = (e) => { if(e.target === overlay) UI.toggle(); };
const panel = document.createElement('div');
panel.className = 'ca-panel';
panel.innerHTML = `
<div class="ca-head">
<div class="ca-brand">
<span>ARNA</span>
<span style="font-size:10px; color:var(--ca-muted); border:1px solid var(--ca-border); padding:1px 4px; border-radius:4px;">${Config.version}</span>
</div>
<button class="ca-close">✕</button>
</div>
<div class="ca-tabs">
<button class="ca-tab active" data-view="search">Search</button>
<button class="ca-tab" data-view="history">History</button>
<button class="ca-tab" data-view="saved">Saved</button>
<button class="ca-tab" data-view="tools">Tools</button>
</div>
<div class="ca-body">
<div class="ca-search-box">
<svg class="ca-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21l-4.35-4.35m1.35-5.65a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<input type="text" class="ca-input" id="ca-user" placeholder="Search username..." autocomplete="off">
</div>
<!-- Search View -->
<div id="view-search">
<div style="font-size: 12px; color: var(--ca-muted); margin-bottom: 8px; display: flex; justify-content: space-between;">
<span>Archives / Recorders</span>
<span id="ca-counter">0 / 0 found</span>
</div>
<div class="ca-grid" id="ca-archives"></div>
<div class="ca-item" id="ca-btn-save" style="justify-content:center; border-style:dashed; opacity:0.8; margin-top: 12px;">
<span class="ca-item-name">Save to Favorites</span>
</div>
</div>
<!-- History View -->
<div id="view-history" style="display:none;">
<div style="font-size: 12px; color: var(--ca-muted); margin-bottom: 8px;">Recent Searches</div>
<div id="ca-history-list"></div>
</div>
<!-- Saved View -->
<div id="view-saved" style="display:none;">
<div style="display:flex; gap:8px; margin-bottom:12px;">
<button id="ca-btn-import" class="ca-btn-small" style="flex:1;">Import JSON</button>
<button id="ca-btn-export" class="ca-btn-small" style="flex:1;">Export JSON</button>
<input type="file" id="ca-file-input" style="display:none;" accept=".json">
</div>
<div id="ca-saved-list"></div>
</div>
<!-- Tools View -->
<div id="view-tools" style="display:none;">
<div style="font-size: 12px; color: var(--ca-muted); margin-bottom: 8px;">Quick Links</div>
<div class="ca-grid">
<div class="ca-item tool-btn" data-url="https://www.cbhours.com/user/{u}.html"><span class="ca-item-name">Schedule</span></div>
<div class="ca-item tool-btn" data-url="https://statbate.com/search/1/{u}"><span class="ca-item-name">Statistics</span></div>
<div class="ca-item tool-btn" data-url="https://camgirlfinder.net/models/sc/{u}"><span class="ca-item-name">Finder</span></div>
<div class="ca-item tool-btn" data-url="https://nrtool.to/nrtool/search?sites={u}"><span class="ca-item-name">Images</span></div>
</div>
<div style="font-size: 12px; color: var(--ca-muted); margin: 16px 0 8px 0;">Manage Sites</div>
<div id="ca-site-toggles" style="max-height: 150px; overflow-y: auto; background: var(--ca-surf); border-radius: 8px; border: 1px solid var(--ca-border);"></div>
</div>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
this.initArchivesGrid(panel);
this.initSiteToggles(panel);
this.setupEventListeners(panel, overlay);
// Auto detect
const input = panel.querySelector('#ca-user');
this.detectUser(input);
},
initArchivesGrid: function(panel) {
const grid = panel.querySelector('#ca-archives');
grid.innerHTML = '';
const disabledSites = Storage.get('ca_disabled_sites', []);
archiveSites.forEach(site => {
if (disabledSites.includes(site.name)) return;
const el = document.createElement('div');
el.className = 'ca-item archive-item';
el.dataset.url = site.url;
el.dataset.name = site.name;
el.innerHTML = `
<img src="${getFaviconUrl(site.domain)}" loading="lazy">
<span class="ca-item-name">${site.name}</span>
<div class="ca-status"></div>
`;
el.onclick = () => {
const u = document.getElementById('ca-user').value.trim();
if (u) Utils.openSafe(site.url.replace('{username}', u));
};
grid.appendChild(el);
});
},
initSiteToggles: function(panel) {
const container = panel.querySelector('#ca-site-toggles');
let disabledSites = Storage.get('ca_disabled_sites', []);
archiveSites.forEach(site => {
const row = document.createElement('div');
row.className = 'ca-site-toggle';
const isChecked = !disabledSites.includes(site.name);
row.innerHTML = `
<span style="font-size: 13px;">${site.name}</span>
<input type="checkbox" ${isChecked ? 'checked' : ''}>
`;
row.querySelector('input').onchange = (e) => {
if (e.target.checked) {
disabledSites = disabledSites.filter(n => n !== site.name);
} else {
if (!disabledSites.includes(site.name)) disabledSites.push(site.name);
}
Storage.set('ca_disabled_sites', disabledSites);
this.initArchivesGrid(document.querySelector('.ca-panel')); // Refresh grid
};
container.appendChild(row);
});
},
setupEventListeners: function(panel, overlay) {
panel.querySelector('.ca-close').onclick = () => UI.toggle();
const tabs = panel.querySelectorAll('.ca-tab');
tabs.forEach(t => {
t.onclick = () => {
tabs.forEach(x => x.classList.remove('active'));
t.classList.add('active');
panel.querySelectorAll('div[id^="view-"]').forEach(v => v.style.display = 'none');
panel.querySelector(`#view-${t.dataset.view}`).style.display = 'block';
if(t.dataset.view === 'saved') UI.loadSaved();
if(t.dataset.view === 'history') UI.loadHistory();
};
});
const input = panel.querySelector('#ca-user');
let debounce;
input.oninput = () => {
clearTimeout(debounce);
debounce = setTimeout(() => UI.checkAll(input.value.trim()), 600);
};
// Save Favorites
panel.querySelector('#ca-btn-save').onclick = () => {
const u = input.value.trim();
if (u && Utils.isValidUsername(u)) {
let current = Storage.get('ca_saved', []);
// Normalize old arrays to objects
current = current.map(item => typeof item === 'string' ? { user: item, platform: 'Unknown' } : item);
if (!current.find(i => i.user === u)) {
current.push({ user: u, platform: Utils.getCurrentPlatform() });
Storage.set('ca_saved', current);
UI.showToast(`Saved ${u}`);
} else {
UI.showToast(`${u} already saved`);
}
}
};
// Import/Export
const fileInput = panel.querySelector('#ca-file-input');
panel.querySelector('#ca-btn-export').onclick = () => {
const saved = Storage.get('ca_saved', []);
if(saved.length === 0) return UI.showToast('Nothing to export');
const blob = new Blob([JSON.stringify(saved, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `arna_backup_${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
UI.showToast('Export successful');
};
panel.querySelector('#ca-btn-import').onclick = () => fileInput.click();
fileInput.onchange = (e) => {
const file = e.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const list = JSON.parse(ev.target.result);
if(Array.isArray(list)) {
let current = Storage.get('ca_saved', []);
current = current.map(item => typeof item === 'string' ? { user: item, platform: 'Unknown' } : item);
const newItems = list.map(x => typeof x === 'string' ? { user: x, platform: 'Imported' } : x)
.filter(x => Utils.isValidUsername(x.user) && !current.find(c => c.user === x.user));
Storage.set('ca_saved', [...current, ...newItems]);
UI.showToast(`Imported ${newItems.length} profiles`);
UI.loadSaved();
} else {
UI.showToast('Invalid JSON format');
}
} catch(err) {
UI.showToast('Error reading file');
}
};
reader.readAsText(file);
fileInput.value = '';
};
// Tools Buttons
panel.querySelectorAll('.tool-btn').forEach(btn => {
btn.onclick = () => {
const u = input.value.trim();
if(u) Utils.openSafe(btn.dataset.url.replace('{u}', u));
};
});
},
checkAll: async function(username) {
if (!username || !Utils.isValidUsername(username)) return;
// Save to history carefully
try {
let history = Storage.get('ca_history', []);
history = history.filter(u => u !== username);
history.unshift(username);
if (history.length > Config.maxHistory) history = history.slice(0, Config.maxHistory);
Storage.set('ca_history', history);
const historyView = document.getElementById('view-history');
if (historyView && historyView.style.display === 'block') this.loadHistory();
} catch(e) {}
const items = Array.from(document.querySelectorAll('.archive-item'));
if (items.length === 0) return;
items.forEach(el => {
el.classList.remove('found', 'not-found');
el.classList.add('checking');
});
const counterEl = document.getElementById('ca-counter');
const totalItems = items.length;
let checkedCount = 0;
let foundCount = 0;
if (counterEl) counterEl.innerText = `0 / ${totalItems} found`;
// Check Cache first
const cachedResults = this.cacheManager.get(username);
const newResultsMap = cachedResults || {};
const handleCompletion = () => {
checkedCount++;
if (checkedCount === totalItems) {
this.cacheManager.set(username, newResultsMap);
}
};
const updateUI = (el, isFound) => {
try {
el.classList.remove('checking');
if (isFound) {
el.classList.add('found');
foundCount++;
} else {
el.classList.add('not-found');
}
if (counterEl) counterEl.innerText = `${foundCount} / ${totalItems} found`;
// Update Badge
const badge = document.getElementById('ca-fab-badge');
if (badge) {
badge.innerText = foundCount;
badge.style.display = foundCount > 0 ? 'flex' : 'none';
}
} catch(e) {}
};
// Process all items simultaneously
items.forEach(async (el) => {
const siteName = el.dataset.name;
if (cachedResults && cachedResults[siteName] !== undefined) {
updateUI(el, cachedResults[siteName]);
handleCompletion();
} else {
let exists = false;
try {
const url = el.dataset.url.replace('{username}', username);
exists = await PageChecker.checkPage(url);
} catch(e) {}
newResultsMap[siteName] = exists;
updateUI(el, exists);
handleCompletion();
}
});
},
loadSaved: function() {
let list = Storage.get('ca_saved', []);
list = list.map(item => typeof item === 'string' ? { user: item, platform: '?' } : item);
const container = document.getElementById('ca-saved-list');
container.innerHTML = '';
if (list.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:20px; color:var(--ca-muted)">No saved profiles</div>';
return;
}
list.forEach(item => {
const div = document.createElement('div');
div.className = 'ca-item';
div.style.justifyContent = 'space-between';
div.innerHTML = `
<div style="display:flex; align-items:center; gap:8px;">
<span style="font-weight:600">${item.user}</span>
<span class="ca-tag">${item.platform || '?'}</span>
</div>
<button class="ca-delete" style="border:none;background:none;cursor:pointer;color:var(--ca-muted);">✕</button>
`;
div.onclick = (e) => {
if (!e.target.classList.contains('ca-delete')) {
document.getElementById('ca-user').value = item.user;
document.querySelector('[data-view="search"]').click();
UI.checkAll(item.user);
}
};
div.querySelector('.ca-delete').onclick = (e) => {
e.stopPropagation();
const newList = list.filter(x => x.user !== item.user);
Storage.set('ca_saved', newList);
UI.loadSaved();
};
container.appendChild(div);
});
},
loadHistory: function() {
const history = Storage.get('ca_history', []);
const container = document.getElementById('ca-history-list');
container.innerHTML = '';
if (history.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:20px; color:var(--ca-muted)">No history yet</div>';
return;
}
history.forEach(u => {
const div = document.createElement('div');
div.className = 'ca-history-item';
div.innerHTML = `<span style="font-weight:500">${u}</span> <span style="font-size:12px;color:var(--ca-muted)">↺</span>`;
div.onclick = () => {
document.getElementById('ca-user').value = u;
document.querySelector('[data-view="search"]').click();
UI.checkAll(u);
};
container.appendChild(div);
});
},
detectUser: function(input) {
try {
const path = window.location.pathname.split('/').filter(Boolean);
let user = '';
const host = window.location.host;
if (host.includes('chaturbate') || host.includes('bongacams')) {
user = path[0];
} else {
user = path[path.length - 1];
}
const ignoredPaths = ['auth', 'tags', 'couples', 'female', 'myfriends', 'male', 'trans', 'new-models', 'spy-mode'];
if (user && Utils.isValidUsername(user) && !ignoredPaths.includes(user.toLowerCase())) {
input.value = user;
UI.checkAll(user);
}
} catch(e) {}
},
showToast: function(msg) {
const t = document.createElement('div');
t.className = 'ca-toast';
t.innerText = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
};
function init() {
injectStyles();
const fab = document.createElement('div');
fab.className = 'ca-fab';
fab.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<div id="ca-fab-badge" class="ca-badge">0</div>
`;
fab.onclick = () => UI.toggle();
document.body.appendChild(fab);
document.addEventListener('keydown', (e) => {
const target = e.target.tagName.toLowerCase();
if (target === 'input' || target === 'textarea') return;
if (e.shiftKey && e.key.toLowerCase() === 'a') {
e.preventDefault();
UI.toggle();
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();