Twitter/X Media Batch Downloader

Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.

目前為 2025-03-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Twitter/X Media Batch Downloader
// @description  Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
// @icon         https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg
// @version      2.6
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @connect      gallerydl.vercel.app
// @connect      pbs.twimg.com
// @connect      video.twimg.com
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// ==/UserScript==

(function() {
  'use strict';

  const defaultSettings = {
      authToken: '',
      batchEnabled: true,
      batchSize: 100,
      timelineType: 'media',
      mediaType: 'all',
      concurrentDownloads: 25,
      cacheDuration: 360
  };

  function getSettings() {
      return {
          authToken: GM_getValue('authToken', defaultSettings.authToken),
          batchEnabled: GM_getValue('batchEnabled', defaultSettings.batchEnabled),
          batchSize: GM_getValue('batchSize', defaultSettings.batchSize),
          timelineType: GM_getValue('timelineType', defaultSettings.timelineType),
          mediaType: GM_getValue('mediaType', defaultSettings.mediaType),
          concurrentDownloads: GM_getValue('concurrentDownloads', defaultSettings.concurrentDownloads),
          cacheDuration: GM_getValue('cacheDuration', defaultSettings.cacheDuration)
      };
  }

  function saveSettings(settings) {
      GM_setValue('authToken', settings.authToken);
      GM_setValue('batchEnabled', settings.batchEnabled);
      GM_setValue('batchSize', settings.batchSize);
      GM_setValue('timelineType', settings.timelineType);
      GM_setValue('mediaType', settings.mediaType);
      GM_setValue('concurrentDownloads', settings.concurrentDownloads);
      GM_setValue('cacheDuration', settings.cacheDuration);
  }

  function formatNumber(num) {
      return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  }

  const cacheManager = {
      set: function(key, data) {
          const settings = getSettings();
          const cacheItem = {
              data: data,
              timestamp: Date.now(),
              expiry: Date.now() + (settings.cacheDuration * 60 * 1000)
          };
          localStorage.setItem(`twitter_dl_${key}`, JSON.stringify(cacheItem));
      },
      
      get: function(key) {
          const cacheItem = localStorage.getItem(`twitter_dl_${key}`);
          if (!cacheItem) return null;
          
          try {
              const parsed = JSON.parse(cacheItem);
              if (Date.now() > parsed.expiry) {
                  localStorage.removeItem(`twitter_dl_${key}`);
                  return null;
              }
              return parsed.data;
          } catch (e) {
              localStorage.removeItem(`twitter_dl_${key}`);
              return null;
          }
      },
      
      clear: function() {
          const keysToRemove = [];
          for (let i = 0; i < localStorage.length; i++) {
              const key = localStorage.key(i);
              if (key.startsWith('twitter_dl_')) {
                  keysToRemove.push(key);
              }
          }
          
          keysToRemove.forEach(key => localStorage.removeItem(key));
      }
  };

  function createDownloadIcon() {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
      svg.setAttribute("viewBox", "0 0 512 512");
      svg.setAttribute("width", "18");
      svg.setAttribute("height", "18");
      svg.style.verticalAlign = "middle";
      svg.style.cursor = "pointer";

      const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
      const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
      style.textContent = ".fa-secondary{opacity:.4}";
      defs.appendChild(style);
      svg.appendChild(defs);

      const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
      secondaryPath.setAttribute("class", "fa-secondary");
      secondaryPath.setAttribute("fill", "currentColor");
      secondaryPath.setAttribute(
          "d",
          "M0 256C0 397.4 114.6 512 256 512s256-114.6 256-256c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 106-86 192-192 192S64 362 64 256c0-17.7-14.3-32-32-32s-32 14.3-32 32z"
      );
      svg.appendChild(secondaryPath);

      const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
      primaryPath.setAttribute("class", "fa-primary");
      primaryPath.setAttribute("fill", "currentColor");
      primaryPath.setAttribute(
          "d",
          "M390.6 185.4c12.5 12.5 12.5 32.8 0 45.3l-112 112c-12.5 12.5-32.8 12.5-45.3 0l-112-112c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 242.7 224 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 57.4-57.4c12.5-12.5 32.8-12.5 45.3 0z"
      );
      svg.appendChild(primaryPath);

      return svg;
  }

  function createGithubIcon() {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
      svg.setAttribute("viewBox", "0 0 24 24");
      svg.setAttribute("width", "24");
      svg.setAttribute("height", "24");
      svg.style.verticalAlign = "middle";
      svg.style.marginRight = "8px";
      
      const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
      path.setAttribute("fill", "currentColor");
      path.setAttribute(
          "d",
          "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
      );
      svg.appendChild(path);
      
      return svg;
  }

  function createConfirmDialog(message, onConfirm, onCancel) {
      const overlay = document.createElement('div');
      overlay.style.cssText = `
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: rgba(0, 0, 0, 0.7);
          display: flex;
          justify-content: center;
          align-items: center;
          z-index: 10001;
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      `;
      
      const dialog = document.createElement('div');
      dialog.style.cssText = `
          background-color: #15202b;
          color: white;
          border-radius: 16px;
          width: 300px;
          max-width: 90%;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
          overflow: hidden;
      `;
      
      const header = document.createElement('div');
      header.style.cssText = `
          padding: 16px;
          border-bottom: 1px solid #334155;
          font-weight: bold;
          font-size: 16px;
          text-align: center;
      `;
      header.textContent = 'Confirmation';
      
      const content = document.createElement('div');
      content.style.cssText = `
          padding: 16px;
          text-align: center;
      `;
      content.textContent = message;
      
      const buttons = document.createElement('div');
      buttons.style.cssText = `
          display: flex;
          padding: 16px;
          border-top: 1px solid #334155;
      `;
      
      const cancelButton = document.createElement('button');
      cancelButton.style.cssText = `
          flex: 1;
          background-color: #64748b;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          margin-right: 8px;
          font-weight: bold;
          cursor: pointer;
          text-align: center;
          transition: background-color 0.2s;
      `;
      cancelButton.textContent = 'Cancel';
      cancelButton.addEventListener('mouseenter', () => {
          cancelButton.style.backgroundColor = '#475569';
      });
      cancelButton.addEventListener('mouseleave', () => {
          cancelButton.style.backgroundColor = '#64748b';
      });
      cancelButton.onclick = () => {
          document.body.removeChild(overlay);
          if (onCancel) onCancel();
      };
      
      const confirmButton = document.createElement('button');
      confirmButton.style.cssText = `
          flex: 1;
          background-color: #ef4444;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          text-align: center;
          transition: background-color 0.2s;
      `;
      confirmButton.textContent = 'Confirm';
      confirmButton.addEventListener('mouseenter', () => {
          confirmButton.style.backgroundColor = '#dc2626';
      });
      confirmButton.addEventListener('mouseleave', () => {
          confirmButton.style.backgroundColor = '#ef4444';
      });
      confirmButton.onclick = () => {
          document.body.removeChild(overlay);
          if (onConfirm) onConfirm();
      };
      
      buttons.appendChild(cancelButton);
      buttons.appendChild(confirmButton);
      
      dialog.appendChild(header);
      dialog.appendChild(content);
      dialog.appendChild(buttons);
      overlay.appendChild(dialog);
      
      document.body.appendChild(overlay);
  }

  function extractUsername() {
      const pathParts = window.location.pathname.split('/').filter(part => part);
      
      if (pathParts.length > 0) {
          return pathParts[0];
      }
      
      return null;
  }

  function formatDate(dateString) {
      const date = new Date(dateString);
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      const hours = String(date.getHours()).padStart(2, '0');
      const minutes = String(date.getMinutes()).padStart(2, '0');
      const seconds = String(date.getSeconds()).padStart(2, '0');
      
      return `${year}${month}${day}_${hours}${minutes}${seconds}`;
  }

  function getCurrentTimestamp() {
      const now = new Date();
      const year = now.getFullYear();
      const month = String(now.getMonth() + 1).padStart(2, '0');
      const day = String(now.getDate()).padStart(2, '0');
      const hours = String(now.getHours()).padStart(2, '0');
      const minutes = String(now.getMinutes()).padStart(2, '0');
      const seconds = String(now.getSeconds()).padStart(2, '0');
      
      return `${year}${month}${day}_${hours}${minutes}${seconds}`;
  }

  function fetchData(url) {
      return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
              method: 'GET',
              url: url,
              responseType: 'json',
              onload: function(response) {
                  if (response.status >= 200 && response.status < 300) {
                      resolve(response.response);
                  } else {
                      reject(new Error(`Request failed with status ${response.status}`));
                  }
              },
              onerror: function() {
                  reject(new Error('Network error'));
              }
          });
      });
  }

  function fetchBinary(url) {
      return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
              method: 'GET',
              url: url,
              responseType: 'blob',
              onload: function(response) {
                  if (response.status >= 200 && response.status < 300) {
                      resolve(response.response);
                  } else {
                      reject(new Error(`Request failed with status ${response.status}`));
                  }
              },
              onerror: function() {
                  reject(new Error('Network error'));
              }
          });
      });
  }

  function getMediaTypeLabel(mediaType) {
      switch(mediaType) {
          case 'image': return 'Image';
          case 'video': return 'Video';
          case 'gif': return 'GIF';
          default: return 'Media';
      }
  }

  function createModal(username) {
      const existingModal = document.getElementById('media-downloader-modal');
      if (existingModal) {
          existingModal.remove();
      }

      const settings = getSettings();
      
      const modal = document.createElement('div');
      modal.id = 'media-downloader-modal';
      modal.style.cssText = `
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: rgba(0, 0, 0, 0.7);
          display: flex;
          justify-content: center;
          align-items: center;
          z-index: 10000;
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      `;

      const modalContent = document.createElement('div');
      modalContent.style.cssText = `
          background-color: #15202b;
          color: white;
          border-radius: 16px;
          width: 500px;
          max-width: 90%;
          max-height: 90vh;
          overflow-y: auto;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      `;

      const header = document.createElement('div');
      header.style.cssText = `
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 16px;
          border-bottom: 1px solid #334155;
      `;

      const title = document.createElement('h2');
      title.textContent = `Download Media: ${username}`;
      title.style.cssText = `
          margin: 0;
          font-size: 18px;
          font-weight: bold;
      `;

      const closeButton = document.createElement('button');
      closeButton.innerHTML = '&times;';
      closeButton.style.cssText = `
          background: none;
          border: none;
          color: white;
          font-size: 24px;
          cursor: pointer;
          padding: 0;
          line-height: 1;
          transition: color 0.2s;
      `;
      closeButton.addEventListener('mouseenter', () => {
          closeButton.style.color = '#0ea5e9';
      });
      closeButton.addEventListener('mouseleave', () => {
          closeButton.style.color = 'white';
      });
      closeButton.onclick = () => modal.remove();

      header.appendChild(title);
      header.appendChild(closeButton);

      const tabs = document.createElement('div');
      tabs.style.cssText = `
          display: flex;
          border-bottom: 1px solid #334155;
      `;

      const mainTab = document.createElement('div');
      mainTab.textContent = 'Main';
      mainTab.className = 'active-tab';
      mainTab.style.cssText = `
          padding: 12px 16px;
          cursor: pointer;
          flex: 1;
          text-align: center;
          border-bottom: 2px solid #0ea5e9;
      `;

      const settingsTab = document.createElement('div');
      settingsTab.textContent = 'Settings';
      settingsTab.style.cssText = `
          padding: 12px 16px;
          cursor: pointer;
          flex: 1;
          text-align: center;
          color: #8899a6;
      `;

      tabs.appendChild(mainTab);
      tabs.appendChild(settingsTab);

      const mainContent = document.createElement('div');
      mainContent.style.cssText = `
          padding: 16px;
      `;

      const settingsContent = document.createElement('div');
      settingsContent.style.cssText = `
          padding: 16px;
          display: none;
      `;

      const fetchButton = document.createElement('button');
      fetchButton.textContent = 'Fetch Media';
      fetchButton.style.cssText = `
          background-color: #0ea5e9;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          margin-bottom: 16px;
          width: 100%;
          text-align: center;
          transition: background-color 0.2s;
      `;
      fetchButton.addEventListener('mouseenter', () => {
          fetchButton.style.backgroundColor = '#0284c7';
      });
      fetchButton.addEventListener('mouseleave', () => {
          fetchButton.style.backgroundColor = '#0ea5e9';
      });

      const infoContainer = document.createElement('div');
      infoContainer.style.cssText = `
          background-color: #192734;
          border-radius: 8px;
          padding: 12px;
          margin-bottom: 16px;
          display: none;
      `;

      const buttonContainer = document.createElement('div');
      buttonContainer.style.cssText = `
          display: flex;
          gap: 8px;
          margin-bottom: 16px;
      `;

      const downloadCurrentButton = document.createElement('button');
      downloadCurrentButton.textContent = 'Download Current Batch';
      downloadCurrentButton.style.cssText = `
          background-color: #0ea5e9;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          flex: 1;
          display: none;
          text-align: center;
          transition: background-color 0.2s;
      `;
      downloadCurrentButton.addEventListener('mouseenter', () => {
          downloadCurrentButton.style.backgroundColor = '#0284c7';
      });
      downloadCurrentButton.addEventListener('mouseleave', () => {
          downloadCurrentButton.style.backgroundColor = '#0ea5e9';
      });

      const downloadAllButton = document.createElement('button');
      downloadAllButton.textContent = 'Download All Batches';
      downloadAllButton.style.cssText = `
          background-color: #22c55e;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          flex: 1;
          display: none;
          text-align: center;
          transition: background-color 0.2s;
      `;
      downloadAllButton.addEventListener('mouseenter', () => {
          downloadAllButton.style.backgroundColor = '#16a34a';
      });
      downloadAllButton.addEventListener('mouseleave', () => {
          downloadAllButton.style.backgroundColor = '#22c55e';
      });

      buttonContainer.appendChild(downloadCurrentButton);
      buttonContainer.appendChild(downloadAllButton);

      const nextBatchButton = document.createElement('button');
      nextBatchButton.textContent = 'Next Batch';
      nextBatchButton.style.cssText = `
          background-color: #0ea5e9;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          margin-bottom: 16px;
          width: 100%;
          display: none;
          text-align: center;
          transition: background-color 0.2s;
      `;
      nextBatchButton.addEventListener('mouseenter', () => {
          nextBatchButton.style.backgroundColor = '#0284c7';
      });
      nextBatchButton.addEventListener('mouseleave', () => {
          nextBatchButton.style.backgroundColor = '#0ea5e9';
      });

      const autoBatchButton = document.createElement('button');
      autoBatchButton.textContent = 'Auto Batch';
      autoBatchButton.style.cssText = `
          background-color: #6366f1;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          margin-bottom: 16px;
          width: 100%;
          display: none;
          text-align: center;
          transition: background-color 0.2s;
      `;
      autoBatchButton.addEventListener('mouseenter', () => {
          autoBatchButton.style.backgroundColor = '#4f46e5';
      });
      autoBatchButton.addEventListener('mouseleave', () => {
          autoBatchButton.style.backgroundColor = '#6366f1';
      });

      const stopBatchButton = document.createElement('button');
      stopBatchButton.textContent = 'Stop Batch';
      stopBatchButton.style.cssText = `
          background-color: #ef4444;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          margin-bottom: 16px;
          width: 100%;
          display: none;
          text-align: center;
          transition: background-color 0.2s;
      `;
      stopBatchButton.addEventListener('mouseenter', () => {
          stopBatchButton.style.backgroundColor = '#dc2626';
      });
      stopBatchButton.addEventListener('mouseleave', () => {
          stopBatchButton.style.backgroundColor = '#ef4444';
      });

      const progressContainer = document.createElement('div');
      progressContainer.style.cssText = `
          margin-top: 16px;
          display: none;
      `;

      const progressText = document.createElement('div');
      progressText.style.cssText = `
          margin-bottom: 8px;
          font-size: 14px;
          text-align: center;
      `;
      progressText.textContent = 'Downloading...';

      const progressBar = document.createElement('div');
      progressBar.style.cssText = `
          width: 100%;
          height: 8px;
          background-color: #192734;
          border-radius: 4px;
          overflow: hidden;
      `;

      const progressFill = document.createElement('div');
      progressFill.style.cssText = `
          height: 100%;
          width: 0%;
          background-color: #0ea5e9;
          transition: width 0.3s;
      `;

      progressBar.appendChild(progressFill);
      progressContainer.appendChild(progressText);
      progressContainer.appendChild(progressBar);

      mainContent.appendChild(fetchButton);
      mainContent.appendChild(infoContainer);
      mainContent.appendChild(buttonContainer);
      mainContent.appendChild(nextBatchButton);
      mainContent.appendChild(autoBatchButton);
      mainContent.appendChild(stopBatchButton);
      mainContent.appendChild(progressContainer);

      const settingsForm = document.createElement('div');
      settingsForm.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 16px;
      `;

      const tokenGroup = document.createElement('div');
      tokenGroup.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 8px;
      `;

      const tokenLabel = document.createElement('label');
      tokenLabel.textContent = 'Auth Token:';
      tokenLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;

      const tokenInputContainer = document.createElement('div');
      tokenInputContainer.style.cssText = `
          position: relative;
          display: flex;
          align-items: center;
      `;

      const tokenInput = document.createElement('input');
      tokenInput.type = 'text';
      tokenInput.value = settings.authToken;
      tokenInput.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;
      tokenInput.addEventListener('input', () => {
          const newSettings = getSettings();
          newSettings.authToken = tokenInput.value;
          saveSettings(newSettings);
          tokenClearButton.style.display = tokenInput.value ? 'block' : 'none';
      });

      const tokenClearButton = document.createElement('button');
      tokenClearButton.innerHTML = '&times;';
      tokenClearButton.style.cssText = `
          position: absolute;
          right: 8px;
          background: none;
          border: none;
          color: #8899a6;
          font-size: 18px;
          cursor: pointer;
          padding: 0;
          display: ${settings.authToken ? 'block' : 'none'};
      `;
      tokenClearButton.addEventListener('click', () => {
          tokenInput.value = '';
          const newSettings = getSettings();
          newSettings.authToken = '';
          saveSettings(newSettings);
          tokenClearButton.style.display = 'none';
      });

      tokenInputContainer.appendChild(tokenInput);
      tokenInputContainer.appendChild(tokenClearButton);
      tokenGroup.appendChild(tokenLabel);
      tokenGroup.appendChild(tokenInputContainer);

      const batchGroup = document.createElement('div');
      batchGroup.style.cssText = `
          display: flex;
          align-items: center;
          gap: 8px;
      `;

      const batchLabel = document.createElement('label');
      batchLabel.textContent = 'Batch:';
      batchLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
          flex: 1;
      `;

      const batchToggle = document.createElement('div');
      batchToggle.style.cssText = `
          position: relative;
          width: 50px;
          height: 24px;
          background-color: ${settings.batchEnabled ? '#0ea5e9' : '#334155'};
          border-radius: 12px;
          cursor: pointer;
          transition: background-color 0.3s;
      `;

      const batchToggleHandle = document.createElement('div');
      batchToggleHandle.style.cssText = `
          position: absolute;
          top: 2px;
          left: ${settings.batchEnabled ? '28px' : '2px'};
          width: 20px;
          height: 20px;
          background-color: white;
          border-radius: 50%;
          transition: left 0.3s;
      `;

      batchToggle.appendChild(batchToggleHandle);
      batchToggle.addEventListener('click', () => {
          const newSettings = getSettings();
          newSettings.batchEnabled = !newSettings.batchEnabled;
          saveSettings(newSettings);
          batchToggle.style.backgroundColor = newSettings.batchEnabled ? '#0ea5e9' : '#334155';
          batchToggleHandle.style.left = newSettings.batchEnabled ? '28px' : '2px';
          batchSizeGroup.style.display = newSettings.batchEnabled ? 'flex' : 'none';
      });

      batchGroup.appendChild(batchLabel);
      batchGroup.appendChild(batchToggle);

      const batchSizeGroup = document.createElement('div');
      batchSizeGroup.style.cssText = `
          display: ${settings.batchEnabled ? 'flex' : 'none'};
          flex-direction: column;
          gap: 8px;
      `;

      const batchSizeLabel = document.createElement('label');
      batchSizeLabel.textContent = 'Batch Size:';
      batchSizeLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;

      const batchSizeSelect = document.createElement('select');
      batchSizeSelect.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;

      const batchSizes = [50, 100, 150, 200];
      batchSizes.forEach(size => {
          const option = document.createElement('option');
          option.value = size;
          option.textContent = size;
          option.selected = size === settings.batchSize;
          batchSizeSelect.appendChild(option);
      });

      batchSizeSelect.addEventListener('change', () => {
          const newSettings = getSettings();
          newSettings.batchSize = parseInt(batchSizeSelect.value);
          saveSettings(newSettings);
      });

      batchSizeGroup.appendChild(batchSizeLabel);
      batchSizeGroup.appendChild(batchSizeSelect);

      const timelineTypeGroup = document.createElement('div');
      timelineTypeGroup.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 8px;
      `;

      const timelineTypeLabel = document.createElement('label');
      timelineTypeLabel.textContent = 'Timeline Type:';
      timelineTypeLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;

      const timelineTypeSelect = document.createElement('select');
      timelineTypeSelect.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;

      const timelineTypes = [
          { value: 'media', label: 'Media' },
          { value: 'timeline', label: 'Post' },
          { value: 'tweets', label: 'Tweets' },
          { value: 'with_replies', label: 'Replies' }
      ];

      timelineTypes.forEach(type => {
          const option = document.createElement('option');
          option.value = type.value;
          option.textContent = type.label;
          option.selected = type.value === settings.timelineType;
          timelineTypeSelect.appendChild(option);
      });

      timelineTypeSelect.addEventListener('change', () => {
          const newSettings = getSettings();
          newSettings.timelineType = timelineTypeSelect.value;
          saveSettings(newSettings);
      });

      timelineTypeGroup.appendChild(timelineTypeLabel);
      timelineTypeGroup.appendChild(timelineTypeSelect);

      const mediaTypeGroup = document.createElement('div');
      mediaTypeGroup.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 8px;
      `;

      const mediaTypeLabel = document.createElement('label');
      mediaTypeLabel.textContent = 'Media Type:';
      mediaTypeLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;

      const mediaTypeSelect = document.createElement('select');
      mediaTypeSelect.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;

      const mediaTypes = [
          { value: 'all', label: 'All' },
          { value: 'image', label: 'Image' },
          { value: 'video', label: 'Video' },
          { value: 'gif', label: 'GIF' }
      ];

      mediaTypes.forEach(type => {
          const option = document.createElement('option');
          option.value = type.value;
          option.textContent = type.label;
          option.selected = type.value === settings.mediaType;
          mediaTypeSelect.appendChild(option);
      });

      mediaTypeSelect.addEventListener('change', () => {
          const newSettings = getSettings();
          newSettings.mediaType = mediaTypeSelect.value;
          saveSettings(newSettings);
      });

      mediaTypeGroup.appendChild(mediaTypeLabel);
      mediaTypeGroup.appendChild(mediaTypeSelect);
      
      const concurrentGroup = document.createElement('div');
      concurrentGroup.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 8px;
      `;

      const concurrentLabel = document.createElement('label');
      concurrentLabel.textContent = 'Batch Download Items:';
      concurrentLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;

      const concurrentSelect = document.createElement('select');
      concurrentSelect.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;

      const concurrentSizes = [5, 10, 20, 25, 50];
      concurrentSizes.forEach(size => {
          const option = document.createElement('option');
          option.value = size;
          option.textContent = size;
          option.selected = size === settings.concurrentDownloads;
          concurrentSelect.appendChild(option);
      });

      concurrentSelect.addEventListener('change', () => {
          const newSettings = getSettings();
          newSettings.concurrentDownloads = parseInt(concurrentSelect.value);
          saveSettings(newSettings);
      });

      concurrentGroup.appendChild(concurrentLabel);
      concurrentGroup.appendChild(concurrentSelect);
      
      const cacheDurationGroup = document.createElement('div');
      cacheDurationGroup.style.cssText = `
          display: flex;
          flex-direction: column;
          gap: 8px;
      `;
      
      const cacheDurationLabel = document.createElement('label');
      cacheDurationLabel.textContent = 'Cache Duration (Hour):';
      cacheDurationLabel.style.cssText = `
          font-size: 14px;
          font-weight: bold;
      `;
      
      const cacheDurationSelect = document.createElement('select');
      cacheDurationSelect.style.cssText = `
          background-color: #192734;
          border: 1px solid #334155;
          border-radius: 4px;
          padding: 8px 12px;
          color: white;
          width: 100%;
          box-sizing: border-box;
      `;

      for (let i = 1; i <= 24; i++) {
          const option = document.createElement('option');
          option.value = i * 60;
          option.textContent = `${i} Hour${i > 1 ? 's' : ''}`;
          option.selected = i === 6 || (settings.cacheDuration === i * 60);
          cacheDurationSelect.appendChild(option);
      }
      
      cacheDurationSelect.addEventListener('change', () => {
          const newSettings = getSettings();
          newSettings.cacheDuration = parseInt(cacheDurationSelect.value);
          saveSettings(newSettings);
      });
      
      cacheDurationGroup.appendChild(cacheDurationLabel);
      cacheDurationGroup.appendChild(cacheDurationSelect);

      const clearCacheButton = document.createElement('button');
      clearCacheButton.textContent = 'Clear Cache';
      clearCacheButton.style.cssText = `
          background-color: #ef4444;
          color: white;
          border: none;
          border-radius: 9999px;
          padding: 8px 16px;
          font-weight: bold;
          cursor: pointer;
          margin-top: 16px;
          width: 100%;
          text-align: center;
          transition: background-color 0.2s;
      `;
      clearCacheButton.addEventListener('mouseenter', () => {
          clearCacheButton.style.backgroundColor = '#dc2626';
      });
      clearCacheButton.addEventListener('mouseleave', () => {
          clearCacheButton.style.backgroundColor = '#ef4444';
      });
      
      clearCacheButton.addEventListener('click', () => {
          createConfirmDialog('Are you sure you want to clear the cache?', () => {
              cacheManager.clear();
              
              const notification = document.createElement('div');
              notification.style.cssText = `
                  position: fixed;
                  bottom: 20px;
                  left: 50%;
                  transform: translateX(-50%);
                  background-color: #0ea5e9;
                  color: white;
                  padding: 12px 24px;
                  border-radius: 9999px;
                  font-weight: bold;
                  z-index: 10002;
                  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
                  text-align: center;
              `;
              notification.textContent = 'Cache cleared successfully';
              document.body.appendChild(notification);
              
              setTimeout(() => {
                  document.body.removeChild(notification);
              }, 3000);
          });
      });

      const githubLink = document.createElement('a');
      githubLink.href = 'https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader';
      githubLink.target = '_blank';
      githubLink.style.cssText = `
          display: flex;
          align-items: center;
          justify-content: center;
          color: #8899a6;
          text-decoration: none;
          margin-top: 16px;
          padding: 8px;
          border-radius: 8px;
          transition: background-color 0.2s, color 0.2s;
      `;
      githubLink.innerHTML = createGithubIcon().outerHTML + 'GitHub Repository';
      
      githubLink.addEventListener('mouseenter', () => {
          githubLink.style.backgroundColor = '#192734';
          githubLink.style.color = '#0ea5e9';
      });
      
      githubLink.addEventListener('mouseleave', () => {
          githubLink.style.backgroundColor = 'transparent';
          githubLink.style.color = '#8899a6';
      });

      settingsForm.appendChild(tokenGroup);
      settingsForm.appendChild(batchGroup);
      settingsForm.appendChild(batchSizeGroup);
      settingsForm.appendChild(timelineTypeGroup);
      settingsForm.appendChild(mediaTypeGroup);
      settingsForm.appendChild(concurrentGroup);
      settingsForm.appendChild(cacheDurationGroup);
      settingsForm.appendChild(clearCacheButton);
      settingsForm.appendChild(githubLink);

      settingsContent.appendChild(settingsForm);

      mainTab.addEventListener('click', () => {
          mainTab.style.borderBottom = '2px solid #0ea5e9';
          mainTab.style.color = 'white';
          settingsTab.style.borderBottom = 'none';
          settingsTab.style.color = '#8899a6';
          mainContent.style.display = 'block';
          settingsContent.style.display = 'none';
      });

      settingsTab.addEventListener('click', () => {
          settingsTab.style.borderBottom = '2px solid #0ea5e9';
          settingsTab.style.color = 'white';
          mainTab.style.borderBottom = 'none';
          mainTab.style.color = '#8899a6';
          settingsContent.style.display = 'block';
          mainContent.style.display = 'none';
      });

      modalContent.appendChild(header);
      modalContent.appendChild(tabs);
      modalContent.appendChild(mainContent);
      modalContent.appendChild(settingsContent);
      modal.appendChild(modalContent);

      let mediaData = {
          username: username,
          currentPage: 0,
          mediaItems: [],
          allMediaItems: [],
          hasMore: false,
          downloading: false,
          totalDownloaded: 0,
          totalToDownload: 0,
          totalItems: 0,
          autoBatchRunning: false
      };

      fetchButton.addEventListener('click', async () => {
          const settings = getSettings();
          
          if (!settings.authToken) {
              alert('Please enter your auth token in the Settings tab');
              settingsTab.click();
              return;
          }

          infoContainer.style.display = 'none';
          buttonContainer.style.display = 'none';
          downloadCurrentButton.style.display = 'none';
          downloadAllButton.style.display = 'none';
          nextBatchButton.style.display = 'none';
          autoBatchButton.style.display = 'none';
          stopBatchButton.style.display = 'none';
          progressContainer.style.display = 'none';
          fetchButton.disabled = true;
          fetchButton.textContent = 'Fetching...';
          
          try {
              const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`;
              let data = cacheManager.get(cacheKey);
              
              if (!data) {
                  let url;
                  if (settings.batchEnabled) {
                      url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`;
                  } else {
                      url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`;
                  }
                  
                  data = await fetchData(url);
                  
                  cacheManager.set(cacheKey, data);
              }
              
              if (data.timeline && data.timeline.length > 0) {
                  mediaData.mediaItems = data.timeline;
                  mediaData.hasMore = data.metadata.has_more;
                  mediaData.totalItems = data.total_urls;
                  
                  if (mediaData.currentPage === 0) {
                      mediaData.allMediaItems = [...data.timeline];
                  } else {
                      mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline];
                  }
                  
                  const mediaTypeLabel = getMediaTypeLabel(settings.mediaType);
                  
                  if (settings.batchEnabled) {
                      infoContainer.innerHTML = `
                          <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
                          <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
                          <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
                          <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
                      `;
                  } else {
                      const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1;
                      
                      infoContainer.innerHTML = `
                          <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
                          <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
                          <div style="margin-top: 8px;"><strong>Part:</strong> ${currentPart}</div>
                          <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
                      `;
                  }
                  
                  infoContainer.style.display = 'block';
                  
                  buttonContainer.style.display = 'flex';
                  downloadCurrentButton.style.display = 'block';
                  downloadAllButton.style.display = 'block';
                  
                  if (settings.batchEnabled && mediaData.hasMore) {
                      nextBatchButton.style.display = 'block';
                      autoBatchButton.style.display = 'block';
                  }
                  
                  downloadCurrentButton.onclick = () => downloadMedia(false);
                  downloadAllButton.onclick = () => downloadMedia(true);
                  
                  fetchButton.disabled = false;
                  fetchButton.textContent = 'Fetch Media';
              } else {
                  infoContainer.innerHTML = '<div style="color: #ef4444;">No media found or invalid token</div>';
                  infoContainer.style.display = 'block';
                  fetchButton.disabled = false;
                  fetchButton.textContent = 'Fetch Media';
              }
          } catch (error) {
              console.error('Error fetching media:', error);
              infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`;
              infoContainer.style.display = 'block';
              fetchButton.disabled = false;
              fetchButton.textContent = 'Fetch Media';
          }
      });

      nextBatchButton.addEventListener('click', () => {
          mediaData.currentPage++;
          fetchButton.click();
      });

      autoBatchButton.addEventListener('click', () => {
          if (mediaData.autoBatchRunning) {
              return;
          }
          
              mediaData.autoBatchRunning = true;
              autoBatchButton.style.display = 'none';
              stopBatchButton.style.display = 'block';
              nextBatchButton.style.display = 'none';
              
              startAutoBatch();
      });
      
      stopBatchButton.addEventListener('click', () => {
          createConfirmDialog('Stop auto batch download?', () => {
              mediaData.autoBatchRunning = false;
              stopBatchButton.style.display = 'none';
              autoBatchButton.style.display = 'block';
              if (mediaData.hasMore) {
                  nextBatchButton.style.display = 'block';
              }
          });
      });
      
      async function startAutoBatch() {
          while (mediaData.hasMore && mediaData.autoBatchRunning) {
              mediaData.currentPage++;
              
              downloadCurrentButton.disabled = true;
              downloadAllButton.disabled = true;
              
              await new Promise(resolve => {
                  const settings = getSettings();
                  const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`;
                  let data = cacheManager.get(cacheKey);
                  
                  if (data) {
                      processNextBatch(data);
                      resolve();
                  } else {
                      let url;
                      if (settings.batchEnabled) {
                          url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`;
                      } else {
                          url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`;
                      }
                      
                      fetchData(url).then(data => {
                          cacheManager.set(cacheKey, data);
                          processNextBatch(data);
                          resolve();
                      }).catch(error => {
                          console.error('Error in auto batch:', error);
                          mediaData.autoBatchRunning = false;
                          stopBatchButton.style.display = 'none';
                          autoBatchButton.style.display = 'block';
                          
                          downloadCurrentButton.disabled = false;
                          downloadAllButton.disabled = false;
                          
                          if (mediaData.hasMore) {
                              nextBatchButton.style.display = 'block';
                          }
                          
                          resolve();
                      });
                  }
              });
              
              await new Promise(resolve => setTimeout(resolve, 1000));
          }
          
          if (mediaData.autoBatchRunning) {
              mediaData.autoBatchRunning = false;
              stopBatchButton.style.display = 'none';
              autoBatchButton.style.display = 'none';
          }
          
          downloadCurrentButton.disabled = false;
          downloadAllButton.disabled = false;
      }
      
      function processNextBatch(data) {
          if (data.timeline && data.timeline.length > 0) {
              mediaData.mediaItems = data.timeline;
              mediaData.hasMore = data.metadata.has_more;
              
              mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline];
              
              const settings = getSettings();
              const mediaTypeLabel = getMediaTypeLabel(settings.mediaType);
              
              infoContainer.innerHTML = `
                  <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
                  <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
                  <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
                  <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
              `;
              
              if (!mediaData.hasMore) {
                  nextBatchButton.style.display = 'none';
                  autoBatchButton.style.display = 'none';
                  stopBatchButton.style.display = 'none';
              }
          } else {
              mediaData.hasMore = false;
              nextBatchButton.style.display = 'none';
              autoBatchButton.style.display = 'none';
              stopBatchButton.style.display = 'none';
          }
      }

      function chunkMediaItems(items) {
          const chunks = [];
          for (let i = 0; i < items.length; i += 500) {
              chunks.push(items.slice(i, i + 500));
          }
          return chunks;
      }

      async function downloadMedia(downloadAll) {
          if (mediaData.downloading) return;
          
          mediaData.downloading = true;
          
          const settings = getSettings();
          const timestamp = getCurrentTimestamp();
          
          let itemsToDownload;
          if (downloadAll) {
              itemsToDownload = mediaData.allMediaItems;
          } else {
              itemsToDownload = mediaData.mediaItems;
          }
          
          mediaData.totalToDownload = itemsToDownload.length;
          mediaData.totalDownloaded = 0;
          
          progressText.textContent = `Downloading 0/${formatNumber(mediaData.totalToDownload)}`;
          progressFill.style.width = '0%';
          progressContainer.style.display = 'block';
          
          fetchButton.disabled = true;
          downloadCurrentButton.disabled = true;
          downloadAllButton.disabled = true;
          nextBatchButton.disabled = true;
          autoBatchButton.disabled = true;
          stopBatchButton.disabled = true;
          
          const chunks = chunkMediaItems(itemsToDownload);
          
          for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
              const chunk = chunks[chunkIndex];
              const zip = new JSZip();
              
              const hasImages = chunk.some(item => item.type === 'photo');
              const hasVideos = chunk.some(item => item.type === 'video');
              const hasGifs = chunk.some(item => item.type === 'gif');
              
              let imageFolder, videoFolder, gifFolder;
              if (settings.mediaType === 'all') {
                  if (hasImages) imageFolder = zip.folder('image');
                  if (hasVideos) videoFolder = zip.folder('video');
                  if (hasGifs) gifFolder = zip.folder('gif');
              }
              
              const filenameMap = {};
              
              const concurrentBatches = [];
              for (let i = 0; i < chunk.length; i += settings.concurrentDownloads) {
                  concurrentBatches.push(chunk.slice(i, i + settings.concurrentDownloads));
              }
              
              for (const batch of concurrentBatches) {
                  const downloadPromises = batch.map(async (item) => {
                      try {
                          const formattedDate = formatDate(item.date);
                          
                          let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`;
                          
                          
                          if (filenameMap[baseFilename] !== undefined) {
                              filenameMap[baseFilename]++;
                              baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, '0')}`;
                          } else {
                              filenameMap[baseFilename] = 0;
                          }
                          
                          const fileExtension = item.type === 'photo' ? 'jpg' : 'mp4';
                          
                          const filename = `${baseFilename}.${fileExtension}`;
                          
                          const blob = await fetchBinary(item.url);
                          
                          if (settings.mediaType === 'all') {
                              if (item.type === 'photo') {
                                  imageFolder.file(filename, blob);
                              } else if (item.type === 'video') {
                                  videoFolder.file(filename, blob);
                              } else if (item.type === 'gif') {
                                  gifFolder.file(filename, blob);
                              }
                          } else {
                              zip.file(filename, blob);
                          }
                          
                          return true;
                      } catch (error) {
                          console.error(`Error downloading ${item.url}:`, error);
                          return false;
                      }
                  });
                  
                  await Promise.all(downloadPromises);
                  
                  mediaData.totalDownloaded += batch.length;
                  progressText.textContent = `Downloading ${formatNumber(mediaData.totalDownloaded)}/${formatNumber(mediaData.totalToDownload)}`;
                  progressFill.style.width = `${(mediaData.totalDownloaded / mediaData.totalToDownload) * 100}%`;
              }
              
              progressText.textContent = `Creating ZIP file ${chunkIndex + 1}/${chunks.length}...`;
              
              try {
                  const zipBlob = await zip.generateAsync({ type: 'blob' });
                  
                  let zipFilename;
                  if (chunks.length === 1 && chunk.length < 500) {
                      zipFilename = `${username}_${timestamp}.zip`;
                  } else if (settings.batchEnabled && !downloadAll) {
                      zipFilename = `${username}_${timestamp}_part_${String(mediaData.currentPage + 1).padStart(2, '0')}.zip`;
                  } else {
                      zipFilename = `${username}_${timestamp}_part_${String(chunkIndex + 1).padStart(2, '0')}.zip`;
                  }
                  
                  const downloadLink = document.createElement('a');
                  downloadLink.href = URL.createObjectURL(zipBlob);
                  downloadLink.download = zipFilename;
                  document.body.appendChild(downloadLink);
                  downloadLink.click();
                  document.body.removeChild(downloadLink);
                  
              } catch (error) {
                  console.error('Error creating ZIP:', error);
                  progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`;
              }
          }
          
          progressText.textContent = 'Download complete!';
          progressFill.style.width = '100%';
          
          setTimeout(() => {
              fetchButton.disabled = false;
              downloadCurrentButton.disabled = false;
              downloadAllButton.disabled = false;
              nextBatchButton.disabled = false;
              autoBatchButton.disabled = false;
              stopBatchButton.disabled = false;
              
              mediaData.downloading = false;
          }, 2000);
      }

      document.body.appendChild(modal);
  }

  function insertDownloadIcon() {
      const usernameDivs = document.querySelectorAll('[data-testid="UserName"]');
  
      usernameDivs.forEach((usernameDiv) => {
          if (!usernameDiv.querySelector(".download-icon")) {
              const username = extractUsername();
              if (!username) return;
              
              const verifiedButton = usernameDiv
                  .querySelector('[aria-label*="verified"], [aria-label*="Verified"]')
                  ?.closest("button");

              const targetElement = verifiedButton
                  ? verifiedButton.parentElement
                  : usernameDiv.querySelector(".css-1jxf684")?.closest("span");

              if (targetElement) {
                  const downloadIcon = createDownloadIcon();
                  
                  const iconDiv = document.createElement("div");
                  iconDiv.className = "download-icon css-175oi2r r-1awozwy r-xoduu5";
                  iconDiv.style.cssText = `
                      display: inline-flex;
                      align-items: center;
                      margin-left: 6px;
                      margin-right: 6px;
                      gap: 6px;
                      padding: 0 3px;
                      transition: transform 0.2s, color 0.2s;
                  `;
                  iconDiv.appendChild(downloadIcon);

                  iconDiv.addEventListener("mouseenter", () => {
                      iconDiv.style.transform = "scale(1.1)";
                      iconDiv.style.color = "#0ea5e9";
                  });

                  iconDiv.addEventListener("mouseleave", () => {
                      iconDiv.style.transform = "scale(1)";
                      iconDiv.style.color = "";
                  });

                  iconDiv.addEventListener("click", (e) => {
                      e.stopPropagation();
                      createModal(username);
                  });    

                  const wrapperDiv = document.createElement("div");
                  wrapperDiv.style.cssText = `
                      display: inline-flex;
                      align-items: center;
                      gap: 4px;
                  `;
                  wrapperDiv.appendChild(iconDiv);
                  targetElement.parentNode.insertBefore(wrapperDiv, targetElement.nextSibling);
              }
          }
      });
  }

  insertDownloadIcon();
  
  let lastUrl = location.href;
  const observer = new MutationObserver(() => {
      const url = location.href;
      if (url !== lastUrl) {
          lastUrl = url;
          setTimeout(insertDownloadIcon, 1000);
      } else {
          insertDownloadIcon();
      }
  });
  
  observer.observe(document.body, {
      childList: true,
      subtree: true,
  });
  
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址