Greasy Fork

Greasy Fork is available in English.

显示推特用户地理位置

在Twitter用户名旁边显示基于账户位置的国家旗帜

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         显示推特用户地理位置
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  在Twitter用户名旁边显示基于账户位置的国家旗帜
// @author       https://x.com/Gufii_666
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      GPL-3.0
// @grant        GM_deleteValue
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';

  // ==================== 国家旗帜映射 ====================
const COUNTRY_FLAGS = {
  "Afghanistan": "阿富汗",
  "Albania": "阿尔巴尼亚",
  "Algeria": "阿尔及利亚",
  "Argentina": "阿根廷",
  "Australia": "澳大利亚",
  "Austria": "奥地利",
  "Bangladesh": "孟加拉国",
  "Belgium": "比利时",
  "Brazil": "巴西",
  "Canada": "加拿大",
  "Chile": "智利",
  "China": "中国",
  "Colombia": "哥伦比亚",
  "Czech Republic": "捷克",
  "Denmark": "丹麦",
  "Egypt": "埃及",
  "Europe": "欧盟",
  "Finland": "芬兰",
  "France": "法国",
  "Germany": "德国",
  "Greece": "希腊",
  "Hong Kong": "中国香港",
  "Hungary": "匈牙利",
  "India": "印度",
  "Indonesia": "印度尼西亚",
  "Iran": "伊朗",
  "Iraq": "伊拉克",
  "Ireland": "爱尔兰",
  "Israel": "以色列",
  "Italy": "意大利",
  "Japan": "日本",
  "Kenya": "肯尼亚",
  "Malaysia": "马来西亚",
  "Mexico": "墨西哥",
  "Netherlands": "荷兰",
  "New Zealand": "新西兰",
  "Nigeria": "尼日利亚",
  "Norway": "挪威",
  "Pakistan": "巴基斯坦",
  "Philippines": "菲律宾",
  "Poland": "波兰",
  "Portugal": "葡萄牙",
  "Romania": "罗马尼亚",
  "Russia": "俄罗斯",
  "Saudi Arabia": "沙特阿拉伯",
  "Singapore": "新加坡",
  "South Africa": "南非",
  "Korea": "韩国",
  "South Korea": "韩国",
  "Spain": "西班牙",
  "Sweden": "瑞典",
  "Switzerland": "瑞士",
  "Taiwan": "中国台湾",
  "Thailand": "泰国",
  "Turkey": "土耳其",
  "Ukraine": "乌克兰",
  "United Arab Emirates": "阿拉伯联合酋长国",
  "United Kingdom": "英国",
  "United States": "美国",
  "Venezuela": "委内瑞拉",
  "Vietnam": "越南"
};

  // 区域名称到国家的映射(用于处理区域名称)
  const REGION_TO_COUNTRY = {
    "East Asia & Pacific": "China", // 默认映射到中国
    "Europe": "Europe",
    "Middle East & North Africa": "Saudi Arabia",
    "Sub-Saharan Africa": "South Africa",
    "Latin America & Caribbean": "Brazil",
    "North America": "United States",
    "South Asia": "India"
  };

  function getCountryFlag(countryName) {
    if (!countryName) return null;

    // 先检查是否是区域名称
    if (REGION_TO_COUNTRY[countryName]) {
      const mappedCountry = REGION_TO_COUNTRY[countryName];
      if (COUNTRY_FLAGS[mappedCountry]) {
        return COUNTRY_FLAGS[mappedCountry];
      }
    }

    // Try exact match first
    if (COUNTRY_FLAGS[countryName]) {
      return COUNTRY_FLAGS[countryName];
    }

    // Try case-insensitive match
    const normalized = countryName.trim();
    for (const [country, flag] of Object.entries(COUNTRY_FLAGS)) {
      if (country.toLowerCase() === normalized.toLowerCase()) {
        return flag;
      }
    }

    // 尝试部分匹配(例如 "Hong Kong" 匹配 "Hong Kong")
    for (const [country, flag] of Object.entries(COUNTRY_FLAGS)) {
      if (normalized.toLowerCase().includes(country.toLowerCase()) ||
          country.toLowerCase().includes(normalized.toLowerCase())) {
        return flag;
      }
    }

    return null;
  }

  // ==================== 配置和状态 ====================
  let locationCache = new Map();
  const CACHE_KEY = 'twitter_location_cache';
  const CACHE_EXPIRY_DAYS = 30;
  const TOGGLE_KEY = 'extension_enabled';
  const DEFAULT_ENABLED = true;

  // Rate limiting
  const requestQueue = [];
  let isProcessingQueue = false;
  let lastRequestTime = 0;
  const MIN_REQUEST_INTERVAL = 2000;
  const MAX_CONCURRENT_REQUESTS = 2;
  let activeRequests = 0;
  let rateLimitResetTime = 0;

  // Observer for dynamically loaded content
  let observer = null;

  // Extension enabled state
  let extensionEnabled = DEFAULT_ENABLED;

  // Track usernames currently being processed
  const processingUsernames = new Set();

  // Debounce timer for processUsernames
  let processTimer = null;
  let isProcessing = false;

  // ==================== 存储管理(使用GM_* API)====================
  function loadEnabledState() {
    try {
      const value = GM_getValue(TOGGLE_KEY, DEFAULT_ENABLED);
      extensionEnabled = value;
      console.log('Extension enabled:', extensionEnabled);
    } catch (error) {
      console.error('Error loading enabled state:', error);
      extensionEnabled = DEFAULT_ENABLED;
    }
  }

  function loadCache() {
    try {
      const cached = GM_getValue(CACHE_KEY, null);
      if (cached) {
        const now = Date.now();

        // Filter out expired entries and null entries
        for (const [username, data] of Object.entries(cached)) {
          if (data.expiry && data.expiry > now && data.location !== null) {
            locationCache.set(username, data.location);
          }
        }
        console.log(`Loaded ${locationCache.size} cached locations (excluding null entries)`);
      }
    } catch (error) {
      console.error('Error loading cache:', error);
    }
  }

  function saveCache() {
    try {
      const cacheObj = {};
      const now = Date.now();
      const expiry = now + (CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000);

      for (const [username, location] of locationCache.entries()) {
        cacheObj[username] = {
          location: location,
          expiry: expiry,
          cachedAt: now
        };
      }

      GM_setValue(CACHE_KEY, cacheObj);
    } catch (error) {
      console.error('Error saving cache:', error);
    }
  }

  function saveCacheEntry(username, location) {
    locationCache.set(username, location);
    // Debounce saves - only save every 5 seconds
    if (!saveCache.timeout) {
      saveCache.timeout = setTimeout(() => {
        saveCache();
        saveCache.timeout = null;
      }, 5000);
    }
  }

  // ==================== 页面脚本注入 ====================
  function injectPageScript() {
    // 检查是否已经注入
    if (unsafeWindow.__twitterLocationFlagInjected) {
      console.log('Page script already injected');
      return;
    }

    try {
      // 直接在页面上下文中定义函数,避免使用 eval
      const pageContext = unsafeWindow;

      // 防止重复注入
      if (pageContext.__twitterLocationFlagInjected) {
        return;
      }
      pageContext.__twitterLocationFlagInjected = true;
      console.log('[Page Script] Twitter Location Flag page script initialized');

      // Store headers from Twitter's own API calls
      let twitterHeaders = null;
      let headersReady = false;

      function captureHeaders(headers) {
        if (!headers) return;

        const headerObj = {};
        if (headers instanceof Headers) {
          headers.forEach((value, key) => {
            headerObj[key] = value;
          });
        } else if (headers instanceof Object) {
          for (const [key, value] of Object.entries(headers)) {
            headerObj[key] = value;
          }
        }

        twitterHeaders = headerObj;
        headersReady = true;
        console.log('Captured Twitter API headers:', Object.keys(headerObj));
      }

      // Intercept fetch to capture Twitter's headers
      const originalFetch = pageContext.fetch;
      pageContext.fetch = function(...args) {
        const url = args[0];
        const options = args[1] || {};

        if (typeof url === 'string' && url.includes('x.com/i/api/graphql')) {
          if (options.headers) {
            captureHeaders(options.headers);
            console.log('Captured Twitter headers:', Object.keys(twitterHeaders || {}));
          }
        }

        return originalFetch.apply(this, args);
      };

      // Also intercept XMLHttpRequest
      const originalXHROpen = XMLHttpRequest.prototype.open;
      const originalXHRSend = XMLHttpRequest.prototype.send;

      XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        this._url = url;
        return originalXHROpen.apply(this, [method, url, ...rest]);
      };

      XMLHttpRequest.prototype.send = function(...args) {
        if (this._url && this._url.includes('x.com/i/api/graphql')) {
          const headers = {};
          if (this._headers) {
            Object.assign(headers, this._headers);
          }
          captureHeaders(headers);
        }
        return originalXHRSend.apply(this, args);
      };

      const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
      XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
        if (!this._headers) this._headers = {};
        this._headers[header] = value;
        return originalSetRequestHeader.apply(this, [header, value]);
      };

      setTimeout(() => {
        if (!headersReady) {
          console.log('No Twitter headers captured yet, using defaults');
          twitterHeaders = {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          };
          headersReady = true;
        }
      }, 3000);

      // 监听测试消息
      pageContext.addEventListener('message', function(event) {
        if (event.source !== pageContext) return;
        if (event.data && event.data.type === '__testPageScript') {
          console.log('[Page Script] Test message received, responding...');
          pageContext.postMessage({ type: '__pageScriptReady' }, '*');
        }
      });

      // Listen for fetch requests from content script via postMessage
      console.log('[Page Script] Message listener registered');
      pageContext.addEventListener('message', async function(event) {
        // 只处理来自同源的消息
        if (event.source !== pageContext) return;

        if (event.data && event.data.type === '__fetchLocation') {
          console.log('[Page Script] Received fetch request for:', event.data.screenName);
          const { screenName, requestId } = event.data;

          // 如果 headers 还没准备好,等待一下,但不要等太久
          if (!headersReady) {
            console.log('[Page Script] Headers not ready, waiting...');
            let waitCount = 0;
            while (!headersReady && waitCount < 20) {
              await new Promise(resolve => setTimeout(resolve, 100));
              waitCount++;
            }
          }

          try {
            const variables = JSON.stringify({ screenName });
            const url = `https://x.com/i/api/graphql/XRqGa7EeokUU5kppkh13EA/AboutAccountQuery?variables=${encodeURIComponent(variables)}`;

            // 构建 headers,优先使用捕获的,否则使用基本 headers
            let headers = {};
            if (twitterHeaders && Object.keys(twitterHeaders).length > 0) {
              headers = { ...twitterHeaders };
              console.log('[Page Script] Using captured headers:', Object.keys(headers));
            } else {
              // 使用基本 headers,浏览器会自动添加必要的认证 headers(通过 cookies)
              headers = {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
              };
              console.log('[Page Script] Using default headers (cookies will be included automatically)');
            }

            console.log('[Page Script] Making request to:', url);
            const response = await fetch(url, {
              method: 'GET',
              credentials: 'include', // 这很重要,确保包含 cookies
              headers: headers,
              referrer: pageContext.location.href,
              referrerPolicy: 'origin-when-cross-origin'
            });

            console.log('[Page Script] Response status:', response.status, response.statusText);

            let location = null;
            if (response.ok) {
              const data = await response.json();
              console.log(`API response for ${screenName}:`, data);
              location = data?.data?.user_result_by_screen_name?.result?.about_profile?.account_based_in || null;
              console.log(`Extracted location for ${screenName}:`, location);

              if (!location && data?.data?.user_result_by_screen_name?.result) {
                console.log('User result available but no location:', {
                  hasAboutProfile: !!data.data.user_result_by_screen_name.result.about_profile,
                  aboutProfile: data.data.user_result_by_screen_name.result.about_profile
                });
              }
            } else {
              const errorText = await response.text().catch(() => '');

              if (response.status === 429) {
                const resetTime = response.headers.get('x-rate-limit-reset');
                const remaining = response.headers.get('x-rate-limit-remaining');
                const limit = response.headers.get('x-rate-limit-limit');

                if (resetTime) {
                  const resetDate = new Date(parseInt(resetTime) * 1000);
                  const now = Date.now();
                  const waitTime = resetDate.getTime() - now;

                  console.log(`Rate limited! Limit: ${limit}, Remaining: ${remaining}`);
                  console.log(`Rate limit resets at: ${resetDate.toLocaleString()}`);
                  console.log(`Waiting ${Math.ceil(waitTime / 1000 / 60)} minutes before retrying...`);

                  pageContext.postMessage({
                    type: '__rateLimitInfo',
                    resetTime: parseInt(resetTime),
                    waitTime: Math.max(0, waitTime)
                  }, '*');
                }
              } else {
                console.log(`Twitter API error for ${screenName}:`, response.status, response.statusText, errorText.substring(0, 200));
              }
            }

            console.log('[Page Script] Sending response for', screenName, 'location:', location);
            pageContext.postMessage({
              type: '__locationResponse',
              screenName,
              location,
              requestId,
              isRateLimited: response.status === 429
            }, '*');
          } catch (error) {
            console.error('Error fetching location:', error);
            pageContext.postMessage({
              type: '__locationResponse',
              screenName,
              location: null,
              requestId
            }, '*');
          }
        }
      });

      console.log('Page script injection completed');

      // 验证注入是否成功
      setTimeout(() => {
        // 通过postMessage测试页面脚本是否响应
        const testId = 'test_' + Date.now();
        let responded = false;
        const testHandler = (event) => {
          if (event.source !== window) return;
          if (event.data && event.data.type === '__pageScriptReady') {
            responded = true;
            window.removeEventListener('message', testHandler);
            console.log('Page script is ready and responding');
          }
        };
        window.addEventListener('message', testHandler);

        // 发送测试消息
        window.postMessage({ type: '__testPageScript', testId }, '*');

        setTimeout(() => {
          if (!responded) {
            console.warn('Page script may not be responding to messages');
          }
          window.removeEventListener('message', testHandler);
        }, 2000);
      }, 500);
    } catch (error) {
      console.error('Failed to inject page script:', error);
    }

    // Listen for rate limit info from page script
    window.addEventListener('message', (event) => {
      if (event.source !== window) return;
      if (event.data && event.data.type === '__rateLimitInfo') {
        rateLimitResetTime = event.data.resetTime;
        const waitTime = event.data.waitTime;
        console.log(`Rate limit detected. Will resume requests in ${Math.ceil(waitTime / 1000 / 60)} minutes`);
      }
    });
  }

  // ==================== API请求处理 ====================
  async function processRequestQueue() {
    if (isProcessingQueue || requestQueue.length === 0) {
      return;
    }

    if (rateLimitResetTime > 0) {
      const now = Math.floor(Date.now() / 1000);
      if (now < rateLimitResetTime) {
        const waitTime = (rateLimitResetTime - now) * 1000;
        console.log(`Rate limited. Waiting ${Math.ceil(waitTime / 1000 / 60)} minutes...`);
        setTimeout(processRequestQueue, Math.min(waitTime, 60000));
        return;
      } else {
        rateLimitResetTime = 0;
      }
    }

    isProcessingQueue = true;

    while (requestQueue.length > 0 && activeRequests < MAX_CONCURRENT_REQUESTS) {
      const now = Date.now();
      const timeSinceLastRequest = now - lastRequestTime;

      if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) {
        await new Promise(resolve => setTimeout(resolve, MIN_REQUEST_INTERVAL - timeSinceLastRequest));
      }

      const { screenName, resolve, reject } = requestQueue.shift();
      activeRequests++;
      lastRequestTime = Date.now();

      makeLocationRequest(screenName)
        .then(location => {
          resolve(location);
        })
        .catch(error => {
          reject(error);
        })
        .finally(() => {
          activeRequests--;
          setTimeout(processRequestQueue, 200);
        });
    }

    isProcessingQueue = false;
  }

  function makeLocationRequest(screenName) {
    return new Promise((resolve, reject) => {
      const requestId = Date.now() + Math.random();
      let resolved = false;
      let retryCount = 0;
      const maxRetries = 3;

      let checkTimer = null;

      const handler = (event) => {
        // 接受来自页面上下文的消息(unsafeWindow)
        // 在油猴脚本中,页面脚本和content script的window是不同的
        // 所以我们需要接受来自任何源的消息,但验证消息内容
        if (!event.data || event.data.type !== '__locationResponse') {
          return;
        }

        // 调试:记录所有收到的消息
        console.log(`[Content Script] Received response for ${event.data.screenName}, requestId: ${event.data.requestId}, expected: ${requestId}`);

        // 检查是否匹配我们的请求
        if (event.data.screenName === screenName &&
            event.data.requestId === requestId) {
          if (resolved) {
            console.log(`[Content Script] Already resolved for ${screenName}, ignoring duplicate response`);
            return;
          }
          resolved = true;
          if (checkTimer) clearTimeout(checkTimer);
          window.removeEventListener('message', handler);
          const location = event.data.location;
          const isRateLimited = event.data.isRateLimited || false;

          console.log(`[Content Script] Processing response for ${screenName}, location: ${location}`);

          if (!isRateLimited) {
            saveCacheEntry(screenName, location || null);
          } else {
            console.log(`Not caching null for ${screenName} due to rate limit`);
          }

          resolve(location || null);
        }
      };
      window.addEventListener('message', handler);

      // 发送请求,如果失败则重试
      const sendRequest = () => {
        console.log(`[Content Script] Sending request for ${screenName}, requestId: ${requestId}`);
        window.postMessage({
          type: '__fetchLocation',
          screenName,
          requestId
        }, '*');
      };

      sendRequest();

      // 设置一个检查,如果3秒后还没收到响应,可能是页面脚本未准备好
      checkTimer = setTimeout(() => {
        if (!resolved && retryCount < maxRetries) {
          retryCount++;
          console.log(`Retrying request for ${screenName} (attempt ${retryCount}/${maxRetries})`);
          // 重新注入页面脚本
          injectPageScript();
          setTimeout(sendRequest, 1000);
        }
      }, 3000);

      // 超时处理
      setTimeout(() => {
        if (!resolved) {
          resolved = true;
          clearTimeout(checkTimer);
          window.removeEventListener('message', handler);
          console.log(`Request timeout for ${screenName}, not caching`);
          resolve(null);
        }
      }, 15000); // 增加到15秒
    });
  }

  async function getUserLocation(screenName) {
    if (locationCache.has(screenName)) {
      const cached = locationCache.get(screenName);
      if (cached !== null) {
        console.log(`Using cached location for ${screenName}: ${cached}`);
        return cached;
      } else {
        console.log(`Found null in cache for ${screenName}, will retry API call`);
        locationCache.delete(screenName);
      }
    }

    console.log(`Queueing API request for ${screenName}`);
    return new Promise((resolve, reject) => {
      requestQueue.push({ screenName, resolve, reject });
      processRequestQueue();
    });
  }

  // ==================== DOM处理 ====================
  function extractUsername(element) {
    const usernameElement = element.querySelector('[data-testid="UserName"], [data-testid="User-Name"]');
    if (usernameElement) {
      const links = usernameElement.querySelectorAll('a[href^="/"]');
      for (const link of links) {
        const href = link.getAttribute('href');
        const match = href.match(/^\/([^\/\?]+)/);
        if (match && match[1]) {
          const username = match[1];
          const excludedRoutes = ['home', 'explore', 'notifications', 'messages', 'i', 'compose', 'search', 'settings', 'bookmarks', 'lists', 'communities'];
          if (!excludedRoutes.includes(username) &&
              !username.startsWith('hashtag') &&
              !username.startsWith('search') &&
              username.length > 0 &&
              username.length < 20) {
            return username;
          }
        }
      }
    }

    const allLinks = element.querySelectorAll('a[href^="/"]');
    const seenUsernames = new Set();

    for (const link of allLinks) {
      const href = link.getAttribute('href');
      if (!href) continue;

      const match = href.match(/^\/([^\/\?]+)/);
      if (!match || !match[1]) continue;

      const potentialUsername = match[1];

      if (seenUsernames.has(potentialUsername)) continue;
      seenUsernames.add(potentialUsername);

      const excludedRoutes = ['home', 'explore', 'notifications', 'messages', 'i', 'compose', 'search', 'settings', 'bookmarks', 'lists', 'communities', 'hashtag'];
      if (excludedRoutes.some(route => potentialUsername === route || potentialUsername.startsWith(route))) {
        continue;
      }

      if (potentialUsername.includes('status') || potentialUsername.match(/^\d+$/)) {
        continue;
      }

      const text = link.textContent?.trim() || '';
      const linkText = text.toLowerCase();
      const usernameLower = potentialUsername.toLowerCase();

      if (text.startsWith('@')) {
        return potentialUsername;
      }

      if (linkText === usernameLower || linkText === `@${usernameLower}`) {
        return potentialUsername;
      }

      const parent = link.closest('[data-testid="UserName"], [data-testid="User-Name"]');
      if (parent) {
        if (potentialUsername.length > 0 && potentialUsername.length < 20 && !potentialUsername.includes('/')) {
          return potentialUsername;
        }
      }

      if (text && text.trim().startsWith('@')) {
        const atUsername = text.trim().substring(1);
        if (atUsername === potentialUsername) {
          return potentialUsername;
        }
      }
    }

    const textContent = element.textContent || '';
    const atMentionMatches = textContent.matchAll(/@([a-zA-Z0-9_]+)/g);
    for (const match of atMentionMatches) {
      const username = match[1];
      const link = element.querySelector(`a[href="/${username}"], a[href^="/${username}?"]`);
      if (link) {
        const isInUserNameContainer = link.closest('[data-testid="UserName"], [data-testid="User-Name"]');
        if (isInUserNameContainer) {
          return username;
        }
      }
    }

    return null;
  }

  function findHandleSection(container, screenName) {
    return Array.from(container.querySelectorAll('div')).find(div => {
      const link = div.querySelector(`a[href="/${screenName}"]`);
      if (link) {
        const text = link.textContent?.trim();
        return text === `@${screenName}`;
      }
      return false;
    });
  }

  function createLoadingShimmer() {
    const shimmer = document.createElement('span');
    shimmer.setAttribute('data-twitter-flag-shimmer', 'true');
    shimmer.style.display = 'inline-block';
    shimmer.style.width = '20px';
    shimmer.style.height = '16px';
    shimmer.style.marginLeft = '4px';
    shimmer.style.marginRight = '4px';
    shimmer.style.verticalAlign = 'middle';
    shimmer.style.borderRadius = '2px';
    shimmer.style.background = 'linear-gradient(90deg, rgba(113, 118, 123, 0.2) 25%, rgba(113, 118, 123, 0.4) 50%, rgba(113, 118, 123, 0.2) 75%)';
    shimmer.style.backgroundSize = '200% 100%';
    shimmer.style.animation = 'shimmer 1.5s infinite';

    if (!document.getElementById('twitter-flag-shimmer-style')) {
      const style = document.createElement('style');
      style.id = 'twitter-flag-shimmer-style';
      style.textContent = `
        @keyframes shimmer {
          0% {
            background-position: -200% 0;
          }
          100% {
            background-position: 200% 0;
          }
        }
      `;
      document.head.appendChild(style);
    }

    return shimmer;
  }

  async function addFlagToUsername(usernameElement, screenName) {
    if (usernameElement.dataset.flagAdded === 'true') {
      return;
    }

    if (processingUsernames.has(screenName)) {
      await new Promise(resolve => setTimeout(resolve, 500));
      if (usernameElement.dataset.flagAdded === 'true') {
        return;
      }
      usernameElement.dataset.flagAdded = 'waiting';
      return;
    }

    usernameElement.dataset.flagAdded = 'processing';
    processingUsernames.add(screenName);

    const userNameContainer = usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]');

    const shimmerSpan = createLoadingShimmer();
    let shimmerInserted = false;

    if (userNameContainer) {
      const handleSection = findHandleSection(userNameContainer, screenName);
      if (handleSection && handleSection.parentNode) {
        try {
          handleSection.parentNode.insertBefore(shimmerSpan, handleSection);
          shimmerInserted = true;
        } catch (e) {
          try {
            userNameContainer.appendChild(shimmerSpan);
            shimmerInserted = true;
          } catch (e2) {
            console.log('Failed to insert shimmer');
          }
        }
      } else {
        try {
          userNameContainer.appendChild(shimmerSpan);
          shimmerInserted = true;
        } catch (e) {
          console.log('Failed to insert shimmer');
        }
      }
    }

    try {
      console.log(`Processing flag for ${screenName}...`);

      const location = await getUserLocation(screenName);
      console.log(`Location for ${screenName}:`, location);

      if (shimmerInserted && shimmerSpan.parentNode) {
        shimmerSpan.remove();
      }

      if (!location) {
        console.log(`No location found for ${screenName}, marking as failed`);
        usernameElement.dataset.flagAdded = 'failed';
        return;
      }

      const flag = getCountryFlag(location);
      if (!flag) {
        console.log(`No flag found for location: ${location}`);
        if (shimmerInserted && shimmerSpan.parentNode) {
          shimmerSpan.remove();
        }
        usernameElement.dataset.flagAdded = 'failed';
        return;
      }

      console.log(`Found flag ${flag} for ${screenName} (${location})`);

      let usernameLink = null;
      const containerForLink = userNameContainer || usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]');

      if (containerForLink) {
        const containerLinks = containerForLink.querySelectorAll('a[href^="/"]');
        for (const link of containerLinks) {
          const text = link.textContent?.trim();
          const href = link.getAttribute('href');
          const match = href.match(/^\/([^\/\?]+)/);

          if (match && match[1] === screenName) {
            if (text === `@${screenName}` || text === screenName) {
              usernameLink = link;
              break;
            }
          }
        }
      }

      if (!usernameLink && containerForLink) {
        const containerLinks = containerForLink.querySelectorAll('a[href^="/"]');
        for (const link of containerLinks) {
          const text = link.textContent?.trim();
          if (text === `@${screenName}`) {
            usernameLink = link;
            break;
          }
        }
      }

      if (!usernameLink) {
        const links = usernameElement.querySelectorAll('a[href^="/"]');
        for (const link of links) {
          const href = link.getAttribute('href');
          const text = link.textContent?.trim();
          if ((href === `/${screenName}` || href.startsWith(`/${screenName}?`)) &&
              (text === `@${screenName}` || text === screenName)) {
            usernameLink = link;
            break;
          }
        }
      }

      if (!usernameLink) {
        const links = usernameElement.querySelectorAll('a[href^="/"]');
        for (const link of links) {
          const href = link.getAttribute('href');
          const match = href.match(/^\/([^\/\?]+)/);
          if (match && match[1] === screenName) {
            const hasVerificationBadge = link.closest('[data-testid="User-Name"]')?.querySelector('[data-testid="icon-verified"]');
            if (!hasVerificationBadge || link.textContent?.trim() === `@${screenName}`) {
              usernameLink = link;
              break;
            }
          }
        }
      }

      if (!usernameLink) {
        console.error(`Could not find username link for ${screenName}`);
        if (shimmerInserted && shimmerSpan.parentNode) {
          shimmerSpan.remove();
        }
        usernameElement.dataset.flagAdded = 'failed';
        return;
      }

      console.log(`Found username link for ${screenName}:`, usernameLink.href, usernameLink.textContent?.trim());

      const existingFlag = usernameElement.querySelector('[data-twitter-flag]');
      if (existingFlag) {
        if (shimmerInserted && shimmerSpan.parentNode) {
          shimmerSpan.remove();
        }
        usernameElement.dataset.flagAdded = 'true';
        return;
      }

      const flagSpan = document.createElement('span');
      flagSpan.textContent = ` ${flag}`;
      flagSpan.setAttribute('data-twitter-flag', 'true');
      flagSpan.style.marginLeft = '4px';
      flagSpan.style.marginRight = '4px';
      flagSpan.style.display = 'inline';
      flagSpan.style.color = 'inherit';
      flagSpan.style.verticalAlign = 'middle';

      const containerForFlag = userNameContainer || usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]');

      if (!containerForFlag) {
        console.error(`Could not find UserName container for ${screenName}`);
        if (shimmerInserted && shimmerSpan.parentNode) {
          shimmerSpan.remove();
        }
        usernameElement.dataset.flagAdded = 'failed';
        return;
      }

      const verificationBadge = containerForFlag.querySelector('[data-testid="icon-verified"]');
      const handleSection = findHandleSection(containerForFlag, screenName);

      let inserted = false;

      if (handleSection && handleSection.parentNode === containerForFlag) {
        try {
          containerForFlag.insertBefore(flagSpan, handleSection);
          inserted = true;
          console.log(`✓ Inserted flag before handle section for ${screenName}`);
        } catch (e) {
          console.log('Failed to insert before handle section:', e);
        }
      }

      if (!inserted && handleSection && handleSection.parentNode) {
        try {
          const handleParent = handleSection.parentNode;
          if (handleParent !== containerForFlag && handleParent.parentNode) {
            handleParent.parentNode.insertBefore(flagSpan, handleParent);
            inserted = true;
            console.log(`✓ Inserted flag before handle parent for ${screenName}`);
          } else if (handleParent === containerForFlag) {
            containerForFlag.insertBefore(flagSpan, handleSection);
            inserted = true;
            console.log(`✓ Inserted flag before handle section (direct child) for ${screenName}`);
          }
        } catch (e) {
          console.log('Failed to insert before handle parent:', e);
        }
      }

      if (!inserted && handleSection) {
        try {
          const displayNameLink = containerForFlag.querySelector('a[href^="/"]');
          if (displayNameLink) {
            const displayNameContainer = displayNameLink.closest('div');
            if (displayNameContainer && displayNameContainer.parentNode) {
              if (displayNameContainer.parentNode === handleSection.parentNode) {
                displayNameContainer.parentNode.insertBefore(flagSpan, handleSection);
                inserted = true;
                console.log(`✓ Inserted flag between display name and handle (siblings) for ${screenName}`);
              } else {
                displayNameContainer.parentNode.insertBefore(flagSpan, displayNameContainer.nextSibling);
                inserted = true;
                console.log(`✓ Inserted flag after display name container for ${screenName}`);
              }
            }
          }
        } catch (e) {
          console.log('Failed to insert after display name:', e);
        }
      }

      if (!inserted) {
        try {
          containerForFlag.appendChild(flagSpan);
          inserted = true;
          console.log(`✓ Inserted flag at end of UserName container for ${screenName}`);
        } catch (e) {
          console.error('Failed to append flag to User-Name container:', e);
        }
      }

      if (inserted) {
        usernameElement.dataset.flagAdded = 'true';
        console.log(`✓ Successfully added flag ${flag} for ${screenName} (${location})`);

        const waitingContainers = document.querySelectorAll(`[data-flag-added="waiting"]`);
        waitingContainers.forEach(container => {
          const waitingUsername = extractUsername(container);
          if (waitingUsername === screenName) {
            addFlagToUsername(container, screenName).catch(() => {});
          }
        });
      } else {
        console.error(`✗ Failed to insert flag for ${screenName} - tried all strategies`);
        if (shimmerInserted && shimmerSpan.parentNode) {
          shimmerSpan.remove();
        }
        usernameElement.dataset.flagAdded = 'failed';
      }
    } catch (error) {
      console.error(`Error processing flag for ${screenName}:`, error);
      if (shimmerInserted && shimmerSpan.parentNode) {
        shimmerSpan.remove();
      }
      usernameElement.dataset.flagAdded = 'failed';
    } finally {
      processingUsernames.delete(screenName);
    }
  }

  function removeAllFlags() {
    const flags = document.querySelectorAll('[data-twitter-flag]');
    flags.forEach(flag => flag.remove());

    const shimmers = document.querySelectorAll('[data-twitter-flag-shimmer]');
    shimmers.forEach(shimmer => shimmer.remove());

    const containers = document.querySelectorAll('[data-flag-added]');
    containers.forEach(container => {
      delete container.dataset.flagAdded;
    });

    console.log('Removed all flags');
  }

  async function processUsernames() {
    if (!extensionEnabled) {
      return;
    }

    // 防止重复处理
    if (isProcessing) {
      return;
    }

    isProcessing = true;

    try {
      const containers = document.querySelectorAll('article[data-testid="tweet"], [data-testid="UserCell"], [data-testid="User-Names"], [data-testid="User-Name"]');

      console.log(`Processing ${containers.length} containers for usernames`);

      let foundCount = 0;
      let processedCount = 0;
      let skippedCount = 0;

      for (const container of containers) {
        const screenName = extractUsername(container);
        if (screenName) {
          foundCount++;
          const status = container.dataset.flagAdded;
          if (!status || status === 'failed') {
            processedCount++;
            addFlagToUsername(container, screenName).catch(err => {
              console.error(`Error processing ${screenName}:`, err);
              container.dataset.flagAdded = 'failed';
            });
          } else {
            skippedCount++;
          }
        }
      }

      if (foundCount > 0 && processedCount > 0) {
        console.log(`Found ${foundCount} usernames, processing ${processedCount} new ones, skipped ${skippedCount} already processed`);
      }
    } finally {
      isProcessing = false;
    }
  }

  // 防抖版本的processUsernames
  function debouncedProcessUsernames() {
    if (processTimer) {
      clearTimeout(processTimer);
    }
    processTimer = setTimeout(() => {
      processUsernames();
    }, 1000);
  }

  function initObserver() {
    if (observer) {
      observer.disconnect();
    }

    observer = new MutationObserver((mutations) => {
      if (!extensionEnabled) {
        return;
      }

      let shouldProcess = false;
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          // 只处理实际的内容节点,忽略脚本和样式
          if (node.nodeType === 1 &&
              (node.tagName === 'ARTICLE' ||
               node.querySelector?.('article[data-testid="tweet"], [data-testid="UserCell"], [data-testid="User-Name"]'))) {
            shouldProcess = true;
            break;
          }
        }
        if (shouldProcess) break;
      }

      if (shouldProcess) {
        debouncedProcessUsernames();
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  // ==================== 初始化 ====================
  async function init() {
    console.log('Twitter Location Flag userscript initialized');

    loadEnabledState();
    loadCache();

    if (!extensionEnabled) {
      console.log('Extension is disabled');
      return;
    }

    injectPageScript();

    setTimeout(() => {
      processUsernames();
    }, 2000);

    initObserver();

    let lastUrl = location.href;
    new MutationObserver(() => {
      const url = location.href;
      if (url !== lastUrl) {
        lastUrl = url;
        console.log('Page navigation detected, reprocessing usernames');
        // 重新注入页面脚本(SPA导航可能丢失)
        injectPageScript();
        setTimeout(() => {
          processUsernames();
        }, 2000);
      }
    }).observe(document, { subtree: true, childList: true });

    setInterval(saveCache, 30000);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();