Multi-archive search tool with local StreaMonitor integration
// ==UserScript==
// @name Cam ARNA
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Multi-archive search tool with local StreaMonitor integration
// @author user006-ui
// @license MIT
// @match https://*.stripchat.com/*
// @match https://*.chaturbate.com/*
// @match https://chaturbate.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect *
// ==/UserScript==
(function() {
'use strict';
// --- Storage Helper ---
const Storage = {
get: (key, defaultValue) => {
try {
const val = localStorage.getItem(key);
return val ? JSON.parse(val) : defaultValue;
} catch (e) { return defaultValue; }
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) { return false; }
}
};
// --- Configuration ---
const SM_Config = {
get enabled() { return Storage.get('sm_enabled', true); },
get url() { return Storage.get('sm_url', "http://192.168.178.33:5000"); },
get user() { return Storage.get('sm_user', "admin"); },
get pass() { return Storage.get('sm_pass', "admin"); }
};
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: 'Camshowrecordings', url: 'https://www.camshowrecordings.com/model/{username}', domain: 'camshowrecordings.com' },
{ name: 'Camwh', url: 'https://camwh.com/tags/{username}/', domain: 'camwh.com' },
{ name: 'TopCamVideos', url: 'https://www.topcamvideos.com/showall/?search={username}', domain: 'topcamvideos.com' },
{ name: 'LoveCamPorn', 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.tv', url: 'https://bestcam.tv/model/{username}', domain: 'bestcam.tv' },
{ name: 'Xhomealone', url: 'https://xhomealone.com/tags/{username}/', domain: 'xhomealone.com' },
{ name: 'Stream-leak', url: 'https://stream-leak.com/models/{username}/', domain: 'stream-leak.com' },
{ name: 'MFCamhub', url: 'https://mfcamhub.com/models/{username}/', domain: 'mfcamhub.com' },
{ name: 'Camshowrecord', url: 'https://camshowrecord.net/video/list?page=1&model={username}', domain: 'camshowrecord.net' },
{ name: 'Camwhoresbay', url: 'https://www.camwhoresbay.com/search/{username}/', domain: 'camwhoresbay.com' },
{ name: 'CamSave1', url: 'https://www.camsave1.com/?feet=0&face=0&ass=0&tits=0&pussy=0&search={username}&women=true&couples=true&men=false&trans=false', 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' }
];
const mainSites = {
'stripchat': 'https://stripchat.com/{username}',
'chaturbate': 'https://chaturbate.com/{username}/'
};
function getFaviconUrl(domain) {
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
}
// --- API Handlers ---
const StreaMonitor = {
checkStatus: function(username, site) {
return new Promise((resolve) => {
if (!SM_Config.enabled) { resolve({ disabled: true }); return; }
if (!username || !site) { resolve({ error: 'No data' }); return; }
const baseUrl = SM_Config.url.replace(/\/$/, "");
GM_xmlhttpRequest({
method: "GET",
url: `${baseUrl}/api/data`,
user: SM_Config.user,
password: SM_Config.pass,
timeout: 4000,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const streamers = data.streamers || [];
const match = streamers.find(s =>
s.username.toLowerCase() === username.toLowerCase() &&
(
s.site.toLowerCase() === site.toLowerCase() ||
(s.url && s.url.toLowerCase().includes(site.toLowerCase())) ||
(site === 'chaturbate' && s.site === 'CB') ||
(site === 'stripchat' && s.site === 'SC')
)
);
resolve({ found: !!match, data: match });
} catch (e) { resolve({ error: 'Data Error' }); }
} else { resolve({ error: `API ${response.status}` }); }
},
onerror: function() { resolve({ error: 'Offline' }); },
ontimeout: function() { resolve({ error: 'Timeout' }); }
});
});
},
addStreamer: function(username, site) {
return new Promise((resolve) => {
const baseUrl = SM_Config.url.replace(/\/$/, "");
const dataStr = `username=${encodeURIComponent(username)}&site=${encodeURIComponent(site)}`;
GM_xmlhttpRequest({
method: "POST",
url: `${baseUrl}/add`,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: dataStr,
user: SM_Config.user,
password: SM_Config.pass,
onload: function(response) {
if (response.status === 200 || response.status === 500) { resolve(true); }
else { resolve(false); }
},
onerror: function() { resolve(false); }
});
});
}
};
const PageChecker = {
cache: {},
// Signatur des 404-Bildes (Showcamrips)
showcamrips404: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUwAAADYCAMAAAC3MCqMAAABCFBMVEUBAQD7+/j5+fb///6GhoRzc3Fra2moqKaSkpBubmxoaGaJiYdfX11iYmBcXFplZWOamphUVFI6Oji1tbNRUU9XV1WXl5WdnZtOTkxLS0mDg4KBgYA2NjRISEagoJ5wcG+srKp3d3ZDQ0F5eXh1dXN9fXx/f32ysrCjo6GNjYy5ubcvLy2vr617e3o+PjyPj44HBwaLi4krKylZWVfExMIzMzGUlJL9/fq8vLpAQD4VFROlpaO/v70ODg1FRUMlJSMoKCYfHx0RERAiIiAbGxoYGBbIyMYLCwnLy8nR0c/Bwb/OzszU1NL29vTb29nm5uTX19Xf393i4uDq6ujt7evw8e7Z2dfz8/Eqcyz2AACcOElEQVR42uSWxZLk0BFFZY+YmZlZLXRRF1NXlZumZ/z",
checkPage: function(url) {
return new Promise((resolve) => {
if (this.cache.hasOwnProperty(url)) { resolve(this.cache[url]); return; }
GM_xmlhttpRequest({
method: 'GET', url: url, timeout: 8000,
onload: (response) => {
const exists = this.analyzeResponse(response, url);
this.cache[url] = exists; resolve(exists);
},
onerror: () => { this.cache[url] = false; resolve(false); }
});
});
},
analyzeResponse: function(response, url) {
if (response.status === 404 || response.status >= 500) return false;
const text = response.responseText;
const lowerText = text.toLowerCase();
// 1. Showcamrips Special
if (url && url.includes('showcamrips') && text.includes(this.showcamrips404)) {
return false;
}
// 2. Camshowrecordings Special
if (url && url.includes('camshowrecordings.com')) {
if (text.includes('class="h1modelindex"')) return false;
const userMatch = url.match(/\/model\/([^/?#]+)/);
if (userMatch && userMatch[1]) {
const username = userMatch[1].toLowerCase();
if (text.includes('class="h1modelpage"') && lowerText.includes(username)) {
return true;
}
return false;
}
return text.includes('class="h1modelpage"');
}
// 3. Camwhores.tv Special
if (url && url.includes('camwhores.tv')) {
if (lowerText.includes('no videos found') || lowerText.includes('no results')) return false;
const userMatch = url.match(/\/search\/([^/?#]+)/);
if (userMatch) {
const rawUser = decodeURIComponent(userMatch[1]).toLowerCase();
const userRegexStr = rawUser.replace(/[\-\_]/g, '[\\s\\-\\_]+');
const titleRegex = new RegExp(`class=["']title["'][^>]*>\\s*[^<]*${userRegexStr}`, 'i');
const linkRegex = new RegExp(`href=["'].*?\/videos\/\\d+\/.*?${userRegexStr}`, 'i');
if (titleRegex.test(text) || linkRegex.test(text)) return true;
return false;
}
}
// 4. Camwhoresbay Special
if (url && url.includes('camwhoresbay.com')) {
const userMatch = url.match(/\/search\/([^/?#]+)/);
if (userMatch) {
const rawUser = decodeURIComponent(userMatch[1]).toLowerCase();
const safeUser = rawUser.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const userRegexStr = safeUser.replace(/[\-\_]/g, '[\\s\\-\\_]*');
const videoLinkRegex = new RegExp(`href=["'][^"']*\\/videos\\/[^"']*${userRegexStr}`, 'i');
const titleRegex = new RegExp(`title=["'][^"']*${userRegexStr}[^"']*["']`, 'i');
if (videoLinkRegex.test(text) || titleRegex.test(text)) {
return true;
}
return false;
}
}
// 5. Camshowrecord Special
if (url && url.includes('camshowrecord.net')) {
if (text.includes('Sorry, no video found for this')) {
return false;
}
return true;
}
// 6. Cumcams Special (Improved for False Positives)
if (url && url.includes('cumcams.cc')) {
// Wir suchen im Kleingedruckten (lowerText) nach der Phrase, unabhängig von HTML-Tags
if (lowerText.includes('performer not found')) {
return false;
}
return true;
}
// Standard Checks
const titleMatch = lowerText.match(/<title[^>]*>(.*?)<\/title>/i);
const title = titleMatch ? titleMatch[1] : '';
const notFoundTerms = [
'no videos found',
'no results found',
'does not exist',
'no videos to show'
];
if (['not found', '404', 'page not found', 'no results'].some(x => title.includes(x))) return false;
if (notFoundTerms.some(x => lowerText.includes(x))) return false;
return true;
},
clearCache: function() { this.cache = {}; }
};
// --- UI & Styles ---
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
:root {
--cam-bg: #09090b;
--cam-surface: #18181b;
--cam-border: #27272a;
--cam-primary: #3b82f6; /* Modern Blue */
--cam-primary-hover: #2563eb;
--cam-text: #f4f4f5;
--cam-text-muted: #a1a1aa;
--cam-danger: #ef4444;
--cam-success: #22c55e;
--cam-radius: 16px;
--cam-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* --- Animations --- */
@keyframes slideUpMobile { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes scaleInDesktop { from { transform: translate(-50%, -45%) scale(0.95); opacity: 0; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
@keyframes spin { to { transform: rotate(360deg); } }
/* --- Base Elements --- */
.cam-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); z-index: 99999; }
.cam-container {
position: fixed; z-index: 100000;
background: var(--cam-bg); color: var(--cam-text);
font-family: var(--cam-font);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid var(--cam-border);
display: flex; flex-direction: column;
overflow: hidden;
}
/* --- Responsive Layout --- */
/* Desktop */
@media (min-width: 601px) {
.cam-container {
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 450px; max-height: 85vh;
border-radius: var(--cam-radius);
animation: scaleInDesktop 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
}
/* Mobile (Bottom Sheet) */
@media (max-width: 600px) {
.cam-container {
bottom: 0; left: 0; right: 0;
width: 100%; max-height: 90vh;
border-radius: 24px 24px 0 0;
border-bottom: none;
animation: slideUpMobile 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
}
/* --- Header --- */
.cam-header {
padding: 16px 20px;
background: var(--cam-surface);
border-bottom: 1px solid var(--cam-border);
display: flex; justify-content: space-between; align-items: center;
flex-shrink: 0;
}
.cam-title { font-size: 18px; font-weight: 700; letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; }
.cam-close { background: none; border: none; color: var(--cam-text-muted); font-size: 24px; cursor: pointer; padding: 4px; border-radius: 50%; transition: 0.2s; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; }
.cam-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
/* --- Navigation --- */
.cam-nav { padding: 12px 20px 0; display: flex; gap: 4px; border-bottom: 1px solid var(--cam-border); background: var(--cam-bg); flex-shrink: 0; }
.cam-nav-item {
flex: 1; padding: 10px; text-align: center;
background: none; border: none;
color: var(--cam-text-muted); font-size: 14px; font-weight: 600;
cursor: pointer; position: relative;
transition: 0.2s;
}
.cam-nav-item:hover { color: var(--cam-text); }
.cam-nav-item.active { color: var(--cam-primary); }
.cam-nav-item.active::after {
content: ''; position: absolute; bottom: 0; left: 0; right: 0;
height: 2px; background: var(--cam-primary); border-radius: 2px 2px 0 0;
}
/* --- Content Area --- */
.cam-body { padding: 20px; overflow-y: auto; overscroll-behavior: contain; flex-grow: 1; }
.cam-body::-webkit-scrollbar { width: 6px; }
.cam-body::-webkit-scrollbar-thumb { background: var(--cam-border); border-radius: 3px; }
/* --- Inputs --- */
.cam-input-wrap { margin-bottom: 20px; position: relative; }
.cam-input {
width: 100%; background: var(--cam-surface);
border: 1px solid var(--cam-border); border-radius: 12px;
padding: 14px 14px 14px 44px;
color: #fff; font-size: 16px;
transition: border-color 0.2s;
}
.cam-input:focus { outline: none; border-color: var(--cam-primary); }
.cam-input-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--cam-text-muted); pointer-events: none; }
/* --- Sections --- */
.cam-section { margin-bottom: 24px; }
.cam-section-head { font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--cam-text-muted); margin-bottom: 12px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
/* --- Cards & Buttons --- */
.cam-card {
background: var(--cam-surface); border: 1px solid var(--cam-border);
border-radius: 12px; overflow: hidden;
}
.cam-btn {
width: 100%; display: flex; align-items: center; gap: 12px;
padding: 14px 16px; background: none; border: none;
color: var(--cam-text); font-size: 15px; font-weight: 500; text-align: left;
cursor: pointer; transition: background 0.2s;
border-bottom: 1px solid var(--cam-border);
}
.cam-btn:last-child { border-bottom: none; }
.cam-btn:hover { background: rgba(255,255,255,0.05); }
.cam-btn:active { background: rgba(255,255,255,0.1); }
.cam-btn-primary {
background: var(--cam-primary); color: #fff; border: none;
justify-content: center; font-weight: 600; border-radius: 12px;
margin-top: 10px;
}
.cam-btn-primary:hover { background: var(--cam-primary-hover); }
/* --- StreaMonitor Card --- */
.cam-sm-card { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; }
.cam-sm-info { display: flex; align-items: center; gap: 10px; }
.cam-sm-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.cam-sm-dot.on { background: var(--cam-success); box-shadow: 0 0 8px var(--cam-success); }
.cam-sm-dot.rec { background: var(--cam-danger); animation: pulse 1.5s infinite; }
@keyframes pulse { 50% { opacity: 0.5; } }
.cam-sm-action { padding: 6px 12px; border-radius: 8px; font-size: 13px; font-weight: 600; border: none; cursor: pointer; background: var(--cam-primary); color: white; }
.cam-sm-action:disabled { background: var(--cam-border); color: var(--cam-text-muted); cursor: default; }
/* --- Grid for Archives --- */
.cam-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.cam-grid-btn {
background: var(--cam-surface); border: 1px solid var(--cam-border);
border-radius: 10px; padding: 12px; display: flex; align-items: center; gap: 10px;
color: var(--cam-text); cursor: pointer; font-size: 13px; font-weight: 500;
transition: 0.2s;
}
.cam-grid-btn:hover { border-color: var(--cam-text-muted); transform: translateY(-1px); }
.cam-grid-btn img { width: 16px; height: 16px; border-radius: 4px; }
.cam-grid-btn.unavailable { opacity: 0.4; pointer-events: none; filter: grayscale(1); }
.cam-grid-btn.checking { opacity: 0.7; pointer-events: none; }
/* --- Settings Toggles --- */
.cam-toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 16px; }
.cam-toggle { position: relative; width: 44px; height: 24px; }
.cam-toggle input { opacity: 0; width: 0; height: 0; }
.cam-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--cam-border); transition: .4s; border-radius: 34px; }
.cam-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .cam-slider { background-color: var(--cam-primary); }
input:checked + .cam-slider:before { transform: translateX(20px); }
.cam-toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); padding: 12px 24px; background: #fff; color: #000; font-weight: 600; border-radius: 50px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 200000; font-size: 14px; animation: slideUpMobile 0.3s; }
/* --- Floating Button --- */
#cam-fab {
position: fixed; bottom: 24px; right: 24px; width: 56px; height: 56px;
border-radius: 28px; background: var(--cam-primary); color: white;
border: none; font-size: 24px; cursor: pointer;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
z-index: 99998; transition: transform 0.2s;
display: flex; align-items: center; justify-content: center;
}
#cam-fab:active { transform: scale(0.9); }
`;
document.head.appendChild(style);
}
// --- Profile Manager ---
const ProfileManager = {
STORAGE_KEY: 'cam_profiles',
get: () => Storage.get('cam_profiles', []),
add: (name) => {
let list = Storage.get('cam_profiles', []);
if (!list.includes(name)) { list.push(name); Storage.set('cam_profiles', list); return true; }
return false;
},
remove: (name) => {
let list = Storage.get('cam_profiles', []);
Storage.set('cam_profiles', list.filter(n => n !== name));
}
};
// --- Main UI Logic ---
const UI = {
isOpen: false,
activeTab: 'search',
createMenu: function() {
if (this.isOpen) return;
this.isOpen = true;
const backdrop = document.createElement('div');
backdrop.className = 'cam-backdrop';
backdrop.onclick = (e) => { if(e.target === backdrop) this.close(); };
const container = document.createElement('div');
container.className = 'cam-container';
// Detect Context
const hostname = window.location.hostname;
const isCB = hostname.includes('chaturbate');
const siteKey = isCB ? 'chaturbate' : 'stripchat';
// Try extract username
let currentUsername = '';
const path = window.location.pathname.split('/').filter(p => p);
if (isCB && path.length === 1 && !['tags','auth','search'].includes(path[0])) currentUsername = path[0];
if (!isCB && path.length >= 1) currentUsername = path[path.length-1];
// Render
container.innerHTML = `
<div class="cam-header">
<div class="cam-title"><span>🌟</span> Cam ARNA</div>
<button class="cam-close">✕</button>
</div>
<div class="cam-nav">
<button class="cam-nav-item active" data-tab="search">Search</button>
<button class="cam-nav-item" data-tab="saved">Saved</button>
<button class="cam-nav-item" data-tab="settings">Settings</button>
</div>
<div class="cam-body">
<div class="cam-input-wrap">
<span class="cam-input-icon">👤</span>
<input type="text" class="cam-input" id="cam-user-input" placeholder="Username..." value="${currentUsername}" autocomplete="off">
</div>
<div id="tab-search" class="cam-tab-content">
${SM_Config.enabled ? `
<div class="cam-section">
<div class="cam-section-head">StreaMonitor (Local)</div>
<div class="cam-sm-card">
<div class="cam-sm-info">
<div id="sm-dot" class="cam-sm-dot"></div>
<span id="sm-text" style="font-size:14px; font-weight:500;">Initializing...</span>
</div>
<button id="sm-btn" class="cam-sm-action" disabled>Check</button>
</div>
</div>
` : ''}
<div class="cam-section">
<div class="cam-section-head">Direct Access</div>
<div class="cam-grid">
<button class="cam-grid-btn main-site" data-site="stripchat" style="border-color: #8b5cf6;">💜 Stripchat</button>
<button class="cam-grid-btn main-site" data-site="chaturbate" style="border-color: #f97316;">🧡 Chaturbate</button>
</div>
<button id="cam-save-btn" class="cam-btn-primary">💾 Save Profile</button>
</div>
<div class="cam-section">
<div class="cam-section-head">Archives <span id="archive-status" style="float:right; font-weight:normal; opacity:0.6;"></span></div>
<div class="cam-grid">
${archiveSites.map(s => `
<button class="cam-grid-btn archive-link" data-url="${s.url}">
<img src="${getFaviconUrl(s.domain)}" onerror="this.style.display='none'"> ${s.name}
</button>
`).join('')}
</div>
</div>
</div>
<div id="tab-saved" class="cam-tab-content" style="display:none;">
<div class="cam-card" id="saved-list"></div>
<div id="saved-empty" style="text-align:center; padding:40px; color:var(--cam-text-muted); display:none;">
No profiles saved yet.
</div>
</div>
<div id="tab-settings" class="cam-tab-content" style="display:none;">
<div class="cam-section">
<div class="cam-section-head">Integrations</div>
<div class="cam-card">
<div class="cam-toggle-row">
<span>Enable StreaMonitor</span>
<label class="cam-toggle">
<input type="checkbox" id="setting-sm-enabled" ${SM_Config.enabled ? 'checked' : ''}>
<span class="cam-slider"></span>
</label>
</div>
${SM_Config.enabled ? `
<div style="padding: 16px; border-top: 1px solid var(--cam-border);">
<div style="margin-bottom:8px; font-size:12px; color:var(--cam-text-muted);">API URL</div>
<input class="cam-input" id="setting-sm-url" value="${SM_Config.url}" style="padding:10px; font-size:14px; margin-bottom:10px;">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px;">
<div>
<div style="margin-bottom:8px; font-size:12px; color:var(--cam-text-muted);">User</div>
<input class="cam-input" id="setting-sm-user" value="${SM_Config.user}" style="padding:10px; font-size:14px;">
</div>
<div>
<div style="margin-bottom:8px; font-size:12px; color:var(--cam-text-muted);">Pass</div>
<input type="password" class="cam-input" id="setting-sm-pass" value="${SM_Config.pass}" style="padding:10px; font-size:14px;">
</div>
</div>
</div>
` : ''}
</div>
</div>
<div class="cam-section">
<div class="cam-section-head">Data</div>
<button class="cam-btn" id="btn-export">📤 Export JSON</button>
<button class="cam-btn" id="btn-import">📥 Import JSON</button>
<input type="file" id="file-import" style="display:none" accept=".json">
</div>
<div style="text-align:center; color:var(--cam-text-muted); font-size:12px; margin-top:30px;">
Cam ARNA v1.8 by user006-ui
</div>
</div>
</div>
`;
document.body.appendChild(backdrop);
document.body.appendChild(container);
// Bind Events
container.querySelector('.cam-close').onclick = () => this.close();
// Tabs
const tabs = container.querySelectorAll('.cam-nav-item');
tabs.forEach(t => t.onclick = () => {
tabs.forEach(x => x.classList.remove('active'));
t.classList.add('active');
container.querySelectorAll('.cam-tab-content').forEach(c => c.style.display = 'none');
container.querySelector(`#tab-${t.dataset.tab}`).style.display = 'block';
if (t.dataset.tab === 'saved') this.renderSaved();
});
// Input
const input = container.querySelector('#cam-user-input');
let debounce;
input.oninput = () => {
clearTimeout(debounce);
debounce = setTimeout(() => {
this.onUserChange(input.value.trim(), siteKey);
}, 800);
};
// Main Buttons
container.querySelectorAll('.main-site').forEach(b => b.onclick = () => {
const u = input.value.trim();
if(u) window.open(mainSites[b.dataset.site].replace('{username}', u), '_blank');
});
// Save Button
container.querySelector('#cam-save-btn').onclick = () => {
const u = input.value.trim();
if(!u) return;
if(ProfileManager.add(u)) this.toast(`Saved ${u}`);
else this.toast('Already saved');
};
// Archive Links
container.querySelectorAll('.archive-link').forEach(b => b.onclick = () => {
if(b.classList.contains('unavailable')) return;
const u = input.value.trim();
if(u) window.open(b.dataset.url.replace('{username}', u), '_blank');
});
// Settings Logic
const toggleSM = container.querySelector('#setting-sm-enabled');
if(toggleSM) toggleSM.onchange = () => {
Storage.set('sm_enabled', toggleSM.checked);
this.close(); this.createMenu(); // Re-render to show/hide sections
};
if(SM_Config.enabled) {
const sUrl = container.querySelector('#setting-sm-url');
const sUser = container.querySelector('#setting-sm-user');
const sPass = container.querySelector('#setting-sm-pass');
[sUrl, sUser, sPass].forEach(el => el.oninput = () => {
Storage.set('sm_url', sUrl.value);
Storage.set('sm_user', sUser.value);
Storage.set('sm_pass', sPass.value);
});
}
// Data Export/Import
container.querySelector('#btn-export').onclick = () => {
const data = JSON.stringify(ProfileManager.get());
const blob = new Blob([data], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'cam_rna_backup.json';
a.click();
};
const fInput = container.querySelector('#file-import');
container.querySelector('#btn-import').onclick = () => fInput.click();
fInput.onchange = (e) => {
const reader = new FileReader();
reader.onload = (ev) => {
try {
const list = JSON.parse(ev.target.result);
if(Array.isArray(list)) {
Storage.set('cam_profiles', list);
this.toast(`Imported ${list.length} profiles`);
this.renderSaved();
}
} catch(err) { this.toast('Error importing'); }
};
if(e.target.files[0]) reader.readAsText(e.target.files[0]);
};
// Initial Check
if(currentUsername) this.onUserChange(currentUsername, siteKey);
},
close: function() {
document.querySelector('.cam-backdrop')?.remove();
document.querySelector('.cam-container')?.remove();
this.isOpen = false;
},
onUserChange: async function(username, site) {
if(!username) return;
// Reset UI
document.querySelectorAll('.archive-link').forEach(b => b.classList.remove('unavailable', 'checking'));
// SM Check
if (SM_Config.enabled) {
const smText = document.querySelector('#sm-text');
const smDot = document.querySelector('#sm-dot');
const smBtn = document.querySelector('#sm-btn');
if (smText) {
smText.innerText = "Checking...";
smText.style.color = "var(--cam-text-muted)";
smDot.className = "cam-sm-dot";
smBtn.disabled = true;
const res = await StreaMonitor.checkStatus(username, site);
if (res.error) {
smText.innerText = res.error;
smText.style.color = "var(--cam-danger)";
} else if (res.found) {
if (res.data.recording) {
smText.innerText = "Recording";
smText.style.color = "var(--cam-danger)";
smDot.classList.add('rec');
} else if (res.data.running) {
smText.innerText = "Online (Monitored)";
smText.style.color = "var(--cam-success)";
smDot.classList.add('on');
} else {
smText.innerText = "Offline (In List)";
smText.style.color = "var(--cam-text-muted)";
}
smBtn.innerText = "Managed";
} else {
smText.innerText = "Not Monitored";
smBtn.innerText = "Add";
smBtn.disabled = false;
smBtn.onclick = async () => {
smBtn.innerText = "...";
await StreaMonitor.addStreamer(username, site);
this.onUserChange(username, site);
};
}
}
}
// Archive Check
const btns = document.querySelectorAll('.archive-link');
const status = document.getElementById('archive-status');
if(status) status.innerText = "Checking...";
let avail = 0;
const promises = Array.from(btns).map(async b => {
b.classList.add('checking');
const ok = await PageChecker.checkPage(b.dataset.url.replace('{username}', username));
b.classList.remove('checking');
if(!ok) b.classList.add('unavailable');
else avail++;
});
await Promise.all(promises);
if(status) status.innerText = `${avail} found`;
},
renderSaved: function() {
const list = ProfileManager.get();
const el = document.getElementById('saved-list');
const empty = document.getElementById('saved-empty');
if(!el) return;
if(list.length === 0) {
el.style.display = 'none';
empty.style.display = 'block';
} else {
el.style.display = 'block';
empty.style.display = 'none';
el.innerHTML = list.map(u => `
<div style="padding:12px 16px; border-bottom:1px solid var(--cam-border); display:flex; justify-content:space-between; align-items:center;">
<span style="font-weight:600;">${u}</span>
<div style="display:flex; gap:8px;">
<button onclick="window.open('https://chaturbate.com/${u}/','_blank')" style="border:none; background:none; cursor:pointer;">🧡</button>
<button onclick="window.open('https://stripchat.com/${u}','_blank')" style="border:none; background:none; cursor:pointer;">💜</button>
<button class="del-btn" data-user="${u}" style="border:none; background:none; cursor:pointer;">🗑️</button>
</div>
</div>
`).join('');
el.querySelectorAll('.del-btn').forEach(b => b.onclick = () => {
ProfileManager.remove(b.dataset.user);
this.renderSaved();
});
}
},
toast: function(msg) {
const t = document.createElement('div');
t.className = 'cam-toast';
t.innerText = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
};
// --- Init ---
function init() {
injectStyles();
// Fab Button
const fab = document.createElement('button');
fab.id = 'cam-fab';
fab.innerHTML = '⚡';
fab.onclick = () => UI.createMenu();
document.body.appendChild(fab);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();