Twitter/X Media Batch Downloader

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

// ==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.3
// @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", "18")
    svg.setAttribute("height", "18")
    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://www.patreon.com/posts/127206894"
        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()
  })
})()