Auto tip models on Chaturbate
// ==UserScript==
// @name CB Tools
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Auto tip models on Chaturbate
// @author abadia97cr
// @match https://chaturbate.com/*
// @match https://*.chaturbate.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ==========================================
// 1. CONFIGURATION AND STATE
// ==========================================
const CONFIG = {
SELECTORS: {
HOST_ID: 'cb-tools-host',
USERNAME_REGEX: /^\/([a-zA-Z0-9_]+)\/?$/
},
ENDPOINTS: {
TIP: (user) => `/tipping/send_tip/${user}/`,
FOLLOW: (action, user) => `/follow/${action}/${user}/`,
PANEL: (user) => `/api/messaging/profile/${user}/`,
TOKEN_STATS: '/api/ts/tipping/token-stats/?max_transaction_id=&cashpage=0'
}
};
const state = {
intervalId: null,
abortController: null,
tippingActive: false,
tipCount: 0,
tokensSent: 0,
tokensDispatched: 0,
targetTokens: 0,
localBalance: null,
minimized: false,
isDragging: false,
pendingConfirm: false,
dragOffset: { x: 0, y: 0 },
rafPending: false
};
// ==========================================
// 2. CORE UTILITIES
// ==========================================
const Core = {
getCsrfToken: () => document.cookie.match(/csrftoken=([^;]+)/)?.[1] || null,
getLoggedInUser: () => window.$reactAppContext?.logged_in_user?.username || null,
getTokenBalance: () => window.$reactAppContext?.logged_in_user?.token_balance ?? null,
getUsernameFromUrl: () => window.location.pathname.match(CONFIG.SELECTORS.USERNAME_REGEX)?.[1] || ''
};
// ==========================================
// 3. UI AND SHADOW DOM
// ==========================================
const UI = {
shadowRoot: null,
elements: {},
initHost: () => {
const host = document.createElement('div');
host.id = CONFIG.SELECTORS.HOST_ID;
document.body.appendChild(host);
UI.shadowRoot = host.attachShadow({ mode: 'closed' });
},
injectCSS: () => {
const style = document.createElement('style');
style.textContent = `
* { box-sizing: border-box; }
#at-panel { position:fixed; bottom:20px; right:20px; z-index:99999; background:#1a1a2e; border:1px solid #e94560; border-radius:12px; padding:0; width:300px; font-family:Segoe UI,Tahoma,sans-serif; color:#eee; box-shadow:0 8px 32px rgba(233,69,96,0.25); overflow:hidden; will-change: transform, left, top; }
.at-header { display:flex; align-items:center; justify-content:space-between; padding:10px 16px; background:#16213e; border-bottom:1px solid #333; cursor:grab; user-select:none }
.at-header h3 { margin:0; font-size:14px; color:#e94560 }
#at-minimize { background:none; border:none; color:#888; font-size:18px; cursor:pointer; padding:0 4px }
#at-minimize:hover { color:#fff }
.at-user-badge { font-size:11px; color:#4ecca3; background:rgba(78,204,163,0.15); padding:2px 8px; border-radius:10px }
.at-user-badge.offline { color:#ff6b6b; background:rgba(255,107,107,0.15) }
.at-tabs { display:flex; border-bottom:1px solid #333 }
.at-tab { flex:1; padding:10px; text-align:center; font-size:13px; font-weight:600; cursor:pointer; background:#16213e; color:#888; border:none; transition:all 0.2s }
.at-tab:hover { color:#ccc }
.at-tab.active { background:#1a1a2e; color:#e94560; border-bottom:2px solid #e94560 }
.at-body { padding:14px 16px }
.at-section { display:none }
.at-section.active { display:block }
label { display:block; font-size:12px; color:#aaa; margin-bottom:4px }
input[type=number], input[type=text] { width:100%; padding:8px 10px; margin-bottom:10px; border:1px solid #333; border-radius:6px; background:#16213e; color:#fff; font-size:14px; outline:none; transition:border-color 0.2s }
input:focus { border-color:#e94560 }
.at-btn { width:100%; padding:10px; border:none; border-radius:8px; font-size:14px; font-weight:600; cursor:pointer; transition:all 0.2s; color:#fff }
.at-btn-primary { background:#e94560 }
.at-btn-primary:hover { background:#c7384f }
.at-btn-primary.running { background:#ff6b6b }
.at-btn-follow { background:#4ecca3 }
.at-btn-follow:hover { background:#3ab88f }
.at-btn-follow.loading, .at-btn-unfollow.loading { opacity:0.7; cursor:wait }
.at-btn-unfollow { background:#ff6b6b }
.at-btn-unfollow:hover { background:#e05555 }
.at-follow-btns { display:flex; gap:8px }
.at-follow-btns .at-btn { flex:1 }
.at-progress-wrap { margin-top:12px; background:#16213e; border-radius:8px; overflow:hidden; height:22px; position:relative }
.at-progress-bar { height:100%; width:0%; background:linear-gradient(90deg,#e94560,#f05a74); border-radius:8px; transition:width 0.3s ease }
.at-progress-text { position:absolute; top:0; left:0; right:0; bottom:0; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:600; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,0.5) }
.at-status { margin-top:8px; font-size:12px; text-align:center; color:#888 }
.at-status.active { color:#4ecca3 }
.at-status.done { color:#f0c040 }
.at-status.error { color:#ff6b6b }
.at-follow-result { margin-top:10px; padding:10px; border-radius:8px; font-size:13px; text-align:center; display:none }
.at-follow-result.success { display:block; background:rgba(78,204,163,0.15); color:#4ecca3; border:1px solid rgba(78,204,163,0.3) }
.at-follow-result.error { display:block; background:rgba(255,107,107,0.15); color:#ff6b6b; border:1px solid rgba(255,107,107,0.3) }
.at-follow-state { text-align:center; padding:6px 10px; border-radius:6px; font-size:12px; font-weight:600; margin-bottom:10px }
.at-follow-state.following { background:rgba(78,204,163,0.15); color:#4ecca3 }
.at-follow-state.not-following { background:rgba(240,192,64,0.15); color:#f0c040 }
.at-follow-state.not-found { background:rgba(255,107,107,0.15); color:#ff6b6b }
.at-follow-state.checking { background:rgba(136,136,136,0.15); color:#888 }
.at-tip-btns { display:flex; gap:8px }
.at-tip-btns .at-btn { flex:1 }
.at-btn-cancel { background:#666; display:none }
.at-btn-cancel:hover { background:#555 }
`;
UI.shadowRoot.appendChild(style);
},
injectHTML: () => {
const currentTarget = Core.getUsernameFromUrl() || '⚠️ Not detected';
const panel = document.createElement('div');
panel.id = 'at-panel';
panel.innerHTML = `
<div class="at-header" id="at-header">
<h3>🎯 CB Tools</h3>
<span class="at-user-badge" id="at-user-badge">...</span>
<button id="at-minimize" title="Minimize">−</button>
</div>
<div class="at-tabs" id="at-tabs">
<button class="at-tab active" data-tab="tip">💰 Auto Tip</button>
<button class="at-tab" data-tab="follow">❤️ Follow</button>
</div>
<div class="at-body" id="at-body">
<div class="at-section active" id="at-section-tip">
<div style="text-align:center;margin-bottom:12px;padding:8px;background:#16213e;border-radius:8px;">
<span style="font-size:11px;color:#aaa;">Sending tips to</span><br>
<span style="font-size:15px;font-weight:700;color:#e94560;" id="at-tip-target">${currentTarget}</span>
</div>
<label>Total tokens to send</label>
<input type="number" id="at-total" min="1" value="100">
<label>Tokens per tip</label>
<input type="number" id="at-per-tip" min="1" value="10">
<label>Message (optional)</label>
<input type="text" id="at-message" value="">
<label>Interval between tips (ms)</label>
<input type="number" id="at-interval" min="1" value="1000" step="1">
<div class="at-tip-btns">
<button class="at-btn at-btn-primary" id="at-start-btn">💰 Send</button>
<button class="at-btn at-btn-cancel" id="at-cancel-btn">✖ Cancel</button>
</div>
<div class="at-progress-wrap">
<div class="at-progress-bar" id="at-progress-bar"></div>
<div class="at-progress-text" id="at-progress-text">0 / 0 tokens</div>
</div>
<div class="at-status" id="at-tip-status">Stopped</div>
</div>
<div class="at-section" id="at-section-follow">
<label>Model username</label>
<input type="text" id="at-follow-user" value="${Core.getUsernameFromUrl()}">
<div class="at-follow-state checking" id="at-follow-state"></div>
<div class="at-follow-btns">
<button class="at-btn at-btn-follow" id="at-follow-btn">❤️ Follow</button>
<button class="at-btn at-btn-unfollow" id="at-unfollow-btn">💔 Unfollow</button>
</div>
<div class="at-follow-result" id="at-follow-result"></div>
<div class="at-status" id="at-follow-status">Enter a username</div>
</div>
</div>
`;
UI.shadowRoot.appendChild(panel);
},
cacheElements: () => {
const qs = (sel) => UI.shadowRoot.querySelector(sel);
UI.elements = {
panel: qs('#at-panel'),
header: qs('#at-header'),
tabs: qs('#at-tabs'),
body: qs('#at-body'),
minimizeBtn: qs('#at-minimize'),
userBadge: qs('#at-user-badge'),
tipTarget: qs('#at-tip-target'),
totalInput: qs('#at-total'),
perTipInput: qs('#at-per-tip'),
msgInput: qs('#at-message'),
intervalInput: qs('#at-interval'),
startBtn: qs('#at-start-btn'),
cancelBtn: qs('#at-cancel-btn'),
progressBar: qs('#at-progress-bar'),
progressText: qs('#at-progress-text'),
tipStatus: qs('#at-tip-status'),
followUser: qs('#at-follow-user'),
followState: qs('#at-follow-state'),
followBtn: qs('#at-follow-btn'),
unfollowBtn: qs('#at-unfollow-btn'),
followResult: qs('#at-follow-result'),
followStatus: qs('#at-follow-status'),
allTabs: UI.shadowRoot.querySelectorAll('.at-tab'),
allSections: UI.shadowRoot.querySelectorAll('.at-section')
};
},
updateBadgeBalance: () => {
const user = Core.getLoggedInUser();
const { userBadge } = UI.elements;
if (user && state.localBalance !== null) {
userBadge.textContent = `👤 ${user} | 🪙 ${state.localBalance}`;
userBadge.classList.remove('offline');
} else {
userBadge.textContent = 'Not logged in';
userBadge.classList.add('offline');
}
},
updateProgress: () => {
const { progressBar, progressText, tipStatus } = UI.elements;
const pct = state.targetTokens > 0 ? Math.min((state.tokensSent / state.targetTokens) * 100, 100) : 0;
progressBar.style.width = `${pct}%`;
progressText.textContent = `${state.tokensSent} / ${state.targetTokens} tokens`;
if (state.tokensSent >= state.targetTokens && state.targetTokens > 0) {
tipStatus.className = 'at-status done';
tipStatus.textContent = `💰 Done! ${state.tipCount} tips sent`;
} else if (state.tippingActive) {
tipStatus.className = 'at-status active';
tipStatus.textContent = `💰 Sending... Tip #${state.tipCount}`;
} else {
tipStatus.className = 'at-status';
tipStatus.textContent = 'Stopped';
}
}
};
// ==========================================
// 4. BUSINESS LOGIC AND API
// ==========================================
const AppLogic = {
fetchTokenBalance: async () => {
try {
const response = await fetch(CONFIG.ENDPOINTS.TOKEN_STATS, {
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) return null;
const data = await response.json();
if (typeof data.token_balance === 'number') {
state.localBalance = data.token_balance;
UI.updateBadgeBalance();
return data.token_balance;
}
} catch (e) { console.warn('[AutoTip] fetchTokenBalance error:', e); }
return null;
},
checkFollowState: async (username) => {
const { followState, followBtn, unfollowBtn } = UI.elements;
if (!username) {
followState.textContent = '';
followState.className = 'at-follow-state';
followBtn.style.opacity = '0.5';
unfollowBtn.style.opacity = '0.5';
return;
}
followState.textContent = '⏳ Checking...';
followState.className = 'at-follow-state checking';
followBtn.style.opacity = '0.5';
unfollowBtn.style.opacity = '0.5';
try {
const response = await fetch(CONFIG.ENDPOINTS.PANEL(username), {
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (response.status === 404) {
followState.textContent = '❌ User not found';
followState.className = 'at-follow-state not-found';
return;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.following) {
followState.textContent = '✅ Following';
followState.className = 'at-follow-state following';
followBtn.style.opacity = '0.5';
unfollowBtn.style.opacity = '1';
} else {
followState.textContent = '⭐ Not following';
followState.className = 'at-follow-state not-following';
followBtn.style.opacity = '1';
unfollowBtn.style.opacity = '0.5';
}
} catch (err) {
followState.textContent = '';
followState.className = 'at-follow-state';
followBtn.style.opacity = '1';
unfollowBtn.style.opacity = '1';
}
},
stopTipping: () => {
state.tippingActive = false;
if (state.abortController) { state.abortController.abort(); state.abortController = null; }
if (state.intervalId) { clearTimeout(state.intervalId); state.intervalId = null; }
state.pendingConfirm = false;
UI.elements.startBtn.textContent = '💰 Send';
UI.elements.startBtn.classList.remove('running');
UI.elements.cancelBtn.style.display = 'none';
UI.updateProgress();
AppLogic.fetchTokenBalance();
},
sendTip: async (amount) => {
const csrfToken = Core.getCsrfToken();
const username = Core.getUsernameFromUrl();
const fromUser = Core.getLoggedInUser();
if (!csrfToken || !username || !fromUser) return;
const formData = new FormData();
formData.append('tip_amount', amount);
formData.append('message', UI.elements.msgInput.value || '');
formData.append('source', 'theater');
formData.append('tip_room_type', 'public');
formData.append('tip_type', 'public');
formData.append('video_mode', 'split');
formData.append('from_username', fromUser);
formData.append('csrfmiddlewaretoken', csrfToken);
try {
const response = await fetch(CONFIG.ENDPOINTS.TIP(username), {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': csrfToken },
body: formData,
credentials: 'same-origin',
signal: state.abortController?.signal,
});
const data = await response.json();
if (data.success) {
state.tipCount++;
state.tokensSent += amount;
state.localBalance = data.token_balance ?? (state.localBalance - amount);
UI.updateBadgeBalance();
UI.updateProgress();
if (state.tokensSent >= state.targetTokens) AppLogic.stopTipping();
} else if (state.tippingActive) {
AppLogic.stopTipping();
UI.elements.tipStatus.textContent = '❌ Tip rejected by server';
UI.elements.tipStatus.className = 'at-status error';
}
} catch (err) {
if (err.name === 'AbortError') return;
if (state.tippingActive) {
AppLogic.stopTipping();
UI.elements.tipStatus.textContent = `❌ Network error: ${err.message}`;
UI.elements.tipStatus.className = 'at-status error';
}
}
},
startTippingFlow: () => {
if (state.tippingActive) return AppLogic.stopTipping();
const total = parseInt(UI.elements.totalInput.value);
const perTip = parseInt(UI.elements.perTipInput.value);
const ms = parseInt(UI.elements.intervalInput.value);
const model = Core.getUsernameFromUrl();
const executeTipping = async () => {
state.pendingConfirm = false;
UI.elements.cancelBtn.style.display = 'none';
state.tipCount = 0;
state.tokensSent = 0;
state.tokensDispatched = 0;
state.targetTokens = total;
state.tippingActive = true;
state.abortController = new AbortController();
UI.elements.startBtn.textContent = '⏹ Stop';
UI.elements.startBtn.classList.add('running');
UI.updateProgress();
while (state.tippingActive && state.tokensDispatched < state.targetTokens) {
const remaining = state.targetTokens - state.tokensDispatched;
const amount = Math.min(perTip, remaining);
state.tokensDispatched += amount;
AppLogic.sendTip(amount);
if (state.tokensDispatched >= state.targetTokens) break;
await new Promise(r => { state.intervalId = setTimeout(r, ms); });
}
};
if (state.pendingConfirm) {
return executeTipping();
}
if (!Core.getLoggedInUser()) return alert('You are not logged in.');
if (!model) return alert('Model not detected. Enter a room.');
if (state.localBalance === null) return alert('Could not retrieve token balance.');
if (state.localBalance < total) return alert(`Insufficient balance. You have: ${state.localBalance}`);
if (!total || total < 1 || !perTip || perTip < 1 || !ms || ms < 1) return alert('All values must be greater than 0.');
if (perTip > total) return alert('Tokens per tip exceeds the total.');
if (total <= 100) {
return executeTipping();
}
UI.elements.startBtn.textContent = '✅ Confirm';
UI.elements.cancelBtn.style.display = 'block';
state.pendingConfirm = true;
},
cancelConfirm: () => {
state.pendingConfirm = false;
UI.elements.startBtn.textContent = '💰 Send';
UI.elements.cancelBtn.style.display = 'none';
},
followAction: async (action) => {
const username = UI.elements.followUser.value.trim().toLowerCase();
const isFollow = action === 'follow';
const btn = isFollow ? UI.elements.followBtn : UI.elements.unfollowBtn;
const { followStatus, followResult } = UI.elements;
if (!username) { followStatus.textContent = '⚠️ Enter a username'; followStatus.className = 'at-status error'; return; }
if (UI.elements.followState.classList.contains('not-found')) {
followStatus.textContent = '❌ User does not exist';
followStatus.className = 'at-status error';
return;
}
if (!Core.getLoggedInUser()) return alert('You are not logged in.');
const csrfToken = Core.getCsrfToken();
if (!csrfToken) return alert('CSRF token not found.');
btn.classList.add('loading');
btn.textContent = '⏳ ...';
followResult.style.display = 'none';
followStatus.textContent = `${isFollow ? 'Following' : 'Unfollowing'} ${username}...`;
followStatus.className = 'at-status active';
try {
const formData = new FormData();
formData.append('csrfmiddlewaretoken', csrfToken);
const response = await fetch(CONFIG.ENDPOINTS.FOLLOW(action, username), {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': csrfToken },
body: formData,
credentials: 'same-origin',
});
if (response.ok) {
await response.json();
followResult.className = 'at-follow-result success';
followResult.textContent = isFollow ? `✅ Now following ${username}` : `✅ Unfollowed ${username}`;
followResult.style.display = 'block';
followStatus.className = 'at-status done';
followStatus.textContent = 'Done';
if (username === Core.getUsernameFromUrl()) {
const nativeBtn = document.querySelector(
isFollow ? '[data-testid="follow-button"]' : '[data-testid="unfollow-button"]'
);
if (nativeBtn) {
nativeBtn.click();
}
}
AppLogic.checkFollowState(username);
} else throw new Error(`HTTP ${response.status}`);
} catch (err) {
followResult.className = 'at-follow-result error';
followResult.textContent = `❌ Error: ${err.message}`;
followResult.style.display = 'block';
followStatus.className = 'at-status error';
} finally {
btn.classList.remove('loading');
btn.textContent = isFollow ? '❤️ Follow' : '💔 Unfollow';
}
}
};
// ==========================================
// 5. SPA NAVIGATION INTERCEPTORS
// ==========================================
const patchHistoryAPI = () => {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function() {
originalPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
};
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
window.dispatchEvent(new Event('replacestate'));
};
const handleUrlChange = () => {
const u = Core.getUsernameFromUrl();
if (UI.elements.tipTarget) UI.elements.tipTarget.textContent = u || '⚠️ Not detected';
if (UI.elements.followUser) UI.elements.followUser.value = u;
if (u) AppLogic.checkFollowState(u);
};
window.addEventListener('popstate', handleUrlChange);
window.addEventListener('pushstate', handleUrlChange);
window.addEventListener('replacestate', handleUrlChange);
};
// ==========================================
// 6. INITIALIZATION AND EVENTS
// ==========================================
const init = () => {
state.localBalance = Core.getTokenBalance();
UI.initHost();
UI.injectCSS();
UI.injectHTML();
UI.cacheElements();
try {
const savedConfig = JSON.parse(localStorage.getItem('cb_tools_config') || '{}');
if (savedConfig.total) UI.elements.totalInput.value = savedConfig.total;
if (savedConfig.perTip) UI.elements.perTipInput.value = savedConfig.perTip;
if (savedConfig.msg) UI.elements.msgInput.value = savedConfig.msg;
if (savedConfig.interval) UI.elements.intervalInput.value = savedConfig.interval;
} catch (e) { console.warn('[AutoTip] Could not load saved config.'); }
const saveInputs = () => {
localStorage.setItem('cb_tools_config', JSON.stringify({
total: UI.elements.totalInput.value,
perTip: UI.elements.perTipInput.value,
msg: UI.elements.msgInput.value,
interval: UI.elements.intervalInput.value
}));
};
UI.elements.totalInput.addEventListener('input', saveInputs);
UI.elements.perTipInput.addEventListener('input', saveInputs);
UI.elements.msgInput.addEventListener('input', saveInputs);
UI.elements.intervalInput.addEventListener('input', saveInputs);
UI.updateBadgeBalance();
patchHistoryAPI();
AppLogic.fetchTokenBalance();
setInterval(() => {
if (!state.tippingActive) AppLogic.fetchTokenBalance();
}, 30000);
const hostNode = document.getElementById(CONFIG.SELECTORS.HOST_ID);
['keydown', 'keyup', 'keypress'].forEach(evtType => {
window.addEventListener(evtType, (e) => {
if (e.composedPath().includes(hostNode)) {
e.stopImmediatePropagation();
}
}, true);
});
UI.elements.allTabs.forEach(tab => {
tab.addEventListener('click', () => {
UI.elements.allTabs.forEach(t => t.classList.remove('active'));
UI.elements.allSections.forEach(s => s.classList.remove('active'));
tab.classList.add('active');
UI.shadowRoot.getElementById(`at-section-${tab.dataset.tab}`).classList.add('active');
setTimeout(() => {
const { panel } = UI.elements;
const rect = panel.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
panel.style.top = `${Math.max(0, window.innerHeight - panel.offsetHeight)}px`;
}
if (rect.right > window.innerWidth) {
panel.style.left = `${Math.max(0, window.innerWidth - panel.offsetWidth)}px`;
}
}, 0);
});
});
UI.elements.minimizeBtn.addEventListener('click', () => {
const { panel, tabs, body, minimizeBtn } = UI.elements;
const rect = panel.getBoundingClientRect();
const anchorRight = rect.right;
const anchorBottom = rect.bottom;
state.minimized = !state.minimized;
tabs.style.display = state.minimized ? 'none' : 'flex';
body.style.display = state.minimized ? 'none' : 'block';
minimizeBtn.textContent = state.minimized ? '+' : '−';
setTimeout(() => {
let newLeft = anchorRight - panel.offsetWidth;
let newTop = anchorBottom - panel.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight));
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
}, 0);
});
UI.elements.header.addEventListener('mousedown', (e) => {
if (e.target.id === 'at-minimize') return;
state.isDragging = true;
const rect = UI.elements.panel.getBoundingClientRect();
state.dragOffset.x = e.clientX - rect.left;
state.dragOffset.y = e.clientY - rect.top;
UI.elements.header.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!state.isDragging) return;
if (!state.rafPending) {
requestAnimationFrame(() => {
let x = e.clientX - state.dragOffset.x;
let y = e.clientY - state.dragOffset.y;
x = Math.max(0, Math.min(x, window.innerWidth - UI.elements.panel.offsetWidth));
y = Math.max(0, Math.min(y, window.innerHeight - UI.elements.panel.offsetHeight));
UI.elements.panel.style.left = `${x}px`;
UI.elements.panel.style.top = `${y}px`;
UI.elements.panel.style.right = 'auto';
UI.elements.panel.style.bottom = 'auto';
state.rafPending = false;
});
state.rafPending = true;
}
});
document.addEventListener('mouseup', () => {
if (state.isDragging) {
state.isDragging = false;
UI.elements.header.style.cursor = 'grab';
}
});
window.addEventListener('resize', () => {
if (state.minimized) return;
const { panel } = UI.elements;
const rect = panel.getBoundingClientRect();
let adjusted = false;
let newTop = rect.top;
let newLeft = rect.left;
if (rect.bottom > window.innerHeight) {
newTop = Math.max(0, window.innerHeight - panel.offsetHeight);
adjusted = true;
}
if (rect.right > window.innerWidth) {
newLeft = Math.max(0, window.innerWidth - panel.offsetWidth);
adjusted = true;
}
if (adjusted) {
panel.style.top = `${newTop}px`;
panel.style.left = `${newLeft}px`;
panel.style.bottom = 'auto';
panel.style.right = 'auto';
}
});
UI.elements.startBtn.addEventListener('click', AppLogic.startTippingFlow);
UI.elements.cancelBtn.addEventListener('click', AppLogic.cancelConfirm);
UI.elements.followBtn.addEventListener('click', () => AppLogic.followAction('follow'));
UI.elements.unfollowBtn.addEventListener('click', () => AppLogic.followAction('unfollow'));
let followCheckTimer = null;
UI.elements.followUser.addEventListener('input', () => {
clearTimeout(followCheckTimer);
followCheckTimer = setTimeout(() => {
const username = UI.elements.followUser.value.trim().toLowerCase();
AppLogic.checkFollowState(username);
}, 600);
});
const initialFollowUser = Core.getUsernameFromUrl();
if (initialFollowUser) AppLogic.checkFollowState(initialFollowUser);
console.log(`[CB Tools] v1.0 loaded.`);
};
init();
})();