Greasy Fork

Greasy Fork is available in English.

Twitter/X Media Batch Downloader

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

当前为 2025-04-22 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==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      3.2
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @connect      api.gallerydl.web.id
// @connect      pbs.twimg.com
// @connect      video.twimg.com
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// ==/UserScript==

;(() => {
  const defaultSettings = {
    patreonAuth: "",
    authToken: "",
    batchEnabled: true,
    batchSize: 100,
    timelineType: "media",
    mediaType: "all",
    concurrentDownloads: 25,
    cacheDuration: 360,
  }

  const batchSizes = [25, 50, 100, 200]
  const concurrentSizes = [5, 10, 20, 25, 50]
  const cacheDurations = [60, 120, 180, 240, 300, 360, 720, 1440]

  function getSettings() {
    return {
      patreonAuth: GM_getValue("patreonAuth", defaultSettings.patreonAuth),
      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("patreonAuth", settings.patreonAuth)
    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 getServiceBaseUrl() {
    return "https://api.gallerydl.web.id"
  }

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

  const cacheManager = {
    set: (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: (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: () => {
      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 createPatreonIcon() {
    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", "20")
    svg.setAttribute("height", "20")
    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",
      "M489.7 153.8c-.1-65.4-51-119-110.7-138.3C304.8-8.5 207-5 136.1 28.4C50.3 68.9 23.3 157.7 22.3 246.2C21.5 319 28.7 510.6 136.9 512c80.3 1 92.3-102.5 129.5-152.3c26.4-35.5 60.5-45.5 102.4-55.9c72-17.8 121.1-74.7 121-150z",
    )
    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.35);
        backdrop-filter: blur(2.5px);
        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: #ffffff;
        color: #334155;
        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 #e2e8f0;
        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 #e2e8f0;
    `

    const cancelButton = document.createElement("button")
    cancelButton.style.cssText = `
        flex: 1;
        background-color: #94a3b8;
        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 = "No"
    cancelButton.addEventListener("mouseenter", () => {
      cancelButton.style.backgroundColor = "#64748b"
    })
    cancelButton.addEventListener("mouseleave", () => {
      cancelButton.style.backgroundColor = "#94a3b8"
    })
    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 = "Yes"
    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 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: (response) => {
          if (response.status >= 200 && response.status < 300) {
            resolve(response.response)
          } else {
            reject(new Error(`Request failed with status ${response.status}`))
          }
        },
        onerror: (error) => {
          reject(new Error(`Network error: ${error?.message || "Unknown error"}`))
        },
        ontimeout: () => {
          reject(new Error("Request timed out"))
        }
      })
    })
  }

  function fetchBinary(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url,
        responseType: "blob",
        onload: (response) => {
          if (response.status >= 200 && response.status < 300) {
            resolve(response.response)
          } else {
            reject(new Error(`Request failed with status ${response.status}`))
          }
        },
        onerror: () => {
          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 createToggleSwitch(options, selectedValue, onChange) {
    const toggleWrapper = document.createElement("div")
    toggleWrapper.style.cssText = `
      position: relative;
      height: 40px;
      background-color: #f1f5f9;
      border-radius: 8px;
      padding: 0;
      cursor: pointer;
      width: 100%;
      margin-bottom: 16px;
      overflow: hidden;
    `
  
    const toggleSlider = document.createElement("div")
    toggleSlider.style.cssText = `
      position: absolute;
      height: 100%;
      background-color: #0ea5e9;
      border-radius: 8px;
      transition: transform 0.3s ease, width 0.3s ease;
      z-index: 1;
    `
  
    const optionsContainer = document.createElement("div")
    optionsContainer.style.cssText = `
      position: relative;
      display: flex;
      height: 100%;
      z-index: 2;
      width: 100%;
    `
  
    const selectedIndex = options.findIndex((option) => option.value === selectedValue)
    const optionWidth = 100 / options.length
  
    toggleSlider.style.width = `${optionWidth}%`
    toggleSlider.style.transform = `translateX(${selectedIndex * 100}%)`
  
    options.forEach((option, index) => {
      const optionElement = document.createElement("div")
      optionElement.style.cssText = `
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        transition: color 0.3s ease;
        color: ${option.value === selectedValue ? "white" : "#64748b"};
        cursor: pointer;
        user-select: none;
        text-align: center;
        height: 100%;
        padding: 0 4px;
      `
  
      if (option.icon) {
        const iconContainer = document.createElement("span")
        iconContainer.style.cssText = `
          display: flex;
          align-items: center;
          justify-content: center;
          margin-right: 6px;
        `
  
        const iconClone = option.icon.cloneNode(true)
  
        const paths = iconClone.querySelectorAll("path")
        paths.forEach((path) => {
          path.setAttribute("fill", option.value === selectedValue ? "white" : "#64748b")
        })
  
        iconContainer.appendChild(iconClone)
        optionElement.appendChild(iconContainer)
      }
  
      const text = document.createElement("span")
      text.textContent = option.label
      text.style.cssText = `
        display: inline-block;
        text-align: center;
      `
      optionElement.appendChild(text)
  
      optionElement.addEventListener("click", (e) => {
        e.stopPropagation()
        onChange(option.value)
  
        toggleSlider.style.transform = `translateX(${index * 100}%)`
  
        optionsContainer.querySelectorAll("div").forEach((opt, i) => {
          opt.style.color = i === index ? "white" : "#64748b"
  
          const optIcon = opt.querySelector("svg")
          if (optIcon) {
            const optPaths = optIcon.querySelectorAll("path")
            optPaths.forEach((path) => {
              path.setAttribute("fill", i === index ? "white" : "#64748b")
            })
          }
        })
      })
  
      optionsContainer.appendChild(optionElement)
    })
  
    toggleWrapper.appendChild(toggleSlider)
    toggleWrapper.appendChild(optionsContainer)
  
    return toggleWrapper
  }

  function createMediaTypeIcons() {
    const allIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    allIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    allIcon.setAttribute("viewBox", "0 0 640 512")
    allIcon.setAttribute("width", "16")
    allIcon.setAttribute("height", "16")
    allIcon.style.verticalAlign = "middle"
    
    const allPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    allPath.setAttribute("fill", "#64748b")
    allPath.setAttribute("d", "M256 48c-8.8 0-16 7.2-16 16l0 224c0 8.7 6.9 15.8 15.6 16l69.1-94.2c4.5-6.2 11.7-9.8 19.4-9.8s14.8 3.6 19.4 9.8L380 232.4l56-85.6c4.4-6.8 12-10.9 20.1-10.9s15.7 4.1 20.1 10.9L578.7 303.8c7.6-1.3 13.3-7.9 13.3-15.8l0-224c0-8.8-7.2-16-16-16L256 48zM192 64c0-35.3 28.7-64 64-64L576 0c35.3 0 64 28.7 64 64l0 224c0 35.3-28.7 64-64 64l-320 0c-35.3 0-64-28.7-64-64l0-224zm-56 64l24 0 0 48 0 88 0 112 0 8 0 80 192 0 0-80 48 0 0 80 48 0c8.8 0 16-7.2 16-16l0-64 48 0 0 64c0 35.3-28.7 64-64 64l-48 0-24 0-24 0-192 0-24 0-24 0-48 0c-35.3 0-64-28.7-64-64L0 192c0-35.3 28.7-64 64-64l48 0 24 0zm-24 48l-48 0c-8.8 0-16 7.2-16 16l0 48 64 0 0-64zm0 288l0-64-64 0 0 48c0 8.8 7.2 16 16 16l48 0zM48 352l64 0 0-64-64 0 0 64zM304 80a32 32 0 1 1 0 64 32 32 0 1 1 0-64z")
    allIcon.appendChild(allPath)
  
    const imageIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    imageIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    imageIcon.setAttribute("viewBox", "0 0 512 512")
    imageIcon.setAttribute("width", "16")
    imageIcon.setAttribute("height", "16")
    imageIcon.style.verticalAlign = "middle"
    
    const imagePath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    imagePath.setAttribute("fill", "#64748b")
    imagePath.setAttribute("d", "M448 80c8.8 0 16 7.2 16 16l0 319.8-5-6.5-136-176c-4.5-5.9-11.6-9.3-19-9.3s-14.4 3.4-19 9.3L202 340.7l-30.5-42.7C167 291.7 159.8 288 152 288s-15 3.7-19.5 10.1l-80 112L48 416.3l0-.3L48 96c0-8.8 7.2-16 16-16l384 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm80 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z")
    imageIcon.appendChild(imagePath)
  
    const videoIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    videoIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    videoIcon.setAttribute("viewBox", "0 0 512 512")
    videoIcon.setAttribute("width", "16")
    videoIcon.setAttribute("height", "16")
    videoIcon.style.verticalAlign = "middle"
    
    const videoPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    videoPath.setAttribute("fill", "#64748b")
    videoPath.setAttribute("d", "M352 432l-192 0 0-112 0-40 192 0 0 40 0 112zm0-200l-192 0 0-40 0-112 192 0 0 112 0 40zM64 80l48 0 0 88-64 0 0-72c0-8.8 7.2-16 16-16zM48 216l64 0 0 80-64 0 0-80zm64 216l-48 0c-8.8 0-16-7.2-16-16l0-72 64 0 0 88zM400 168l0-88 48 0c8.8 0 16 7.2 16 16l0 72-64 0zm0 48l64 0 0 80-64 0 0-80zm0 128l64 0 0 72c0 8.8-7.2 16-16 16l-48 0 0-88zM448 32L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64z")
    videoIcon.appendChild(videoPath)
  
    const gifIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    gifIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    gifIcon.setAttribute("viewBox", "0 0 576 512")
    gifIcon.setAttribute("width", "16")
    gifIcon.setAttribute("height", "16")
    gifIcon.style.verticalAlign = "middle"
    
    const gifPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    gifPath.setAttribute("fill", "#64748b")
    gifPath.setAttribute("d", "M512 80c8.8 0 16 7.2 16 16l0 320c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16L48 96c0-8.8 7.2-16 16-16l448 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM296 160c-13.3 0-24 10.7-24 24l0 144c0 13.3 10.7 24 24 24s24-10.7 24-24l0-144c0-13.3-10.7-24-24-24zm56 24l0 80 0 64c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-32 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-88 0c-13.3 0-24 10.7-24 24zM128 256c0-26.5 21.5-48 48-48c8 0 15.4 1.9 22 5.3c11.8 6.1 26.3 1.5 32.3-10.3s1.5-26.3-10.3-32.3c-13.2-6.8-28.2-10.7-44-10.7c-53 0-96 43-96 96s43 96 96 96c19.6 0 37.5-6.1 52.8-15.8c7-4.4 11.2-12.1 11.2-20.3l0-51.9c0-13.3-10.7-24-24-24l-32 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l8 0 0 13.1c-5.3 1.9-10.6 2.9-16 2.9c-26.5 0-48-21.5-48-48z")
    gifIcon.appendChild(gifPath)
  
    return {
      all: allIcon,
      image: imageIcon,
      video: videoIcon,
      gif: gifIcon
    }
  }

  function createSlider(options, selectedValue, onChange) {
    const toggleOptions = options.map((option) => {
      let label = option.toString()
      if (typeof option === "number" && option >= 60 && option % 60 === 0) {
        label = `${option / 60}h`
      }
      return { value: option, label: label }
    })

    return createToggleSwitch(toggleOptions, selectedValue, onChange)
  }

  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.35);
        backdrop-filter: blur(2.5px);
        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: #ffffff;
    color: #334155;
    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 #e2e8f0;
    `

    const title = document.createElement("h2")
    title.innerHTML = `Download ${getMediaTypeLabel(settings.mediaType)}: <span style="color: #0ea5e9">${username}</span>`
    title.style.cssText = `
    margin: 0;
    font-size: 18px;
    font-weight: bold;
    color: #334155;
`

    const closeButton = document.createElement("button")
    closeButton.innerHTML = "&times;"
    closeButton.style.cssText = `
        background: none;
        border: none;
        color: #0f172a;
        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 = "#0f172a"
    })
    closeButton.onclick = () => modal.remove()

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

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

    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: #64748b;
    `

    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")
    const mediaTypeLabelText = getMediaTypeLabel(settings.mediaType).toLowerCase()
    const fetchIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    fetchIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    fetchIcon.setAttribute("viewBox", "0 0 448 512")
    fetchIcon.setAttribute("width", "16")
    fetchIcon.setAttribute("height", "16")
    fetchIcon.style.marginRight = "8px"

    const fetchPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    fetchPath.setAttribute("fill", "currentColor")
    fetchPath.setAttribute(
      "d",
      "M374.6 214.6l-128 128c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 242.7 192 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 73.4-73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3zM64 352l0 64c0 17.7 14.3 32 32 32l256 0c17.7 0 32-14.3 32-32l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 64c0 53-43 96-96 96L96 512c-53 0-96-43-96-96l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32z",
    )
    fetchIcon.appendChild(fetchPath)

    const fetchButtonText = document.createElement("span")
    fetchButtonText.textContent =
      settings.mediaType === "all"
        ? "Fetch Media"
        : `Fetch ${mediaTypeLabelText === "gif" ? "GIF" : mediaTypeLabelText.charAt(0).toUpperCase() + mediaTypeLabelText.slice(1)}`

    fetchButton.innerHTML = ""
    fetchButton.appendChild(fetchIcon)
    fetchButton.appendChild(fetchButtonText)

    fetchButton.style.cssText = `
        background-color: #22c55e;
        color: white;
        border: none;
        border-radius: 9999px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        margin: 16px auto;
        width: 50%;
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        transition: background-color 0.2s;
    `
    fetchButton.addEventListener("mouseenter", () => {
      fetchButton.style.backgroundColor = "#16a34a"
    })
    fetchButton.addEventListener("mouseleave", () => {
      fetchButton.style.backgroundColor = "#22c55e"
    })

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

    const buttonContainer = document.createElement("div")
    buttonContainer.style.cssText = `
        display: none;
        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: block;
        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: #0ea5e9;
        color: white;
        border: none;
        border-radius: 9999px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        flex: 1;
        display: block;
        text-align: center;
        transition: background-color 0.2s;
    `
    downloadAllButton.addEventListener("mouseenter", () => {
      downloadAllButton.style.backgroundColor = "#0284c7"
    })
    downloadAllButton.addEventListener("mouseleave", () => {
      downloadAllButton.style.backgroundColor = "#0ea5e9"
    })

    const downloadButton = document.createElement("button")
    downloadButton.textContent = "Download"
    downloadButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 9999px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        width: 50%;
        margin-left: auto;
        margin-right: auto;
        display: block;
        text-align: center;
        transition: background-color 0.2s;
    `
    downloadButton.addEventListener("mouseenter", () => {
      downloadButton.style.backgroundColor = "#0284c7"
    })
    downloadButton.addEventListener("mouseleave", () => {
      downloadButton.style.backgroundColor = "#0ea5e9"
    })
    downloadButton.onclick = () => downloadMedia(false)

    if (settings.batchEnabled) {
      buttonContainer.style.display = "none"
    } else {
      buttonContainer.appendChild(downloadButton)
    }

    const batchButtonsContainer = document.createElement("div")
    batchButtonsContainer.style.cssText = `
        display: none;
        gap: 8px;
        margin-bottom: 16px;
    `

    const nextBatchButton = document.createElement("button")
    nextBatchButton.textContent = "Next Batch"
    nextBatchButton.style.cssText = `
        background-color: #6366f1;
        color: white;
        border: none;
        border-radius: 9999px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        flex: 1;
        display: block;
        text-align: center;
        transition: background-color 0.2s;
    `
    nextBatchButton.addEventListener("mouseenter", () => {
      nextBatchButton.style.backgroundColor = "#4f46e5"
    })
    nextBatchButton.addEventListener("mouseleave", () => {
      nextBatchButton.style.backgroundColor = "#6366f1"
    })

    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;
        flex: 1;
        display: block;
        text-align: center;
        transition: background-color 0.2s;
    `
    autoBatchButton.addEventListener("mouseenter", () => {
      autoBatchButton.style.backgroundColor = "#4f46e5"
    })
    autoBatchButton.addEventListener("mouseleave", () => {
      autoBatchButton.style.backgroundColor = "#6366f1"
    })

    batchButtonsContainer.appendChild(nextBatchButton)
    batchButtonsContainer.appendChild(autoBatchButton)

    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: #f1f5f9;
        border-radius: 4px;
        overflow: hidden;
    `

    const progressFill = document.createElement("div")
    progressFill.style.cssText = `
    height: 100%;
    width: 0%;
    background-color: #0ea5e9;
    transition: width 0.3s ease-in-out;
    will-change: width;
`

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

    mainContent.appendChild(fetchButton)
    mainContent.appendChild(infoContainer)
    mainContent.appendChild(buttonContainer)
    mainContent.appendChild(batchButtonsContainer)
    mainContent.appendChild(stopBatchButton)
    mainContent.appendChild(progressContainer)

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

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

    const patreonAuthLabel = document.createElement("label")
    patreonAuthLabel.textContent = "Patreon Auth:"
    patreonAuthLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: #334155;
    `

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

    const patreonAuthInput = document.createElement("input")
    patreonAuthInput.type = "text"
    patreonAuthInput.value = settings.patreonAuth
    patreonAuthInput.style.cssText = `
    background-color: #f1f5f9;
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 8px 12px;
    color: #64748b;
    width: 100%;
    box-sizing: border-box;
    transition: all 0.2s ease;
`
    patreonAuthInput.addEventListener("focus", () => {
      patreonAuthInput.style.border = "1px solid #0ea5e9"
      patreonAuthInput.style.outline = "none"
    })
    patreonAuthInput.addEventListener("blur", () => {
      patreonAuthInput.style.border = "1px solid transparent"
    })

    patreonAuthInput.addEventListener("input", () => {
      const newSettings = getSettings()
      newSettings.patreonAuth = patreonAuthInput.value
      saveSettings(newSettings)
      patreonAuthClearButton.style.display = patreonAuthInput.value ? "block" : "none"
    })

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

    patreonAuthInputContainer.appendChild(patreonAuthInput)
    patreonAuthInputContainer.appendChild(patreonAuthClearButton)
    patreonAuthGroup.appendChild(patreonAuthLabel)
    patreonAuthGroup.appendChild(patreonAuthInputContainer)

    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;
        color: #334155;
    `

    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: #f1f5f9;
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 8px 12px;
    color: #64748b;
    width: 100%;
    box-sizing: border-box;
    transition: all 0.2s ease;
`
    tokenInput.addEventListener("focus", () => {
      tokenInput.style.border = "1px solid #0ea5e9"
      tokenInput.style.outline = "none"
    })
    tokenInput.addEventListener("blur", () => {
      tokenInput.style.border = "1px solid transparent"
    })

    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: #64748b;
        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.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: #334155;
        flex: 1;
        display: flex;
        align-items: center;
    `
    
    const batchLabelText = document.createElement("span")
    batchLabelText.textContent = "Batch:"
    batchLabelText.style.marginRight = "8px"
    batchLabel.appendChild(batchLabelText)
    
    const batchStatusText = document.createElement("span")
    batchStatusText.textContent = settings.batchEnabled ? "Enabled" : "Disabled"
    batchStatusText.style.cssText = `
        font-size: 14px;
        font-weight: normal;
        color: ${settings.batchEnabled ? "#22c55e" : "#64748b"};
    `
    batchLabel.appendChild(batchStatusText)
    
    const batchToggle = document.createElement("div")
    batchToggle.style.cssText = `
        position: relative;
        width: 50px;
        height: 24px;
        background-color: ${settings.batchEnabled ? "#22c55e" : "#cbd5e1"};
        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 ? "#22c55e" : "#cbd5e1"
        batchToggleHandle.style.left = newSettings.batchEnabled ? "28px" : "2px"
        
        batchStatusText.textContent = newSettings.batchEnabled ? "Enabled" : "Disabled"
        batchStatusText.style.color = newSettings.batchEnabled ? "#22c55e" : "#64748b"
        
        batchSizeGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
    
        if (!newSettings.batchEnabled) {
            buttonContainer.innerHTML = ""
            buttonContainer.appendChild(downloadButton)
            buttonContainer.style.display = infoContainer.style.display === "block" ? "block" : "none"
        } else {
            buttonContainer.innerHTML = ""
            buttonContainer.style.display = "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;
        color: #334155;
    `

    const batchSizeToggle = createSlider(batchSizes, settings.batchSize, (value) => {
      const newSettings = getSettings()
      newSettings.batchSize = value
      saveSettings(newSettings)
      settings.batchSize = value
    })

    batchSizeGroup.appendChild(batchSizeLabel)
    batchSizeGroup.appendChild(batchSizeToggle)

    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;
        color: #334155;
    `

    const timelineTypeOptions = [
      { value: "media", label: "Media" },
      { value: "timeline", label: "Post" },
      { value: "tweets", label: "Tweets" },
      { value: "with_replies", label: "Replies" },
    ]

    const timelineTypeToggle = createToggleSwitch(timelineTypeOptions, settings.timelineType, (value) => {
      const newSettings = getSettings()
      newSettings.timelineType = value
      saveSettings(newSettings)
      settings.timelineType = value
    })

    timelineTypeGroup.appendChild(timelineTypeLabel)
    timelineTypeGroup.appendChild(timelineTypeToggle)

    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;
        color: #334155;
    `

    const mediaTypeIcons = createMediaTypeIcons()
    const mediaTypeOptions = [
      { value: "all", label: "All", icon: mediaTypeIcons.all },
      { value: "image", label: "Image", icon: mediaTypeIcons.image },
      { value: "video", label: "Video", icon: mediaTypeIcons.video },
      { value: "gif", label: "GIF", icon: mediaTypeIcons.gif },
    ]

    const mediaTypeToggle = createToggleSwitch(mediaTypeOptions, settings.mediaType, (value) => {
      const newSettings = getSettings()
      newSettings.mediaType = value
      saveSettings(newSettings)
      settings.mediaType = value

      const newMediaTypeLabel = getMediaTypeLabel(value).toLowerCase()
      const newFetchButtonText =
        value === "all"
          ? "Fetch Media"
          : `Fetch ${newMediaTypeLabel === "gif" ? "GIF" : newMediaTypeLabel.charAt(0).toUpperCase() + newMediaTypeLabel.slice(1)}`

      fetchButton.innerHTML = ""
      const newFetchIcon = fetchIcon.cloneNode(true)
      fetchButton.appendChild(newFetchIcon)
      fetchButton.appendChild(document.createTextNode(newFetchButtonText))

      title.innerHTML = `Download ${getMediaTypeLabel(value)}: <span style="color: #0ea5e9">${username}</span>`
    })

    mediaTypeGroup.appendChild(mediaTypeLabel)
    mediaTypeGroup.appendChild(mediaTypeToggle)

    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;
        color: #334155;
    `

    const concurrentToggle = createSlider(concurrentSizes, settings.concurrentDownloads, (value) => {
      const newSettings = getSettings()
      newSettings.concurrentDownloads = value
      saveSettings(newSettings)
      settings.concurrentDownloads = value
    })

    concurrentGroup.appendChild(concurrentLabel)
    concurrentGroup.appendChild(concurrentToggle)

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

    const cacheDurationLabel = document.createElement("label")
    cacheDurationLabel.textContent = "Cache Duration:"
    cacheDurationLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: #334155;
    `

    const cacheDurationOptions = cacheDurations.map((duration) => {
      let label = duration.toString() + "m"
      if (duration >= 60 && duration % 60 === 0) {
        label = `${duration / 60}h`
      }
      return { value: duration, label: label }
    })

    const cacheDurationToggle = createToggleSwitch(cacheDurationOptions, settings.cacheDuration, (value) => {
      const newSettings = getSettings()
      newSettings.cacheDuration = value
      saveSettings(newSettings)
      settings.cacheDuration = value
    })

    cacheDurationGroup.appendChild(cacheDurationLabel)
    cacheDurationGroup.appendChild(cacheDurationToggle)

    const clearCacheButton = document.createElement("button")
    const trashIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    trashIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    trashIcon.setAttribute("viewBox", "0 0 448 512")
    trashIcon.setAttribute("width", "16")
    trashIcon.setAttribute("height", "16")
    trashIcon.style.marginRight = "8px"

    const trashPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    trashPath.setAttribute("fill", "currentColor")
    trashPath.setAttribute(
      "d",
      "M135.2 17.7C140.6 6.8 151.7 0 163.8 0L284.2 0c12.1 0 23.2 6.8 28.6 17.7L320 32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 7.2-14.3zM32 128l384 0 0 320c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-320zm96 64c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16z",
    )
    trashIcon.appendChild(trashPath)

    clearCacheButton.appendChild(trashIcon)
    clearCacheButton.appendChild(document.createTextNode("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: 50%;
    margin-left: auto;
    margin-right: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    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 about clearing 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 patreonLink = document.createElement("a")
    patreonLink.href = "https://www.patreon.com/exyezed"
    patreonLink.target = "_blank"
    patreonLink.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: center;
        color: #64748b;
        text-decoration: none;
        margin-top: 16px;
        padding: 8px;
        border-radius: 8px;
        transition: background-color 0.2s, color 0.2s;
    `
    patreonLink.innerHTML = createPatreonIcon().outerHTML + "Patreon Authentication"

    patreonLink.addEventListener("mouseenter", () => {
      patreonLink.style.backgroundColor = "#f1f5f9"
      patreonLink.style.color = "#0ea5e9"
    })

    patreonLink.addEventListener("mouseleave", () => {
      patreonLink.style.backgroundColor = "transparent"
      patreonLink.style.color = "#64748b"
    })

    settingsForm.appendChild(patreonAuthGroup)
    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(patreonLink)

    settingsContent.appendChild(settingsForm)

    mainTab.addEventListener("click", () => {
      mainTab.style.borderBottom = "2px solid #0ea5e9"
      mainTab.style.color = "#0f172a"
      settingsTab.style.borderBottom = "none"
      settingsTab.style.color = "#64748b"
      mainContent.style.display = "block"
      settingsContent.style.display = "none"
    })

    settingsTab.addEventListener("click", () => {
      settingsTab.style.borderBottom = "2px solid #0ea5e9"
      settingsTab.style.color = "#0f172a"
      mainTab.style.borderBottom = "none"
      mainTab.style.color = "#64748b"
      settingsContent.style.display = "block"
      mainContent.style.display = "none"
    })

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

    const 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) {
        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.35);
                backdrop-filter: blur(2.5px);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 10001;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            `

        const popup = document.createElement("div")
        popup.style.cssText = `
                background-color: #ffffff;
                color: #0f172a;
                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 #e2e8f0;
                font-weight: bold;
                font-size: 16px;
                text-align: center;
            `
        header.textContent = "Authentication Required"

        const content = document.createElement("div")
        content.style.cssText = `
                padding: 16px;
                text-align: center;
            `

        const authLink = document.createElement("a")
        authLink.href = "https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader#-how-to-obtain-auth-token"
        authLink.target = "_blank"
        authLink.textContent = "How to Obtain Auth Token"
        authLink.style.cssText = `
                color: #0ea5e9;
                text-decoration: none;
                cursor: pointer;
            `
        content.appendChild(authLink)

        const buttonContainer = document.createElement("div")
        buttonContainer.style.cssText = `
                padding: 16px;
                display: flex;
                justify-content: center;
                border-top: 1px solid #e2e8f0;
            `

        const okButton = document.createElement("button")
        okButton.style.cssText = `
                background-color: #0ea5e9;
                color: white;
                border: none;
                border-radius: 9999px;
                padding: 8px 24px;
                font-weight: bold;
                cursor: pointer;
                transition: background-color 0.2s;
            `
        okButton.textContent = "OK"
        okButton.addEventListener("mouseenter", () => {
          okButton.style.backgroundColor = "#0284c7"
        })
        okButton.addEventListener("mouseleave", () => {
          okButton.style.backgroundColor = "#0ea5e9"
        })
        okButton.onclick = () => {
          document.body.removeChild(overlay)
          settingsTab.click()
        }

        buttonContainer.appendChild(okButton)
        popup.appendChild(header)
        popup.appendChild(content)
        popup.appendChild(buttonContainer)
        overlay.appendChild(popup)

        document.body.appendChild(overlay)
        return
      }

      infoContainer.style.display = "none"
      buttonContainer.style.display = "none"
      nextBatchButton.style.display = "none"
      autoBatchButton.style.display = "none"
      stopBatchButton.style.display = "none"
      progressContainer.style.display = "none"
      fetchButton.disabled = true
      fetchButton.innerHTML = ""
      fetchButton.appendChild(document.createTextNode("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 = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
          } else {
            url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
          }

          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"

          if (settings.batchEnabled) {
            buttonContainer.innerHTML = ""
            buttonContainer.appendChild(downloadCurrentButton)
            buttonContainer.appendChild(downloadAllButton)
            buttonContainer.style.display = "flex"
          } else {
            buttonContainer.innerHTML = ""
            buttonContainer.appendChild(downloadButton)
            buttonContainer.style.display = "block"
          }

          if (settings.batchEnabled && mediaData.hasMore) {
            batchButtonsContainer.style.display = "flex"
            nextBatchButton.style.display = "block"
            autoBatchButton.style.display = "block"
          }

          downloadCurrentButton.onclick = () => downloadMedia(false)
          downloadAllButton.onclick = () => downloadMedia(true)

          fetchButton.disabled = false
          const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
          const updatedFetchButtonText =
            settings.mediaType === "all"
              ? "Fetch Media"
              : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`

          fetchButton.innerHTML = ""
          const updatedFetchIcon = fetchIcon.cloneNode(true)
          fetchButton.appendChild(updatedFetchIcon)
          fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
        } else {
          infoContainer.innerHTML = '<div style="color: #ef4444;">No media found, invalid token, or invalid Patreon authentication.</div>'
          infoContainer.style.display = "block"
          fetchButton.disabled = false
          const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
          const updatedFetchButtonText =
            settings.mediaType === "all"
              ? "Fetch Media"
              : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`

          fetchButton.innerHTML = ""
          const updatedFetchIcon = fetchIcon.cloneNode(true)
          fetchButton.appendChild(updatedFetchIcon)
          fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
        }
      } catch (error) {
        infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`
        infoContainer.style.display = "block"
        fetchButton.disabled = false
        const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
        const updatedFetchButtonText =
          settings.mediaType === "all"
            ? "Fetch Media"
            : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`

        fetchButton.innerHTML = ""
        const updatedFetchIcon = fetchIcon.cloneNode(true)
        fetchButton.appendChild(updatedFetchIcon)
        fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
      }
    })

    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}`
          const data = cacheManager.get(cacheKey)

          if (data) {
            processNextBatch(data)
            resolve()
          } else {
            let url
            if (settings.batchEnabled) {
              url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
            } else {
              url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
            }

            fetchData(url)
              .then((data) => {
                cacheManager.set(cacheKey, data)
                processNextBatch(data)
                resolve()
              })
              .catch(() => {
                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
      if (settings.batchEnabled) {
        downloadCurrentButton.disabled = true
        downloadAllButton.disabled = true
      } else {
        downloadButton.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]

        if (chunk.length === 1 && chunks.length === 1) {
          try {
            const item = chunk[0]
            const formattedDate = formatDate(item.date)
            const baseFilename = `${username}_${formattedDate}_${item.tweet_id}`
            const fileExtension = item.type === "photo" ? "jpg" : "mp4"
            const filename = `${baseFilename}.${fileExtension}`

            progressText.textContent = `Downloading 0/1`

            const blob = await fetchBinary(item.url)

            const downloadLink = document.createElement("a")
            downloadLink.href = URL.createObjectURL(blob)
            downloadLink.download = filename
            document.body.appendChild(downloadLink)
            downloadLink.click()
            document.body.removeChild(downloadLink)

            mediaData.totalDownloaded = 1
            progressText.textContent = `Downloading 1/1`
            progressFill.style.width = "100%"

            continue
          } catch (error) {}
        }

        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 = {}

        let completedCount = 0

        for (let i = 0; i < chunk.length; i++) {
          const item = chunk[i]
          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}`

            completedCount = mediaData.totalDownloaded + i
            progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
            progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`

            await new Promise((resolve) => setTimeout(resolve, 0))

            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)
            }

            completedCount = mediaData.totalDownloaded + i + 1
            progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
            progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`

            await new Promise((resolve) => setTimeout(resolve, 0))
          } catch (error) {
            console.error("Error downloading item:", error)
          }
        }

        mediaData.totalDownloaded += chunk.length

        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) {
          progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`
        }
      }

      progressText.textContent = "Download complete!"
      progressFill.style.width = "100%"

      setTimeout(() => {
        fetchButton.disabled = false
        if (settings.batchEnabled) {
          downloadCurrentButton.disabled = false
          downloadAllButton.disabled = false
        } else {
          downloadButton.disabled = false
        }
        nextBatchButton.disabled = false
        autoBatchButton.disabled = false
        stopBatchButton.disabled = false

        mediaData.downloading = false
      }, 2000)
    }

    document.body.appendChild(modal)
  }

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

  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()

  function checkForUserNameElement() {
    const usernameDivs = document.querySelectorAll('[data-testid="UserName"]')
    if (usernameDivs.length > 0) {
      insertDownloadIcon()
    }
  }

  setInterval(checkForUserNameElement, 100)

  let lastUrl = location.href
  let lastUsername = extractUsername()

  function checkForChanges() {
    const currentUrl = location.href
    const currentUsername = extractUsername()

    if (currentUrl !== lastUrl || currentUsername !== lastUsername) {
      lastUrl = currentUrl
      lastUsername = currentUsername

      document.querySelectorAll(".download-icon").forEach((icon) => {
        const wrapper = icon.closest("div[style*='display: inline-flex']")
        if (wrapper) {
          wrapper.remove()
        }
      })

      setTimeout(insertDownloadIcon, 50)
    }
  }

  const observer = new MutationObserver(() => {
    checkForChanges()
    checkForUserNameElement()
  })

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

  setInterval(checkForChanges, 300)

  const originalPushState = history.pushState
  const originalReplaceState = history.replaceState

  history.pushState = function () {
    originalPushState.apply(this, arguments)
    checkForChanges()
    insertDownloadIcon()
  }

  history.replaceState = function () {
    originalReplaceState.apply(this, arguments)
    checkForChanges()
    insertDownloadIcon()
  }

  window.addEventListener("popstate", () => {
    checkForChanges()
    insertDownloadIcon()
  })
})()