CB Tools

Auto tip models on Chaturbate

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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