TikTok Profile Inspector

Extract detailed TikTok profile info with draggable/resizable UI

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TikTok Profile Inspector
// @author       anxiass (https://www.tiktok.com/@anxiass)
// @namespace    https://tiktok.com/
// @version      2.8.0
// @license      MIT License
// @icon         https://www.tiktok.com/favicon.ico
// @description  Extract detailed TikTok profile info with draggable/resizable UI
// @match        https://www.tiktok.com/*
// @run-at       document-end
// @grant        GM_setClipboard
// @grant        GM_notification
// ==/UserScript==

(function () {
  'use strict';

  let uiVisible = true;
  let altPressedOnce = false;
  let altTimeout = null;

  let isDragging = false;
  let dragOffsetX = 0;
  let dragOffsetY = 0;

  let isResizing = false;
  let resizeDirection = '';
  let resizeStartX = 0;
  let resizeStartY = 0;
  let resizeStartWidth = 0;
  let resizeStartHeight = 0;
  let resizeStartLeft = 0;
  let resizeStartTop = 0;

  let lastProfileData = null;
  let urlCheckInterval = null;
  let lastCheckedUsername = null;
  let highlightTimeout = null;

  const STORAGE_KEY = 'tiktok_profile_inspector_data';
  const HIGHLIGHT_KEY = 'tiktok_profile_inspector_highlight_dismissed';

  // Check if highlight should be shown
  function shouldShowHighlight() {
    try {
      const dismissed = localStorage.getItem(HIGHLIGHT_KEY);
      return !dismissed;
    } catch (e) {
      return true;
    }
  }

  // Dismiss highlight permanently
  function dismissHighlight() {
    try {
      localStorage.setItem(HIGHLIGHT_KEY, 'true');
      const tooltip = document.getElementById('tt-refresh-tooltip');
      if (tooltip) {
        tooltip.style.opacity = '0';
        setTimeout(() => tooltip.remove(), 300);
      }
    } catch (e) {
      console.error('Failed to dismiss highlight:', e);
    }
  }

  // Reset highlight (called from settings)
  function resetHighlight() {
    try {
      localStorage.removeItem(HIGHLIGHT_KEY);
      showNotification('Refresh button highlight reset');
      // Recreate tooltip if we're on the page
      const refreshBtn = document.getElementById('tt-refresh-btn');
      if (refreshBtn && !document.getElementById('tt-refresh-tooltip')) {
        showRefreshTooltip();
      }
    } catch (e) {
      console.error('Failed to reset highlight:', e);
    }
  }

  // Show refresh tooltip
  function showRefreshTooltip() {
    if (!shouldShowHighlight()) return;

    const refreshBtn = document.getElementById('tt-refresh-btn');
    if (!refreshBtn) return;

    const tooltip = document.createElement('div');
    tooltip.id = 'tt-refresh-tooltip';
    tooltip.className = 'tt-refresh-tooltip';
    tooltip.innerHTML = `
      <div class="tt-refresh-tooltip-content">
        <strong>Refresh Button</strong><br>
        Reloads page to fetch updated profile data
        <button class="tt-refresh-tooltip-close">×</button>
      </div>
    `;

    refreshBtn.parentElement.style.position = 'relative';
    refreshBtn.parentElement.appendChild(tooltip);

    // Pulse animation on button
    refreshBtn.style.animation = 'pulse 2s infinite';

    // Close button
    tooltip.querySelector('.tt-refresh-tooltip-close').onclick = () => {
      dismissHighlight();
      refreshBtn.style.animation = '';
    };

    // Auto-dismiss after 10 seconds
    highlightTimeout = setTimeout(() => {
      dismissHighlight();
      refreshBtn.style.animation = '';
    }, 10000);
  }

  // Monitor URL changes and auto-fill username
  function monitorURL() {
    const currentUsername = getCurrentUsername();

    if (currentUsername && currentUsername !== lastCheckedUsername) {
      lastCheckedUsername = currentUsername;

      // Auto-fill the username input
      const input = document.getElementById('tt-username-input');
      if (input) {
        input.value = currentUsername;
        showNotification(`Profile detected: @${currentUsername}`);
      }

      // Auto-load profile data
      setTimeout(() => {
        updateFromUserInfo();
      }, 1500);
    }
  }

  // LocalStorage helpers
  function saveToStorage(username, data) {
    try {
      const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
      stored[username] = {
        data: data,
        timestamp: Date.now()
      };
      localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
    } catch (e) {
      console.error('Failed to save to storage:', e);
    }
  }

  function loadFromStorage(username) {
    try {
      const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
      return stored[username]?.data || null;
    } catch (e) {
      console.error('Failed to load from storage:', e);
      return null;
    }
  }

  function clearStorage() {
    try {
      localStorage.removeItem(STORAGE_KEY);
      showNotification('Storage cleared');
    } catch (e) {
      console.error('Failed to clear storage:', e);
    }
  }

  function getAllStoredProfiles() {
    try {
      const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
      return Object.keys(stored).map(username => ({
        username,
        timestamp: stored[username].timestamp
      }));
    } catch (e) {
      return [];
    }
  }

  // Copy to clipboard
  function copyToClipboard(text, label = 'Text') {
    if (typeof GM_setClipboard !== 'undefined') {
      GM_setClipboard(text);
      showNotification(`${label} copied!`);
    } else {
      navigator.clipboard.writeText(text).then(() => {
        showNotification(`${label} copied!`);
      }).catch(() => {
        showNotification('Copy failed', true);
      });
    }
  }

  // Show notification
  function showNotification(message, isError = false) {
    const notification = document.createElement('div');
    notification.className = 'tt-notification' + (isError ? ' tt-notification-error' : '');
    notification.textContent = message;
    document.body.appendChild(notification);

    setTimeout(() => notification.classList.add('tt-notification-show'), 10);

    setTimeout(() => {
      notification.classList.remove('tt-notification-show');
      setTimeout(() => notification.remove(), 300);
    }, 2500);
  }

  function getUserInfoObject() {
    const s = [...document.querySelectorAll('script')]
      .find(s => s.textContent.includes('"userInfo"'));
    if (!s) return null;

    const m = s.textContent.match(/"userInfo":(\{[^]*?\})(?=,"[^"]+":)/);
    if (!m) return null;

    try {
      const jsonStr = m[1]
        .replace(/\\u002F/g, '/')
        .replace(/\\/g, '\\\\')
        .replace(/\\\\"/g, '\\"')
        .replace(/\\\\u/g, '\\u');
      return JSON.parse(jsonStr);
    } catch (e) {
      try {
        const text = s.textContent;
        const start = text.indexOf('"userInfo":') + 11;
        let braceCount = 0, inString = false, escape = false, end = start;

        for (let i = start; i < text.length; i++) {
          const char = text[i];
          if (escape) { escape = false; continue; }
          if (char === '\\') { escape = true; continue; }
          if (char === '"') { inString = !inString; continue; }
          if (!inString) {
            if (char === '{') braceCount++;
            if (char === '}') {
              braceCount--;
              if (braceCount === 0) { end = i + 1; break; }
            }
          }
        }
        return JSON.parse(text.substring(start, end).replace(/\\u002F/g, '/'));
      } catch (e2) {
        console.error('Fallback failed', e2);
        return null;
      }
    }
  }

  function getCurrentUsername() {
    const match = window.location.pathname.match(/\/@([^\/\?]+)/);
    return match ? match[1] : null;
  }

  function goToProfile(username) {
    if (!username) return;
    username = username.replace(/^@+/, '').trim();
    if (!username) return;
    location.href = `https://www.tiktok.com/@${encodeURIComponent(username)}`;
    setTimeout(() => updateFromUserInfo(), 3000);
  }

  function formatNumber(num) {
    if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B';
    if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M';
    if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K';
    return num.toString();
  }

  function formatDate(ts) {
    if (!ts) return 'Unknown';
    return new Date(ts * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
  }

  // Detect changes between old and new data
  function detectChanges(oldData, newData) {
    if (!oldData || !newData) return [];
    const changes = [];

    const oldStats = oldData.stats || oldData.statsV2 || {};
    const newStats = newData.stats || newData.statsV2 || {};

    const compareField = (label, oldVal, newVal) => {
      if (oldVal !== newVal) {
        changes.push(`${label}: ${oldVal} → ${newVal}`);
      }
    };

    compareField('Followers', oldStats.followerCount, newStats.followerCount);
    compareField('Following', oldStats.followingCount, newStats.followingCount);
    compareField('Likes', oldStats.heartCount || oldStats.heart, newStats.heartCount || newStats.heart);
    compareField('Friends', oldStats.friendCount, newStats.friendCount);
    compareField('Videos', oldStats.videoCount, newStats.videoCount);
    compareField('Nickname', oldData.user?.nickname, newData.user?.nickname);
    compareField('Bio', oldData.user?.signature, newData.user?.signature);

    return changes;
  }

  // Show export menu
  function showExportMenu() {
    const info = getUserInfoObject();
    if (!info) {
      showNotification('No data to export', true);
      return;
    }

    const modal = document.createElement('div');
    modal.className = 'tt-modal';
    modal.innerHTML = `
      <div class="tt-modal-content">
        <div class="tt-modal-header">
          <h3>Export Profile Data</h3>
          <button class="tt-modal-close">×</button>
        </div>
        <div class="tt-modal-body">
          <p class="tt-export-desc">Select the data you want to export:</p>
          <div class="tt-export-options">
            <label class="tt-checkbox-label">
              <input type="checkbox" value="basic" checked> Basic Info (ID, Username, Nickname)
            </label>
            <label class="tt-checkbox-label">
              <input type="checkbox" value="stats" checked> Statistics (Followers, Likes, etc.)
            </label>
            <label class="tt-checkbox-label">
              <input type="checkbox" value="settings" checked> Privacy Settings
            </label>
            <label class="tt-checkbox-label">
              <input type="checkbox" value="advanced" checked> Advanced Info (SecUID, Room ID, etc.)
            </label>
            <label class="tt-checkbox-label">
              <input type="checkbox" value="raw"> Raw JSON (Complete data)
            </label>
          </div>
        </div>
        <div class="tt-modal-footer">
          <button class="tt-btn" id="tt-export-cancel">Cancel</button>
          <button class="tt-btn tt-btn-primary" id="tt-export-confirm">Export</button>
        </div>
      </div>
    `;

    document.body.appendChild(modal);
    setTimeout(() => modal.classList.add('tt-modal-show'), 10);

    modal.querySelector('.tt-modal-close').onclick = () => closeModal(modal);
    modal.querySelector('#tt-export-cancel').onclick = () => closeModal(modal);
    modal.onclick = (e) => { if (e.target === modal) closeModal(modal); };

    modal.querySelector('#tt-export-confirm').onclick = () => {
      const checkboxes = modal.querySelectorAll('input[type="checkbox"]:checked');
      const selected = Array.from(checkboxes).map(cb => cb.value);

      if (selected.length === 0) {
        showNotification('Please select at least one option', true);
        return;
      }

      exportSelectedData(info, selected);
      closeModal(modal);
    };
  }

  function closeModal(modal) {
    modal.classList.remove('tt-modal-show');
    setTimeout(() => modal.remove(), 300);
  }

  function exportSelectedData(info, selected) {
    const u = info.user;
    const s = info.stats || info.statsV2 || {};
    let exportData = {};

    if (selected.includes('raw')) {
      exportData = info;
    } else {
      if (selected.includes('basic')) {
        exportData.basic = {
          id: u.id,
          shortId: u.shortId,
          uniqueId: u.uniqueId,
          nickname: u.nickname,
          signature: u.signature,
          verified: u.verified,
          privateAccount: u.privateAccount,
          createTime: u.createTime,
          language: u.language,
          region: u.region
        };
      }
      if (selected.includes('stats')) {
        exportData.stats = {
          followerCount: s.followerCount,
          followingCount: s.followingCount,
          heartCount: s.heartCount || s.heart,
          videoCount: s.videoCount,
          diggCount: s.diggCount,
          friendCount: s.friendCount
        };
      }
      if (selected.includes('settings')) {
        exportData.settings = {
          commentSetting: u.commentSetting,
          duetSetting: u.duetSetting,
          stitchSetting: u.stitchSetting,
          downloadSetting: u.downloadSetting,
          followingVisibility: u.followingVisibility,
          openFavorite: u.openFavorite
        };
      }
      if (selected.includes('advanced')) {
        exportData.advanced = {
          secUid: u.secUid,
          roomId: u.roomId,
          relation: u.relation,
          ttSeller: u.ttSeller,
          ftc: u.ftc,
          secret: u.secret,
          isADVirtual: u.isADVirtual,
          profileEmbedPermission: u.profileEmbedPermission
        };
      }
    }

    const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `tiktok-${u.uniqueId || 'profile'}-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(a.href);
    showNotification('Data exported!');
  }

  // Show settings menu
  function showSettingsMenu() {
    const profiles = getAllStoredProfiles();

    const modal = document.createElement('div');
    modal.className = 'tt-modal';
    modal.innerHTML = `
      <div class="tt-modal-content">
        <div class="tt-modal-header">
          <h3>Settings</h3>
          <button class="tt-modal-close">×</button>
        </div>
        <div class="tt-modal-body">
          <div class="tt-settings-section">
            <h4>Stored Profiles</h4>
            <p class="tt-settings-desc">${profiles.length} profile(s) cached</p>
            ${profiles.length > 0 ? `
              <div class="tt-stored-profiles">
                ${profiles.map(p => `
                  <div class="tt-stored-profile">
                    <span>@${p.username}</span>
                    <span class="tt-stored-date">${new Date(p.timestamp).toLocaleDateString()}</span>
                  </div>
                `).join('')}
              </div>
            ` : '<p class="tt-empty-text">No profiles cached yet</p>'}
          </div>
          <div class="tt-settings-section">
            <h4>Data Management</h4>
            <button class="tt-btn" id="tt-clear-storage">Clear All Cached Data</button>
          </div>
          <div class="tt-settings-section">
            <h4>Interface</h4>
            <button class="tt-btn" id="tt-reset-highlight">Reset Refresh Button Highlight</button>
          </div>
        </div>
        <div class="tt-modal-footer">
          <button class="tt-btn tt-btn-primary" id="tt-settings-close">Close</button>
        </div>
      </div>
    `;

    document.body.appendChild(modal);
    setTimeout(() => modal.classList.add('tt-modal-show'), 10);

    modal.querySelector('.tt-modal-close').onclick = () => closeModal(modal);
    modal.querySelector('#tt-settings-close').onclick = () => closeModal(modal);
    modal.onclick = (e) => { if (e.target === modal) closeModal(modal); };

    modal.querySelector('#tt-clear-storage').onclick = () => {
      if (confirm('Are you sure you want to clear all cached profile data?')) {
        clearStorage();
        closeModal(modal);
      }
    };

    modal.querySelector('#tt-reset-highlight').onclick = () => {
      resetHighlight();
      closeModal(modal);
    };
  }

  function startDrag(e) {
    if (e.target.closest('.tt-resize-handle') || !e.target.closest('.tt-header')) return;
    isDragging = true;
    const panel = document.getElementById('tt-info-panel-inner');
    const rect = panel.getBoundingClientRect();
    dragOffsetX = e.clientX - rect.left;
    dragOffsetY = e.clientY - rect.top;
    panel.style.transition = 'none';
    document.body.style.userSelect = 'none';
    document.body.style.cursor = 'grabbing';
  }

  function drag(e) {
    if (!isDragging) return;
    const panel = document.getElementById('tt-info-panel-inner');
    const container = document.getElementById('tt-info-panel');
    let newLeft = Math.max(0, Math.min(e.clientX - dragOffsetX, window.innerWidth - panel.offsetWidth));
    let newTop = Math.max(0, Math.min(e.clientY - dragOffsetY, window.innerHeight - panel.offsetHeight));
    container.style.left = newLeft + 'px';
    container.style.top = newTop + 'px';
    container.style.right = 'auto';
  }

  function stopDrag() {
    if (!isDragging) return;
    isDragging = false;
    const panel = document.getElementById('tt-info-panel-inner');
    panel.style.transition = '';
    document.body.style.userSelect = '';
    document.body.style.cursor = '';
  }

  function startResize(e, direction) {
    e.stopPropagation();
    isResizing = true;
    resizeDirection = direction;

    const panel = document.getElementById('tt-info-panel-inner');
    const container = document.getElementById('tt-info-panel');
    const rect = panel.getBoundingClientRect();
    const containerRect = container.getBoundingClientRect();

    resizeStartX = e.clientX;
    resizeStartY = e.clientY;
    resizeStartWidth = rect.width;
    resizeStartHeight = rect.height;
    resizeStartLeft = containerRect.left;
    resizeStartTop = containerRect.top;

    panel.style.transition = 'none';
    document.body.style.userSelect = 'none';
    document.body.style.cursor = direction.includes('n') || direction.includes('s') ?
      (direction.includes('e') || direction.includes('w') ? 'nwse-resize' : 'ns-resize') :
      'ew-resize';
  }

  function resize(e) {
    if (!isResizing) return;

    const panel = document.getElementById('tt-info-panel-inner');
    const container = document.getElementById('tt-info-panel');

    const deltaX = e.clientX - resizeStartX;
    const deltaY = e.clientY - resizeStartY;

    let newWidth = resizeStartWidth;
    let newHeight = resizeStartHeight;
    let newLeft = resizeStartLeft;
    let newTop = resizeStartTop;

    if (resizeDirection.includes('e')) {
      newWidth = Math.max(320, Math.min(900, resizeStartWidth + deltaX));
    }
    if (resizeDirection.includes('w')) {
      newWidth = Math.max(320, Math.min(900, resizeStartWidth - deltaX));
      newLeft = resizeStartLeft + (resizeStartWidth - newWidth);
    }
    if (resizeDirection.includes('s')) {
      newHeight = Math.max(400, Math.min(window.innerHeight - 100, resizeStartHeight + deltaY));
    }
    if (resizeDirection.includes('n')) {
      newHeight = Math.max(400, Math.min(window.innerHeight - 100, resizeStartHeight - deltaY));
      newTop = resizeStartTop + (resizeStartHeight - newHeight);
    }

    panel.style.width = newWidth + 'px';
    panel.style.maxHeight = newHeight + 'px';
    container.style.left = newLeft + 'px';
    container.style.top = newTop + 'px';
    container.style.right = 'auto';
  }

  function stopResize() {
    if (!isResizing) return;
    isResizing = false;
    const panel = document.getElementById('tt-info-panel-inner');
    panel.style.transition = '';
    document.body.style.userSelect = '';
    document.body.style.cursor = '';
  }

  function createUI() {
    if (document.getElementById('tt-info-panel')) return;

    const style = `
      @keyframes pulse {
        0%, 100% { box-shadow: 0 0 0 0 rgba(32, 213, 236, 0.4); }
        50% { box-shadow: 0 0 0 6px rgba(32, 213, 236, 0); }
      }
      .tt-refresh-tooltip{position:absolute;top:calc(100% + 8px);right:0;background:linear-gradient(135deg,rgba(32,213,236,0.95),rgba(10,189,227,0.95));color:#fff;padding:12px 16px;border-radius:8px;font-size:12px;z-index:10;min-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.3);opacity:0;animation:fadeIn .3s forwards}
      @keyframes fadeIn{to{opacity:1}}
      .tt-refresh-tooltip-content{position:relative;line-height:1.5}
      .tt-refresh-tooltip-content strong{display:block;margin-bottom:4px;font-size:13px}
      .tt-refresh-tooltip-close{position:absolute;top:-8px;right:-12px;background:rgba(0,0,0,.2);border:none;color:#fff;width:24px;height:24px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;transition:background .2s}
      .tt-refresh-tooltip-close:hover{background:rgba(0,0,0,.4)}
      .tt-notification{position:fixed;top:20px;right:20px;padding:12px 20px;background:rgba(32,213,236,0.95);color:#fff;border-radius:8px;font-size:13px;font-weight:600;z-index:99999999;opacity:0;transform:translateX(400px);transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 12px rgba(0,0,0,.3);max-width:450px;line-height:1.4}
      .tt-notification-show{opacity:1;transform:translateX(0)}
      .tt-notification-error{background:rgba(254,44,85,0.95)}
      #tt-info-panel{position:fixed;top:70px;right:20px;z-index:9999999;font-family:TikTokFont,Arial,Tahoma,PingFangSC,sans-serif}
      #tt-info-panel-inner{width:400px;max-height:90vh;overflow:hidden;background:linear-gradient(135deg,rgba(18,18,18,0.98) 0%,rgba(25,25,35,0.98) 100%);border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.08),inset 0 1px 0 rgba(255,255,255,.05);display:flex;flex-direction:column;position:relative;backdrop-filter:blur(20px)}
      #tt-info-panel-content{overflow-y:auto;flex:1}
      #tt-info-panel-content::-webkit-scrollbar{width:0;height:0}
      .tt-header{padding:18px 24px 14px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;justify-content:space-between;align-items:center;cursor:grab;user-select:none;background:linear-gradient(180deg,rgba(255,255,255,.03) 0%,transparent 100%);flex-shrink:0}
      .tt-header:active{cursor:grabbing}
      .tt-header-left{display:flex;flex-direction:column;gap:4px}
      .tt-header-title{font-size:16px;font-weight:700;color:rgba(255,255,255,.95);letter-spacing:-.02em}
      .tt-header-subtitle{font-size:11px;color:rgba(255,255,255,.4);font-weight:500}
      .tt-header-actions{display:flex;gap:6px}
      .tt-resize-handle{position:absolute;width:12px;height:12px;z-index:10}
      .tt-resize-handle-nw{top:0;left:0;cursor:nw-resize}
      .tt-resize-handle-ne{top:0;right:0;cursor:ne-resize}
      .tt-resize-handle-sw{bottom:0;left:0;cursor:sw-resize}
      .tt-resize-handle-se{bottom:0;right:0;cursor:se-resize}
      .tt-resize-handle-n{top:0;left:50%;transform:translateX(-50%);width:50px;height:4px;cursor:n-resize}
      .tt-resize-handle-s{bottom:0;left:50%;transform:translateX(-50%);width:50px;height:4px;cursor:s-resize}
      .tt-resize-handle-w{left:0;top:50%;transform:translateY(-50%);width:4px;height:50px;cursor:w-resize}
      .tt-resize-handle-e{right:0;top:50%;transform:translateY(-50%);width:4px;height:50px;cursor:e-resize}
      .tt-btn{height:32px;padding:0 14px;border:none;border-radius:6px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.9);font-size:12px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .2s cubic-bezier(.4,0,.2,1);border:1px solid rgba(255,255,255,.05)}
      .tt-btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.1);transform:translateY(-1px)}
      .tt-btn:active{transform:translateY(0)}
      .tt-btn-primary{background:linear-gradient(135deg,#fe2c55 0%,#ff4d6d 100%);border-color:transparent;box-shadow:0 2px 8px rgba(254,44,85,.3)}
      .tt-btn-primary:hover{background:linear-gradient(135deg,#e62649 0%,#ff3d5d 100%);box-shadow:0 4px 12px rgba(254,44,85,.4)}
      .tt-btn-small{height:26px;padding:0 12px;font-size:11px;border-radius:5px}
      .tt-btn-icon{padding:0;width:32px;height:32px;border-radius:6px;font-size:16px}
      .tt-search-section{padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(0,0,0,.1);flex-shrink:0}
      .tt-search-label{font-size:12px;font-weight:600;color:rgba(255,255,255,.6);margin-bottom:10px;display:block}
      .tt-search-input{width:100%;height:40px;padding:0 14px;border:1px solid rgba(255,255,255,.12);border-radius:8px;font-size:13px;color:rgba(255,255,255,.95);background:rgba(255,255,255,.04);box-sizing:border-box;margin-bottom:12px;transition:all .2s}
      .tt-search-input:focus{outline:none;border-color:rgba(254,44,85,.5);background:rgba(255,255,255,.06);box-shadow:0 0 0 3px rgba(254,44,85,.1)}
      .tt-search-input::placeholder{color:rgba(255,255,255,.3)}
      .tt-button-group{display:grid;grid-template-columns:1fr 1fr;gap:8px}
      .tt-profile-section{padding:24px}
      .tt-profile-header{display:flex;gap:16px;margin-bottom:20px}
      .tt-avatar-wrapper{position:relative;width:72px;height:72px;flex-shrink:0}
      .tt-avatar{width:72px;height:72px;border-radius:50%;overflow:hidden;background:rgba(255,255,255,.05);border:2px solid rgba(255,255,255,.1);box-shadow:0 4px 12px rgba(0,0,0,.3)}
      .tt-avatar img{width:100%;height:100%;object-fit:cover}
      .tt-verified-badge{position:absolute;bottom:-2px;right:-2px;width:22px;height:22px;background:linear-gradient(135deg,#20d5ec 0%,#0abde3 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;border:2px solid rgba(18,18,18,.98);box-shadow:0 2px 8px rgba(32,213,236,.4)}
      .tt-verified-badge svg{width:12px;height:12px;fill:#fff}
      .tt-profile-info{flex:1;min-width:0}
      .tt-nickname{font-size:18px;font-weight:700;color:rgba(255,255,255,.95);margin-bottom:4px;line-height:1.3}
      .tt-username{font-size:14px;color:rgba(255,255,255,.45);margin-bottom:10px}
      .tt-badges{display:flex;flex-wrap:wrap;gap:6px}
      .tt-badge{padding:4px 10px;border-radius:4px;font-size:11px;font-weight:600;background:rgba(255,255,255,.08);color:rgba(255,255,255,.7);border:1px solid rgba(255,255,255,.05)}
      .tt-bio{margin:12px 0 20px;font-size:13px;color:rgba(255,255,255,.75);line-height:1.6;word-break:break-word;padding:14px;background:rgba(255,255,255,.03);border-radius:8px;border:1px solid rgba(255,255,255,.05)}
      .tt-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
      .tt-stat-item{padding:14px;display:flex;flex-direction:column;align-items:center;gap:4px;background:rgba(255,255,255,.04);border-radius:10px;border:1px solid rgba(255,255,255,.06);transition:all .2s}
      .tt-stat-item:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.1);transform:translateY(-2px)}
      .tt-stat-value{font-size:16px;font-weight:700;color:rgba(255,255,255,.95)}
      .tt-stat-label{font-size:11px;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.05em}
      .tt-info-section{padding:0 24px 24px}
      .tt-info-title{font-size:13px;font-weight:700;color:rgba(255,255,255,.6);margin-bottom:12px;text-transform:uppercase;letter-spacing:.05em}
      .tt-info-row{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-radius:6px;margin-bottom:4px;background:rgba(255,255,255,.02);transition:all .2s;gap:12px}
      .tt-info-row:hover{background:rgba(255,255,255,.04)}
      .tt-info-row-content{display:flex;justify-content:space-between;align-items:center;flex:1;min-width:0}
      .tt-info-label{font-size:13px;color:rgba(255,255,255,.5);font-weight:500;flex-shrink:0}
      .tt-info-value{font-size:13px;color:rgba(255,255,255,.95);text-align:right;word-break:break-word;font-weight:600;flex:1;min-width:0;padding-left:12px}
      .tt-info-value-mono{font-family:'SF Mono',Monaco,monospace;font-size:11px}
      .tt-info-value-link{color:#20d5ec;text-decoration:none;transition:color .2s}
      .tt-info-value-link:hover{color:#0abde3;text-decoration:underline}
      .tt-info-copy{flex-shrink:0}
      .tt-footer{padding:16px 24px;background:rgba(0,0,0,.2);border-top:1px solid rgba(255,255,255,.08);font-size:11px;color:rgba(255,255,255,.4);display:flex;justify-content:space-between;align-items:center;flex-shrink:0}
      .tt-kbd{padding:3px 8px;border-radius:4px;background:rgba(255,255,255,.06);font-family:monospace;font-size:10px;margin:0 3px;border:1px solid rgba(255,255,255,.08)}
      .tt-empty-state{padding:60px 24px;text-align:center}
      .tt-empty-title{font-size:15px;font-weight:700;color:rgba(255,255,255,.6);margin-bottom:8px}
      .tt-empty-text{font-size:13px;color:rgba(255,255,255,.4);line-height:1.6}
      .tt-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:99999999;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .3s;backdrop-filter:blur(4px)}
      .tt-modal-show{opacity:1}
      .tt-modal-content{background:linear-gradient(135deg,rgba(25,25,35,0.98) 0%,rgba(18,18,18,0.98) 100%);border-radius:12px;max-width:500px;width:90%;max-height:80vh;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.08);transform:scale(0.9);transition:transform .3s}
      .tt-modal-show .tt-modal-content{transform:scale(1)}
      .tt-modal-header{padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;justify-content:space-between;align-items:center}
      .tt-modal-header h3{margin:0;font-size:18px;font-weight:700;color:rgba(255,255,255,.95)}
      .tt-modal-close{background:none;border:none;color:rgba(255,255,255,.6);font-size:28px;cursor:pointer;padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:all .2s}
      .tt-modal-close:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.9)}
      .tt-modal-body{padding:24px;max-height:60vh;overflow-y:auto}
      .tt-modal-body::-webkit-scrollbar{width:8px}
      .tt-modal-body::-webkit-scrollbar-track{background:transparent}
      .tt-modal-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:4px}
      .tt-modal-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.08);display:flex;justify-content:flex-end;gap:12px}
      .tt-export-desc,.tt-settings-desc{font-size:13px;color:rgba(255,255,255,.6);margin-bottom:16px}
      .tt-export-options{display:flex;flex-direction:column;gap:12px}
      .tt-checkbox-label{display:flex;align-items:center;gap:12px;padding:12px;background:rgba(255,255,255,.03);border-radius:8px;border:1px solid rgba(255,255,255,.05);cursor:pointer;transition:all .2s;font-size:14px;color:rgba(255,255,255,.9)}
      .tt-checkbox-label:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.1)}
      .tt-checkbox-label input{cursor:pointer;width:18px;height:18px}
      .tt-settings-section{margin-bottom:24px}
      .tt-settings-section:last-child{margin-bottom:0}
      .tt-settings-section h4{margin:0 0 12px 0;font-size:15px;font-weight:600;color:rgba(255,255,255,.8)}
      .tt-stored-profiles{display:flex;flex-direction:column;gap:8px;max-height:200px;overflow-y:auto;padding:12px;background:rgba(255,255,255,.02);border-radius:8px;border:1px solid rgba(255,255,255,.05)}
      .tt-stored-profile{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:rgba(255,255,255,.03);border-radius:6px;font-size:13px;color:rgba(255,255,255,.9)}
      .tt-stored-date{font-size:11px;color:rgba(255,255,255,.5)}
    `;

    const container = document.createElement('div');
    container.id = 'tt-info-panel';
    container.innerHTML = `
      <style>${style}</style>
      <div id="tt-info-panel-inner">
        <div class="tt-header">
          <div class="tt-header-left">
            <div class="tt-header-title">Profile Inspector</div>
            <div class="tt-header-subtitle" id="tt-current-url">No profile loaded</div>
          </div>
          <div class="tt-header-actions">
            <button id="tt-refresh-btn" class="tt-btn tt-btn-small">Refresh</button>
            <button id="tt-export-btn" class="tt-btn tt-btn-small">Export</button>
            <button id="tt-settings-btn" class="tt-btn tt-btn-icon">⚙️</button>
          </div>
        </div>
        <div id="tt-info-panel-content">
          <div class="tt-search-section">
            <label class="tt-search-label">Navigate to Profile</label>
            <input id="tt-username-input" class="tt-search-input" type="text" placeholder="Enter username..."/>
            <button id="tt-go-btn" class="tt-btn tt-btn-primary" style="width: 100%;">Go to Profile</button>
          </div>
          <div id="tt-content-area">
            <div class="tt-empty-state">
              <div class="tt-empty-title">No profile loaded</div>
              <div class="tt-empty-text">Enter a username or navigate to a profile<br>then click Refresh to load data</div>
            </div>
          </div>
        </div>
        <div class="tt-footer">
          <div>Toggle <span class="tt-kbd">Alt</span></div>
          <div>Destroy <span class="tt-kbd">Alt</span> × 2</div>
        </div>
        <div class="tt-resize-handle tt-resize-handle-nw"></div>
        <div class="tt-resize-handle tt-resize-handle-ne"></div>
        <div class="tt-resize-handle tt-resize-handle-sw"></div>
        <div class="tt-resize-handle tt-resize-handle-se"></div>
        <div class="tt-resize-handle tt-resize-handle-n"></div>
        <div class="tt-resize-handle tt-resize-handle-s"></div>
        <div class="tt-resize-handle tt-resize-handle-w"></div>
        <div class="tt-resize-handle tt-resize-handle-e"></div>
      </div>
    `;

    document.body.appendChild(container);

    const input = container.querySelector('#tt-username-input');
    const goBtn = container.querySelector('#tt-go-btn');
    const refreshBtn = container.querySelector('#tt-refresh-btn');
    const exportBtn = container.querySelector('#tt-export-btn');
    const settingsBtn = container.querySelector('#tt-settings-btn');
    const header = container.querySelector('.tt-header');

    // Show refresh tooltip if not dismissed
    setTimeout(() => showRefreshTooltip(), 1000);

    goBtn.addEventListener('click', () => goToProfile(input.value));
    input.addEventListener('keydown', e => { if (e.key === 'Enter') goToProfile(input.value); });
    refreshBtn.addEventListener('click', () => {
      showNotification('Refreshing profile...');
      // Always reload the page to get fresh data
      // This works on all pages including video pages with comments
      setTimeout(() => {
        location.reload();
      }, 500);
    });
    exportBtn.addEventListener('click', showExportMenu);
    settingsBtn.addEventListener('click', showSettingsMenu);
    header.addEventListener('mousedown', startDrag);

    // Add resize handle listeners
    container.querySelectorAll('.tt-resize-handle').forEach(handle => {
      const direction = handle.className.split('-').pop();
      handle.addEventListener('mousedown', (e) => startResize(e, direction));
    });

    document.addEventListener('mousemove', drag);
    document.addEventListener('mouseup', stopDrag);
    document.addEventListener('mousemove', resize);
    document.addEventListener('mouseup', stopResize);

    updateFromUserInfo();
    updateCurrentURL();
  }

  function updateCurrentURL() {
    const urlDisplay = document.getElementById('tt-current-url');
    if (!urlDisplay) return;
    const username = getCurrentUsername();
    urlDisplay.textContent = username ? `@${username}` : 'Not on a profile page';
  }

  function createCopyButton(text, label) {
    const btn = document.createElement('button');
    btn.className = 'tt-btn tt-btn-small';
    btn.textContent = 'Copy';
    btn.title = `Copy ${label}`;
    btn.addEventListener('click', e => { e.stopPropagation(); copyToClipboard(text, label); });
    return btn;
  }

  function updateFromUserInfo() {
    const info = getUserInfoObject();
    const container = document.getElementById('tt-info-panel');
    if (!container) return;

    const contentArea = container.querySelector('#tt-content-area');
    updateCurrentURL();

    if (!info || !info.user) {
      contentArea.innerHTML = '<div class="tt-empty-state"><div class="tt-empty-title">No data found</div><div class="tt-empty-text">Make sure you\'re on a profile page<br>Wait a moment, then click Refresh</div></div>';
      return;
    }

    const u = info.user;
    const s = info.stats || info.statsV2 || {};

    // Save to storage
    if (u.uniqueId) {
      saveToStorage(u.uniqueId, info);
    }

    // Store current data for change detection
    lastProfileData = info;

    let badges = '';
    if (u.verified) badges += '<span class="tt-badge">Verified</span>';
    if (u.privateAccount) badges += '<span class="tt-badge">Private</span>';
    if (u.commerceUserInfo?.commerceUser) badges += '<span class="tt-badge">Shop</span>';

    const avatarUrl = (u.avatarLarger || u.avatarMedium || u.avatarThumb || '').replace(/\\u002F/g, '/');
    const verifiedBadge = u.verified ? '<div class="tt-verified-badge"><svg viewBox="0 0 48 48"><path d="M0 24C0 10.7 10.7 0 24 0s24 10.7 24 24-10.7 24-24 24S0 37.3 0 24z"/><path d="M19.5 33.5l-9-9 2.83-2.83 6.17 6.17 14.67-14.67L37 16z" fill="#fff"/></svg></div>' : '';

    const stats = [
      { label: 'Following', value: formatNumber(parseInt(s.followingCount) || 0) },
      { label: 'Followers', value: formatNumber(parseInt(s.followerCount) || 0) },
      { label: 'Likes', value: formatNumber(parseInt(s.heartCount || s.heart) || 0) },
      { label: 'Friends', value: formatNumber(parseInt(s.friendCount) || 0) }
    ];

    const detailRows = [
      { label: 'User ID', value: u.id || 'N/A', copyable: true },
      { label: 'Short ID', value: u.shortId || 'N/A', copyable: true },
      { label: 'Unique ID', value: u.uniqueId || 'N/A', copyable: true },
      { label: 'Videos', value: s.videoCount || '0' },
      { label: 'Liked Videos (if count is 0, liked is set to private)', value: formatNumber(parseInt(s.diggCount) || 0) },
      { label: 'Created', value: formatDate(u.createTime) },
      { label: 'Language', value: u.language ? u.language.toUpperCase() : 'N/A' },
      { label: 'Region', value: u.region || 'Unknown', isLink: true, linkUrl: 'https://omar-thing.site/' },
      { label: 'Nickname Modified', value: u.nickNameModifyTime ? formatDate(u.nickNameModifyTime) : 'N/A' },
      { label: 'UniqueID Modified', value: u.uniqueIdModifyTime ? formatDate(u.uniqueIdModifyTime) : 'N/A' }
    ];

    const settingsRows = [
      { label: 'Comments', value: u.commentSetting === 0 ? 'Everyone' : 'Friends' },
      { label: 'Duet', value: u.duetSetting === 0 ? 'Everyone' : (u.duetSetting === 1 ? 'Friends' : 'Off') },
      { label: 'Stitch', value: u.stitchSetting === 0 ? 'Everyone' : (u.stitchSetting === 1 ? 'Friends' : 'Off') },
      { label: 'Download', value: u.downloadSetting === 0 ? 'On' : 'Off' },
      { label: 'Following List', value: u.followingVisibility === 0 ? 'Public' : (u.followingVisibility === 1 ? 'Friends' : 'Private') },
      { label: 'Open Favorite', value: u.openFavorite ? 'Yes' : 'No' }
    ];

    const advancedRows = [
      { label: 'Relation', value: u.relation === 0 ? 'Not following' : (u.relation === 1 ? 'Following' : (u.relation === 2 ? 'Friends' : 'Unknown')) },
      { label: 'TT Seller', value: u.ttSeller ? 'Yes' : 'No' },
      { label: 'FTC', value: u.ftc ? 'Yes' : 'No' },
      { label: 'Secret Account', value: u.secret ? 'Yes' : 'No' },
      { label: 'AD Virtual', value: u.isADVirtual ? 'Yes' : 'No' },
      { label: 'Room ID', value: u.roomId || 'None', copyable: u.roomId },
      { label: 'Profile Embed', value: u.profileEmbedPermission === 1 ? 'Allowed' : 'Disabled' },
      { label: 'SecUID', value: u.secUid || 'N/A', copyable: true, mono: true }
    ];

    const renderRow = (row) => {
      const valueContent = row.isLink
        ? `<a href="${row.linkUrl}" target="_blank" class="tt-info-value-link">${row.value} →</a>`
        : row.value;

      return `
        <div class="tt-info-row">
          <div class="tt-info-row-content">
            <div class="tt-info-label">${row.label}</div>
            <div class="tt-info-value ${row.mono ? 'tt-info-value-mono' : ''}">${valueContent}</div>
          </div>
          ${row.copyable && row.value !== 'N/A' && row.value !== 'None' ? `<div class="tt-info-copy" data-copy="${row.value}" data-label="${row.label}"></div>` : ''}
        </div>
      `;
    };

    contentArea.innerHTML = `
      <div class="tt-profile-section">
        <div class="tt-profile-header">
          <div class="tt-avatar-wrapper">
            <div class="tt-avatar">${avatarUrl ? `<img src="${avatarUrl}" alt="Avatar">` : ''}</div>
            ${verifiedBadge}
          </div>
          <div class="tt-profile-info">
            <div class="tt-nickname">${u.nickname || 'No nickname'}</div>
            <div class="tt-username">@${u.uniqueId || 'unknown'}</div>
            ${badges ? `<div class="tt-badges">${badges}</div>` : ''}
          </div>
        </div>
        ${u.signature ? `<div class="tt-bio">${u.signature}</div>` : ''}
        <div class="tt-stats">${stats.map(s => `<div class="tt-stat-item"><div class="tt-stat-value">${s.value}</div><div class="tt-stat-label">${s.label}</div></div>`).join('')}</div>
      </div>
      <div class="tt-info-section">
        <div class="tt-info-title">Account Details</div>
        ${detailRows.map(renderRow).join('')}
      </div>
      <div class="tt-info-section">
        <div class="tt-info-title">Privacy Settings</div>
        ${settingsRows.map(renderRow).join('')}
      </div>
      <div class="tt-info-section">
        <div class="tt-info-title">Additional Info</div>
        ${advancedRows.map(renderRow).join('')}
      </div>
    `;

    contentArea.querySelectorAll('.tt-info-copy').forEach(el => {
      el.appendChild(createCopyButton(el.getAttribute('data-copy'), el.getAttribute('data-label')));
    });
  }

  function toggleVisibility() {
    const container = document.getElementById('tt-info-panel');
    if (!container) return;
    uiVisible = !uiVisible;
    container.style.display = uiVisible ? 'block' : 'none';
  }

  function destroyUI() {
    if (urlCheckInterval) clearInterval(urlCheckInterval);
    if (highlightTimeout) clearTimeout(highlightTimeout);
    const container = document.getElementById('tt-info-panel');
    if (container?.parentNode) container.parentNode.removeChild(container);
    document.removeEventListener('mousemove', drag);
    document.removeEventListener('mouseup', stopDrag);
    document.removeEventListener('mousemove', resize);
    document.removeEventListener('mouseup', stopResize);
    window.removeEventListener('keydown', keyHandler, true);
  }

  function keyHandler(e) {
    if (e.key !== 'Alt') return;
    e.preventDefault();
    if (!altPressedOnce) {
      altPressedOnce = true;
      toggleVisibility();
      altTimeout = setTimeout(() => { altPressedOnce = false; }, 400);
    } else {
      clearTimeout(altTimeout);
      altPressedOnce = false;
      destroyUI();
    }
  }

  function init() {
    createUI();
    window.addEventListener('keydown', keyHandler, true);

    // Start URL monitoring (checks every 1.5 seconds)
    urlCheckInterval = setInterval(() => {
      monitorURL();
    }, 1500);

    // Initial check
    monitorURL();

    // Also monitor for URL changes via mutation observer
    let lastUrl = location.href;
    new MutationObserver(() => {
      const currentUrl = location.href;
      if (currentUrl !== lastUrl) {
        lastUrl = currentUrl;
        lastCheckedUsername = null; // Reset to trigger new check
        updateCurrentURL();

        // Small delay then check for username
        setTimeout(() => {
          monitorURL();
        }, 1000);
      }
    }).observe(document, { subtree: true, childList: true });
  }

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    init();
  } else {
    window.addEventListener('DOMContentLoaded', init);
  }
})();