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-03-24 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴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      2.7
// @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==

;(() => {
  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: (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 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);
        backdrop-filter: blur(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: #0f172a;
        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 = "No"
    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 = "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: () => {
          reject(new Error("Network error"))
        },
      })
    })
  }

  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 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);
        backdrop-filter: blur(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: #0f172a;
        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")
    const mediaTypeLabelText = getMediaTypeLabel(settings.mediaType).toLowerCase()
    fetchButton.textContent =
      settings.mediaType === "all"
        ? "Fetch Media"
        : `Fetch ${mediaTypeLabelText.charAt(0).toUpperCase() + mediaTypeLabelText.slice(1)}`

    fetchButton.style.cssText = `
        background-color: #22c55e;
        color: white;
        border: none;
        border-radius: 9999px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        margin-top: 16px;
        margin-bottom: 16px;
        width: 50%;
        margin-left: auto;
        margin-right: auto;
        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: #192734;
        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.appendChild(downloadCurrentButton)
      buttonContainer.appendChild(downloadAllButton)
    } 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: #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(batchButtonsContainer)
    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"

      if (newSettings.batchEnabled) {
        if (buttonContainer.contains(downloadButton)) {
          buttonContainer.removeChild(downloadButton)
          buttonContainer.appendChild(downloadCurrentButton)
          buttonContainer.appendChild(downloadAllButton)
          buttonContainer.style.display = "flex"
        }
      } else {
        if (buttonContainer.contains(downloadCurrentButton)) {
          buttonContainer.removeChild(downloadCurrentButton)
          buttonContainer.removeChild(downloadAllButton)
          buttonContainer.appendChild(downloadButton)
          buttonContainer.style.display = "block"
        }
      }
    })

    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 = Number.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)

      const newMediaTypeLabel = getMediaTypeLabel(newSettings.mediaType).toLowerCase()
      fetchButton.textContent =
        newSettings.mediaType === "all"
          ? "Fetch Media"
          : `Fetch ${newMediaTypeLabel.charAt(0).toUpperCase() + newMediaTypeLabel.slice(1)}`
    })

    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 = Number.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:"
    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 = Number.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: 50%;
        margin-left: auto;
        margin-right: auto;
        display: block;
        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 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 + "Twitter/X Media Batch Downloader"

    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)

    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.7);
                backdrop-filter: blur(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: #0f172a;
                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 = "Authentication Required"

        const content = document.createElement("div")
        content.style.cssText = `
                padding: 16px;
                text-align: center;
            `
        content.textContent = "Please input your Auth Token"

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

        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.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"

          if (settings.batchEnabled) {
            buttonContainer.style.display = "flex"
          } else {
            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()
          fetchButton.textContent =
            settings.mediaType === "all"
              ? "Fetch Media"
              : `Fetch ${currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
        } else {
          infoContainer.innerHTML = '<div style="color: #ef4444;">No media found or invalid token</div>'
          infoContainer.style.display = "block"
          fetchButton.disabled = false
          const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
          fetchButton.textContent =
            settings.mediaType === "all"
              ? "Fetch Media"
              : `Fetch ${currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
        }
      } 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
        const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
        fetchButton.textContent =
          settings.mediaType === "all"
            ? "Fetch Media"
            : `Fetch ${currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
      }
    })

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

            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) {
            console.error(`Error downloading single file:`, 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 = {}

        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}_${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
        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) {
      console.log("Detected navigation change:", lastUsername, "->", currentUsername)
      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()
  })
})()