Extract detailed TikTok profile info with draggable/resizable UI
// ==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);
}
})();