CB Tools

Auto tip models on Chaturbate

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();