Twitter/X Media Batch Downloader

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

目前為 2025-06-26 提交的版本,檢視 最新版本

// ==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      4.0
// @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      backup.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 JSZip = window.JSZip;

  const API_URLS = {
    DEFAULT: "https://api.gallerydl.web.id",
    BACKUP: "https://backup.gallerydl.web.id"
  }

  const ICONS = {
    PREV: `<svg style="width: 12px; height: 12px; margin-right: 6px; display: inline-block; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
      <path fill="currentColor" d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/>
    </svg>`,
    NEXT: `<svg style="width: 12px; height: 12px; margin-left: 6px; display: inline-block; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
      <path fill="currentColor" d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/>
    </svg>`,
    AUTO: `<svg style="width: 12px; height: 12px; margin-right: 6px; display: inline-block; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
      <path fill="currentColor" d="M327.5 85.2c-4.5 1.7-7.5 6-7.5 10.8s3 9.1 7.5 10.8L384 128l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L448 128l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L448 64 426.8 7.5C425.1 3 420.8 0 416 0s-9.1 3-10.8 7.5L384 64 327.5 85.2zM205.1 73.3c-2.6-5.7-8.3-9.3-14.5-9.3s-11.9 3.6-14.5 9.3L123.3 187.3 9.3 240C3.6 242.6 0 248.3 0 254.6s3.6 11.9 9.3 14.5l114.1 52.7L176 435.8c2.6 5.7 8.3 9.3 14.5 9.3s11.9-3.6 14.5-9.3l52.7-114.1 114.1-52.7c5.7-2.6 9.3-8.3 9.3-14.5s-3.6-11.9-9.3-14.5L257.8 187.4 205.1 73.3zM384 384l-56.5 21.2c-4.5 1.7-7.5 6-7.5 10.8s3 9.1 7.5 10.8L384 448l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L448 448l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L448 384l-21.2-56.5c-1.7-4.5-6-7.5-10.8-7.5s-9.1 3-10.8 7.5L384 384z"/>
    </svg>`,
    STOP: `<svg style="width: 12px; height: 12px; margin-right: 6px; display: inline-block; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
      <path fill="currentColor" d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/>
    </svg>`,
    CLEAR: {
      PATH: "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",
      VIEWBOX: "0 0 448 512"
    },
    RESET: {
      PATH: "M463.5 224l8.5 0c13.3 0 24-10.7 24-24l0-128c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8l119.5 0z",
      VIEWBOX: "0 0 512 512"
    },
    PATREON: {
      PATH: "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",
      VIEWBOX: "0 0 512 512"
    },
    INFO: {
      PATH: "M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336c-13.3 0-24 10.7-24 24s10.7 24 24 24l80 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-8 0 0-88c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l24 0 0 64-24 0zm40-144a32 32 0 1 0 0-64 32 32 0 1 0 0 64z",
      VIEWBOX: "0 0 512 512"
    },
    DOWNLOAD: {
      SECONDARY_PATH: "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",
      PRIMARY_PATH: "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",
      VIEWBOX: "0 0 512 512"
    },
    FETCH: {
      PATH: "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",
      VIEWBOX: "0 0 448 512"
    },
    ACCOUNT: {
      PATH: "M412.1 416.6C398.1 361.1 347.9 320 288 320l-64 0c-59.9 0-110.1 41.1-124.1 96.6C58 375.9 32 319 32 256C32 132.3 132.3 32 256 32s224 100.3 224 224c0 63-26 119.9-67.9 160.6zm-28.5 23.4C347.5 465.2 303.5 480 256 480s-91.5-14.8-127.7-39.9c4-49.3 45.3-88.1 95.7-88.1l64 0c50.4 0 91.6 38.8 95.7 88.1zM256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-256a48 48 0 1 1 0-96 48 48 0 1 1 0 96zm-80-48a80 80 0 1 0 160 0 80 80 0 1 0 -160 0z",
      VIEWBOX: "0 0 512 512"
    },
    TOTAL: {
      PATH: "M320 464c8.8 0 16-7.2 16-16l0-288-80 0c-17.7 0-32-14.3-32-32l0-80L64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l256 0zM0 64C0 28.7 28.7 0 64 0L229.5 0c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3L384 448c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64z",
      VIEWBOX: "0 0 384 512"
    },
    BATCH: {
      PATH: "M48 272L48 64c0-8.8 7.2-16 16-16l160 0 0 80c0 17.7 14.3 32 32 32l80 0 0 112L48 272zm288 48l16 0 32 0 0-165.5c0-17-6.7-33.3-18.7-45.3L274.7 18.7C262.7 6.7 246.5 0 229.5 0L64 0C28.7 0 0 28.7 0 64L0 320l32 0 16 0 288 0zM0 352l0 64 48 0 0-64L0 352zM64 512l0-48c-8.8 0-16-7.2-16-16L0 448c0 35.3 28.7 64 64 64zm256-48l0 48c35.3 0 64-28.7 64-64l-48 0c0 8.8-7.2 16-16 16zm64-112l-48 0 0 64 48 0 0-64zM96 464l0 48 80 0 0-48-80 0zm112 0l0 48 80 0 0-48-80 0z",
      VIEWBOX: "0 0 384 512"
    },
    ZIP: {
      PATH: "M64 464c-8.8 0-16-7.2-16-16L48 64c0-8.8 7.2-16 16-16l48 0c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l48 0 0 80c0 17.7 14.3 32 32 32l80 0 0 288c0 8.8-7.2 16-16 16L64 464zM64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-293.5c0-17-6.7-33.3-18.7-45.3L274.7 18.7C262.7 6.7 246.5 0 229.5 0L64 0zm48 112c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm0 64c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm-6.3 71.8L82.1 335.9c-1.4 5.4-2.1 10.9-2.1 16.4c0 35.2 28.8 63.7 64 63.7s64-28.5 64-63.7c0-5.5-.7-11.1-2.1-16.4l-23.5-88.2c-3.7-14-16.4-23.8-30.9-23.8l-14.8 0c-14.5 0-27.2 9.7-30.9 23.8zM128 336l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z",
      VIEWBOX: "0 0 384 512"
    },
    MEDIA_TYPE: {
      ALL: {
        PATH: "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",
        VIEWBOX: "0 0 640 512"
      },
      IMAGE: {
        PATH: "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",
        VIEWBOX: "0 0 512 512"
      },
      VIDEO: {
        PATH: "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",
        VIEWBOX: "0 0 512 512"
      },
      GIF: {
        PATH: "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",
        VIEWBOX: "0 0 576 512"
      }
    }
  }

  const defaultSettings = {
    patreonAuth: "",
    authToken: "",
    batchEnabled: false,
    autoBatchEnabled: false,
    batchSize: 100,
    startingBatch: 0,
    timelineType: "media",
    mediaType: "all",
    concurrentDownloads: 50,
    cacheDuration: 360,
    apiServer: "default",
    darkTheme: false,
  }
  const batchSizes = [25, 50, 100, 200]
  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),
      autoBatchEnabled: GM_getValue("autoBatchEnabled", defaultSettings.autoBatchEnabled),
      batchSize: GM_getValue("batchSize", defaultSettings.batchSize),
      startingBatch: GM_getValue("startingBatch", defaultSettings.startingBatch),
      timelineType: GM_getValue("timelineType", defaultSettings.timelineType),
      mediaType: GM_getValue("mediaType", defaultSettings.mediaType),
      concurrentDownloads: GM_getValue("concurrentDownloads", defaultSettings.concurrentDownloads),
      cacheDuration: GM_getValue("cacheDuration", defaultSettings.cacheDuration),
      apiServer: GM_getValue("apiServer", defaultSettings.apiServer),
      darkTheme: GM_getValue("darkTheme", defaultSettings.darkTheme),
    }
  }

  function saveSettings(settings) {
    GM_setValue("patreonAuth", settings.patreonAuth)
    GM_setValue("authToken", settings.authToken)
    GM_setValue("batchEnabled", settings.batchEnabled)
    GM_setValue("autoBatchEnabled", settings.autoBatchEnabled)
    GM_setValue("batchSize", settings.batchSize)
    GM_setValue("startingBatch", settings.startingBatch)
    GM_setValue("timelineType", settings.timelineType)
    GM_setValue("mediaType", settings.mediaType)
    GM_setValue("concurrentDownloads", settings.concurrentDownloads)
    GM_setValue("cacheDuration", settings.cacheDuration)
    GM_setValue("apiServer", settings.apiServer)
    GM_setValue("darkTheme", settings.darkTheme)
  }

  function getServiceBaseUrl() {
    const settings = getSettings()
    return settings.apiServer === "default"
      ? API_URLS.DEFAULT
      : API_URLS.BACKUP
  }

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

  const cacheManager = {
    set: (key, data, success = true) => {
      if (!success) return;

      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", ICONS.DOWNLOAD.VIEWBOX)
    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", ICONS.DOWNLOAD.SECONDARY_PATH)
    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", ICONS.DOWNLOAD.PRIMARY_PATH)
    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", ICONS.PATREON.VIEWBOX)
    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", ICONS.PATREON.PATH)
    svg.appendChild(path)

    return svg
  }

  function createInfoIcon() {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", ICONS.INFO.VIEWBOX)
    svg.setAttribute("width", "16")
    svg.setAttribute("height", "16")
    svg.style.marginLeft = "8px"
    svg.style.cursor = "pointer"
    svg.style.color = "#64748b"
    svg.style.transition = "color 0.2s ease"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", ICONS.INFO.PATH)
    svg.appendChild(path)

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

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

    return svg
  }

  function createInfoTooltip(message) {
    let activeTooltip = null

    function showTooltip(e) {
      if (activeTooltip) {
        document.body.removeChild(activeTooltip)
      }

      const tooltip = document.createElement("div")
      tooltip.textContent = message
      tooltip.style.cssText = `
        position: fixed;
        background-color: #1f2937;
        color: white;
        padding: 8px 12px;
        border-radius: 6px;
        font-size: 12px;
        line-height: 1.4;
        max-width: 300px;
        z-index: 10003;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        pointer-events: none;
        white-space: normal;
        word-wrap: break-word;
      `

      document.body.appendChild(tooltip)
      activeTooltip = tooltip

      const rect = e.target.getBoundingClientRect()
      const tooltipRect = tooltip.getBoundingClientRect()

      let top = rect.bottom + 8
      let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2)

      if (left < 8) left = 8
      if (left + tooltipRect.width > window.innerWidth - 8) {
        left = window.innerWidth - tooltipRect.width - 8
      }
      if (top + tooltipRect.height > window.innerHeight - 8) {
        top = rect.top - tooltipRect.height - 8
      }

      tooltip.style.top = top + "px"
      tooltip.style.left = left + "px"
    }

    function hideTooltip() {
      if (activeTooltip) {
        document.body.removeChild(activeTooltip)
        activeTooltip = null
      }
    }

    return { showTooltip, hideTooltip }
  }

  function createAccountIcon(isDarkTheme = false) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", ICONS.ACCOUNT.VIEWBOX)
    svg.setAttribute("width", "12")
    svg.setAttribute("height", "12")
    svg.style.marginRight = "6px"
    svg.style.color = isDarkTheme ? "#ffffff" : "#64748b"
    svg.style.verticalAlign = "middle"
    svg.style.display = "inline-block"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", ICONS.ACCOUNT.PATH)
    svg.appendChild(path)

    return svg
  }

  function createTotalItemsIcon(isDarkTheme = false) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", ICONS.TOTAL.VIEWBOX)
    svg.setAttribute("width", "12")
    svg.setAttribute("height", "12")
    svg.style.marginRight = "6px"
    svg.style.color = isDarkTheme ? "#ffffff" : "#64748b"
    svg.style.verticalAlign = "middle"
    svg.style.display = "inline-block"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", ICONS.TOTAL.PATH)
    svg.appendChild(path)

    return svg
  }

  function createCurrentBatchIcon(isDarkTheme = false) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", ICONS.BATCH.VIEWBOX)
    svg.setAttribute("width", "12")
    svg.setAttribute("height", "12")
    svg.style.marginRight = "6px"
    svg.style.color = isDarkTheme ? "#ffffff" : "#64748b"
    svg.style.verticalAlign = "middle"
    svg.style.display = "inline-block"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", ICONS.BATCH.PATH)
    svg.appendChild(path)

    return svg
  }

  function createTotalZipIcon(isDarkTheme = false) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", ICONS.ZIP.VIEWBOX)
    svg.setAttribute("width", "12")
    svg.setAttribute("height", "12")
    svg.style.marginRight = "6px"
    svg.style.color = isDarkTheme ? "#ffffff" : "#64748b"
    svg.style.verticalAlign = "middle"
    svg.style.display = "inline-block"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", ICONS.ZIP.PATH)
    svg.appendChild(path)

    return svg
  }

  function createMediaTypeIcon(mediaType, isDarkTheme = false) {
    let iconData
    switch (mediaType) {
      case 'image':
        iconData = ICONS.MEDIA_TYPE.IMAGE
        break
      case 'video':
        iconData = ICONS.MEDIA_TYPE.VIDEO
        break
      case 'gif':
        iconData = ICONS.MEDIA_TYPE.GIF
        break
      default:
        iconData = ICONS.MEDIA_TYPE.ALL
    }

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    svg.setAttribute("viewBox", iconData.VIEWBOX)
    svg.setAttribute("width", "12")
    svg.setAttribute("height", "12")
    svg.style.marginRight = "6px"
    svg.style.color = isDarkTheme ? "#ffffff" : "#64748b"
    svg.style.verticalAlign = "middle"
    svg.style.display = "inline-block"

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
    path.setAttribute("fill", "currentColor")
    path.setAttribute("d", iconData.PATH)
    svg.appendChild(path)

    return svg
  }

  function createAuthTokenPopup() {
    const settings = getSettings()
    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: ${settings.darkTheme ? "#1f2937" : "#ffffff"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
        font-weight: bold;
        font-size: 16px;
        text-align: center;
        background-color: ${settings.darkTheme ? "#374151" : "#f8fafc"};
    `
    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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
    `

    const okButton = document.createElement("button")
    okButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 6px;
        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)
    }

    buttonContainer.appendChild(okButton)
    popup.appendChild(header)
    popup.appendChild(content)
    popup.appendChild(buttonContainer)
    overlay.appendChild(popup)
    document.body.appendChild(overlay)
    return overlay
  }

  function createPatreonAuthPopup() {
    const settings = getSettings()
    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: ${settings.darkTheme ? "#1f2937" : "#ffffff"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#0f172a"};
        border-radius: 16px;
        width: 320px;
        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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
        font-weight: bold;
        font-size: 16px;
        text-align: center;
        background-color: ${settings.darkTheme ? "#374151" : "#f8fafc"};
    `
    header.textContent = "Patreon Authentication Required"

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

    const message = document.createElement("p")
    message.style.cssText = `
        margin-bottom: 16px;
        line-height: 1.5;
    `
    message.textContent = "Please enter your Patreon authentication code. This feature requires a paid membership to access."
    content.appendChild(message)

    const patreonButton = document.createElement("a")
    patreonButton.href = "https://www.patreon.com/exyezed"
    patreonButton.target = "_blank"
    patreonButton.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: ${settings.darkTheme ? "#374151" : "#f1f5f9"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#0f172a"};
        text-decoration: none;
        padding: 10px 16px;
        border-radius: 8px;
        margin-top: 8px;
        transition: background-color 0.2s;
    `
    patreonButton.innerHTML = createPatreonIcon().outerHTML + "Join Patreon Membership"
    patreonButton.addEventListener("mouseenter", () => {
      patreonButton.style.backgroundColor = settings.darkTheme ? "#4b5563" : "#e2e8f0"
    })
    patreonButton.addEventListener("mouseleave", () => {
      patreonButton.style.backgroundColor = settings.darkTheme ? "#374151" : "#f1f5f9"
    })
    content.appendChild(patreonButton)

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

    const okButton = document.createElement("button")
    okButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 6px;
        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)
    }

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

    document.body.appendChild(overlay)
    return overlay }

  function createInfoDialog(message, title = "Information") {
    const settings = getSettings()
    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: ${settings.darkTheme ? "#1f2937" : "#ffffff"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
        font-weight: bold;
        font-size: 16px;
        text-align: center;
        background-color: ${settings.darkTheme ? "#374151" : "#f8fafc"};
    `
    header.textContent = title

    const content = document.createElement("div")
    content.style.cssText = `
        padding: 20px 16px;
        text-align: center;
        line-height: 1.5;
    `
    content.textContent = message

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

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

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

    document.body.appendChild(overlay)
    return overlay
  }

  function createConfirmDialog(message, onConfirm, onCancel) {
    const settings = getSettings()
    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: ${settings.darkTheme ? "#1f2937" : "#ffffff"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
        font-weight: bold;
        font-size: 16px;
        text-align: center;
        background-color: ${settings.darkTheme ? "#374151" : "#f8fafc"};
    `
    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 ${settings.darkTheme ? "#374151" : "#e2e8f0"};
    `

    const cancelButton = document.createElement("button")
    cancelButton.style.cssText = `
        flex: 1;
        background-color: ${settings.darkTheme ? "#374151" : "#94a3b8"};
        color: white;
        border: none;
        border-radius: 6px;
        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 = settings.darkTheme ? "#4b5563" : "#64748b"
    })
    cancelButton.addEventListener("mouseleave", () => {
      cancelButton.style.backgroundColor = settings.darkTheme ? "#374151" : "#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: 6px;
        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 settings = getSettings()
    const toggleWrapper = document.createElement("div")
    toggleWrapper.style.cssText = `
      position: relative;
      height: 40px;
      background-color: ${settings.darkTheme ? "#374151" : "#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", ICONS.MEDIA_TYPE.ALL.VIEWBOX)
    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", ICONS.MEDIA_TYPE.ALL.PATH)
    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", ICONS.MEDIA_TYPE.IMAGE.VIEWBOX)
    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", ICONS.MEDIA_TYPE.IMAGE.PATH)
    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", ICONS.MEDIA_TYPE.VIDEO.VIEWBOX)
    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", ICONS.MEDIA_TYPE.VIDEO.PATH)
    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", ICONS.MEDIA_TYPE.GIF.VIEWBOX)
    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", ICONS.MEDIA_TYPE.GIF.PATH)
    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) {
    let autoBatchStarted = false
    let autoBatchCancelled = false
    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: ${settings.darkTheme ? "#1f2937" : "#ffffff"};
    color: ${settings.darkTheme ? "#f1f5f9" : "#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 ${settings.darkTheme ? "#374151" : "#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: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
    `

    const closeButton = document.createElement("button")
    closeButton.innerHTML = "&times;"
    closeButton.style.cssText = `
        background: none;
        border: none;
        color: ${settings.darkTheme ? "#f1f5f9" : "#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 = settings.darkTheme ? "#f1f5f9" : "#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 ${settings.darkTheme ? "#374151" : "#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;
        color: ${settings.darkTheme ? "#f1f5f9" : "#0f172a"};
    `

    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", ICONS.FETCH.VIEWBOX);
    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", ICONS.FETCH.PATH);
    fetchIcon.appendChild(fetchPath);

    const fetchButtonText = document.createElement("span");
    fetchButtonText.textContent =
      settings.autoBatchEnabled
        ? "Auto Fetch"
        : 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: 6px;
        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: ${settings.darkTheme ? "#374151" : "#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 batchNavContainer = document.createElement("div");
    batchNavContainer.style.cssText = `
        display: none;
        flex-direction: column;
        gap: 8px;
        margin-bottom: 16px;
    `;

    const prevBatchButton = document.createElement("button");
    prevBatchButton.innerHTML = `<span style="display: flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;">${ICONS.PREV}<span>Prev Batch</span></span>`;
    prevBatchButton.style.cssText = `
        background-color: #6366f1;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        width: 40%;
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
        transition: background-color 0.2s;
    `;

    prevBatchButton.addEventListener("mouseenter", () => {
      prevBatchButton.style.backgroundColor = "#4f46e5";
    });
    prevBatchButton.addEventListener("mouseleave", () => {
      prevBatchButton.style.backgroundColor = "#6366f1";
    });

    const nextBatchButton = document.createElement("button");
    nextBatchButton.innerHTML = `<span style="display: flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;"><span>Next Batch</span>${ICONS.NEXT}</span>`;
    nextBatchButton.style.cssText = `
        background-color: #6366f1;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        width: 40%;
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
        transition: background-color 0.2s;
    `;

    nextBatchButton.addEventListener("mouseenter", () => {
      nextBatchButton.style.backgroundColor = "#4f46e5";
    });
    nextBatchButton.addEventListener("mouseleave", () => {
      nextBatchButton.style.backgroundColor = "#6366f1";
    });

    const buttonWrapper = document.createElement("div");
      buttonWrapper.style.cssText = `
        display: flex;
        gap: 8px;
        justify-content: center;
        width: 100%;
    `;

    buttonWrapper.appendChild(prevBatchButton);
    buttonWrapper.appendChild(nextBatchButton);
    batchNavContainer.appendChild(buttonWrapper);

    const stopFetchButton = document.createElement("button");
    stopFetchButton.innerHTML = `<span style="display: flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;">${ICONS.STOP}<span>Stop Fetch</span></span>`;

    stopFetchButton.style.cssText = `
        background-color: #ef4444;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        width: 140px; /* Fixed width instead of percentage */
        display: none;
        align-items: center;
        justify-content: center;
        text-align: center;
        transition: background-color 0.2s;
    `;

    stopFetchButton.addEventListener("mouseenter", () => { stopFetchButton.style.backgroundColor = "#dc2626"; });
    stopFetchButton.addEventListener("mouseleave", () => { stopFetchButton.style.backgroundColor = "#ef4444"; });
    stopFetchButton.addEventListener("click", () => {
        autoBatchCancelled = true;
        autoBatchStarted = false;
        batchNavContainer.style.display = "flex";
        prevBatchButton.style.display = mediaData.currentPage > getSettings().startingBatch ? "block" : "none";
        nextBatchButton.style.display = mediaData.hasMore ? "block" : "none";
        stopFetchButton.style.display = "none";
        batchInfoContainer.style.display = "flex";
        infoContainer.style.display = "block";
        fetchButtonText.textContent = "Auto Fetch";
    });

    const stopFetchWrapper = document.createElement("div");
    stopFetchWrapper.style.cssText = `
        display: flex;
        justify-content: center;
        width: 100%;
        margin-top: 8px; /* Add spacing between button rows */
    `;
    stopFetchWrapper.appendChild(stopFetchButton);
    batchNavContainer.appendChild(stopFetchWrapper);

    const batchInfoContainer = document.createElement("div");
    batchInfoContainer.style.cssText = `
        display: none;
        flex-direction: column;
        gap: 8px;
        margin-bottom: 16px;
        padding: 12px;
        background-color: ${settings.darkTheme ? "#374151" : "#f8fafc"};
        border-radius: 8px;
        border: 1px solid ${settings.darkTheme ? "#4b5563" : "#e2e8f0"};
    `

    const batchStatsRow = document.createElement("div")
    batchStatsRow.style.cssText = `
        display: flex;
        justify-content: space-between;
        align-items: center;
    `

    const currentBatchLabel = document.createElement("div")
    currentBatchLabel.style.cssText = `
        font-size: 14px;
        color: ${settings.darkTheme ? "#ffffff" : "#475569"};
    `

    const totalBatchLabel = document.createElement("div")
    totalBatchLabel.style.cssText = `
        font-size: 14px;
        color: ${settings.darkTheme ? "#ffffff" : "#475569"};
    `

    batchStatsRow.appendChild(currentBatchLabel)
    batchStatsRow.appendChild(totalBatchLabel)

    const downloadButtonsRow = document.createElement("div")
    downloadButtonsRow.style.cssText = `
        display: flex;
        gap: 8px;
        justify-content: center;
    `

    const downloadCurrentButton = document.createElement("button")
    downloadCurrentButton.innerHTML = ""
    const currentIcon = createDownloadIcon()
    currentIcon.style.width = "12px"
    currentIcon.style.height = "12px"
    currentIcon.style.marginRight = "4px"
    const currentText = document.createElement("span")
    currentText.textContent = "Download Current"
    downloadCurrentButton.appendChild(currentIcon)
    downloadCurrentButton.appendChild(currentText)
    downloadCurrentButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 6px 12px;
        font-weight: bold;
        cursor: pointer;
        font-size: 12px;
        transition: background-color 0.2s;
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
    `
    downloadCurrentButton.addEventListener("mouseenter", () => {
      downloadCurrentButton.style.backgroundColor = "#0284c7"
    })
    downloadCurrentButton.addEventListener("mouseleave", () => {
      downloadCurrentButton.style.backgroundColor = "#0ea5e9"
    })

    const downloadAllButton = document.createElement("button")
    downloadAllButton.innerHTML = ""
    const allIcon = createDownloadIcon()
    allIcon.style.width = "12px"
    allIcon.style.height = "12px"
    allIcon.style.marginRight = "4px"
    const allText = document.createElement("span")
    allText.textContent = "Download All"
    downloadAllButton.appendChild(allIcon)
    downloadAllButton.appendChild(allText)
    downloadAllButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 6px 12px;
        font-weight: bold;
        cursor: pointer;
        font-size: 12px;
        transition: background-color 0.2s;
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
    `;
    downloadAllButton.addEventListener("mouseenter", () => {
      downloadAllButton.style.backgroundColor = "#0284c7";
    });
    downloadAllButton.addEventListener("mouseleave", () => {
      downloadAllButton.style.backgroundColor = "#0ea5e9";
    });

    downloadButtonsRow.appendChild(downloadCurrentButton);
    downloadButtonsRow.appendChild(downloadAllButton);

    batchInfoContainer.appendChild(batchStatsRow);
    batchInfoContainer.appendChild(downloadButtonsRow);

    const downloadButton = document.createElement("button");
    const downloadIcon = createDownloadIcon();
    downloadIcon.style.width = "16px";
    downloadIcon.style.height = "16px";
    downloadIcon.style.marginRight = "8px";

    downloadButton.innerHTML = "";
    downloadButton.appendChild(downloadIcon);
    downloadButton.appendChild(document.createTextNode("Download"));
    downloadButton.style.cssText = `
        background-color: #0ea5e9;
        color: white;
        border: none;
        border-radius: 6px;
        padding: 8px 16px;
        font-weight: bold;
        cursor: pointer;
        width: 50%;
        margin-left: auto;
        margin-right: auto;
        display: flex;
        align-items: center;
        justify-content: center;
        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 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: ${settings.darkTheme ? "#374151" : "#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(batchInfoContainer)
    mainContent.appendChild(batchNavContainer)
    mainContent.appendChild(buttonContainer)
    mainContent.appendChild(batchButtonsContainer)
    mainContent.appendChild(progressContainer);

    const settingsTabs = document.createElement("div")
    settingsTabs.style.cssText = `
        display: flex;
        border-bottom: 1px solid ${settings.darkTheme ? "#374151" : "#e2e8f0"};
        margin-bottom: 16px;
        width: 100%;
    `

    const fetchTab = document.createElement("button")
    fetchTab.textContent = "Fetch"
    fetchTab.style.cssText = `
        background: none;
        border: none;
        padding: 12px 16px;
        cursor: pointer;
        color: ${settings.darkTheme ? "#f1f5f9" : "#0f172a"};
        border-bottom: 2px solid #0ea5e9;
        font-weight: bold;
        font-size: 14px;
        flex: 1;
        text-align: center;
    `

    const authTab = document.createElement("button")
    authTab.textContent = "Auth"
    authTab.style.cssText = `
        background: none;
        border: none;
        padding: 12px 16px;
        cursor: pointer;
        color: #64748b;
        border-bottom: none;
        font-weight: bold;
        font-size: 14px;
        flex: 1;
        text-align: center;
    `

    const advancedTab = document.createElement("button")
    advancedTab.textContent = "Advanced"
    advancedTab.style.cssText = `
        background: none;
        border: none;
        padding: 12px 16px;
        cursor: pointer;
        color: #64748b;
        border-bottom: none;
        font-weight: bold;
        font-size: 14px;
        flex: 1;
        text-align: center;    `;
    
    settingsTabs.appendChild(fetchTab);
    settingsTabs.appendChild(authTab);
    settingsTabs.appendChild(advancedTab);

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

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

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

    const advancedTabContent = document.createElement("div")
    advancedTabContent.style.cssText = `
        display: none;
        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: ${settings.darkTheme ? "#f1f5f9" : "#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: ${settings.darkTheme ? "#374151" : "#f1f5f9"};
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 8px 12px;
    color: ${settings.darkTheme ? "#cbd5e1" : "#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: ${settings.darkTheme ? "#f1f5f9" : "#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: ${settings.darkTheme ? "#374151" : "#f1f5f9"};
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 8px 12px;
    color: ${settings.darkTheme ? "#cbd5e1" : "#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 apiServerGroup = document.createElement("div")
    apiServerGroup.style.cssText = `
        display: flex;
        flex-direction: column;
        gap: 8px;
    `

    const apiServerLabel = document.createElement("label")
    apiServerLabel.textContent = "Service:"
    apiServerLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
    `

    const apiServerOptions = [
      { value: "default", label: "Default" },
      { value: "backup", label: "Backup" }
    ]

    const apiServerToggle = createToggleSwitch(apiServerOptions, settings.apiServer, (value) => {
      const newSettings = getSettings()
      newSettings.apiServer = value
      saveSettings(newSettings)
      settings.apiServer = value
    })

    apiServerGroup.appendChild(apiServerLabel)
    apiServerGroup.appendChild(apiServerToggle)

    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: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        flex: 1;
        display: flex;
        align-items: center;
    `
    const batchLabelText = document.createElement("span")
    batchLabelText.textContent = "Batch:"
    batchLabel.appendChild(batchLabelText)

    const batchInfoIcon = createInfoIcon()
    const batchTooltip = createInfoTooltip("When enabled, the media fetching process is performed in batches")
    batchInfoIcon.addEventListener("mouseenter", batchTooltip.showTooltip)
    batchInfoIcon.addEventListener("mouseleave", batchTooltip.hideTooltip)
    batchLabel.appendChild(batchInfoIcon)

    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"
        autoBatchGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
        batchSizeGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
        startingBatchGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
        if (!newSettings.batchEnabled) {
            newSettings.autoBatchEnabled = false
            saveSettings(newSettings)
            autoBatchToggle.style.backgroundColor = "#cbd5e1"
            autoBatchHandle.style.left = "2px"
            const mt = getSettings().mediaType
            const mtLabel = getMediaTypeLabel(mt).toLowerCase()
            const labelText = mt === "all" ? "Fetch Media" : `Fetch ${mtLabel === "gif" ? "GIF" : mtLabel.charAt(0).toUpperCase() + mtLabel.slice(1)}`

            fetchButtonText.textContent = labelText
            fetchButton.innerHTML = ""
            let updatedFetchIcon = fetchIcon.cloneNode(true)
            fetchButton.appendChild(updatedFetchIcon)
            fetchButton.appendChild(fetchButtonText)
            if (typeof stopFetchButton !== 'undefined') stopFetchButton.style.display = "none"
            if (typeof prevBatchButton !== 'undefined') prevBatchButton.style.display = "block"
            if (typeof nextBatchButton !== 'undefined') nextBatchButton.style.display = "block"
        }
    })

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

    const autoBatchGroup = document.createElement("div")
    autoBatchGroup.style.cssText = `
        display: ${settings.batchEnabled ? "flex" : "none"};
        align-items: center;
        gap: 8px;
    `
    const autoBatchLabel = document.createElement("label")
    autoBatchLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        flex: 1;
        display: flex;
        align-items: center;
    `
    autoBatchLabel.textContent = "Auto Fetch"
    const autoBatchToggle = document.createElement("div")
    autoBatchToggle.style.cssText = `
        position: relative;
        width: 50px;
        height: 24px;
        background-color: ${settings.autoBatchEnabled ? "#22c55e" : "#cbd5e1"};
        border-radius: 12px;
        cursor: pointer;
        transition: background-color 0.3s;
    `
    const autoBatchHandle = document.createElement("div")
    autoBatchHandle.style.cssText = `
        position: absolute;
        top: 2px;
        left: ${settings.autoBatchEnabled ? "28px" : "2px"};
        width: 20px;
        height: 20px;
        background-color: white;
        border-radius: 50%;
        transition: left 0.3s;
    `
    autoBatchToggle.appendChild(autoBatchHandle)
    autoBatchToggle.addEventListener("click", () => {
        const newSettings = getSettings()
        const willEnable = !newSettings.autoBatchEnabled
        newSettings.autoBatchEnabled = willEnable
        saveSettings(newSettings)
        autoBatchCancelled = false
        autoBatchStarted = false
        autoBatchToggle.style.backgroundColor = willEnable ? "#22c55e" : "#cbd5e1"
        autoBatchHandle.style.left = willEnable ? "28px" : "2px"
          const label = willEnable
            ? "Auto Fetch"
            : (newSettings.mediaType === "all"
                ? "Fetch Media"
                : newSettings.mediaType === "image"
                    ? "Fetch Photos"
                    : "Fetch Videos")
        fetchButtonText.textContent = label

        if (willEnable) {
        const keyPrefix = `twitter_dl_${newSettings.timelineType}_${newSettings.mediaType}_${username}_`
        const keySuffix = `_${newSettings.batchSize}_batch_true`
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i)
            if (key.startsWith(keyPrefix) && key.endsWith(keySuffix)) {
                const pageNum = parseInt(key.slice(keyPrefix.length, key.length - keySuffix.length), 10)
                if (!isNaN(pageNum)) {
                    const cachedData = cacheManager.get(key)
                    if (cachedData && cachedData.timeline) {
                        mediaData.batchData[pageNum] = cachedData.timeline
                    }
                }
            }
        }
        const batchNums = Object.keys(mediaData.batchData).map(Number)
        mediaData.currentPage = batchNums.length > 0 ? Math.max(...batchNums) + 1 : 0
        fetchButtonText.textContent = "Auto Fetch"
    } else {
        const mt = newSettings.mediaType
        const label = mt === "all" ? "Fetch Media" : `Fetch ${getMediaTypeLabel(mt).toUpperCase()}`
        fetchButtonText.textContent = label
    }
    })
    autoBatchGroup.appendChild(autoBatchLabel)
    autoBatchGroup.appendChild(autoBatchToggle)

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

    const batchSizeLabel = document.createElement("label")
    batchSizeLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        display: flex;
        align-items: center;
    `
    const batchSizeLabelText = document.createElement("span")
    batchSizeLabelText.textContent = "Batch Size:"
    batchSizeLabel.appendChild(batchSizeLabelText)

    const batchSizeInfoIcon = createInfoIcon()
    const batchSizeTooltip = createInfoTooltip("Number of media items fetched in a single request")
    batchSizeInfoIcon.addEventListener("mouseenter", batchSizeTooltip.showTooltip)
    batchSizeInfoIcon.addEventListener("mouseleave", batchSizeTooltip.hideTooltip)
    batchSizeLabel.appendChild(batchSizeInfoIcon)

    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 startingBatchGroup = document.createElement("div")
    startingBatchGroup.style.cssText = `
    display: ${settings.batchEnabled ? "flex" : "none"};
    flex-direction: column;
    gap: 8px;
    `

    const startingBatchLabel = document.createElement("label")
    startingBatchLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        display: flex;
        align-items: center;
    `
    const startingBatchLabelText = document.createElement("span")
    startingBatchLabelText.textContent = "Starting Batch:"
    startingBatchLabel.appendChild(startingBatchLabelText)

    const startingBatchInfoIcon = createInfoIcon()
    const startingBatchTooltip = createInfoTooltip("Determines which batch number to start fetching from")
    startingBatchInfoIcon.addEventListener("mouseenter", startingBatchTooltip.showTooltip)
    startingBatchInfoIcon.addEventListener("mouseleave", startingBatchTooltip.hideTooltip)
    startingBatchLabel.appendChild(startingBatchInfoIcon)

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

    startingBatchInput.addEventListener("input", () => {
      const newSettings = getSettings()
      newSettings.startingBatch = parseInt(startingBatchInput.value) || 0
      saveSettings(newSettings)
      settings.startingBatch = newSettings.startingBatch
    })

    startingBatchGroup.appendChild(startingBatchLabel)
    startingBatchGroup.appendChild(startingBatchInput)

    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: ${settings.darkTheme ? "#f1f5f9" : "#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: ${settings.darkTheme ? "#f1f5f9" : "#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.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        display: flex;
        align-items: center;
    `

    const concurrentLabelText = document.createElement("span")
    concurrentLabelText.textContent = "Batch Download Items:"
    concurrentLabel.appendChild(concurrentLabelText)

    const concurrentInfoIcon = createInfoIcon()
    const concurrentTooltip = createInfoTooltip("Total item that are downloaded in a single request")
    concurrentInfoIcon.addEventListener("mouseenter", concurrentTooltip.showTooltip)
    concurrentInfoIcon.addEventListener("mouseleave", concurrentTooltip.hideTooltip)
    concurrentLabel.appendChild(concurrentInfoIcon)
    
    const concurrentToggle = document.createElement("div")
    concurrentToggle.textContent = "Fixed to 50 concurrent downloads"
    concurrentToggle.style.cssText = `
        padding: 10px;
        background-color: ${settings.darkTheme ? "#374151" : "#f1f5f9"};
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        border-radius: 8px;
        text-align: center;
    `

    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: ${settings.darkTheme ? "#f1f5f9" : "#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 darkThemeGroup = document.createElement("div")
    darkThemeGroup.style.cssText = `
        display: flex;
        align-items: center;
        gap: 8px;
    `
    const darkThemeLabel = document.createElement("label")
    darkThemeLabel.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        color: ${settings.darkTheme ? "#f1f5f9" : "#334155"};
        flex: 1;
        display: flex;
        align-items: center;
    `
    const darkThemeLabelText = document.createElement("span")
    darkThemeLabelText.textContent = "Dark Theme:"
    darkThemeLabel.appendChild(darkThemeLabelText)

    const darkThemeToggle = document.createElement("div")
    darkThemeToggle.style.cssText = `
        position: relative;
        width: 50px;
        height: 24px;
        background-color: ${settings.darkTheme ? "#22c55e" : "#cbd5e1"};
        border-radius: 12px;
        cursor: pointer;
        transition: background-color 0.3s;
    `

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

    darkThemeToggle.appendChild(darkThemeToggleHandle)
    darkThemeToggle.addEventListener("click", () => {
        const newSettings = getSettings()
        newSettings.darkTheme = !newSettings.darkTheme
        saveSettings(newSettings)
        settings.darkTheme = newSettings.darkTheme
        darkThemeToggle.style.backgroundColor = newSettings.darkTheme ? "#22c55e" : "#cbd5e1"
        darkThemeToggleHandle.style.left = newSettings.darkTheme ? "28px" : "2px"
        updateTheme()
      })

    darkThemeGroup.appendChild(darkThemeLabel)
    darkThemeGroup.appendChild(darkThemeToggle)

    const buttonsContainer = document.createElement("div")
    buttonsContainer.style.cssText = `
    display: flex;
    gap: 16px;
    margin-top: 16px;
    width: 100%;
    justify-content: center;
    align-items: center;
    `

    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", ICONS.CLEAR.VIEWBOX)
    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", ICONS.CLEAR.PATH)
    trashIcon.appendChild(trashPath)
    clearCacheButton.appendChild(trashIcon)
    clearCacheButton.appendChild(document.createTextNode("Clear Cache"))
    clearCacheButton.style.cssText = `
    background-color: #ef4444;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 8px 16px;
    font-weight: bold;
    cursor: pointer;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    transition: background-color 0.2s;
    `

    const resetDefaultButton = document.createElement("button")
    const resetIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    resetIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
    resetIcon.setAttribute("viewBox", ICONS.RESET.VIEWBOX)
    resetIcon.setAttribute("width", "16")
    resetIcon.setAttribute("height", "16")
    resetIcon.style.marginRight = "8px"

    const resetPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
    resetPath.setAttribute("fill", "currentColor")
    resetPath.setAttribute("d", ICONS.RESET.PATH)
    resetIcon.appendChild(resetPath)

    resetDefaultButton.appendChild(resetIcon)
    resetDefaultButton.appendChild(document.createTextNode("Reset Default"))
    resetDefaultButton.style.cssText = `
    background-color: #6366f1;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 8px 16px;
    font-weight: bold;
    cursor: pointer;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    transition: background-color 0.2s;
    `
    buttonsContainer.appendChild(clearCacheButton)
    buttonsContainer.appendChild(resetDefaultButton)

    clearCacheButton.addEventListener("mouseenter", () => {
      clearCacheButton.style.backgroundColor = "#dc2626"
    })
    clearCacheButton.addEventListener("mouseleave", () => {
      clearCacheButton.style.backgroundColor = "#ef4444"
    })

    resetDefaultButton.addEventListener("mouseenter", () => {
      resetDefaultButton.style.backgroundColor = "#5b56f4"
    })
    resetDefaultButton.addEventListener("mouseleave", () => {
      resetDefaultButton.style.backgroundColor = "#6366f1"
    })
    clearCacheButton.addEventListener("click", () => {
      createConfirmDialog("Are you sure about clearing the cache?", () => {
        cacheManager.clear()

        createInfoDialog("Cache has been successfully cleared.", "Clear Successful");
      });
    });;

    resetDefaultButton.addEventListener("click", () => {
      createConfirmDialog("Are you sure you want to reset all settings to default values?", () => {
        const currentSettings = getSettings()
        const preservedPatreonAuth = currentSettings.patreonAuth
        const preservedAuthToken = currentSettings.authToken

        const resetSettings = { ...defaultSettings }
        resetSettings.patreonAuth = preservedPatreonAuth
        resetSettings.authToken = preservedAuthToken
          saveSettings(resetSettings);

        startingBatchInput.value = defaultSettings.startingBatch;

        batchToggle.style.backgroundColor = defaultSettings.batchEnabled ? "#22c55e" : "#cbd5e1";
        batchToggleHandle.style.left = defaultSettings.batchEnabled ? "28px" : "2px";

        batchSizeGroup.style.display = defaultSettings.batchEnabled ? "flex" : "none";
        startingBatchGroup.style.display = defaultSettings.batchEnabled ? "flex" : "none";
        autoBatchGroup.style.display = defaultSettings.batchEnabled ? "flex" : "none";

        patreonAuthClearButton.style.display = preservedPatreonAuth ? "block" : "none";
        tokenClearButton.style.display = preservedAuthToken ? "block" : "none";

        const timelineTypeOptions = ["media", "timeline", "tweets", "with_replies"];
        const timelineTypeIndex = timelineTypeOptions.indexOf(defaultSettings.timelineType);
        const timelineTypeSlider = timelineTypeToggle.querySelector("div:first-child");
        const timelineTypeContainer = timelineTypeToggle.querySelector("div:last-child");

        if (timelineTypeSlider && timelineTypeContainer && timelineTypeIndex !== -1) {
          timelineTypeSlider.style.transform = `translateX(${timelineTypeIndex * 100}%)`;
          timelineTypeContainer.querySelectorAll("div").forEach((opt, i) => {
            opt.style.color = i === timelineTypeIndex ? "white" : "#64748b";
            const optIcon = opt.querySelector("svg");
            if (optIcon) {
              const optPaths = optIcon.querySelectorAll("path");
              optPaths.forEach((path) => {
                path.setAttribute("fill", i === timelineTypeIndex ? "white" : "#64748b");
              });
            }
          });
        }

        const mediaTypeOptions = ["all", "image", "video", "gif"];
        const mediaTypeIndex = mediaTypeOptions.indexOf(defaultSettings.mediaType);
        const mediaTypeSlider = mediaTypeToggle.querySelector("div:first-child");
        const mediaTypeContainer = mediaTypeToggle.querySelector("div:last-child");

        if (mediaTypeSlider && mediaTypeContainer && mediaTypeIndex !== -1) {
          mediaTypeSlider.style.transform = `translateX(${mediaTypeIndex * 100}%)`;
          mediaTypeContainer.querySelectorAll("div").forEach((opt, i) => {
            opt.style.color = i === mediaTypeIndex ? "white" : "#64748b";
            const optIcon = opt.querySelector("svg");
            if (optIcon) {
              const optPaths = optIcon.querySelectorAll("path");
              optPaths.forEach((path) => {
                path.setAttribute("fill", i === mediaTypeIndex ? "white" : "#64748b");
              });
            }
          });
        }

        const batchSizeIndex = batchSizes.indexOf(defaultSettings.batchSize);
        const batchSizeSlider = batchSizeToggle.querySelector("div:first-child");
        const batchSizeContainer = batchSizeToggle.querySelector("div:last-child");

        if (batchSizeSlider && batchSizeContainer && batchSizeIndex !== -1) {
          batchSizeSlider.style.transform = `translateX(${batchSizeIndex * 100}%)`;
          batchSizeContainer.querySelectorAll("div").forEach((opt, i) => {
            opt.style.color = i === batchSizeIndex ? "white" : "#64748b";
            const optIcon = opt.querySelector("svg");
            if (optIcon) {
              const optPaths = optIcon.querySelectorAll("path");
              optPaths.forEach((path) => {
                path.setAttribute("fill", i === batchSizeIndex ? "white" : "#64748b");
              });
            }
          });
        }

        const cacheDurationIndex = cacheDurations.indexOf(defaultSettings.cacheDuration);
        const cacheDurationSlider = cacheDurationToggle.querySelector("div:first-child");
        const cacheDurationContainer = cacheDurationToggle.querySelector("div:last-child");

        if (cacheDurationSlider && cacheDurationContainer && cacheDurationIndex !== -1) {
          cacheDurationSlider.style.transform = `translateX(${cacheDurationIndex * 100}%)`;
          cacheDurationContainer.querySelectorAll("div").forEach((opt, i) => {
            opt.style.color = i === cacheDurationIndex ? "white" : "#64748b";
            const optIcon = opt.querySelector("svg");
            if (optIcon) {
              const optPaths = optIcon.querySelectorAll("path");
              optPaths.forEach((path) => {
                path.setAttribute("fill", i === cacheDurationIndex ? "white" : "#64748b");
              });
            }
          });
        }
        const apiServerOptions = ["default", "backup"];
        const apiServerIndex = apiServerOptions.indexOf(defaultSettings.apiServer);
        const apiServerSlider = apiServerToggle.querySelector("div:first-child");
        const apiServerContainer = apiServerToggle.querySelector("div:last-child");

        if (apiServerSlider && apiServerContainer && apiServerIndex !== -1) {
          apiServerSlider.style.transform = `translateX(${apiServerIndex * 100}%)`;
          apiServerContainer.querySelectorAll("div").forEach((opt, i) => {
            opt.style.color = i === apiServerIndex ? "white" : "#64748b";
            const optIcon = opt.querySelector("svg");
            if (optIcon) {
              const optPaths = optIcon.querySelectorAll("path");
              optPaths.forEach((path) => {
                path.setAttribute("fill", i === apiServerIndex ? "white" : "#64748b");
              });
            }
          });
        }

        darkThemeToggle.style.backgroundColor = defaultSettings.darkTheme ? "#22c55e" : "#cbd5e1";
        darkThemeToggleHandle.style.left = defaultSettings.darkTheme ? "28px" : "2px";
        settings.darkTheme = defaultSettings.darkTheme;
        updateTheme();

        const newMediaTypeLabel = getMediaTypeLabel(defaultSettings.mediaType).toLowerCase();
        const newFetchButtonText = defaultSettings.mediaType === "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(defaultSettings.mediaType)}: <span style="color: #0ea5e9">${username}</span>`;

        setTimeout(() => {
          createInfoDialog("Settings have been successfully reset to default values.", "Reset Successful");
        }, 100);
      })
    })

    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 = settings.darkTheme ? "#374151" : "#f1f5f9";
      patreonLink.style.color = "#0ea5e9";
    });
    
    patreonLink.addEventListener("mouseleave", () => {
      patreonLink.style.backgroundColor = "transparent";
      patreonLink.style.color = "#64748b";
    });
    
    fetchTabContent.appendChild(batchGroup)
    fetchTabContent.appendChild(autoBatchGroup)
    fetchTabContent.appendChild(batchSizeGroup)
    fetchTabContent.appendChild(startingBatchGroup)
    fetchTabContent.appendChild(mediaTypeGroup)
    fetchTabContent.appendChild(timelineTypeGroup)

    authTabContent.appendChild(patreonAuthGroup)
    authTabContent.appendChild(tokenGroup)
    authTabContent.appendChild(patreonLink)

    advancedTabContent.appendChild(darkThemeGroup);
    advancedTabContent.appendChild(apiServerGroup);
    advancedTabContent.appendChild(cacheDurationGroup);
    advancedTabContent.appendChild(buttonsContainer);    
    
    function switchTab(activeTab, activeContent) {
      const allTabs = [fetchTab, authTab, advancedTab];
      const allContents = [fetchTabContent, authTabContent, advancedTabContent];

      allTabs.forEach(tab => {
        if (tab && tab.style) {
          tab.style.color = "#64748b";
          tab.style.borderBottom = "none";
        }
      });

      allContents.forEach(content => {
        if (content && content.style) {
          content.style.display = "none";
        }
      });

      if (activeTab && activeTab.style) {
        activeTab.style.color = settings.darkTheme ? "#f1f5f9" : "#0f172a";
        activeTab.style.borderBottom = "2px solid #0ea5e9";
      }
      if (activeContent && activeContent.style) {
        activeContent.style.display = "flex";
      }
    }    
    fetchTab.addEventListener("click", function() {
      switchTab(fetchTab, fetchTabContent);
    });
    authTab.addEventListener("click", function() {
      switchTab(authTab, authTabContent);
    });
    advancedTab.addEventListener("click", function() {
      switchTab(advancedTab, advancedTabContent);
    });

    const settingsForm = document.createElement("div");
    settingsForm.style.cssText = `
        display: flex;
        flex-direction: column;
    `;
        
    settingsForm.appendChild(settingsTabs);
    settingsForm.appendChild(fetchTabContent);
    settingsForm.appendChild(authTabContent);
    settingsForm.appendChild(advancedTabContent);

    settingsContent.appendChild(settingsForm);

    mainTab.addEventListener("click", function() {
      mainTab.style.borderBottom = "2px solid #0ea5e9"
      mainTab.style.color = settings.darkTheme ? "#f1f5f9" : "#0f172a"
      settingsTab.style.borderBottom = "none"
      settingsTab.style.color = "#64748b"
      mainContent.style.display = "block"
      settingsContent.style.display = "none"
    });

    settingsTab.addEventListener("click", function() {
      settingsTab.style.borderBottom = "2px solid #0ea5e9"
      settingsTab.style.color = settings.darkTheme ? "#f1f5f9" : "#0f172a"
      mainTab.style.borderBottom = "none"
      mainTab.style.color = "#64748b"
      settingsContent.style.display = "block"
      mainContent.style.display = "none"

      switchTab(fetchTab, fetchTabContent);
    });

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

    const mediaData = {
      username: username,
      currentPage: getSettings().startingBatch,
      mediaItems: [],
      allMediaItems: [],
      batchData: {},
      hasMore: false,
      downloading: false,
      totalDownloaded: 0,
      totalToDownload: 0,
      totalItems: 0,
      nextBatchClicked: false, 
    }

    function refreshInfoDisplays() {
      const isDark = getSettings().darkTheme

      if (infoContainer.innerHTML && mediaData.totalItems > 0) {
        const settings = getSettings()
        const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)

        if (settings.batchEnabled && mediaData.mediaItems && mediaData.mediaItems.length > 0) {
          const accountIconHtml = createAccountIcon(isDark).outerHTML
          const mediaIconHtml = createMediaTypeIcon(settings.mediaType, isDark).outerHTML
          const totalItemsIconHtml = createTotalItemsIcon(isDark).outerHTML

          infoContainer.innerHTML = `
            <div style="margin-bottom: 8px; display: flex; align-items: center;">${accountIconHtml}<strong style="margin-right: 6px;">Account:</strong><span>${mediaData.username}</span></div>
            <div style="margin-bottom: 8px; display: flex; align-items: center;">${mediaIconHtml}<strong style="margin-right: 6px;">${mediaTypeLabel} Found:</strong><span>${formatNumber(mediaData.totalItems)}</span></div>
            <div style="margin-bottom: 8px; display: flex; align-items: center;">${totalItemsIconHtml}<strong style="margin-right: 6px;">Total Items:</strong><span>${formatNumber(mediaData.allMediaItems.length)}</span></div>
          `
        } else if (mediaData.allMediaItems && mediaData.allMediaItems.length > 0) {
          const accountIconHtml = createAccountIcon(isDark).outerHTML
          const mediaIconHtml = createMediaTypeIcon(settings.mediaType, isDark).outerHTML
          const totalZipIconHtml = createTotalZipIcon(isDark).outerHTML
          const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1

          infoContainer.innerHTML = `
            <div style="margin-bottom: 8px; display: flex; align-items: center;">${accountIconHtml}<strong style="margin-right: 6px;">Account:</strong><span>${mediaData.username}</span></div>
            <div style="margin-bottom: 8px; display: flex; align-items: center;">${mediaIconHtml}<strong style="margin-right: 6px;">${mediaTypeLabel} Found:</strong><span>${formatNumber(mediaData.totalItems)}</span></div>
            <div style="margin-top: 8px; display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px;">Total ZIP ${currentPart === 1 ? 'File' : 'Files'}:</strong><span>${currentPart}</span></div>
          `
        }
      }

      if (infoContainer.innerHTML) {
        const accountIcons = infoContainer.querySelectorAll('svg[viewBox="0 0 24 24"]')
        accountIcons.forEach(icon => {
          icon.style.color = isDark ? "#ffffff" : "#64748b"
        })

        const mediaIcons = infoContainer.querySelectorAll('svg[viewBox="0 0 576 512"], svg[viewBox="0 0 512 512"], svg[viewBox="0 0 640 512"]')
        mediaIcons.forEach(icon => {
          icon.style.color = isDark ? "#ffffff" : "#64748b"
        })

        const totalIcons = infoContainer.querySelectorAll('svg[viewBox="0 0 448 512"]')
        totalIcons.forEach(icon => {
          icon.style.color = isDark ? "#ffffff" : "#64748b"
        })
      }
    }

    function updateBatchLabels() {
      const isDark = getSettings().darkTheme
      const settings = getSettings()

      if (currentBatchLabel && currentBatchLabel.innerHTML) {
        const currentBatchIconHtml = createCurrentBatchIcon(isDark).outerHTML
        currentBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${currentBatchIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Current Batch:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${mediaData.currentPage + 1}</span></div>`
      }

      if (totalBatchLabel && totalBatchLabel.innerHTML && mediaData.totalItems > 0) {
        const totalBatches = Math.ceil(mediaData.totalItems / settings.batchSize)
        const totalZipIconHtml = createTotalZipIcon(isDark).outerHTML
        totalBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Total ZIP ${totalBatches === 1 ? 'File' : 'Files'}:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${totalBatches}</span></div>`
      }

      if (currentBatchLabel && currentBatchLabel.innerHTML) {
        const batchIcons = currentBatchLabel.querySelectorAll('svg')
        batchIcons.forEach(icon => {
          icon.style.color = isDark ? "#ffffff" : "#64748b"
        })
      }

      if (totalBatchLabel && totalBatchLabel.innerHTML) {
        const zipIcons = totalBatchLabel.querySelectorAll('svg')
        zipIcons.forEach(icon => {
          icon.style.color = isDark ? "#ffffff" : "#64748b"
        })
      }
    }

    function updateTheme() {
      const isDark = getSettings().darkTheme
      modalContent.style.backgroundColor = isDark ? "#1f2937" : "#ffffff"
      modalContent.style.color = isDark ? "#f1f5f9" : "#334155"
      header.style.borderBottom = `1px solid ${isDark ? "#374151" : "#e2e8f0"}`
      title.style.color = isDark ? "#f1f5f9" : "#334155"
      closeButton.style.color = isDark ? "#f1f5f9" : "#0f172a"
      tabs.style.borderBottom = `1px solid ${isDark ? "#374151" : "#e2e8f0"}`
      mainTab.style.color = mainContent.style.display === "block" ? (isDark ? "#f1f5f9" : "#0f172a") : "#64748b"
      settingsTab.style.color = settingsContent.style.display === "block" ? (isDark ? "#f1f5f9" : "#0f172a") : "#64748b"
      infoContainer.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
      batchInfoContainer.style.backgroundColor = isDark ? "#374151" : "#f8fafc"
      batchInfoContainer.style.border = `1px solid ${isDark ? "#4b5563" : "#e2e8f0"}`
      settingsTabs.style.borderBottom = `1px solid ${isDark ? "#374151" : "#e2e8f0"}`

      const allSettingTabs = [fetchTab, authTab, advancedTab]
      allSettingTabs.forEach(tab => {
        if (tab.style.borderBottom.includes("solid")) {
            tab.style.color = isDark ? "#f1f5f9" : "#0f172a"
        }
      })
      patreonAuthLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      patreonAuthInput.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
      autoBatchLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      patreonAuthInput.style.color = isDark ? "#cbd5e1" : "#64748b"
      tokenLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      tokenInput.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
      tokenInput.style.color = isDark ? "#cbd5e1" : "#64748b"
      apiServerLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      batchLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      batchSizeLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      startingBatchLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      startingBatchInput.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
      startingBatchInput.style.color = isDark ? "#cbd5e1" : "#64748b"
      timelineTypeLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      mediaTypeLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      concurrentLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      cacheDurationLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      darkThemeLabel.style.color = isDark ? "#f1f5f9" : "#334155"
      const toggleSwitches = [timelineTypeToggle, mediaTypeToggle, batchSizeToggle, concurrentToggle, cacheDurationToggle, apiServerToggle]
      toggleSwitches.forEach(toggle => {
        if (toggle) {
          toggle.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
        }
      })

      if (progressBar) {
        progressBar.style.backgroundColor = isDark ? "#374151" : "#f1f5f9"
      }

      if (currentBatchLabel) {
        currentBatchLabel.style.color = isDark ? "#ffffff" : "#475569"
      }
      if (totalBatchLabel) {
        totalBatchLabel.style.color = isDark ? "#ffffff" : "#475569"
      }

      refreshInfoDisplays()

      updateBatchLabels()
    }

    fetchButton.addEventListener("click", async () => {
      const settings = getSettings()
      if (!settings.authToken) {
        createAuthTokenPopup()
        return
      }
      if (!settings.patreonAuth) {
        createPatreonAuthPopup()
        return
      }
        if (settings.autoBatchEnabled && !autoBatchStarted) {
        autoBatchCancelled = false
        autoBatchStarted = true
        const batchNums = Object.keys(mediaData.batchData).map(Number)
        mediaData.currentPage = batchNums.length > 0 ? Math.max(...batchNums) + 1 : 0
      }
      if (!settings.autoBatchEnabled || (settings.autoBatchEnabled && !autoBatchStarted && Object.keys(mediaData.batchData).length === 0)) {
        infoContainer.style.display = "none"
        buttonContainer.style.display = "none"
        nextBatchButton.style.display = "none"
        prevBatchButton.style.display = "none"
        progressContainer.style.display = "none"
        if (settings.autoBatchEnabled) {
          autoBatchStarted = true
          mediaData.currentPage = 0
        }
      }
      fetchButton.disabled = true
      fetchButton.innerHTML = ""
      fetchButton.appendChild(document.createTextNode("Fetching..."))

      try {
        const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
        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)
          if (data && data.timeline && data.timeline.length > 0) {
            cacheManager.set(cacheKey, data, true)
          }
        }
        if (data.timeline && data.timeline.length > 0) {
          mediaData.mediaItems = data.timeline
          mediaData.hasMore = data.metadata.has_more
          mediaData.totalItems = data.total_urls

          mediaData.batchData[mediaData.currentPage] = [...data.timeline]

          const currentBatchKeys = Object.keys(mediaData.batchData).map(Number)
          for (const batchNum of currentBatchKeys) {
            if (batchNum > mediaData.currentPage) {
              delete mediaData.batchData[batchNum]
            }
          }
          mediaData.allMediaItems = []
          const maxBatch = Math.max(...Object.keys(mediaData.batchData).map(Number))
          for (let i = 0; i <= maxBatch; i++) {
            if (mediaData.batchData[i]) {
              mediaData.allMediaItems = [...mediaData.allMediaItems, ...mediaData.batchData[i]]
            }
          }

          const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)

          if (settings.batchEnabled) {
          const accountIconHtml = createAccountIcon(settings.darkTheme).outerHTML
            const mediaIconHtml = createMediaTypeIcon(settings.mediaType, settings.darkTheme).outerHTML
            const totalItemsIconHtml = createTotalItemsIcon(settings.darkTheme).outerHTML
            infoContainer.innerHTML = `
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${accountIconHtml}<strong style="margin-right: 6px;">Account:</strong><span>${data.account_info.name}</span></div>
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${mediaIconHtml}<strong style="margin-right: 6px;">${mediaTypeLabel} Found:</strong><span>${formatNumber(data.total_urls)}</span></div>
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${totalItemsIconHtml}<strong style="margin-right: 6px;">Total Items:</strong><span>${formatNumber(mediaData.allMediaItems.length)}</span></div>
            `
              const currentBatchIconHtml = createCurrentBatchIcon(settings.darkTheme).outerHTML
            currentBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${currentBatchIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Current Batch:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${mediaData.currentPage + 1}</span></div>`

            const totalBatches = Math.ceil(data.total_urls / settings.batchSize)
            const totalZipIconHtml = createTotalZipIcon(settings.darkTheme).outerHTML
            totalBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Total ZIP ${totalBatches === 1 ? 'File' : 'Files'}:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${totalBatches}</span></div>`
            batchInfoContainer.style.display = "flex"
            batchNavContainer.style.display = "flex"
          } else {
            const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1
            const accountIconHtml = createAccountIcon(settings.darkTheme).outerHTML
            const mediaIconHtml = createMediaTypeIcon(settings.mediaType, settings.darkTheme).outerHTML
            const totalZipIconHtml = createTotalZipIcon(settings.darkTheme).outerHTML
            infoContainer.innerHTML = `
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${accountIconHtml}<strong style="margin-right: 6px;">Account:</strong><span>${data.account_info.name}</span></div>
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${mediaIconHtml}<strong style="margin-right: 6px;">${mediaTypeLabel} Found:</strong><span>${formatNumber(data.total_urls)}</span></div>
              <div style="margin-top: 8px; display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px;">Total ZIP ${currentPart === 1 ? 'File' : 'Files'}:</strong><span>${currentPart}</span></div>
            `

            batchInfoContainer.style.display = "none"
            batchNavContainer.style.display = "none"
          }

          infoContainer.style.display = "block"

          if (settings.batchEnabled) {
            buttonContainer.style.display = "none"
          } else {
            buttonContainer.innerHTML = ""
            buttonContainer.appendChild(downloadButton)
            buttonContainer.style.display = "block"
          }
          if (settings.batchEnabled && mediaData.hasMore) {
            nextBatchButton.style.display = "block"
          }
            if (settings.batchEnabled && mediaData.currentPage > getSettings().startingBatch) {
            prevBatchButton.style.display = "block"
          }

          reassignDownloadHandlers()

          fetchButton.disabled = false
          const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()

          const updatedFetchButtonText =
            settings.autoBatchEnabled
              ? "Auto Fetch"
              : (settings.mediaType === "all"
                  ? "Fetch Media"
                  : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`)

          fetchButton.innerHTML = ""
          let updatedFetchIcon1 = fetchIcon.cloneNode(true)
          fetchButton.appendChild(updatedFetchIcon1)
          fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))

          console.log('Fetch success - Next Batch Debug:', {
            hasMore: mediaData.hasMore,
            nextBatchClicked: mediaData.nextBatchClicked,
            shouldShowPopup: !mediaData.hasMore && mediaData.nextBatchClicked,
            autoBatchEnabled: settings.autoBatchEnabled
          });

          if (!mediaData.hasMore && mediaData.nextBatchClicked && !settings.autoBatchEnabled) {
            console.log('Showing next batch completion popup after successful fetch');
            createInfoDialog("Next batch has completed! All available batches have been fetched.");
          }

          if (mediaData.nextBatchClicked) {
            console.log('Resetting nextBatchClicked flag after successful fetch');
            mediaData.nextBatchClicked = false;
          }

          if (settings.autoBatchEnabled && !autoBatchCancelled) {
            prevBatchButton.style.display = "none"
            nextBatchButton.style.display = "none"
            stopFetchButton.style.display = "block"
            if (mediaData.hasMore) {
              setTimeout(() => {
                if (settings.autoBatchEnabled && !autoBatchCancelled) {
                  mediaData.currentPage++
                  fetchButton.click()
                }
              }, 1000)
            } else {
              stopFetchButton.style.display = "none"
              prevBatchButton.style.display = mediaData.currentPage > 0 ? "block" : "none"
              nextBatchButton.style.display = "none"
              autoBatchStarted = false

              createInfoDialog("Auto fetch has completed! All available batches have been fetched.")
            }
          }
        } else {
          console.log('No media found branch:', {
            nextBatchClicked: mediaData.nextBatchClicked
          });

          if (mediaData.nextBatchClicked) {
            console.log('Showing next batch completion popup from no media found branch');
            createInfoDialog("Next batch has completed! All available batches have been fetched.");
            mediaData.nextBatchClicked = false; 
          }

          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 = ""
          let updatedFetchIcon2 = fetchIcon.cloneNode(true)
          fetchButton.appendChild(updatedFetchIcon2)
          fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
        }
      } catch (error) {
        infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`
        infoContainer.style.display = "block"
        fetchButton.disabled = false

        if (mediaData.currentPage > getSettings().startingBatch) {
          prevBatchButton.style.display = "block"
        }

        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 = ""
        let updatedFetchIcon3 = fetchIcon.cloneNode(true)
        fetchButton.appendChild(updatedFetchIcon3)
        fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
      }
    });

      nextBatchButton.addEventListener("click", () => {
      console.log('Next Batch Button clicked, setting flag to true');
      mediaData.nextBatchClicked = true; 
      mediaData.currentPage++

      autoBatchCancelled = true
      autoBatchStarted = false

      const settings = getSettings()
      const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
      const cachedData = cacheManager.get(cacheKey)

      if (cachedData && cachedData.timeline && cachedData.timeline.length > 0) {
        mediaData.mediaItems = cachedData.timeline
        mediaData.hasMore = cachedData.metadata.has_more
        mediaData.totalItems = cachedData.total_urls

        mediaData.batchData[mediaData.currentPage] = [...cachedData.timeline]

        mediaData.allMediaItems = []
        const maxBatch = Math.max(...Object.keys(mediaData.batchData).map(Number))
        for (let i = 0; i <= maxBatch; i++) {
          if (mediaData.batchData[i]) {
            mediaData.allMediaItems = [...mediaData.allMediaItems, ...mediaData.batchData[i]]
          }
        }

        refreshInfoDisplays()

        if (settings.batchEnabled) {
          prevBatchButton.style.display = mediaData.currentPage > 0 ? "block" : "none"
          nextBatchButton.style.display = mediaData.hasMore ? "block" : "none"
          infoContainer.style.display = "block"
          buttonContainer.style.display = "none"
          batchInfoContainer.style.display = "flex"
          batchNavContainer.style.display = "flex"
        }

        const currentBatchIconHtml = createCurrentBatchIcon(settings.darkTheme).outerHTML
        currentBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${currentBatchIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Current Batch:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${mediaData.currentPage + 1}</span></div>`
        const totalBatches = Math.ceil(cachedData.total_urls / settings.batchSize)
        const totalZipIconHtml = createTotalZipIcon(settings.darkTheme).outerHTML
        totalBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Total ZIP ${totalBatches === 1 ? 'File' : 'Files'}:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${totalBatches}</span></div>`
        console.log('Next Batch Debug:', {
          hasMore: mediaData.hasMore,
          nextBatchClicked: mediaData.nextBatchClicked,
          shouldShowPopup: !mediaData.hasMore && mediaData.nextBatchClicked
        });

        if (!mediaData.hasMore && mediaData.nextBatchClicked) {
          console.log('Showing next batch completion popup');
          createInfoDialog("Next batch has completed! All available batches have been fetched.");
        }
        console.log('Resetting nextBatchClicked flag to false');
        mediaData.nextBatchClicked = false;

        reassignDownloadHandlers()
      } else {
        const wasAutoBatchEnabled = getSettings().autoBatchEnabled;

        if (wasAutoBatchEnabled) {
          const newSettings = getSettings();
          newSettings.autoBatchEnabled = false;
          saveSettings(newSettings);
        }

        fetchButton.click();

        if (wasAutoBatchEnabled) {
          setTimeout(() => {
            const newSettings = getSettings();
            newSettings.autoBatchEnabled = true;
            saveSettings(newSettings);

            autoBatchCancelled = true;
            autoBatchStarted = false;
          }, 100);
        }
      }
    })

    prevBatchButton.addEventListener("click", () => {
      if (mediaData.currentPage > 0) {
        mediaData.currentPage--

        autoBatchCancelled = true
        autoBatchStarted = false

        const settings = getSettings()
        const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
        let data = cacheManager.get(cacheKey)

        if (data && data.timeline && data.timeline.length > 0) {
          mediaData.mediaItems = data.timeline
          mediaData.hasMore = data.metadata.has_more
          mediaData.totalItems = data.total_urls

          mediaData.batchData[mediaData.currentPage] = [...data.timeline]

          const currentBatchKeys = Object.keys(mediaData.batchData).map(Number)
          for (const batchNum of currentBatchKeys) {
            if (batchNum > mediaData.currentPage) {
              delete mediaData.batchData[batchNum]
            }
          }
          mediaData.allMediaItems = []
          const maxBatch = Math.max(...Object.keys(mediaData.batchData).map(Number))
          for (let i = 0; i <= maxBatch; i++) {
            if (mediaData.batchData[i]) {
              mediaData.allMediaItems = [...mediaData.allMediaItems, ...mediaData.batchData[i]]
            }
          }

          const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)

          if (settings.batchEnabled) {
            const accountIconHtml = createAccountIcon(settings.darkTheme).outerHTML
            const mediaIconHtml = createMediaTypeIcon(settings.mediaType, settings.darkTheme).outerHTML
            const totalItemsIconHtml = createTotalItemsIcon(settings.darkTheme).outerHTML
            infoContainer.innerHTML = `
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${accountIconHtml}<strong style="margin-right: 6px;">Account:</strong><span>${data.account_info.name}</span></div>
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${mediaIconHtml}<strong style="margin-right: 6px;">${mediaTypeLabel} Found:</strong><span>${formatNumber(data.total_urls)}</span></div>
              <div style="margin-bottom: 8px; display: flex; align-items: center;">${totalItemsIconHtml}<strong style="margin-right: 6px;">Total Items:</strong><span>${formatNumber(mediaData.allMediaItems.length)}</span></div>
            `

            const currentBatchIconHtml = createCurrentBatchIcon(settings.darkTheme).outerHTML
            currentBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${currentBatchIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Current Batch:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${mediaData.currentPage + 1}</span></div>`

            const totalBatches = Math.ceil(data.total_urls / settings.batchSize)
            const totalZipIconHtml = createTotalZipIcon(settings.darkTheme).outerHTML
            totalBatchLabel.innerHTML = `<div style="display: flex; align-items: center;">${totalZipIconHtml}<strong style="margin-right: 6px; color: ${settings.darkTheme ? '#ffffff' : '#475569'};">Total ZIP ${totalBatches === 1 ? 'File' : 'Files'}:</strong><span style="color: ${settings.darkTheme ? '#ffffff' : '#475569'};">${totalBatches}</span></div>`
            batchInfoContainer.style.display = "flex"
            batchNavContainer.style.display = "flex"
          }

          infoContainer.style.display = "block"

          if (settings.batchEnabled) {
            buttonContainer.style.display = "none"
          } else {
            buttonContainer.innerHTML = ""
            buttonContainer.appendChild(downloadButton)
            buttonContainer.style.display = "block"
          }
          if (settings.batchEnabled && mediaData.hasMore) {
            nextBatchButton.style.display = "block"
          }
          if (settings.batchEnabled && mediaData.currentPage > 0) {
            prevBatchButton.style.display = "block"
          }
            reassignDownloadHandlers()
        } else {
          fetchButton.click()
        }
      }
    })

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

    function reassignDownloadHandlers() {
      const settings = getSettings()
      if (settings.batchEnabled) {
        downloadCurrentButton.onclick = () => downloadMedia(false)
        downloadAllButton.onclick = () => downloadMedia(true)
      }
    }

    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

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

        const downloadInBatches = async (items, batchSize) => {
          for (let i = 0; i < items.length; i += batchSize) {
            const batch = items.slice(i, i + batchSize)

            const downloadPromises = batch.map(async (item) => {
              try {
                const formattedDate = formatDate(item.date)
                let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`

                if (filenameMap[baseFilename] !== undefined) {
                  filenameMap[baseFilename]++
                  baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, "0")}`
                } else {
                  filenameMap[baseFilename] = 0
                }

                const fileExtension = item.type === "photo" ? "jpg" : "mp4"
                const filename = `${baseFilename}.${fileExtension}`

                const blob = await fetchBinary(item.url)

                if (settings.mediaType === "all") {
                  if (item.type === "photo") {
                    imageFolder.file(filename, blob)
                  } else if (item.type === "video") {
                    videoFolder.file(filename, blob)
                  } else if (item.type === "gif") {
                    gifFolder.file(filename, blob)
                  }
                } else {
                  zip.file(filename, blob)
                }

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

              } catch (error) {
                console.error("Error downloading item:", error)
                mediaData.totalDownloaded++
              }
            })

            await Promise.all(downloadPromises)
          }
        }

        await downloadInBatches(chunk, settings.concurrentDownloads)

        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}`
        }
      }
      mediaData.downloading = false
      progressContainer.style.display = "none"
      fetchButton.disabled = false
      if (settings.batchEnabled) {
        downloadCurrentButton.disabled = false
        downloadAllButton.disabled = false
        reassignDownloadHandlers()
      } else {
        downloadButton.disabled = false
      }
      nextBatchButton.disabled = false
      createInfoDialog("All media downloaded successfully.", "Download Complete")
      return
    }

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

QingJ © 2025

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