Threads Plugin

為 Threads 貼文提供圖片與影片下載、批次資源選擇、貼文文字複製,以及去除追蹤碼的連結複製功能。

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Threads Plugin
// @name:zh-TW   Threads Plugin
// @name:en      Threads Plugin
// @namespace    https://github.com/Jwander0820
// @version      5.1.0
// @description  為 Threads 貼文提供圖片與影片下載、批次資源選擇、貼文文字複製,以及去除追蹤碼的連結複製功能。
// @description:zh-TW 為 Threads 貼文提供圖片與影片下載、批次資源選擇、貼文文字複製,以及去除追蹤碼的連結複製功能。
// @description:en Download images and videos from Threads posts, select media in batches, copy post text, and copy links with tracking parameters removed.
// @author       Jwander
// @license      MIT
// @homepageURL  https://github.com/Jwander0820/threads-plugin
// @supportURL   https://github.com/Jwander0820/threads-plugin/issues
// @icon         https://raw.githubusercontent.com/Jwander0820/threads-plugin/main/extension/icons/icon-128.png
// @icon64       https://raw.githubusercontent.com/Jwander0820/threads-plugin/main/extension/icons/icon-128.png
// @match        https://www.threads.com/*
// @match        https://threads.com/*
// @match        https://www.threads.net/*
// @match        https://threads.net/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @connect      threads.com
// @connect      www.threads.com
// @connect      threads.net
// @connect      www.threads.net
// @connect      instagram.com
// @connect      *.instagram.com
// @connect      cdninstagram.com
// @connect      *.cdninstagram.com
// @connect      fbcdn.net
// @connect      *.fbcdn.net
// @run-at       document-start
// ==/UserScript==

// GENERATED FILE. DO NOT EDIT DIRECTLY.
// Source files are under /src.
'use strict';
(() => {
  // src/shared/options.js
  var DEFAULT_OPTIONS = Object.freeze({
    enablePostMediaPicker: true,
    hoverScanIntervalMs: 160,
    layoutRefreshIntervalMs: 260,
    backgroundScanIntervalMs: 5e3,
    ignoreHorizontalOnlyScroll: true
  });
  function normalizeNumber(value, fallback, min, max) {
    const number = Number(value);
    if (!Number.isFinite(number)) return fallback;
    return Math.min(max, Math.max(min, Math.round(number)));
  }
  function normalizeOptions(value) {
    let stored = value;
    if (typeof stored === "string") {
      try {
        stored = JSON.parse(stored);
      } catch {
        stored = {};
      }
    }
    if (!stored || typeof stored !== "object") stored = {};
    return Object.freeze({
      enablePostMediaPicker: stored.enablePostMediaPicker !== false,
      hoverScanIntervalMs: normalizeNumber(stored.hoverScanIntervalMs, DEFAULT_OPTIONS.hoverScanIntervalMs, 0, 2e3),
      layoutRefreshIntervalMs: normalizeNumber(stored.layoutRefreshIntervalMs, DEFAULT_OPTIONS.layoutRefreshIntervalMs, 0, 5e3),
      backgroundScanIntervalMs: normalizeNumber(stored.backgroundScanIntervalMs, DEFAULT_OPTIONS.backgroundScanIntervalMs, 3e3, 6e4),
      ignoreHorizontalOnlyScroll: stored.ignoreHorizontalOnlyScroll !== false
    });
  }

  // src/shared/media-policy.js
  var MEDIA_HOST_EXACT_ALLOWLIST = /* @__PURE__ */ new Set([
    "threads.com",
    "www.threads.com",
    "threads.net",
    "www.threads.net"
  ]);
  var MEDIA_HOST_SUFFIX_ALLOWLIST = Object.freeze([
    "instagram.com",
    "cdninstagram.com",
    "fbcdn.net"
  ]);
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "webp", "avif", "gif", "heic", "heif"]);
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "m4v", "mov", "webm"]);
  var MEDIA_URL_MAX_LENGTH = 8192;
  function sanitizeMediaFilenamePart(value) {
    const cleaned = String(value || "unknown").replace(/^@/, "").replace(/[\\/:*?\x22<>|]+/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_");
    return cleaned || "unknown";
  }
  function defaultBaseUrl() {
    return globalThis.location?.href || "https://www.threads.com/";
  }
  function normalizeUrl(rawUrl, baseUrl = defaultBaseUrl()) {
    if (!rawUrl || typeof rawUrl !== "string" || rawUrl.length > MEDIA_URL_MAX_LENGTH) return null;
    let url = rawUrl.trim().replace(/\\u0026/gi, "&").replace(/\\\//g, "/").replace(/&amp;/gi, "&").replace(/[),.;\]}]+$/g, "");
    if (url.length > MEDIA_URL_MAX_LENGTH || !/^https?:\/\//i.test(url)) return null;
    try {
      const normalized = new URL(url, baseUrl).href;
      return normalized.length <= MEDIA_URL_MAX_LENGTH ? normalized : null;
    } catch {
      return null;
    }
  }
  function hostnameMatchesSuffix(hostname, suffix) {
    const normalizedHostname = String(hostname || "").toLowerCase().replace(/\.$/, "");
    const normalizedSuffix = String(suffix || "").toLowerCase().replace(/\.$/, "");
    return normalizedHostname === normalizedSuffix || normalizedHostname.endsWith(`.${normalizedSuffix}`);
  }
  function isAllowedMediaHostname(hostname) {
    const normalizedHostname = String(hostname || "").toLowerCase();
    if (!normalizedHostname || normalizedHostname.endsWith(".")) return false;
    if (MEDIA_HOST_EXACT_ALLOWLIST.has(normalizedHostname)) return true;
    return MEDIA_HOST_SUFFIX_ALLOWLIST.some(
      (suffix) => hostnameMatchesSuffix(normalizedHostname, suffix)
    );
  }
  function getMediaPathExtension(pathname) {
    let decodedPath = String(pathname || "");
    try {
      decodedPath = decodeURIComponent(decodedPath);
    } catch {
      return "";
    }
    const match = decodedPath.match(/\.([a-z0-9]+)$/i);
    return match ? match[1].toLowerCase() : "";
  }
  function validateMediaUrl(rawUrl, expectedType = null) {
    const normalized = normalizeUrl(rawUrl);
    if (!normalized) return { ok: false, reason: "invalid_url" };
    let parsed;
    try {
      parsed = new URL(normalized);
    } catch {
      return { ok: false, reason: "invalid_url" };
    }
    if (parsed.protocol !== "https:") return { ok: false, reason: "https_required" };
    if (parsed.username || parsed.password) return { ok: false, reason: "credentials_not_allowed" };
    if (parsed.port && parsed.port !== "443") return { ok: false, reason: "port_not_allowed" };
    if (!isAllowedMediaHostname(parsed.hostname)) return { ok: false, reason: "host_not_allowed" };
    const extension = getMediaPathExtension(parsed.pathname);
    const type = VIDEO_EXTENSIONS.has(extension) ? "video" : IMAGE_EXTENSIONS.has(extension) ? "image" : null;
    if (!type) return { ok: false, reason: "extension_not_allowed" };
    if (expectedType && type !== expectedType) return { ok: false, reason: "media_type_mismatch" };
    return {
      ok: true,
      url: parsed.href,
      type,
      extension,
      hostname: parsed.hostname.toLowerCase()
    };
  }
  function isImageUrl(url) {
    return validateMediaUrl(url, "image").ok;
  }
  function formatUtcTimestamp(dateLike, now = () => /* @__PURE__ */ new Date()) {
    const date = dateLike instanceof Date && !Number.isNaN(dateLike.getTime()) ? dateLike : now();
    const pad = (num) => String(num).padStart(2, "0");
    return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
  }
  function guessExtension(type, url, contentType) {
    const validated = validateMediaUrl(url, type);
    if (validated.ok) {
      if (validated.extension === "jpeg") return "jpg";
      if (type === "image" && ["heic", "heif"].includes(validated.extension) && /(?:^|_)dst-jpe?g(?:_|$)/i.test(new URL(validated.url).searchParams.get("stp") || "")) {
        return "jpg";
      }
      return validated.extension;
    }
    if (/mp4/i.test(contentType || "")) return "mp4";
    if (/webm/i.test(contentType || "")) return "webm";
    if (/png/i.test(contentType || "")) return "png";
    if (/webp/i.test(contentType || "")) return "webp";
    if (/avif/i.test(contentType || "")) return "avif";
    return type === "video" ? "mp4" : "jpg";
  }
  function buildMediaFilename({ type, url, contentType, postInfo, sequence, now }) {
    const safePostInfo = postInfo || {};
    const extension = guessExtension(type, url, contentType);
    const kind = type === "video" ? "video" : "photo";
    const timestamp = formatUtcTimestamp(safePostInfo.createdAt, now);
    const author = sanitizeMediaFilenamePart(safePostInfo.author);
    const postId = sanitizeMediaFilenamePart(safePostInfo.postId);
    const safeSequence = sanitizeMediaFilenamePart(sequence);
    return `${author}_${timestamp}_${postId}_${kind}_${safeSequence}.${extension}`;
  }

  // src/shared/route-media-state.js
  var NON_SEMANTIC_ROUTE_QUERY_KEYS = /* @__PURE__ */ new Set(["fbclid", "igsh", "igshid", "share", "xmt"]);
  var VOLATILE_MEDIA_QUERY_KEYS = /* @__PURE__ */ new Set(["bytestart", "byteend", "ccb", "edm", "efg", "oh", "oe", "stp"]);
  function buildMediaRouteKey(urlLike, fallbackBase = globalThis.location?.href || "https://www.threads.com/") {
    let parsed;
    try {
      parsed = new URL(String(urlLike || fallbackBase), fallbackBase);
    } catch {
      return "";
    }
    const semanticParams = Array.from(parsed.searchParams.entries()).filter(([key]) => {
      const normalizedKey = key.toLowerCase();
      return !normalizedKey.startsWith("utm_") && !NON_SEMANTIC_ROUTE_QUERY_KEYS.has(normalizedKey);
    }).sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue));
    const query = new URLSearchParams(semanticParams).toString();
    return `${parsed.origin}${parsed.pathname}${query ? `?${query}` : ""}`;
  }
  function transitionMediaRouteScope(currentScope, nextRouteKey) {
    if (!currentScope.routeKey) return { ...currentScope, routeKey: nextRouteKey, changed: false };
    if (currentScope.routeKey === nextRouteKey) return { ...currentScope, changed: false };
    return { routeKey: nextRouteKey, changed: true };
  }
  function getMediaUrlIdentityAliases(url) {
    const validated = validateMediaUrl(url);
    if (!validated.ok) return [];
    try {
      const parsed = new URL(validated.url);
      const aliases = [];
      const cacheKey = parsed.searchParams.get("ig_cache_key");
      if (cacheKey) aliases.push(`ig:${cacheKey}`);
      const stableParams = Array.from(parsed.searchParams.entries()).filter(([key]) => key.toLowerCase() !== "ig_cache_key").filter(([key]) => !VOLATILE_MEDIA_QUERY_KEYS.has(key.toLowerCase())).filter(([key]) => !key.toLowerCase().startsWith("_nc_")).sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue));
      const canonicalQuery = new URLSearchParams(stableParams).toString();
      const canonicalUrl = `${parsed.protocol}//${parsed.hostname.toLowerCase()}${parsed.pathname}`;
      aliases.push(`url:${canonicalUrl}${canonicalQuery ? `?${canonicalQuery}` : ""}`);
      return aliases;
    } catch {
      return [];
    }
  }
  function getMediaUrlIdentity(url) {
    return getMediaUrlIdentityAliases(url)[0] || "";
  }
  function areMediaUrlsEquivalent(firstUrl, secondUrl) {
    const firstAliases = new Set(getMediaUrlIdentityAliases(firstUrl));
    return firstAliases.size > 0 && getMediaUrlIdentityAliases(secondUrl).some((alias) => firstAliases.has(alias));
  }
  function mergeMediaUrlCache(currentUrls, nextUrl, limit = 32) {
    const safeLimit = Math.max(0, Math.floor(Number(limit) || 0));
    const validated = validateMediaUrl(nextUrl);
    if (!validated.ok) return Array.from(currentUrls || []).slice(0, safeLimit);
    return [
      validated.url,
      ...Array.from(currentUrls || []).filter((currentUrl) => !areMediaUrlsEquivalent(currentUrl, validated.url))
    ].slice(0, safeLimit);
  }

  // src/shared/network-policy.js
  var NETWORK_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
  var CAPTURE_OPERATION_ALLOWLIST = Object.freeze([
    "BarcelonaFeedQuery",
    "BarcelonaFollowingFeedQuery",
    "BarcelonaForYouFeedQuery",
    "BarcelonaPostPageCommentsQuery",
    "BarcelonaPostPageQuery",
    "BarcelonaProfileRepliesTabQuery",
    "BarcelonaProfileThreadsTabQuery",
    "BarcelonaSearchQuery",
    "BarcelonaThreadQuery",
    "BarcelonaTimelineQuery"
  ]);
  var INSPECTABLE_NETWORK_OPERATIONS = new Set(CAPTURE_OPERATION_ALLOWLIST);
  var CAPTURE_HOSTS = /* @__PURE__ */ new Set([
    "threads.com",
    "www.threads.com",
    "threads.net",
    "www.threads.net",
    "instagram.com",
    "www.instagram.com"
  ]);
  function defaultPageUrl() {
    return globalThis.location?.href || "https://www.threads.com/";
  }
  function isSensitiveThreadsRoute(urlLike, baseUrl = defaultPageUrl()) {
    try {
      const parsed = new URL(String(urlLike || baseUrl), baseUrl);
      let pathname = parsed.pathname;
      for (let pass = 0; pass < 8; pass += 1) {
        const decoded = decodeURIComponent(pathname);
        if (decoded === pathname) break;
        pathname = decoded;
      }
      if (/%[0-9a-f]{2}/i.test(pathname)) return true;
      const canonicalSegments = [];
      pathname.replace(/\\/g, "/").split("/").forEach((segment) => {
        if (!segment || segment === ".") return;
        if (segment === "..") {
          canonicalSegments.pop();
          return;
        }
        canonicalSegments.push(segment);
      });
      const canonicalPathname = "/" + canonicalSegments.join("/");
      return /^\/(?:accounts?|challenge|direct|inbox|login|messages?|oauth|privacy|security|settings|two_factor)(?:\/|$)/i.test(canonicalPathname);
    } catch {
      return true;
    }
  }
  function isKnownCaptureEndpoint(urlLike, baseUrl = defaultPageUrl()) {
    try {
      const parsed = new URL(String(urlLike || ""), baseUrl);
      return parsed.protocol === "https:" && (!parsed.port || parsed.port === "443") && CAPTURE_HOSTS.has(parsed.hostname.toLowerCase()) && ["/api/graphql", "/graphql/query"].includes(parsed.pathname.replace(/\/$/, ""));
    } catch {
      return false;
    }
  }
  function getHeaderValue(headers, name) {
    if (!headers) return "";
    if (typeof headers.get === "function") return headers.get(name) || "";
    if (typeof headers === "string") {
      const normalizedName = String(name || "").toLowerCase();
      const headerLine = headers.split(/\r?\n/).find((line) => {
        const separatorIndex = line.indexOf(":");
        return separatorIndex > 0 && line.slice(0, separatorIndex).trim().toLowerCase() === normalizedName;
      });
      return headerLine ? headerLine.slice(headerLine.indexOf(":") + 1).trim() : "";
    }
    if (Array.isArray(headers)) {
      return headers.find(([key2]) => String(key2).toLowerCase() === name.toLowerCase())?.[1] || "";
    }
    const key = Object.keys(headers).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
    return key ? headers[key] : "";
  }
  function getOperationsFromBody(body) {
    if (!body) return [];
    const operations = /* @__PURE__ */ new Set();
    const addOperations = (getValues) => {
      ["fb_api_req_friendly_name", "operationName", "operation_name"].forEach((key) => {
        const rawValues = getValues(key);
        const values = Array.isArray(rawValues) ? rawValues : [rawValues];
        values.filter(Boolean).forEach((value) => operations.add(String(value)));
      });
    };
    if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
      addOperations((key) => body.getAll(key));
      return Array.from(operations);
    }
    if (typeof FormData !== "undefined" && body instanceof FormData) {
      addOperations((key) => body.getAll(key));
      return Array.from(operations);
    }
    if (typeof body === "object" && !(typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) && !(typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body))) {
      addOperations((key) => body[key]);
      return Array.from(operations);
    }
    if (typeof body !== "string") return [];
    try {
      const params = new URLSearchParams(body);
      addOperations((key) => params.getAll(key));
    } catch {
    }
    try {
      const parsed = JSON.parse(body);
      addOperations((key) => parsed?.[key]);
    } catch {
    }
    return Array.from(operations);
  }
  function classifyNetworkCaptureRequest({ url, method = "GET", headers, body, routeUrl }) {
    const pageUrl = routeUrl || defaultPageUrl();
    if (isSensitiveThreadsRoute(pageUrl)) return { allowed: false, reason: "sensitive_route" };
    if (!isKnownCaptureEndpoint(url, pageUrl)) return { allowed: false, reason: "unknown_endpoint" };
    if (!["GET", "POST"].includes(String(method || "GET").toUpperCase())) {
      return { allowed: false, reason: "method_not_allowed" };
    }
    let parsed;
    try {
      parsed = new URL(String(url), pageUrl);
    } catch {
      return { allowed: false, reason: "invalid_url" };
    }
    const declaredOperations = /* @__PURE__ */ new Set();
    const addDeclaredOperation = (value) => {
      const values = Array.isArray(value) ? value : [value];
      values.filter(Boolean).forEach((item) => {
        String(item).split(",").map((part) => part.trim()).filter(Boolean).forEach((part) => declaredOperations.add(part));
      });
    };
    ["fb_api_req_friendly_name", "operationName", "operation_name"].forEach(
      (key) => addDeclaredOperation(parsed.searchParams.getAll(key))
    );
    addDeclaredOperation(getHeaderValue(headers, "x-fb-friendly-name"));
    addDeclaredOperation(getOperationsFromBody(body));
    if (declaredOperations.size !== 1) {
      return {
        allowed: false,
        reason: declaredOperations.size === 0 ? "unknown_operation" : "operation_conflict",
        operation: Array.from(declaredOperations).join(",")
      };
    }
    const [operation] = declaredOperations;
    if (!INSPECTABLE_NETWORK_OPERATIONS.has(operation)) {
      return { allowed: false, reason: "unknown_operation", operation };
    }
    return {
      allowed: true,
      reason: "",
      operation: String(operation),
      requestUrl: parsed.href,
      sourceRouteKey: buildMediaRouteKey(pageUrl)
    };
  }
  function isInspectableResponseMime(contentType) {
    const baseType = String(contentType || "").split(";", 1)[0].trim().toLowerCase();
    return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/plain";
  }
  function getContentLength(headers) {
    const value = Number(getHeaderValue(headers, "content-length"));
    return Number.isFinite(value) && value >= 0 ? value : null;
  }
  function getBoundedResponseMaxBytes(configuredMaxBytes) {
    return Math.min(
      NETWORK_RESPONSE_MAX_BYTES,
      Math.max(1, Number(configuredMaxBytes) || NETWORK_RESPONSE_MAX_BYTES)
    );
  }
  function utf8ByteLength(text) {
    if (typeof TextEncoder === "function") return new TextEncoder().encode(String(text || "")).byteLength;
    return unescape(encodeURIComponent(String(text || ""))).length;
  }
  function getJsonStringByteLength(value, limit = Number.MAX_SAFE_INTEGER) {
    const text = String(value);
    let bytes = 2;
    for (let index = 0; index < text.length; index += 1) {
      const code = text.charCodeAt(index);
      if (code === 34 || code === 92) bytes += 2;
      else if (code <= 31) bytes += 6;
      else if (code <= 127) bytes += 1;
      else if (code <= 2047) bytes += 2;
      else if (code >= 55296 && code <= 56319) {
        const nextCode = text.charCodeAt(index + 1);
        if (nextCode >= 56320 && nextCode <= 57343) {
          bytes += 4;
          index += 1;
        } else bytes += 6;
      } else if (code >= 56320 && code <= 57343) bytes += 6;
      else bytes += 3;
      if (bytes > limit) return bytes;
    }
    return bytes;
  }
  function isJsonValueWithinByteLimit(rootValue, maxBytes) {
    let remaining = maxBytes;
    let nodeCount = 0;
    const visited = /* @__PURE__ */ new WeakSet();
    const stack = [rootValue];
    const consume = (bytes) => (remaining -= bytes) >= 0;
    while (stack.length > 0) {
      const value = stack.pop();
      if (value === null) {
        if (!consume(4)) return false;
        continue;
      }
      const valueType = typeof value;
      if (valueType === "string") {
        if (!consume(getJsonStringByteLength(value, remaining))) return false;
        continue;
      }
      if (valueType === "number") {
        if (!consume(Number.isFinite(value) ? String(value).length : 4)) return false;
        continue;
      }
      if (valueType === "boolean") {
        if (!consume(value ? 4 : 5)) return false;
        continue;
      }
      if (valueType !== "object" || visited.has(value) || typeof value.toJSON === "function") return false;
      visited.add(value);
      if (++nodeCount > 5e4) return false;
      if (Array.isArray(value)) {
        if (!consume(2 + Math.max(0, value.length - 1))) return false;
        for (let index = value.length - 1; index >= 0; index -= 1) stack.push(value[index]);
        continue;
      }
      let keys;
      try {
        keys = Object.keys(value);
      } catch {
        return false;
      }
      if (!consume(2 + Math.max(0, keys.length - 1))) return false;
      for (let index = keys.length - 1; index >= 0; index -= 1) {
        const key = keys[index];
        if (!consume(getJsonStringByteLength(key, remaining) + 1)) return false;
        try {
          stack.push(value[key]);
        } catch {
          return false;
        }
      }
    }
    return true;
  }
  function captureMayContinue(options) {
    if (options?.signal?.aborted) return false;
    return typeof options?.shouldContinue !== "function" || options.shouldContinue() === true;
  }
  function cancelWithoutWaiting(target, reason) {
    try {
      Promise.resolve(target?.cancel?.(reason)).catch(() => {
      });
    } catch {
    }
  }
  function createAbortRace(signal, onAbort) {
    if (!signal) return null;
    let abort;
    const promise = new Promise((_, reject) => {
      abort = () => {
        onAbort?.();
        reject(new Error("capture_stopped"));
      };
      if (signal.aborted) abort();
      else signal.addEventListener("abort", abort, { once: true });
    });
    return {
      promise,
      dispose() {
        signal.removeEventListener?.("abort", abort);
      }
    };
  }
  async function readFetchResponseTextLimited(response, maxBytes, options) {
    if (!captureMayContinue(options)) throw new Error("capture_stopped");
    const clone = response.clone();
    const reader = clone.body?.getReader?.();
    if (!reader) {
      const textPromise = clone.text();
      const abortRace2 = createAbortRace(options?.signal, () => cancelWithoutWaiting(clone.body, "capture_stopped"));
      try {
        const text = abortRace2 ? await Promise.race([textPromise, abortRace2.promise]) : await textPromise;
        if (!captureMayContinue(options)) throw new Error("capture_stopped");
        if (utf8ByteLength(text) > maxBytes) throw new Error("response_too_large");
        return text;
      } finally {
        abortRace2?.dispose();
      }
    }
    const decoder = new TextDecoder();
    let totalBytes = 0;
    let output = "";
    const abortRace = createAbortRace(options?.signal, () => cancelWithoutWaiting(reader, "capture_stopped"));
    try {
      while (true) {
        if (!captureMayContinue(options)) {
          cancelWithoutWaiting(reader, "capture_stopped");
          throw new Error("capture_stopped");
        }
        const readResult = reader.read();
        const { done, value } = abortRace ? await Promise.race([readResult, abortRace.promise]) : await readResult;
        if (!captureMayContinue(options)) {
          cancelWithoutWaiting(reader, "capture_stopped");
          throw new Error("capture_stopped");
        }
        if (done) break;
        totalBytes += value?.byteLength || 0;
        if (totalBytes > maxBytes) {
          cancelWithoutWaiting(reader, "response_too_large");
          throw new Error("response_too_large");
        }
        output += decoder.decode(value, { stream: true });
      }
      return output + decoder.decode();
    } finally {
      abortRace?.dispose();
    }
  }
  async function inspectResponse(response, captureContext, options = {}) {
    if (!captureMayContinue(options)) return false;
    if (!captureContext?.allowed || !response || typeof response.clone !== "function") return false;
    if (response.url && !isKnownCaptureEndpoint(response.url)) return false;
    if (response.status < 200 || response.status >= 300) return false;
    if (!isInspectableResponseMime(response.headers?.get?.("content-type") || "")) return false;
    const maxBytes = getBoundedResponseMaxBytes(options.maxBytes);
    const contentLength = getContentLength(response.headers);
    if (contentLength !== null && contentLength > maxBytes) return false;
    try {
      const text = await readFetchResponseTextLimited(response, maxBytes, options);
      if (!captureMayContinue(options)) return false;
      options.extractor?.(text, captureContext.sourceRouteKey);
      return true;
    } catch {
      return false;
    }
  }
  async function readXhrResponseText(xhr, maxBytes, options) {
    if (!captureMayContinue(options)) return null;
    const responseType = String(xhr.responseType || "").toLowerCase();
    let text;
    if (responseType === "" || responseType === "text") {
      try {
        text = xhr.responseText;
      } catch {
        return null;
      }
    } else if (responseType === "json") {
      if (!isJsonValueWithinByteLimit(xhr.response, maxBytes)) return null;
      try {
        text = JSON.stringify(xhr.response);
      } catch {
        return null;
      }
    } else if (responseType === "blob") {
      const blob = xhr.response;
      if (!blob || blob.size > maxBytes || typeof blob.text !== "function") return null;
      text = await blob.text();
    } else if (responseType === "arraybuffer") {
      const buffer = xhr.response;
      if (!buffer || typeof buffer.byteLength !== "number" || buffer.byteLength > maxBytes) return null;
      text = new TextDecoder().decode(buffer);
    } else return null;
    if (!captureMayContinue(options)) return null;
    return typeof text === "string" && text.length <= maxBytes && utf8ByteLength(text) <= maxBytes ? text : null;
  }
  async function inspectXhrResponse(xhr, captureContext, options = {}) {
    if (!captureMayContinue(options)) return false;
    if (!captureContext?.allowed) return false;
    if (xhr.responseURL && !isKnownCaptureEndpoint(xhr.responseURL)) return false;
    if (Number(xhr.status) < 200 || Number(xhr.status) >= 300) return false;
    if (!isInspectableResponseMime(xhr.getResponseHeader?.("content-type") || "")) return false;
    const maxBytes = getBoundedResponseMaxBytes(options.maxBytes);
    const declaredLength = Number(xhr.getResponseHeader?.("content-length"));
    if (Number.isFinite(declaredLength) && declaredLength > maxBytes) return false;
    try {
      const text = await readXhrResponseText(xhr, maxBytes, options);
      if (text === null || !captureMayContinue(options)) return false;
      options.extractor?.(text, captureContext.sourceRouteKey);
      return true;
    } catch {
      return false;
    }
  }

  // src/shared/post-model.js
  function normalizePostIdentity(value) {
    const identity = String(value || "");
    return /^[A-Za-z0-9_-]{1,80}$/.test(identity) ? identity : null;
  }
  function parsePostInfoFromUrl(url, baseUrl = globalThis.location?.href || "https://www.threads.com/") {
    if (typeof url !== "string" || !url.trim()) return null;
    try {
      const parsed = new URL(url, baseUrl);
      if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port && parsed.port !== "443") {
        return null;
      }
      const match = parsed.pathname.match(/^\/@([^/]+)\/post\/([^/?#]+)\/?$/);
      if (!match) return null;
      if (!["threads.com", "www.threads.com", "threads.net", "www.threads.net"].includes(parsed.hostname.toLowerCase())) {
        return null;
      }
      const author = decodeURIComponent(match[1]);
      const postId = decodeURIComponent(match[2]);
      if (!/^[A-Za-z0-9._]{1,30}$/.test(author) || !normalizePostIdentity(postId) || postId.length < 5) {
        return null;
      }
      return {
        author,
        postId,
        postUrl: parsed.href
      };
    } catch {
      return null;
    }
  }
  function isLikelyPostCode(value) {
    return typeof value === "string" && /^[A-Za-z0-9_-]{5,32}$/.test(value) && !/^\d+$/.test(value);
  }
  function getPostCodeFromObject(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return null;
    for (const key of ["permalink", "url", "share_url", "post_url"]) {
      const parsed = parsePostInfoFromUrl(value[key]);
      if (parsed?.postId) return parsed.postId;
    }
    const typeName = String(value.__typename || value.typename || value.type || "");
    const hasMediaShape = [
      "carousel_media",
      "image_versions",
      "image_versions2",
      "media",
      "playable_url",
      "video_url",
      "video_versions"
    ].some((key) => key in value);
    if (!hasMediaShape && !/(?:post|thread|media)/i.test(typeName)) return null;
    for (const key of ["code", "shortcode", "media_code", "post_code", "thread_code", "permalink_code"]) {
      if (isLikelyPostCode(value[key])) return value[key];
    }
    return null;
  }
  function pickBestStructuredMediaUrl(candidates, expectedType) {
    return Array.from(candidates || []).map((candidate) => {
      const rawUrl = typeof candidate === "string" ? candidate : candidate?.url || candidate?.src || candidate?.playable_url || "";
      return {
        validated: validateMediaUrl(rawUrl, expectedType),
        score: (Number(candidate?.width) || 0) * (Number(candidate?.height) || 0) * 1e3 + (Number(candidate?.bandwidth || candidate?.bitrate) || 0)
      };
    }).filter((candidate) => candidate.validated.ok).sort((a, b) => b.score - a.score)[0]?.validated.url || null;
  }
  function collectStructuredMediaUrls(value, inheritedPostCode = null) {
    const records = [];
    const recordKeys = /* @__PURE__ */ new Set();
    const visited = /* @__PURE__ */ new WeakSet();
    const addRecord = (type, rawUrl, postId, preserveDuplicateSlot = false) => {
      if (!postId) return;
      const validated = validateMediaUrl(rawUrl, type);
      if (!validated.ok) return;
      const identity = getMediaUrlIdentity(validated.url);
      const key = `${postId}:${type}:${identity || validated.url}`;
      if (!preserveDuplicateSlot && recordKeys.has(key)) return;
      recordKeys.add(key);
      records.push({ type, url: validated.url, postId });
    };
    const visit = (node, postCode, depth, preserveDuplicateSlots = false) => {
      if (!node || depth > 40) return;
      if (Array.isArray(node)) {
        node.forEach((child) => visit(child, postCode, depth + 1, preserveDuplicateSlots));
        return;
      }
      if (typeof node !== "object" || visited.has(node)) return;
      visited.add(node);
      const nextPostCode = getPostCodeFromObject(node) || postCode;
      const hasCarouselMedia = Array.isArray(node.carousel_media) && node.carousel_media.length > 0;
      if (!hasCarouselMedia) {
        const renditionVideoUrl = pickBestStructuredMediaUrl(node.video_versions, "video");
        const directVideoUrl = ["playable_url", "video_url"].map((key) => validateMediaUrl(node[key], "video")).find((result) => result.ok)?.url || null;
        const videoUrl = renditionVideoUrl || directVideoUrl;
        if (videoUrl) addRecord("video", videoUrl, nextPostCode, preserveDuplicateSlots);
        const imageCandidates = node.image_versions2?.candidates || node.image_versions?.candidates;
        const imageUrl = pickBestStructuredMediaUrl(imageCandidates, "image");
        if (!videoUrl && imageUrl) addRecord("image", imageUrl, nextPostCode, preserveDuplicateSlots);
        if (!videoUrl && !imageUrl) {
          ["display_url", "image_url", "thumbnail_src", "thumbnail_url"].forEach((key) => {
            if (typeof node[key] === "string") addRecord("image", node[key], nextPostCode, preserveDuplicateSlots);
          });
        }
      }
      Object.entries(node).forEach(([key, child]) => {
        if ([
          "display_url",
          "image_url",
          "image_versions",
          "image_versions2",
          "playable_url",
          "thumbnail_src",
          "thumbnail_url",
          "video_url",
          "video_versions"
        ].includes(key)) return;
        if (["author", "owner", "profile", "user"].includes(key)) return;
        const crossesPostBoundary = /(?:parent_post|quoted|reply_to|repost)/i.test(key);
        const childPostCode = crossesPostBoundary ? null : nextPostCode;
        const childPreservesDuplicateSlots = !crossesPostBoundary && (preserveDuplicateSlots || key === "carousel_media");
        visit(child, childPostCode, depth + 1, childPreservesDuplicateSlots);
      });
    };
    visit(value, inheritedPostCode, 0);
    return records;
  }

  // src/shared/threads-runtime.js
  var MAX_STRUCTURED_RECORDS_PER_ROUTE = 512;
  var MAX_POST_COUNTERS_PER_ROUTE = 160;
  async function createThreadsRuntime({
    platform,
    captureSource = globalThis.window,
    document = globalThis.document,
    window = globalThis.window,
    initialOptions,
    clock = globalThis
  }) {
    if (!platform) throw new TypeError("platform adapter is required");
    const IS_NODE_RUNTIME2 = !document || !window;
    const STYLE_ID = "tm-target-downloader-style";
    const TOAST_ID = "tm-target-downloader-toast";
    const BUTTON_CLASS = "tm-target-download-button";
    const POST_TOOL_CLASS = "tm-post-media-tool-button";
    const COPY_TOOL_CLASS = "tm-post-copy-tool-button";
    const LINK_TOOL_CLASS = "tm-post-link-tool-button";
    const CLEAN_LINK_MENU_CLASS = "tm-clean-link-menu-item";
    const CLEAN_LINK_ICON_PATH = "M245.14 352.14c8.49-8.49 22.27-8.49 30.76 0 8.5 8.5 8.5 22.27 0 30.76l-58.53 58.54c-20.35 20.34-47.15 30.51-73.94 30.51s-53.6-10.17-73.94-30.51c-20.34-20.35-30.52-47.15-30.52-73.94 0-26.78 10.18-53.6 30.52-73.94l58.53-58.53c8.5-8.5 22.27-8.5 30.77 0 8.49 8.49 8.49 22.27 0 30.76l-58.54 58.53c-11.84 11.85-17.77 27.51-17.77 43.18 0 15.67 5.93 31.33 17.77 43.17 11.85 11.85 27.51 17.78 43.18 17.78 15.67 0 31.33-5.93 43.17-17.77l58.54-58.54zm46.1-92.68c8.47 8.48 8.47 22.24 0 30.71-8.48 8.47-22.23 8.47-30.71 0l-39.78-39.78c-8.47-8.48-8.47-22.23 0-30.71 8.48-8.47 22.23-8.47 30.71 0l39.78 39.78zm45.28 245.07-25.07 5.19c-3.24.66-6.43-1.44-7.09-4.68l-16.18-78.09a6.006 6.006 0 0 1 4.66-7.11l25.05-5.29c3.27-.65 6.45 1.44 7.11 4.69l16.21 78.2c.66 3.25-1.44 6.43-4.69 7.09zM178.82 6.26 203.39.18c3.22-.8 6.48 1.17 7.28 4.38l19.46 77.29c.8 3.23-1.16 6.5-4.39 7.31l-24.8 6.28c-3.23.8-6.5-1.16-7.31-4.38l-19.46-77.43c-.81-3.23 1.16-6.5 4.38-7.31l.27-.06zm264.17 419.63-17.86 18.43a6.03 6.03 0 0 1-8.52 0l-57.22-55.51a6.015 6.015 0 0 1-.11-8.5l17.8-18.39c2.32-2.38 6.13-2.44 8.51-.12l57.28 55.58a6.027 6.027 0 0 1 .12 8.51zm68.81-112.11-6.62 24.69c-.85 3.21-4.15 5.12-7.37 4.26l-77.08-20.62c-3.22-.86-5.12-4.16-4.27-7.38l6.64-24.72c.86-3.21 4.16-5.12 7.38-4.27l77.05 20.67c3.21.85 5.12 4.16 4.27 7.37zM.38 201.65l6.97-24.15a6.025 6.025 0 0 1 7.42-4.11l76.66 21.79c3.2.91 5.05 4.25 4.15 7.45l-6.96 24.61a6.034 6.034 0 0 1-7.42 4.17L4.38 209.55a6.035 6.035 0 0 1-4.15-7.45l.15-.45zM65.14 87.17l17.84-17.81c2.35-2.34 6.17-2.33 8.51.02l56.38 56.41c2.33 2.35 2.33 6.15 0 8.49l-18.06 18.11a6.014 6.014 0 0 1-8.5.02L64.85 95.97a6.03 6.03 0 0 1 0-8.52l.29-.28zm200.98 71.28c-8.49 8.5-22.27 8.5-30.76 0-8.5-8.49-8.5-22.26 0-30.76l59.26-59.26 1.38-1.27c20.23-19.51 46.43-29.26 72.56-29.26 26.78 0 53.58 10.18 73.93 30.53 20.35 20.35 30.53 47.16 30.53 73.94 0 26.79-10.18 53.59-30.52 73.94l-59.26 59.26c-8.5 8.49-22.27 8.49-30.77 0-8.49-8.49-8.49-22.27 0-30.76l59.27-59.27c11.84-11.84 17.77-27.5 17.77-43.17 0-15.67-5.93-31.33-17.77-43.17-11.86-11.86-27.52-17.79-43.18-17.79-15.3 0-30.55 5.59-42.22 16.76l-60.22 60.28z";
    const MODAL_ID = "tm-post-media-modal";
    const LOG_PREFIX = "[Threads Target Downloader]";
    const SCAN_DEBOUNCE_MS = 400;
    const MIN_MEDIA_SIZE = 96;
    const MAX_POST_CONTEXT_ANCESTORS = 24;
    const DOWNLOAD_TIMEOUT_MS = 3e4;
    const DOWNLOAD_WATCHDOG_MS = 35e3;
    const BLOB_DOWNLOAD_WATCHDOG_MS = 15 * 60 * 1e3;
    const SHARE_CONTEXT_TIMEOUT_MS = 12e3;
    const USER_ACTIVATION_TOKEN_MAX_AGE_MS = 10 * 60 * 1e3;
    const trustedActivationTokens = /* @__PURE__ */ new WeakSet();
    const USER_OPTIONS = { ...normalizeOptions(initialOptions || DEFAULT_OPTIONS) };
    let started = false;
    let stopped = false;
    const state = {
      buttonByElement: /* @__PURE__ */ new WeakMap(),
      elementByButton: /* @__PURE__ */ new WeakMap(),
      liveButtons: /* @__PURE__ */ new Set(),
      postCounters: /* @__PURE__ */ new Map(),
      videoUrlsByPostId: /* @__PURE__ */ new Map(),
      imageUrlsByPostId: /* @__PURE__ */ new Map(),
      structuredMediaItemsByPostId: /* @__PURE__ */ new Map(),
      captureRouteGeneration: "",
      scannedScriptContents: /* @__PURE__ */ new WeakMap(),
      mediaRouteKey: "",
      hoverButton: null,
      hoverElement: null,
      hoverMoveRaf: 0,
      hoverScanTimer: 0,
      lastHoverScanAt: 0,
      pendingHoverPoint: null,
      pointerDragActive: false,
      hideTimer: 0,
      detailButton: null,
      detailUiCache: null,
      modalReturnFocus: null,
      copyButtonByRoot: /* @__PURE__ */ new WeakMap(),
      copyButtonByShare: /* @__PURE__ */ new WeakMap(),
      copyContextByButton: /* @__PURE__ */ new WeakMap(),
      liveCopyButtons: /* @__PURE__ */ new Set(),
      linkButtonByRoot: /* @__PURE__ */ new WeakMap(),
      linkButtonByShare: /* @__PURE__ */ new WeakMap(),
      linkContextByButton: /* @__PURE__ */ new WeakMap(),
      liveLinkButtons: /* @__PURE__ */ new Set(),
      detailRoute: "",
      modalItems: [],
      scanTimer: 0,
      pendingScanNetworkRefresh: false,
      rafRefresh: 0,
      layoutRefreshTimer: 0,
      lastLayoutRefreshAt: 0,
      backgroundScanIntervalId: 0,
      uiRefreshIntervalId: 0,
      pendingLayoutFullRefresh: false,
      scrollPositions: /* @__PURE__ */ new WeakMap(),
      disposeSettingsUi: null,
      disposeStyles: null,
      pendingShareContext: null,
      cleanLinkMenuTimer: 0,
      nativeShareCloseTimer: 0,
      suppressNativeShareContextUntil: 0,
      batchDownloadInProgress: false,
      listenerDisposers: [],
      observer: null,
      initObserverTimer: 0,
      startupTimers: [],
      uninstallNetworkCapture: null,
      lifecycleController: new AbortController(),
      diagnostics: {
        observerCallbacks: 0,
        fullRefreshes: 0,
        networkPayloadParses: 0,
        routeTransitions: 0
      }
    };
    function listen(target, type, handler, options) {
      target.addEventListener(type, handler, options);
      state.listenerDisposers.push(() => target.removeEventListener(type, handler, options));
    }
    function saveUserOptions() {
      Promise.resolve(platform.saveOptions(normalizeOptions(USER_OPTIONS))).catch((error) => {
        warn("Saving options failed", error);
      });
    }
    function setUserOption(key, value) {
      if (!(key in DEFAULT_OPTIONS)) return;
      Object.assign(USER_OPTIONS, normalizeOptions({ ...USER_OPTIONS, [key]: value }));
      saveUserOptions();
      applyUserOptions();
      void registerUserOptionMenu();
    }
    function resetUserOptions() {
      Object.assign(USER_OPTIONS, DEFAULT_OPTIONS);
      saveUserOptions();
      applyUserOptions();
      void registerUserOptionMenu();
      toast("Threads Media Downloader 設定已還原預設。");
    }
    function promptNumberOption(key, label, min, max) {
      const currentValue = USER_OPTIONS[key];
      const input = window.prompt(`${label}
目前值:${currentValue} ms
建議範圍:${min}-${max} ms`, String(currentValue));
      if (input == null) return;
      const normalized = normalizeOptions({ ...USER_OPTIONS, [key]: input })[key];
      setUserOption(key, normalized);
      toast(`${label} 已設定為 ${normalized} ms。`);
    }
    function applyUserOptions() {
      clearHoverScanQueue();
      window.clearTimeout(state.layoutRefreshTimer);
      state.layoutRefreshTimer = 0;
      state.scrollPositions = /* @__PURE__ */ new WeakMap();
      startBackgroundScanInterval();
      if (!isPostMediaPickerEnabled()) {
        cleanupDetailButton();
      }
      refreshButtons({ scanNetwork: false });
    }
    async function registerUserOptionMenu() {
      state.disposeSettingsUi?.();
      state.disposeSettingsUi = await platform.installSettingsUi({
        commands: [
          {
            label: `${USER_OPTIONS.enablePostMediaPicker ? "✓" : "□"} 批次下載選擇器:${USER_OPTIONS.enablePostMediaPicker ? "開啟" : "關閉"}`,
            run: () => {
              setUserOption("enablePostMediaPicker", !USER_OPTIONS.enablePostMediaPicker);
              toast(`批次下載選擇器已${USER_OPTIONS.enablePostMediaPicker ? "開啟" : "關閉"}。`);
            }
          },
          { label: `設定 Hover 掃描間隔:${USER_OPTIONS.hoverScanIntervalMs} ms`, run: () => promptNumberOption("hoverScanIntervalMs", "Hover 掃描間隔", 0, 2e3) },
          { label: `設定 Scroll/Resize 刷新間隔:${USER_OPTIONS.layoutRefreshIntervalMs} ms`, run: () => promptNumberOption("layoutRefreshIntervalMs", "Scroll/Resize 刷新間隔", 0, 5e3) },
          { label: `設定背景完整掃描間隔:${USER_OPTIONS.backgroundScanIntervalMs} ms`, run: () => promptNumberOption("backgroundScanIntervalMs", "背景完整掃描間隔", 3e3, 6e4) },
          {
            label: `${USER_OPTIONS.ignoreHorizontalOnlyScroll ? "✓" : "□"} 忽略橫向輪播 Scroll:${USER_OPTIONS.ignoreHorizontalOnlyScroll ? "開啟" : "關閉"}`,
            run: () => {
              setUserOption("ignoreHorizontalOnlyScroll", !USER_OPTIONS.ignoreHorizontalOnlyScroll);
              toast(`忽略橫向輪播 Scroll 已${USER_OPTIONS.ignoreHorizontalOnlyScroll ? "開啟" : "關閉"}。`);
            }
          },
          { label: "還原 Threads Downloader 預設設定", run: resetUserOptions }
        ]
      });
    }
    const PLUGIN_CSS = `
        .${BUTTON_CLASS} {
            position: fixed !important;
            z-index: 2147483647 !important;
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            width: 34px !important;
            min-width: 34px !important;
            height: 34px !important;
            border: 1px solid rgba(255, 255, 255, 0.45) !important;
            border-radius: 999px !important;
            padding: 0 !important;
            color: #fff !important;
            background: rgba(0, 0, 0, 0.72) !important;
            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.26) !important;
            font: 800 18px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
            letter-spacing: 0 !important;
            text-align: center !important;
            white-space: nowrap !important;
            cursor: pointer !important;
            user-select: none !important;
            pointer-events: auto !important;
            backdrop-filter: blur(6px) !important;
            opacity: 1 !important;
        }

        .${BUTTON_CLASS} svg {
            width: 20px !important;
            height: 20px !important;
            display: block !important;
            fill: none !important;
            stroke: currentColor !important;
            stroke-width: 1.75 !important;
            stroke-linecap: round !important;
            stroke-linejoin: round !important;
            pointer-events: none !important;
            flex: 0 0 auto !important;
        }

        .${BUTTON_CLASS}:hover {
            background: rgba(0, 0, 0, 0.9) !important;
        }

        .${BUTTON_CLASS}[data-tm-busy="1"] {
            opacity: 0.68 !important;
            cursor: wait !important;
        }

        .${BUTTON_CLASS}[data-tm-hidden="1"] {
            display: none !important;
        }

        .${POST_TOOL_CLASS},
        .${COPY_TOOL_CLASS},
        .${LINK_TOOL_CLASS} {
            width: 40px !important;
            height: 36px !important;
            min-width: 40px !important;
            flex: 0 0 40px !important;
            border: 0 !important;
            border-radius: 999px !important;
            padding: 0 !important;
            color: rgb(228, 230, 235) !important;
            background: transparent !important;
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            align-self: center !important;
            cursor: pointer !important;
            pointer-events: auto !important;
            vertical-align: middle !important;
            margin: 0 !important;
            line-height: 1 !important;
        }

        .tm-post-media-tool-slot {
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            width: 40px !important;
            height: 36px !important;
            min-width: 40px !important;
            flex: 0 0 40px !important;
            vertical-align: middle !important;
        }

        .${POST_TOOL_CLASS}:hover,
        .${COPY_TOOL_CLASS}:hover,
        .${LINK_TOOL_CLASS}:hover {
            background: rgba(255, 255, 255, 0.08) !important;
        }

        .tm-post-media-tool-fallback {
            display: flex !important;
            justify-content: flex-start !important;
            align-items: center !important;
            min-height: 38px !important;
            margin-top: 8px !important;
        }

        .${POST_TOOL_CLASS} svg,
        .${COPY_TOOL_CLASS} svg,
        .${LINK_TOOL_CLASS} svg {
            width: 20px !important;
            height: 20px !important;
            display: block !important;
            fill: none !important;
            stroke: currentColor !important;
            stroke-width: 1.75 !important;
            stroke-linecap: round !important;
            stroke-linejoin: round !important;
            transform: translateY(0.5px) !important;
            pointer-events: none !important;
        }

        #${MODAL_ID} {
            position: fixed !important;
            inset: 0 !important;
            z-index: 2147483647 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            background: rgba(0, 0, 0, 0.46) !important;
            color: #111 !important;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
        }

        #${MODAL_ID}[data-tm-hidden="1"] {
            display: none !important;
        }

        #${MODAL_ID} .tm-modal {
            width: min(640px, calc(100vw - 24px)) !important;
            max-height: min(720px, calc(100vh - 24px)) !important;
            background: #f4f5f7 !important;
            border-radius: 8px !important;
            box-shadow: 0 18px 70px rgba(0, 0, 0, 0.45) !important;
            overflow: hidden !important;
            display: flex !important;
            flex-direction: column !important;
        }

        #${MODAL_ID} .tm-modal-head {
            position: relative !important;
            padding: 12px 58px 10px !important;
            text-align: center !important;
            border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
        }

        #${MODAL_ID} .tm-modal-title {
            font-size: 18px !important;
            font-weight: 760 !important;
            line-height: 1.25 !important;
        }

        #${MODAL_ID} .tm-modal-subtitle {
            margin-top: 2px !important;
            color: #3454d1 !important;
            font-size: 14px !important;
            user-select: text !important;
        }

        #${MODAL_ID} .tm-close {
            position: absolute !important;
            top: 8px !important;
            right: 12px !important;
            width: 34px !important;
            height: 34px !important;
            border: 0 !important;
            background: transparent !important;
            color: #111 !important;
            font-size: 32px !important;
            line-height: 1 !important;
            cursor: pointer !important;
        }

        #${MODAL_ID} .tm-actions {
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            gap: 14px !important;
            padding: 10px 16px !important;
            flex-wrap: wrap !important;
        }

        #${MODAL_ID} .tm-actions button {
            min-height: 34px !important;
            border: 1px solid rgba(0, 0, 0, 0.28) !important;
            border-radius: 4px !important;
            padding: 6px 14px !important;
            background: #fff !important;
            color: #111 !important;
            font-size: 15px !important;
            cursor: pointer !important;
        }

        #${MODAL_ID} .tm-select-row {
            display: flex !important;
            align-items: center !important;
            gap: 10px !important;
            padding: 0 22px 10px !important;
            font-size: 22px !important;
            font-weight: 760 !important;
        }

        #${MODAL_ID} input[type="checkbox"] {
            width: 28px !important;
            height: 28px !important;
            cursor: pointer !important;
        }

        #${MODAL_ID} .tm-list {
            overflow: auto !important;
            padding: 0 20px 20px !important;
        }

        #${MODAL_ID} .tm-item {
            min-height: 118px !important;
            display: grid !important;
            grid-template-columns: 58px 1fr 38px !important;
            gap: 0 !important;
            border: 1px solid rgba(0, 0, 0, 0.32) !important;
            border-radius: 8px !important;
            overflow: hidden !important;
            background: #f8f9fb !important;
            margin-bottom: 8px !important;
        }

        #${MODAL_ID} .tm-check-cell {
            background: #ddd !important;
            border-right: 1px solid rgba(0, 0, 0, 0.28) !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
        }

        #${MODAL_ID} .tm-preview {
            min-height: 118px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            flex-direction: column !important;
            gap: 8px !important;
            padding: 8px !important;
            font-size: 18px !important;
        }

        #${MODAL_ID} .tm-preview > img {
            max-width: 140px !important;
            max-height: 86px !important;
            object-fit: contain !important;
        }

        #${MODAL_ID} .tm-video-thumbnail {
            position: relative !important;
            width: min(var(--tm-video-thumbnail-width, 168px), 100%) !important;
            aspect-ratio: var(--tm-video-aspect-ratio, 16 / 9) !important;
            overflow: hidden !important;
            border-radius: 7px !important;
            background: #202124 !important;
            box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12) !important;
        }

        #${MODAL_ID} .tm-video-thumbnail[data-orientation="portrait"] {
            --tm-video-thumbnail-width: 80px;
        }

        #${MODAL_ID} .tm-video-thumbnail[data-orientation="square"] {
            --tm-video-thumbnail-width: 118px;
        }

        #${MODAL_ID} .tm-video-thumbnail img,
        #${MODAL_ID} .tm-video-thumbnail video {
            position: relative !important;
            z-index: 1 !important;
            display: block !important;
            width: 100% !important;
            height: 100% !important;
            object-fit: cover !important;
            background: transparent !important;
        }

        #${MODAL_ID} .tm-video-thumbnail-fallback {
            position: absolute !important;
            inset: 0 !important;
            display: grid !important;
            place-items: center !important;
            color: rgba(255, 255, 255, 0.74) !important;
            font-size: 13px !important;
            letter-spacing: 0.08em !important;
        }

        #${MODAL_ID} .tm-video-play-badge {
            position: absolute !important;
            z-index: 2 !important;
            left: 50% !important;
            top: 50% !important;
            width: 34px !important;
            height: 34px !important;
            display: grid !important;
            place-items: center !important;
            border: 1px solid rgba(255, 255, 255, 0.82) !important;
            border-radius: 999px !important;
            background: rgba(0, 0, 0, 0.58) !important;
            color: #fff !important;
            font-size: 15px !important;
            line-height: 1 !important;
            transform: translate(-50%, -50%) !important;
            pointer-events: none !important;
        }

        #${MODAL_ID} .tm-open-cell {
            display: flex !important;
            align-items: flex-start !important;
            justify-content: center !important;
            padding-top: 12px !important;
        }

        #${MODAL_ID} .tm-open {
            border: 0 !important;
            background: transparent !important;
            font-size: 24px !important;
            cursor: pointer !important;
        }

        #${MODAL_ID} .tm-empty {
            padding: 36px 24px !important;
            text-align: center !important;
            color: #333 !important;
            font-size: 16px !important;
        }

        #${TOAST_ID} {
            position: fixed !important;
            right: 18px !important;
            bottom: 22px !important;
            z-index: 2147483647 !important;
            max-width: min(420px, calc(100vw - 28px)) !important;
            padding: 10px 12px !important;
            border-radius: 10px !important;
            color: #fff !important;
            background: rgba(24, 24, 26, 0.95) !important;
            box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28) !important;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
            font-size: 13px !important;
            line-height: 1.35 !important;
            opacity: 0 !important;
            transform: translateY(8px) !important;
            transition: opacity 160ms ease, transform 160ms ease !important;
            pointer-events: none !important;
        }

        #${TOAST_ID}.tm-show {
            opacity: 1 !important;
            transform: translateY(0) !important;
        }
    `;
    function log(...args) {
      console.log(LOG_PREFIX, ...args);
    }
    function warn(...args) {
      console.warn(LOG_PREFIX, ...args);
    }
    function toast(message) {
      if (!document?.body) return;
      let toastNode = document.getElementById(TOAST_ID);
      if (!toastNode) {
        toastNode = document.createElement("div");
        toastNode.id = TOAST_ID;
        document.body.appendChild(toastNode);
      }
      toastNode.textContent = message;
      toastNode.classList.add("tm-show");
      window.clearTimeout(toastNode.__tmTimer);
      toastNode.__tmTimer = window.setTimeout(() => {
        toastNode.classList.remove("tm-show");
      }, 2800);
    }
    function pickBestFromSrcset(srcset) {
      if (!srcset) return null;
      const candidates = srcset.split(",").map((item) => item.trim()).map((item) => {
        const parts = item.split(/\s+/);
        const url = normalizeUrl(parts[0]);
        const descriptor = parts[1] || "";
        let weight = 0;
        if (descriptor.endsWith("w")) {
          weight = parseInt(descriptor, 10);
        } else if (descriptor.endsWith("x")) {
          weight = Math.round(parseFloat(descriptor) * 1e3);
        }
        return { url, weight: Number.isFinite(weight) ? weight : 0 };
      }).filter((item) => item.url);
      candidates.sort((a, b) => b.weight - a.weight);
      return candidates[0]?.url || null;
    }
    function isVisibleRect(rect) {
      return rect.width >= MIN_MEDIA_SIZE && rect.height >= MIN_MEDIA_SIZE && rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
    }
    function isVisibleTextRect(rect) {
      return rect.width > 0 && rect.height > 0;
    }
    function getCssBackgroundImageUrl(element) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
      const bg = window.getComputedStyle(element).backgroundImage || "";
      const match = bg.match(/url\((["']?)(.*?)\1\)/i);
      return match ? normalizeUrl(match[2]) : null;
    }
    function visibleMediaCount(node) {
      return Array.from(node.querySelectorAll?.("img, video") || []).filter((media) => isVisibleRect(media.getBoundingClientRect())).length;
    }
    function isLikelyPostImage(img) {
      const rect = img.getBoundingClientRect();
      const src = img.currentSrc || img.src || "";
      const alt = img.getAttribute("alt") || "";
      if (!src || src.startsWith("data:") || src.startsWith("blob:")) return false;
      if (!isVisibleRect(rect)) return false;
      if (/profile_pic|s150x150|s320x320|emoji|sprite|static|favicon/i.test(src)) return false;
      if (/avatar|profile picture/i.test(alt)) return false;
      return Boolean(resolveImageUrl(img));
    }
    function isLikelyDetailPostImage(img) {
      const src = img.currentSrc || img.src || img.getAttribute("src") || "";
      const alt = img.getAttribute("alt") || "";
      const width = Number(img.getAttribute("width")) || img.naturalWidth || img.getBoundingClientRect().width;
      const height = Number(img.getAttribute("height")) || img.naturalHeight || img.getBoundingClientRect().height;
      if (!src || src.startsWith("data:") || src.startsWith("blob:")) return false;
      if (/profile_pic|s150x150|emoji|sprite|static|favicon/i.test(src)) return false;
      if (/avatar|profile picture|大頭貼/i.test(alt)) return false;
      if (width < MIN_MEDIA_SIZE || height < MIN_MEDIA_SIZE) return false;
      return Boolean(resolveImageUrl(img));
    }
    function isLikelyPostVideo(video) {
      return isVisibleRect(video.getBoundingClientRect());
    }
    function resolveImageUrl(img) {
      const urls = [
        pickBestFromSrcset(img.getAttribute("srcset") || img.srcset),
        img.currentSrc,
        img.src,
        img.getAttribute("src")
      ];
      const picture = img.closest("picture");
      if (picture) {
        picture.querySelectorAll("source[srcset]").forEach((source) => {
          urls.push(pickBestFromSrcset(source.getAttribute("srcset") || source.srcset));
        });
      }
      return urls.map((url) => normalizeUrl(url)).find(isImageUrl) || null;
    }
    function resolveVideoUrl(video, contextElement) {
      syncMediaRouteScope();
      scanInlineScriptsForVideoUrls();
      const urls = [
        video.currentSrc,
        video.src,
        video.getAttribute("src")
      ];
      video.querySelectorAll("source[src]").forEach((source) => {
        urls.push(source.src, source.getAttribute("src"));
      });
      const postContext = findPostContext(contextElement || video);
      return selectPostBoundVideoUrl({
        directUrls: urls,
        postId: postContext?.postId,
        videoUrlsByPostId: state.videoUrlsByPostId
      });
    }
    function selectPostBoundVideoUrl({ directUrls = [], postId, videoUrlsByPostId }) {
      const directUrl = directUrls.map((url) => validateMediaUrl(url, "video")).find((result) => result.ok)?.url;
      if (directUrl) return directUrl;
      if (!postId || postId === "unknown" || !(videoUrlsByPostId instanceof Map)) return null;
      const identity = normalizePostIdentity(postId);
      if (!identity) return null;
      const validatedCachedUrls = (videoUrlsByPostId.get(identity) || []).map((url) => validateMediaUrl(url, "video")).filter((result) => result.ok).map((result) => result.url);
      const distinctUrls = /* @__PURE__ */ new Map();
      validatedCachedUrls.forEach((url) => {
        distinctUrls.set(getMediaUrlIdentity(url) || url, url);
      });
      return distinctUrls.size === 1 ? distinctUrls.values().next().value : null;
    }
    function findPostInfoInNode(node) {
      if (node.matches?.('a[href*="/post/"]')) {
        const ownMatch = parsePostInfoFromUrl(node.href);
        if (ownMatch) return ownMatch;
      }
      const links = Array.from(node.querySelectorAll?.('a[href*="/post/"]') || []);
      return links.map((link) => parsePostInfoFromUrl(link.href)).find(Boolean) || null;
    }
    function rectCenterDistanceScore(sourceRect, candidateRect) {
      if (!candidateRect || candidateRect.width === 0 || candidateRect.height === 0) {
        return Number.MAX_SAFE_INTEGER;
      }
      const sourceX = sourceRect.left + sourceRect.width / 2;
      const sourceY = sourceRect.top + sourceRect.height / 2;
      const candidateX = candidateRect.left + candidateRect.width / 2;
      const candidateY = candidateRect.top + candidateRect.height / 2;
      return Math.abs(sourceY - candidateY) * 3 + Math.abs(sourceX - candidateX);
    }
    function isInsideNestedPostBlock(element, root) {
      const pressableRoot = element?.closest?.("[data-pressable-container]");
      return Boolean(
        pressableRoot && pressableRoot !== root && root?.contains?.(pressableRoot) && hasPostBoundaryCues(pressableRoot)
      );
    }
    function findBestPostInfoInNode(node, element, excludeNestedPostBlocks = false) {
      if (node.matches?.('a[href*="/post/"]')) {
        const ownMatch = parsePostInfoFromUrl(node.href);
        if (ownMatch) return ownMatch;
      }
      const links = Array.from(node.querySelectorAll?.('a[href*="/post/"]') || []).filter((link) => !excludeNestedPostBlocks || !isInsideNestedPostBlock(link, node)).filter((link) => {
        const boundarySelector = "article,[role=article],[data-pressable-container]";
        const elementBoundary = element?.closest?.(boundarySelector) || null;
        const linkBoundary = link.closest?.(boundarySelector) || null;
        if (!elementBoundary || !node.contains?.(elementBoundary)) {
          return !linkBoundary || linkBoundary === node;
        }
        return !linkBoundary || linkBoundary === elementBoundary;
      });
      const sourceRect = element.getBoundingClientRect();
      const candidates = links.map((link) => ({ link, info: parsePostInfoFromUrl(link.href) })).filter((item) => item.info).map((item) => ({
        ...item,
        score: item.link.contains(element) ? -1 : rectCenterDistanceScore(sourceRect, item.link.getBoundingClientRect())
      })).sort((a, b) => a.score - b.score);
      return candidates[0]?.info || null;
    }
    function isModalControlIntent(eventTarget, modalControls, intent) {
      return eventTarget === modalControls?.[intent];
    }
    function findPostTimeInNode(node) {
      const timeNode = node.matches?.("time[datetime]") ? node : node.querySelector?.("time[datetime]");
      const datetime = timeNode?.getAttribute?.("datetime");
      if (!datetime) return null;
      const date = new Date(datetime);
      return Number.isNaN(date.getTime()) ? null : date;
    }
    function findBestPostTimeInNode(node, element) {
      const timeNodes = [
        ...node.matches?.("time[datetime]") ? [node] : [],
        ...Array.from(node.querySelectorAll?.("time[datetime]") || [])
      ];
      const sourceRect = element.getBoundingClientRect();
      const best = timeNodes.map((timeNode) => {
        const datetime = timeNode.getAttribute("datetime");
        const date = datetime ? new Date(datetime) : null;
        return {
          date,
          score: rectCenterDistanceScore(sourceRect, timeNode.getBoundingClientRect())
        };
      }).filter((item) => item.date && !Number.isNaN(item.date.getTime())).sort((a, b) => a.score - b.score)[0];
      return best?.date || null;
    }
    function getOwnedDetailPostFallback(element, detailRoot, pageInfo) {
      if (!pageInfo?.postId || !detailRoot) return null;
      return isMediaOwnedByPost(element, detailRoot, pageInfo.postId) ? pageInfo : null;
    }
    function acceptPostContextCandidate(info, pageInfo, element, detailRoot, crossedUnresolvedPostBoundary = false) {
      if (!info) return null;
      if (crossedUnresolvedPostBoundary) return null;
      if (pageInfo?.postId && info.postId === pageInfo.postId && !getOwnedDetailPostFallback(element, detailRoot, pageInfo)) return null;
      return info;
    }
    function findPostContext(element) {
      let nearestTime = null;
      let node = element;
      const pageInfo = parsePostInfoFromUrl(location.href);
      let detailRoot = null;
      let crossedUnresolvedPostBoundary = false;
      for (let depth = 0; node && depth < MAX_POST_CONTEXT_ANCESTORS; depth += 1) {
        nearestTime = nearestTime || findBestPostTimeInNode(node, element);
        const info = findBestPostInfoInNode(node, element);
        if (info) {
          if (pageInfo?.postId && info.postId === pageInfo.postId) {
            detailRoot = detailRoot || findDetailPostRoot();
          }
          const acceptedInfo = acceptPostContextCandidate(
            info,
            pageInfo,
            element,
            detailRoot,
            crossedUnresolvedPostBoundary
          );
          if (acceptedInfo) return { ...acceptedInfo, createdAt: nearestTime };
          break;
        }
        if (isPostBoundaryNode(node) && hasPostBoundaryCues(node)) {
          crossedUnresolvedPostBoundary = true;
        }
        node = node.parentElement;
      }
      if (pageInfo) {
        detailRoot = detailRoot || findDetailPostRoot();
        const ownedPageInfo = getOwnedDetailPostFallback(element, detailRoot, pageInfo);
        if (ownedPageInfo) {
          return {
            ...ownedPageInfo,
            createdAt: nearestTime || findPostTimeInNode(detailRoot)
          };
        }
      }
      return {
        author: "unknown",
        postId: "unknown",
        postUrl: location.href,
        createdAt: nearestTime || null
      };
    }
    function nextPostSequence(postInfo) {
      const key = `${postInfo.author}_${postInfo.postId}`;
      const next = (state.postCounters.get(key) || 0) + 1;
      state.postCounters.delete(key);
      state.postCounters.set(key, next);
      trimMapToSize(state.postCounters, MAX_POST_COUNTERS_PER_ROUTE);
      return String(next).padStart(2, "0");
    }
    function buildFilename(item, contentType) {
      const postInfo = item.postInfo || (item.element ? findPostContext(item.element) : getCurrentDetailPostInfo() || { author: "unknown", postId: "unknown", createdAt: null });
      const sequence = item.sequence || nextPostSequence(postInfo);
      return buildMediaFilename({
        type: item.type,
        url: item.url,
        contentType,
        postInfo,
        sequence
      });
    }
    function isTrustedUserActivation(event, userActivation) {
      if (event?.isTrusted !== true) return false;
      const activationState = userActivation === void 0 ? typeof navigator !== "undefined" ? navigator.userActivation : null : userActivation;
      if (!activationState || activationState.isActive === true) return true;
      return event.type === "click" && Number(event.detail) === 0;
    }
    function createUserActivationToken(event, userActivation) {
      if (!isTrustedUserActivation(event, userActivation)) return null;
      const token = Object.freeze({
        createdAt: Date.now(),
        routeKey: getMediaRouteKey()
      });
      trustedActivationTokens.add(token);
      return token;
    }
    function isValidUserActivationToken(token, now = Date.now()) {
      if (!token || typeof token !== "object" || !trustedActivationTokens.has(token)) return false;
      if (now - token.createdAt > USER_ACTIVATION_TOKEN_MAX_AGE_MS) return false;
      return token.routeKey === getMediaRouteKey();
    }
    function copyText(text, activationToken) {
      if (!isValidUserActivationToken(activationToken)) return false;
      Promise.resolve(platform.writeClipboard(text)).catch(() => {
      });
      return true;
    }
    function buildCleanThreadsPostUrl(postInfo) {
      if (!postInfo?.author || !postInfo?.postId) return "";
      return `https://www.threads.com/@${postInfo.author}/post/${postInfo.postId}`;
    }
    function getRenderedText(element) {
      if (!element) return "";
      return String(element.innerText || "").replace(/\r\n?/g, "\n").replace(/^\n+|\n+$/g, "");
    }
    function stripTrailingCarouselCounter(text) {
      let output = String(text || "");
      const counterPatterns = [
        /\n[ \t\u00a0]*\d+[ \t\u00a0]*\n[ \t\u00a0]*\/[ \t\u00a0]*\n[ \t\u00a0]*\d+[ \t\u00a0]*$/,
        /\n[ \t\u00a0]*\d+[ \t\u00a0]*\/[ \t\u00a0]*\d+[ \t\u00a0]*$/
      ];
      counterPatterns.forEach((pattern) => {
        output = output.replace(pattern, "");
      });
      return output.replace(/[ \t\u00a0]+$/g, "").replace(/\n+$/g, "");
    }
    function cleanPostTextFragment(text) {
      return String(text || "").replace(/\r\n?/g, "\n").replace(/[ \t\u00a0]*(?:\n[ \t\u00a0]*)?(?:翻譯|查看翻譯)[ \t\u00a0]*$/i, "").replace(/[ \t\u00a0]+$/g, "").replace(/^\n+|\n+$/g, "");
    }
    function isThreadsMusicPlaybackControl(element) {
      if (!element?.matches?.("button,[role=button]")) return false;
      const label = String(element.getAttribute?.("aria-label") || "").trim();
      return /^(?:播放|暫停|暂停)音[樂乐]$/.test(label) || /^(?:play|pause)\s+music$/i.test(label) || /^(?:音楽を再生|音楽を一時停止)$/.test(label);
    }
    function getThreadsMusicAttachmentTop(root) {
      const rootRect = root.getBoundingClientRect();
      return Array.from(root.querySelectorAll("[aria-label]")).filter(isThreadsMusicPlaybackControl).map((element) => element.getBoundingClientRect()).filter((rect) => Number.isFinite(rect.top) && rect.bottom > rootRect.top).map((rect) => rect.top).filter((top) => top >= rootRect.top).sort((a, b) => a - b)[0];
    }
    function getPostBlockTextBoundary(root, actionBar) {
      const rootRect = root.getBoundingClientRect();
      const actionTop = actionBar?.getBoundingClientRect?.().top;
      const musicAttachmentTop = getThreadsMusicAttachmentTop(root);
      const mediaTop = Array.from(root.querySelectorAll("img, video")).filter(isDownloadableHoverMedia).map((element) => element.getBoundingClientRect()).filter((rect) => rect.width >= MIN_MEDIA_SIZE && rect.height >= MIN_MEDIA_SIZE).map((rect) => rect.top).filter((top) => top >= rootRect.top).sort((a, b) => a - b)[0];
      return Math.min(
        Number.isFinite(mediaTop) ? mediaTop : Infinity,
        Number.isFinite(musicAttachmentTop) ? musicAttachmentTop : Infinity,
        Number.isFinite(actionTop) ? actionTop : Infinity,
        rootRect.bottom
      );
    }
    function isExcludedPostBlockTextElement(element, root, boundaryTop, postInfo) {
      if (!element || !root.contains(element)) return true;
      if (element.closest(`.${POST_TOOL_CLASS}, .${COPY_TOOL_CLASS}, .${LINK_TOOL_CLASS}, .${BUTTON_CLASS}, #${MODAL_ID}`)) return true;
      const interactiveAncestor = element.closest('button, [role="button"], nav');
      if (interactiveAncestor && interactiveAncestor !== root && root.contains(interactiveAncestor)) return true;
      const enclosingLink = element.closest("a[href]");
      if (enclosingLink && (enclosingLink === element || enclosingLink.contains(element))) {
        const href = enclosingLink.getAttribute("href") || "";
        if (/\/post\//i.test(href)) {
          const linkInfo = parsePostInfoFromUrl(href);
          const belongsToCurrentPost = Boolean(
            postInfo?.postId && linkInfo?.postId === postInfo.postId
          );
          if (!belongsToCurrentPost) return true;
        } else if (/\/@[^/]+\/?$|\/search(?:\?|$)/i.test(href)) {
          return true;
        }
      }
      if (element.querySelector("time, img, video")) return true;
      const rect = element.getBoundingClientRect();
      if (!isVisibleTextRect(rect) || rect.top >= boundaryTop || rect.bottom <= root.getBoundingClientRect().top) return true;
      const text = getRenderedText(element);
      if (!text) return true;
      if (postInfo?.author && text.replace(/^@/, "") === postInfo.author.replace(/^@/, "")) return true;
      if (/^\d[\d,.]*\s*$/.test(text)) return true;
      if (/^\d+\s*(秒|分鐘?|分|小時|天|週|周|個月|月|年)\s*$/.test(text)) return true;
      return false;
    }
    function scorePostBlockTextElement(element, root, boundaryTop) {
      const text = getRenderedText(element);
      const rect = element.getBoundingClientRect();
      const rootRect = root.getBoundingClientRect();
      const lineCount = text.split("\n").length;
      const whiteSpace = window.getComputedStyle(element).whiteSpace || "";
      const hasNestedInteractiveContent = Boolean(element.querySelector('button, [role="button"], img, video, time'));
      let score = text.length * 8;
      score += Math.min(lineCount, 20) * 30;
      score += element.matches('[dir="auto"]') ? 420 : 0;
      score += /pre|break-spaces/.test(whiteSpace) ? 220 : 0;
      score += rect.width >= 180 ? 80 : 0;
      score += Math.min(160, Math.max(0, rect.top - rootRect.top) * 0.45);
      if (hasNestedInteractiveContent) score -= 900;
      if (rect.bottom > boundaryTop + 4) score -= 500;
      if (text.length <= 2) score -= 80;
      return score;
    }
    function extractPostBlockText(root, actionBar) {
      if (!root) return "";
      const boundaryTop = getPostBlockTextBoundary(root, actionBar);
      const postInfo = findBestPostInfoInNode(root, actionBar || root, true) || findPostInfoInNode(root);
      const collectCandidates = (elements) => elements.filter((element) => !isExcludedPostBlockTextElement(element, root, boundaryTop, postInfo)).map((element) => ({
        element,
        text: cleanPostTextFragment(getRenderedText(element)),
        rect: element.getBoundingClientRect(),
        score: scorePostBlockTextElement(element, root, boundaryTop)
      })).filter((item) => item.text).sort((a, b) => b.score - a.score);
      let candidates = collectCandidates(Array.from(root.querySelectorAll('[dir="auto"]')));
      if (candidates.length === 0) {
        candidates = collectCandidates(Array.from(root.querySelectorAll("p, div, span")));
      }
      const orderedCandidates = candidates.filter((item) => !candidates.some(
        (other) => other !== item && item.element.contains(other.element) && other.text === item.text
      )).sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
      const fragments = [];
      orderedCandidates.forEach((item) => {
        const text = item.text;
        if (!text) return;
        if (fragments.some((fragment) => fragment === text || fragment.includes(text))) return;
        for (let index = fragments.length - 1; index >= 0; index -= 1) {
          if (text.includes(fragments[index])) {
            fragments.splice(index, 1);
          }
        }
        fragments.push(text);
      });
      return stripTrailingCarouselCounter(fragments.join("\n"));
    }
    function copyPostBlockText(root, actionBar, activationToken) {
      if (!isValidUserActivationToken(activationToken)) return false;
      const text = extractPostBlockText(root, actionBar);
      if (!text) {
        toast("找不到這則貼文的文字。");
        return false;
      }
      copyText(text, activationToken);
      toast("這則貼文的文字已複製到剪貼簿。");
      return true;
    }
    function copyPostBlockCleanLink(root, shareButton, activationToken) {
      if (!isValidUserActivationToken(activationToken)) return false;
      const postInfo = root ? findBestPostInfoInNode(root, shareButton || root, true) : parsePostInfoFromUrl(location.href);
      const cleanUrl = buildCleanThreadsPostUrl(postInfo || parsePostInfoFromUrl(location.href));
      if (!cleanUrl) {
        toast("找不到這則貼文的連結。");
        return false;
      }
      copyText(cleanUrl, activationToken);
      toast("這則貼文的無追蹤碼連結已複製到剪貼簿。");
      return true;
    }
    function getDownloadErrorText(error) {
      if (typeof error === "string") return error.toLowerCase();
      return [error?.error, error?.code, error?.name, error?.message, error?.details].filter(Boolean).join(" ").toLowerCase();
    }
    function isSecurityDownloadError(error) {
      return /(?:not[_\s-]?whitelisted|not[_\s-]?permitted|forbidden|unauthori[sz]ed|permission|security|whitelist)/i.test(getDownloadErrorText(error));
    }
    function isTransientDownloadError(error) {
      if (isSecurityDownloadError(error)) return false;
      return /(?:timeout|network|connection|not[_\s-]?succeeded|server|temporar|download[_\s-]?failed)/i.test(getDownloadErrorText(error));
    }
    function triggerBlobDownload(blob, filename) {
      const blobUrl = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = blobUrl;
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      link.remove();
      window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1200);
    }
    function downloadViaBlob(item, filename, activationToken, options = {}) {
      if (stopped || state.lifecycleController.signal.aborted) {
        return Promise.reject(new Error("runtime_stopped"));
      }
      if (!isValidUserActivationToken(activationToken)) {
        return Promise.reject(new Error("trusted_user_activation_required"));
      }
      const expectedType = item?.type === "image" || item?.type === "video" ? item.type : null;
      const validated = validateMediaUrl(item?.url, expectedType);
      if (!validated.ok) return Promise.reject(new Error(`unsafe_media_url:${validated.reason}`));
      if (typeof platform.requestMedia !== "function") {
        return Promise.reject(new Error("media_request_unsupported"));
      }
      const watchdogMs = Math.max(1, Number(options.watchdogMs) || BLOB_DOWNLOAD_WATCHDOG_MS);
      const timeoutMs = Math.max(watchdogMs, Number(options.timeoutMs) || DOWNLOAD_TIMEOUT_MS);
      const saveBlob = typeof options.saveBlob === "function" ? options.saveBlob : triggerBlobDownload;
      const setTimer = options.setTimeoutFn || setTimeout;
      const clearTimer = options.clearTimeoutFn || clearTimeout;
      return new Promise((resolve, reject) => {
        let settled = false;
        let requestHandle = null;
        let watchdogTimer = null;
        let signalAbortHandler = null;
        let lifecycleAbortHandler = null;
        const settle = (error, value) => {
          if (settled) return;
          settled = true;
          if (watchdogTimer) clearTimer(watchdogTimer);
          if (options.signal && signalAbortHandler) {
            options.signal.removeEventListener?.("abort", signalAbortHandler);
          }
          if (lifecycleAbortHandler) {
            state.lifecycleController.signal.removeEventListener?.("abort", lifecycleAbortHandler);
          }
          if (error) reject(error);
          else resolve(value);
        };
        const abortRequest = (reason) => {
          try {
            requestHandle?.abort?.();
          } catch (error) {
            warn("Media request abort failed", error);
          }
          settle(reason instanceof Error ? reason : new Error(String(reason || "download aborted")));
        };
        const armWatchdog = () => {
          if (settled) return;
          if (watchdogTimer) clearTimer(watchdogTimer);
          watchdogTimer = setTimer(() => {
            abortRequest(new Error("XHR download watchdog timeout"));
          }, watchdogMs);
        };
        signalAbortHandler = () => abortRequest(new Error("download aborted"));
        lifecycleAbortHandler = () => abortRequest(new Error("runtime stopped"));
        if (options.signal?.aborted || state.lifecycleController.signal.aborted) {
          settle(new Error("download aborted"));
          return;
        }
        options.signal?.addEventListener?.("abort", signalAbortHandler, { once: true });
        state.lifecycleController.signal.addEventListener?.("abort", lifecycleAbortHandler, { once: true });
        const requestDetails = {
          method: "GET",
          url: validated.url,
          responseType: "blob",
          anonymous: true,
          timeout: timeoutMs,
          onload: (response) => {
            if (settled || stopped || state.lifecycleController.signal.aborted) return;
            if (response.status < 200 || response.status >= 300) {
              settle(new Error(`HTTP ${response.status}`));
              return;
            }
            const finalUrl = response.finalUrl || response.responseURL || validated.url;
            const finalValidation = validateMediaUrl(finalUrl, validated.type);
            if (!finalValidation.ok) {
              settle(new Error(`unsafe_final_media_url:${finalValidation.reason}`));
              return;
            }
            const declaredMediaMimes = [
              response.response?.type,
              getHeaderValue(response.responseHeaders, "content-type")
            ].map((value) => String(value || "").split(";", 1)[0].trim().toLowerCase()).filter(Boolean);
            if (declaredMediaMimes.length === 0) {
              settle(new Error("unexpected_media_mime:missing"));
              return;
            }
            const unexpectedMime = declaredMediaMimes.find(
              (mime) => mime !== "application/octet-stream" && !mime.startsWith(`${validated.type}/`)
            );
            if (unexpectedMime) {
              settle(new Error(`unexpected_media_mime:${unexpectedMime}`));
              return;
            }
            const blobType = declaredMediaMimes.find((mime) => mime.startsWith(`${validated.type}/`)) || declaredMediaMimes[0];
            const finalName = filename || buildFilename({ ...item, url: finalValidation.url }, blobType);
            if (settled || stopped || state.lifecycleController.signal.aborted) return;
            try {
              saveBlob(response.response, finalName);
              settle(null, finalName);
            } catch (error) {
              settle(error);
            }
          },
          onerror: (error) => settle(error instanceof Error ? error : new Error(getDownloadErrorText(error) || "XHR download failed")),
          ontimeout: () => settle(new Error("XHR download timeout")),
          onprogress: () => armWatchdog(),
          onabort: () => settle(new Error("XHR download aborted"))
        };
        try {
          requestHandle = platform.requestMedia(requestDetails);
        } catch (error) {
          settle(error);
          return;
        }
        if (!settled) {
          armWatchdog();
        }
      });
    }
    function downloadItem(item, activationToken, options = {}) {
      if (stopped || !isValidUserActivationToken(activationToken)) return Promise.resolve(false);
      const expectedType = item?.type === "image" || item?.type === "video" ? item.type : null;
      const validated = validateMediaUrl(item?.url, expectedType);
      if (!validated.ok) {
        warn("unsafe media URL rejected", validated.reason);
        return Promise.resolve(false);
      }
      const safeItem = { ...item, type: validated.type, url: validated.url };
      const postInfo = safeItem.postInfo || (safeItem.contextElement || safeItem.element ? findPostContext(safeItem.contextElement || safeItem.element) : getCurrentDetailPostInfo() || { author: "unknown", postId: "unknown", postUrl: location.href, createdAt: null });
      const sequence = nextPostSequence(postInfo);
      const filename = buildFilename({ ...safeItem, postInfo, sequence });
      const timeoutMs = Math.max(1, Number(options.timeoutMs) || DOWNLOAD_TIMEOUT_MS);
      const watchdogMs = Math.max(timeoutMs, Number(options.watchdogMs) || DOWNLOAD_WATCHDOG_MS);
      const blobWatchdogMs = Math.max(watchdogMs, Number(options.blobWatchdogMs) || BLOB_DOWNLOAD_WATCHDOG_MS);
      const overallWatchdogMs = Math.max(watchdogMs, Number(options.overallWatchdogMs) || watchdogMs * 2 + 5e3);
      const fallbackOverallWatchdogMs = Math.max(overallWatchdogMs, blobWatchdogMs + 5e3);
      const setTimer = options.setTimeoutFn || setTimeout;
      const clearTimer = options.clearTimeoutFn || clearTimeout;
      const fallbackController = typeof AbortController === "function" ? new AbortController() : null;
      toast(`Download requested: ${filename}`);
      return new Promise((resolve) => {
        let settled = false;
        let fallbackStarted = false;
        let downloadHandle = null;
        let watchdogTriggered = false;
        let gmWatchdogTimer = null;
        let overallWatchdogTimer = null;
        const stopDownload = () => {
          try {
            downloadHandle?.abort?.();
          } catch {
          }
          fallbackController?.abort();
          finish(false);
        };
        const finish = (success) => {
          if (settled) return;
          settled = true;
          if (gmWatchdogTimer) clearTimer(gmWatchdogTimer);
          if (overallWatchdogTimer) clearTimer(overallWatchdogTimer);
          if (!success) fallbackController?.abort();
          state.lifecycleController.signal.removeEventListener?.("abort", stopDownload);
          resolve(Boolean(success));
        };
        state.lifecycleController.signal.addEventListener?.("abort", stopDownload, { once: true });
        const failWithoutFallback = (error) => {
          warn("download rejected without blob fallback", error);
          toast("Download failed. Please retry or open the post link manually.");
          finish(false);
        };
        const tryBlobFallback = (error, force = false) => {
          if (settled || fallbackStarted) return;
          if (isSecurityDownloadError(error)) {
            failWithoutFallback(error);
            return;
          }
          if (!force && !isTransientDownloadError(error)) {
            failWithoutFallback(error);
            return;
          }
          fallbackStarted = true;
          if (gmWatchdogTimer) clearTimer(gmWatchdogTimer);
          if (overallWatchdogTimer) clearTimer(overallWatchdogTimer);
          overallWatchdogTimer = setTimer(() => {
            fallbackController?.abort();
            finish(false);
          }, fallbackOverallWatchdogMs);
          warn("Direct download failed, trying anonymous blob fallback", error);
          downloadViaBlob(safeItem, filename, activationToken, {
            ...options,
            timeoutMs,
            watchdogMs: blobWatchdogMs,
            signal: fallbackController?.signal
          }).then((finalName) => {
            toast(`Download started: ${finalName}`);
            finish(true);
          }).catch((blobError) => {
            warn("fallback download failed", blobError);
            toast("Download failed. Please retry or open the post link manually.");
            finish(false);
          });
        };
        const armGmIdleWatchdog = () => {
          if (settled || fallbackStarted) return;
          if (gmWatchdogTimer) clearTimer(gmWatchdogTimer);
          if (overallWatchdogTimer) clearTimer(overallWatchdogTimer);
          overallWatchdogTimer = setTimer(() => {
            watchdogTriggered = true;
            try {
              downloadHandle?.abort?.();
            } catch (error) {
              warn("Direct download abort failed", error);
            }
            fallbackController?.abort();
            finish(false);
          }, overallWatchdogMs);
          gmWatchdogTimer = setTimer(() => {
            watchdogTriggered = true;
            try {
              downloadHandle?.abort?.();
            } catch (error) {
              warn("Direct download abort failed", error);
            }
            tryBlobFallback(new Error("direct_download_watchdog_timeout"), true);
          }, watchdogMs);
        };
        if (typeof platform.downloadMedia !== "function") {
          tryBlobFallback(new Error("direct_download_unsupported"), true);
          return;
        }
        const details = {
          url: safeItem.url,
          name: filename,
          saveAs: false,
          onload: () => {
            if (fallbackStarted || settled) return;
            toast(`Download started: ${filename}`);
            finish(true);
          },
          onerror: (error) => {
            if (fallbackStarted || settled) return;
            if (isSecurityDownloadError(error)) {
              failWithoutFallback(error);
              return;
            }
            tryBlobFallback(error);
          },
          ontimeout: (error) => {
            if (fallbackStarted || settled) return;
            tryBlobFallback(error || new Error("direct_download_timeout"), true);
          },
          onprogress: () => {
            armGmIdleWatchdog();
          },
          onabort: (error) => {
            if (!watchdogTriggered && !fallbackStarted && !settled) {
              failWithoutFallback(error || new Error("direct_download_aborted"));
            }
          }
        };
        try {
          downloadHandle = platform.downloadMedia(details);
        } catch (error) {
          if (isSecurityDownloadError(error)) failWithoutFallback(error);
          else tryBlobFallback(error);
          return;
        }
        if (!settled && !fallbackStarted) {
          armGmIdleWatchdog();
        }
      });
    }
    function mediaItemFromElement(element) {
      scanInlineScriptsForVideoUrls();
      if (element?.tagName === "IMG") {
        const video = findAssociatedVideoForImage(element);
        if (video) {
          const videoUrl = resolveVideoUrl(video, element);
          return videoUrl ? { type: "video", url: videoUrl, element: video, contextElement: element } : null;
        }
        if (isLikelyVideoThumbnail(element)) {
          const postContext2 = findPostContext(element);
          const mappedPostUrl = pickMappedVideoUrlForThumbnail(element, postContext2);
          return mappedPostUrl ? { type: "video", url: mappedPostUrl, element, contextElement: element } : null;
        }
        const url = resolveImageUrl(element);
        return url ? { type: "image", url, element } : null;
      }
      if (element?.tagName === "VIDEO") {
        const url = resolveVideoUrl(element);
        return url ? { type: "video", url, element } : null;
      }
      const containedMedia = Array.from(element?.querySelectorAll?.("img, video") || []).filter(isDownloadableHoverMedia).map((media) => ({ media, rect: media.getBoundingClientRect() })).sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height)[0]?.media;
      if (containedMedia) return mediaItemFromElement(containedMedia);
      const postContext = element ? findPostContext(element) : null;
      const mappedVideoUrl = postContext ? pickMappedVideoUrlForSurface(element, postContext) : null;
      if (mappedVideoUrl) {
        return { type: "video", url: mappedVideoUrl, element, contextElement: element };
      }
      const bgUrl = getCssBackgroundImageUrl(element);
      if (bgUrl && isImageUrl(bgUrl) && !isLikelyVideoSurface(element)) {
        return { type: "image", url: bgUrl, element };
      }
      return null;
    }
    function getAssociatedVideoElement(element) {
      if (element?.tagName === "VIDEO") return element;
      if (element?.tagName !== "IMG") return null;
      const directVideo = findAssociatedVideoForImage(element);
      if (directVideo) return directVideo;
      const postRoot = findPostRoot(element);
      const imgRect = element.getBoundingClientRect();
      return Array.from(postRoot.querySelectorAll?.("video") || []).filter(isLikelyPostVideo).find((video) => rectsOverlap(imgRect, video.getBoundingClientRect())) || null;
    }
    async function nudgeVideoLoading(element) {
      if (stopped) return;
      const video = getAssociatedVideoElement(element);
      if (!video) return;
      try {
        video.preload = "auto";
        video.load?.();
      } catch (error) {
      }
      try {
        const playPromise = video.play?.();
        if (playPromise?.catch) {
          await playPromise.catch(() => {
          });
        }
      } catch (error) {
      }
    }
    async function mediaItemFromElementWithRetry(element) {
      if (stopped) return null;
      let item = mediaItemFromElement(element);
      if (item) return item;
      const mayBeVideo = element?.tagName === "VIDEO" || element?.tagName === "IMG" && isLikelyVideoThumbnail(element);
      if (!mayBeVideo) return null;
      toast("Resolving video URL...");
      await nudgeVideoLoading(element);
      for (let attempt = 0; attempt < 10; attempt += 1) {
        await new Promise((resolve) => window.setTimeout(resolve, 250));
        if (stopped) return null;
        scanInlineScriptsForVideoUrls();
        item = mediaItemFromElement(element);
        if (item) return item;
      }
      return null;
    }
    function blockEvent(event) {
      event.preventDefault();
      event.stopPropagation();
      if (typeof event.stopImmediatePropagation === "function") {
        event.stopImmediatePropagation();
      }
    }
    async function activateButton(button, activationToken, options = {}) {
      if (stopped || !isValidUserActivationToken(activationToken)) return false;
      if (button.dataset.tmBusy === "1") return false;
      const element = options.element || state.elementByButton.get(button);
      const resolveMediaItem = options.resolveMediaItem || mediaItemFromElementWithRetry;
      const startDownload = options.downloadItemFn || downloadItem;
      const setTimer = options.setTimeoutFn || ((callback, delayMs) => window.setTimeout(callback, delayMs));
      button.dataset.tmBusy = "1";
      const item = element ? await resolveMediaItem(element) : null;
      if (stopped) {
        button.dataset.tmBusy = "0";
        return false;
      }
      if (!item) {
        toast("Cannot find video URL yet. Play the video once, then press the button again.");
        setTimer(() => {
          button.dataset.tmBusy = "0";
        }, 700);
        return false;
      }
      return Promise.resolve(startDownload(item, activationToken)).finally(() => {
        setTimer(() => {
          button.dataset.tmBusy = "0";
        }, 700);
      });
    }
    function handleButtonClick(event) {
      blockEvent(event);
      const activationToken = createUserActivationToken(event);
      if (activationToken) activateButton(event.currentTarget, activationToken);
    }
    function stopButtonEvent(event) {
      blockEvent(event);
    }
    function findButtonFromEvent(event) {
      const path = typeof event.composedPath === "function" ? event.composedPath() : [];
      const pathButton = path.find((node) => node?.classList?.contains?.(BUTTON_CLASS));
      if (pathButton) return pathButton;
      return event.target?.closest?.(`.${BUTTON_CLASS}`) || null;
    }
    function handleGlobalButtonEvent(event) {
      const button = findButtonFromEvent(event);
      if (!button) return;
      blockEvent(event);
      if (event.type === "click") {
        const activationToken = createUserActivationToken(event);
        if (activationToken) activateButton(button, activationToken);
      }
    }
    function bindGlobalButtonEvents() {
      [
        "pointerdown",
        "pointerup",
        "mousedown",
        "mouseup",
        "touchstart",
        "touchend",
        "click",
        "dblclick"
      ].forEach((eventName) => {
        listen(window, eventName, handleGlobalButtonEvent, true);
        listen(document, eventName, handleGlobalButtonEvent, true);
      });
    }
    function findMediaContainer(element) {
      const base = element.closest?.("picture") || element;
      const mediaRect = element.getBoundingClientRect();
      let best = base.parentElement || base;
      let node = best;
      for (let depth = 0; node && depth < 6; depth += 1) {
        const rect = node.getBoundingClientRect();
        const closeWidth = rect.width <= mediaRect.width + 96;
        const closeHeight = rect.height <= mediaRect.height + 96;
        const containsOnlyThisMedia = visibleMediaCount(node) <= 1;
        if (rect.width >= mediaRect.width * 0.85 && rect.height >= mediaRect.height * 0.85 && closeWidth && closeHeight && containsOnlyThisMedia) {
          best = node;
          node = node.parentElement;
          continue;
        }
        break;
      }
      return best;
    }
    function prepareContainer(container) {
      const computedPosition = window.getComputedStyle(container).position;
      if (computedPosition === "static") {
        container.style.position = "relative";
      }
    }
    function isVideoTargetElement(element) {
      if (element?.tagName === "VIDEO") return true;
      if (element?.tagName === "IMG") {
        return Boolean(findAssociatedVideoForImage(element)) || isLikelyVideoThumbnail(element);
      }
      return false;
    }
    function ensureHoverButton() {
      if (state.hoverButton) return state.hoverButton;
      const button = document.createElement("button");
      button.type = "button";
      button.className = BUTTON_CLASS;
      button.innerHTML = `
            <svg aria-hidden="true" viewBox="0 0 24 24">
                <path d="M12 4v12M8 12l4 4 4-4M4 20h16" />
            </svg>
        `;
      button.title = "Download this Threads media";
      button.setAttribute("aria-label", "Download this Threads media");
      button.dataset.tmHidden = "1";
      button.addEventListener("click", handleButtonClick, true);
      button.addEventListener("mousedown", stopButtonEvent, true);
      button.addEventListener("pointerdown", stopButtonEvent, true);
      button.addEventListener("mouseenter", () => window.clearTimeout(state.hideTimer), true);
      button.addEventListener("mouseleave", scheduleHideHoverButton, true);
      document.body.appendChild(button);
      state.hoverButton = button;
      state.elementByButton.set(button, null);
      state.liveButtons.add(button);
      return button;
    }
    function showHoverButton(element) {
      if (!element || !document.documentElement.contains(element)) return;
      const button = ensureHoverButton();
      state.hoverElement = element;
      state.elementByButton.set(button, element);
      button.dataset.tmHidden = "0";
      button.dataset.tmKind = isVideoTargetElement(element) ? "video" : "photo";
      positionFloatingButton(button, element);
    }
    function hideHoverButton() {
      const button = state.hoverButton;
      if (!button) {
        state.hoverElement = null;
        return;
      }
      if (button.dataset.tmHidden === "1" && !state.hoverElement) return;
      state.hoverElement = null;
      button.dataset.tmHidden = "1";
      button.style.left = "";
      button.style.top = "";
      state.elementByButton.set(button, null);
    }
    function scheduleHideHoverButton() {
      window.clearTimeout(state.hideTimer);
      state.hideTimer = window.setTimeout(() => {
        if (document.querySelector(`.${BUTTON_CLASS}:hover`)) return;
        hideHoverButton();
      }, 140);
    }
    function positionFloatingButton(button, element) {
      const rect = element.getBoundingClientRect();
      button.style.left = `${Math.max(8, Math.round(rect.left + 10))}px`;
      button.style.top = `${Math.max(8, Math.round(rect.top + 10))}px`;
    }
    function isPlausibleStandaloneVideoAnchor(video) {
      const rect = video.getBoundingClientRect();
      if (!isVisibleRect(rect)) return false;
      const desktopLayout = window.innerWidth >= 760;
      const looksLikeChromeOrHiddenPlayer = desktopLayout && rect.left < 120 && rect.top < 120;
      if (looksLikeChromeOrHiddenPlayer) return false;
      return true;
    }
    function isDownloadableHoverMedia(element) {
      if (!element) return false;
      if (element.tagName === "IMG") return isLikelyPostImage(element);
      if (element.tagName === "VIDEO") return isLikelyPostVideo(element) && isPlausibleStandaloneVideoAnchor(element);
      return false;
    }
    function isPlausibleHoverRect(rect) {
      if (!isVisibleRect(rect)) return false;
      const desktopLayout = window.innerWidth >= 760;
      if (desktopLayout && rect.left < 120 && rect.top < 120) return false;
      return true;
    }
    function isLikelyVideoSurface(element) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
      const rect = element.getBoundingClientRect();
      if (!isVisibleRect(rect)) return false;
      return hasOverlappingVideoControl(rect, element) || Array.from(element.querySelectorAll?.("video") || []).some(isLikelyPostVideo);
    }
    function isLikelyMediaSurface(element) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
      if (element === document.body || element === document.documentElement) return false;
      if (element.classList?.contains?.(BUTTON_CLASS) || element.classList?.contains?.(POST_TOOL_CLASS) || element.classList?.contains?.(COPY_TOOL_CLASS) || element.classList?.contains?.(LINK_TOOL_CLASS)) return false;
      const rect = element.getBoundingClientRect();
      if (!isPlausibleHoverRect(rect)) return false;
      if (rect.width > Math.min(window.innerWidth - 24, 820)) return false;
      if (rect.height > Math.min(window.innerHeight - 24, 820)) return false;
      const bgUrl = getCssBackgroundImageUrl(element);
      if (bgUrl && isImageUrl(bgUrl)) return true;
      if (isLikelyVideoSurface(element)) return true;
      return false;
    }
    function rectContainsPoint(rect, x, y) {
      return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
    }
    function isPlausiblePointSearchScope(element, x, y) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
      if (element === document.body || element === document.documentElement) return false;
      const rect = element.getBoundingClientRect();
      if (!isPlausibleHoverRect(rect) || !rectContainsPoint(rect, x, y)) return false;
      return rect.width <= Math.min(window.innerWidth, 920) && rect.height <= Math.min(window.innerHeight, 920);
    }
    function collectPointMediaCandidates(scope, x, y, candidates, seen) {
      if (!scope || scope.nodeType !== Node.ELEMENT_NODE) return;
      const addMedia = (element) => {
        if (!element || seen.has(element) || !isDownloadableHoverMedia(element)) return;
        const rect = element.getBoundingClientRect();
        if (!isPlausibleHoverRect(rect) || !rectContainsPoint(rect, x, y)) return;
        seen.add(element);
        candidates.push({ element, rect });
      };
      addMedia(scope);
      if (!isPlausiblePointSearchScope(scope, x, y)) return;
      Array.from(scope.querySelectorAll?.("img, video") || []).forEach(addMedia);
    }
    function findMediaFromPoint(x, y) {
      if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
      if (typeof document.elementsFromPoint !== "function") {
        const fallbackCandidates = Array.from(document.querySelectorAll("img, video")).filter(isDownloadableHoverMedia).map((element) => ({ element, rect: element.getBoundingClientRect() })).filter((item) => isPlausibleHoverRect(item.rect)).filter((item) => rectContainsPoint(item.rect, x, y)).sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
        return fallbackCandidates[0]?.element || null;
      }
      const candidates = [];
      const seen = /* @__PURE__ */ new Set();
      const pointElements = document.elementsFromPoint(x, y).filter((element) => !element?.classList?.contains?.(BUTTON_CLASS)).filter((element) => !element?.classList?.contains?.(POST_TOOL_CLASS)).filter((element) => !element?.classList?.contains?.(COPY_TOOL_CLASS)).filter((element) => !element?.classList?.contains?.(LINK_TOOL_CLASS)).slice(0, 12);
      pointElements.forEach((element) => {
        collectPointMediaCandidates(element, x, y, candidates, seen);
        let node = element.parentElement;
        for (let depth = 0; node && depth < 4; depth += 1, node = node.parentElement) {
          collectPointMediaCandidates(node, x, y, candidates, seen);
        }
      });
      candidates.sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
      return candidates[0]?.element || null;
    }
    function findMediaFromEvent(event) {
      return findMediaFromPoint(event.clientX, event.clientY);
    }
    function findMediaSurfaceFromEvent(event) {
      const path = typeof event.composedPath === "function" ? event.composedPath() : [];
      const candidates = path.filter((node) => node?.nodeType === Node.ELEMENT_NODE).filter(isLikelyMediaSurface).map((element) => ({ element, rect: element.getBoundingClientRect() })).sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
      return candidates[0]?.element || null;
    }
    function isPrimaryPointerDown(event) {
      return Boolean(event?.buttons && event.buttons & 1);
    }
    function getHoverScanIntervalMs() {
      const value = Number(USER_OPTIONS.hoverScanIntervalMs);
      return Number.isFinite(value) ? Math.max(0, value) : 160;
    }
    function clearHoverScanQueue() {
      window.clearTimeout(state.hoverScanTimer);
      state.hoverScanTimer = 0;
      if (state.hoverMoveRaf) {
        window.cancelAnimationFrame(state.hoverMoveRaf);
        state.hoverMoveRaf = 0;
      }
    }
    function setPointerDragActive(event) {
      if (findButtonFromEvent(event)) return;
      state.pointerDragActive = true;
      state.pendingHoverPoint = null;
      clearHoverScanQueue();
      hideHoverButton();
    }
    function clearPointerDragActive() {
      state.pointerDragActive = false;
    }
    function processMediaPointerMove(point) {
      const button = state.hoverButton;
      if (button && (point.target === button || button.contains(point.target))) {
        return;
      }
      const media = findMediaFromPoint(point.clientX, point.clientY);
      if (!media) {
        scheduleHideHoverButton();
        return;
      }
      window.clearTimeout(state.hideTimer);
      showHoverButton(media);
    }
    function runQueuedHoverScan() {
      if (state.hoverMoveRaf) return;
      state.hoverMoveRaf = window.requestAnimationFrame(() => {
        state.hoverMoveRaf = 0;
        state.lastHoverScanAt = Date.now();
        const point = state.pendingHoverPoint;
        state.pendingHoverPoint = null;
        if (point) processMediaPointerMove(point);
      });
    }
    function scheduleHoverScan() {
      const interval = getHoverScanIntervalMs();
      const elapsed = Date.now() - state.lastHoverScanAt;
      if (elapsed >= interval) {
        window.clearTimeout(state.hoverScanTimer);
        state.hoverScanTimer = 0;
        runQueuedHoverScan();
        return;
      }
      if (state.hoverScanTimer) return;
      state.hoverScanTimer = window.setTimeout(() => {
        state.hoverScanTimer = 0;
        runQueuedHoverScan();
      }, interval - elapsed);
    }
    function handleMediaPointerMove(event) {
      if (state.pointerDragActive || isPrimaryPointerDown(event)) {
        state.pendingHoverPoint = null;
        clearHoverScanQueue();
        hideHoverButton();
        return;
      }
      state.pendingHoverPoint = {
        clientX: event.clientX,
        clientY: event.clientY,
        target: event.target
      };
      scheduleHoverScan();
    }
    function bindHoverEvents() {
      if (window.PointerEvent) {
        listen(document, "pointermove", handleMediaPointerMove, true);
        listen(document, "pointerdown", setPointerDragActive, true);
        listen(document, "pointerup", clearPointerDragActive, true);
        listen(document, "pointercancel", clearPointerDragActive, true);
      } else {
        listen(document, "mousemove", handleMediaPointerMove, true);
        listen(document, "mousedown", setPointerDragActive, true);
        listen(document, "mouseup", clearPointerDragActive, true);
      }
      listen(document, "scroll", refreshButtonsSoon, true);
      listen(window, "resize", refreshButtonsSoon, true);
    }
    function rectsOverlap(a, b) {
      const left = Math.max(a.left, b.left);
      const top = Math.max(a.top, b.top);
      const right = Math.min(a.right, b.right);
      const bottom = Math.min(a.bottom, b.bottom);
      const width = Math.max(0, right - left);
      const height = Math.max(0, bottom - top);
      const overlapArea = width * height;
      const smallerArea = Math.min(a.width * a.height, b.width * b.height);
      return smallerArea > 0 && overlapArea / smallerArea > 0.45;
    }
    function findAssociatedVideoForImage(img, knownVideos) {
      const postRoot = findPostRoot(img);
      const videos = (knownVideos || Array.from(postRoot?.querySelectorAll?.("video") || [])).filter(isLikelyPostVideo).filter((video) => !postRoot || findPostRoot(video) === postRoot);
      const imgRect = img.getBoundingClientRect();
      let node = img;
      for (let depth = 0; node && depth < 7; depth += 1) {
        const containedVideo = Array.from(node.querySelectorAll?.("video") || []).find((video) => isLikelyPostVideo(video) && rectsOverlap(imgRect, video.getBoundingClientRect()));
        if (containedVideo) return containedVideo;
        const hasVideoUi = hasOverlappingVideoControl(imgRect, node);
        if (hasVideoUi && videos.length === 1) return videos[0];
        node = node.parentElement;
      }
      return videos.find((video) => rectsOverlap(imgRect, video.getBoundingClientRect())) || null;
    }
    function findPostRoot(element) {
      const nearestBoundary = element.closest?.('article,[role="article"],[data-pressable-container]');
      if (nearestBoundary && findBestPostInfoInNode(nearestBoundary, element, true)) {
        return nearestBoundary;
      }
      let node = element;
      for (let depth = 0; node && depth < 14; depth += 1) {
        if (findBestPostInfoInNode(node, element, true)) return node;
        node = node.parentElement;
      }
      return element.parentElement || element;
    }
    function pickMappedVideoUrlForThumbnail(img, postContext) {
      const urls = state.videoUrlsByPostId.get(postContext.postId) || [];
      if (urls.length === 0) return null;
      const root = findPostRoot(img);
      const thumbnails = Array.from(root.querySelectorAll?.("img") || []).filter(isLikelyPostImage).filter(isLikelyVideoThumbnail);
      if (urls.length === 1 && thumbnails.length <= 1 && thumbnails.includes(img)) {
        return urls[0];
      }
      return null;
    }
    function pickMappedVideoUrlForSurface(surface, postContext) {
      const urls = state.videoUrlsByPostId.get(postContext.postId) || [];
      if (urls.length === 0) return null;
      const root = findPostRoot(surface);
      const surfaceRect = surface.getBoundingClientRect();
      const surfaces = [
        ...Array.from(root.querySelectorAll?.("img") || []).filter(isLikelyPostImage).filter(isLikelyVideoThumbnail),
        ...Array.from(root.querySelectorAll?.('div, a, [role="button"]') || []).filter(isLikelyMediaSurface).filter(isLikelyVideoSurface)
      ];
      const uniqueSurfaces = surfaces.filter((item, index) => surfaces.indexOf(item) === index).map((item) => ({ item, rect: item.getBoundingClientRect() })).sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
      const matchingSurfaces = uniqueSurfaces.filter(({ rect }) => rectsOverlap(surfaceRect, rect));
      if (urls.length === 1 && uniqueSurfaces.length <= 1 && matchingSurfaces.length === 1) {
        return urls[0];
      }
      return null;
    }
    function isLikelyVideoThumbnail(img) {
      const imgRect = img.getBoundingClientRect();
      let node = img;
      for (let depth = 0; node && depth < 7; depth += 1) {
        const hasVideo = Array.from(node.querySelectorAll?.("video") || []).some((video) => isLikelyPostVideo(video) && rectsOverlap(imgRect, video.getBoundingClientRect()));
        if (hasVideo) return true;
        if (hasOverlappingVideoControl(imgRect, node)) return true;
        node = node.parentElement;
      }
      return false;
    }
    function hasOverlappingVideoControl(mediaRect, root) {
      const labelledControls = Array.from(root.querySelectorAll?.('[aria-label], [role="button"]') || []);
      return labelledControls.some((uiNode) => {
        const label = uiNode.getAttribute("aria-label") || uiNode.textContent || "";
        if (!/play|pause|mute|unmute|audio|sound/i.test(label)) return false;
        const controlRect = uiNode.getBoundingClientRect();
        return rectsOverlap(mediaRect, controlRect);
      });
    }
    function getCurrentDetailPostInfo() {
      return parsePostInfoFromUrl(location.href);
    }
    function isDetailPostPage() {
      return Boolean(getCurrentDetailPostInfo());
    }
    function getVisibleRectScore(element) {
      const rect = element.getBoundingClientRect();
      if (rect.width <= 0 || rect.height <= 0) return Number.MAX_SAFE_INTEGER;
      return Math.abs(rect.top - 80) + Math.max(0, rect.left);
    }
    function scoreDetailPostRootCandidate(node, link) {
      const rect = node.getBoundingClientRect();
      if (rect.width < 260 || rect.height < 140) return Number.NEGATIVE_INFINITY;
      if (node === document.body || node === document.documentElement) return Number.NEGATIVE_INFINITY;
      const mediaCount = node.querySelectorAll?.("img, video")?.length || 0;
      if (mediaCount === 0) return Number.NEGATIVE_INFINITY;
      let score = 0;
      if (node.hasAttribute?.("data-pressable-container")) score += 2e4;
      if (node.querySelector?.("time[datetime]")) score += 3e3;
      if (findDetailActionBar(node)) score += 2500;
      if (Array.from(node.querySelectorAll?.("svg[aria-label], svg") || []).some(isShareSvg)) score += 1200;
      if (link && node.contains(link)) score += 1e3;
      score += Math.min(mediaCount, 12) * 120;
      score += Math.min(rect.height, 1200);
      score -= Math.abs(rect.top - 80) * 0.5;
      if (rect.height > window.innerHeight * 3) score -= 6e3;
      return score;
    }
    function findDetailPostRootByPermalink() {
      const info = getCurrentDetailPostInfo();
      if (!info?.postId) return null;
      const candidates = [];
      Array.from(document.querySelectorAll('a[href*="/post/"]')).forEach((link) => {
        const linkInfo = parsePostInfoFromUrl(link.href);
        if (linkInfo?.postId !== info.postId) return;
        let node = link;
        for (let depth = 0; node && depth < 16; depth += 1) {
          if (node.nodeType === Node.ELEMENT_NODE) {
            const score = scoreDetailPostRootCandidate(node, link);
            if (Number.isFinite(score)) {
              candidates.push({ node, score });
            }
          }
          node = node.parentElement;
        }
      });
      return candidates.sort((a, b) => b.score - a.score)[0]?.node || null;
    }
    function findDetailPostRoot() {
      if (!isDetailPostPage()) return null;
      const permalinkRoot = findDetailPostRootByPermalink();
      if (permalinkRoot) return permalinkRoot;
      const articleCandidates = Array.from(document.querySelectorAll('article,[role="article"]')).filter((node) => {
        const rect = node.getBoundingClientRect();
        return rect.width > 260 && rect.height > 140 && rect.bottom > 0 && rect.top < window.innerHeight;
      }).sort((a, b) => getVisibleRectScore(a) - getVisibleRectScore(b));
      if (articleCandidates[0]) return articleCandidates[0];
      const media = Array.from(document.querySelectorAll("img, video")).filter(isDownloadableHoverMedia).sort((a, b) => getVisibleRectScore(a) - getVisibleRectScore(b))[0];
      return media ? findPostRoot(media) : null;
    }
    function findDetailActionBar(root) {
      if (!root) return null;
      const candidates = Array.from(root.querySelectorAll("div")).map((node) => {
        const rect = node.getBoundingClientRect();
        const controls = Array.from(node.querySelectorAll('button,a,[role="button"]')).filter((control) => {
          const controlRect = control.getBoundingClientRect();
          return controlRect.width >= 20 && controlRect.height >= 20;
        });
        return { node, rect, controls };
      }).filter((item) => item.rect.width > 180 && item.rect.height >= 28 && item.rect.height <= 80 && item.controls.length >= 3).sort((a, b) => b.rect.top - a.rect.top);
      return candidates[0]?.node || null;
    }
    function isShareSvg(svg) {
      const label = svg.getAttribute("aria-label") || svg.querySelector?.("title")?.textContent || "";
      return /分享|share/i.test(label);
    }
    function findShareSvgInControl(control) {
      if (!control) return null;
      if (control.tagName?.toLowerCase?.() === "svg" && isShareSvg(control)) {
        return control;
      }
      return Array.from(control.querySelectorAll?.("svg[aria-label], svg") || []).find(isShareSvg) || null;
    }
    function findShareSvgFromEvent(event) {
      const path = typeof event.composedPath === "function" ? event.composedPath() : [];
      const pathSvg = path.find((node) => node?.tagName?.toLowerCase?.() === "svg" && isShareSvg(node));
      if (pathSvg) return pathSvg;
      const pathControl = path.find(
        (node) => node?.matches?.('[role="button"], button, a, [tabindex="0"]')
      );
      const pathControlSvg = findShareSvgInControl(pathControl);
      if (pathControlSvg) return pathControlSvg;
      const targetControl = event.target?.closest?.(
        'svg[aria-label], [role="button"], button, a, [tabindex="0"]'
      );
      return findShareSvgInControl(targetControl);
    }
    function removeInjectedCleanLinkMenuItems() {
      document.querySelectorAll(`.${CLEAN_LINK_MENU_CLASS}`).forEach((item) => item.remove());
    }
    function clearNativeShareContext(reason = "cleared") {
      const context = state.pendingShareContext;
      if (state.nativeShareCloseTimer) {
        const clearTimer = typeof window !== "undefined" ? window.clearTimeout : clearTimeout;
        clearTimer(state.nativeShareCloseTimer);
        state.nativeShareCloseTimer = 0;
      }
      if (state.cleanLinkMenuTimer) {
        const clearTimer = typeof window !== "undefined" ? window.clearTimeout : clearTimeout;
        clearTimer(state.cleanLinkMenuTimer);
        state.cleanLinkMenuTimer = 0;
      }
      if (context?.expiryTimer) {
        const clearTimer = typeof window !== "undefined" ? window.clearTimeout : clearTimeout;
        clearTimer(context.expiryTimer);
      }
      if (context?.routeTimer) {
        const clearIntervalFn = typeof window !== "undefined" ? window.clearInterval : clearInterval;
        clearIntervalFn(context.routeTimer);
      }
      context?.cleanItem?.remove?.();
      state.pendingShareContext = null;
      return reason;
    }
    function getShareContextInvalidReason(context, currentRouteKey = getMediaRouteKey(), now = Date.now()) {
      if (!context?.cleanUrl) return "missing_context";
      if (context.routeKey !== currentRouteKey) return "route_change";
      if (now >= context.expiresAt) return "expired";
      if (context.menuContainer && !context.menuContainer.isConnected) return "menu_closed";
      if (context.menuContainer && context.nativeItem && (!context.nativeItem.isConnected || !context.menuContainer.contains?.(context.nativeItem))) return "native_item_removed";
      if (context.menuContainer && context.cleanItem && (!context.cleanItem.isConnected || context.cleanItem.parentElement !== context.menuItemParent || !context.menuContainer.contains?.(context.cleanItem))) return "clean_item_removed";
      return "";
    }
    function pruneNativeShareContext() {
      const context = state.pendingShareContext;
      const invalidReason = getShareContextInvalidReason(context);
      if (invalidReason) {
        clearNativeShareContext(invalidReason);
        return false;
      }
      if (context.menuContainer && !isVisibleMenuItem(context.menuContainer)) {
        clearNativeShareContext("menu_hidden");
        return false;
      }
      return true;
    }
    function rememberNativeShareContext(event) {
      if (Date.now() < state.suppressNativeShareContextUntil) return;
      if (!isTrustedUserActivation(event)) return;
      const shareSvg = findShareSvgFromEvent(event);
      if (!shareSvg) return;
      const shareButton = findShareIconSlot(shareSvg, document.body) || findClickableAncestor(shareSvg, document.body);
      if (!shareButton) return;
      const root = findPostBlockRootFromShareButton(shareButton) || findPostRoot(shareButton);
      const postInfo = root ? findBestPostInfoInNode(root, shareButton, true) : parsePostInfoFromUrl(location.href);
      const cleanUrl = buildCleanThreadsPostUrl(postInfo || parsePostInfoFromUrl(location.href));
      if (!cleanUrl) return;
      const routeKey = getMediaRouteKey();
      const currentContext = state.pendingShareContext;
      if (currentContext?.shareButton === shareButton && currentContext.routeKey === routeKey && Date.now() - currentContext.createdAt < 1500) {
        scheduleCleanLinkMenuInjection(0);
        return;
      }
      clearNativeShareContext("replaced");
      removeInjectedCleanLinkMenuItems();
      const createdAt = Date.now();
      const baselineMenuContainers = new Set(
        findNativeCopyLinkMenuItems().map(findNativeShareMenuContainer).filter(Boolean)
      );
      const context = {
        cleanUrl,
        shareButton,
        createdAt,
        expiresAt: createdAt + SHARE_CONTEXT_TIMEOUT_MS,
        routeKey,
        baselineMenuContainers,
        menuContainer: null,
        nativeItem: null,
        cleanItem: null,
        expiryTimer: window.setTimeout(() => {
          if (state.pendingShareContext?.createdAt === createdAt) {
            clearNativeShareContext("expired");
          }
        }, SHARE_CONTEXT_TIMEOUT_MS),
        routeTimer: 0
      };
      state.pendingShareContext = context;
      context.routeTimer = window.setInterval(() => {
        if (state.pendingShareContext === context && context.routeKey !== getMediaRouteKey()) {
          clearNativeShareContext("route_change");
        }
      }, 200);
      scheduleCleanLinkMenuInjection(0);
    }
    function normalizeMenuItemText(value) {
      return String(value || "").replace(/\s+/g, " ").trim();
    }
    function isNativeCopyLinkActionRect(rect, viewportWidth, viewportHeight) {
      return rect.width > 24 && rect.height > 24 && rect.bottom > 0 && rect.right > 0 && rect.top < viewportHeight && rect.left < viewportWidth;
    }
    function findNativeCopyLinkMenuItems(container = document) {
      return Array.from(container.querySelectorAll(
        '[role="menuitem"], [role="button"], button, [tabindex="0"]'
      )).filter((element) => !element.classList?.contains?.(CLEAN_LINK_MENU_CLASS)).filter((element) => {
        const text = normalizeMenuItemText(element.innerText || element.textContent);
        return /^(複製連結|Copy link)$/i.test(text);
      }).filter((element) => {
        const rect = element.getBoundingClientRect();
        return isNativeCopyLinkActionRect(
          rect,
          window.innerWidth,
          window.innerHeight
        );
      }).sort((a, b) => {
        const aRect = a.getBoundingClientRect();
        const bRect = b.getBoundingClientRect();
        return aRect.width * aRect.height - bRect.width * bRect.height;
      });
    }
    function findNativeCopyLinkMenuItem(container = document) {
      return findNativeCopyLinkMenuItems(container)[0] || null;
    }
    function findNativeShareMenuContainer(nativeItem) {
      if (!nativeItem) return null;
      const container = nativeItem.closest?.('[role="menu"]') || nativeItem.closest?.('[role="dialog"][aria-modal="true"], [aria-modal="true"]');
      const body = typeof document !== "undefined" ? document.body : null;
      const documentElement = typeof document !== "undefined" ? document.documentElement : null;
      if (!container || container === body || container === documentElement) return null;
      return container;
    }
    function replaceNativeCopyLinkLabel(menuItem) {
      const textNodes = [];
      const walker = document.createTreeWalker(menuItem, NodeFilter.SHOW_TEXT);
      let node = walker.nextNode();
      while (node) {
        textNodes.push(node);
        node = walker.nextNode();
      }
      const labelNode = textNodes.find(
        (textNode) => /^(複製連結|Copy link)$/i.test(normalizeMenuItemText(textNode.nodeValue))
      );
      if (labelNode) {
        labelNode.nodeValue = "原始連結";
      }
      menuItem.setAttribute("aria-label", "複製去除追蹤碼的連結");
      menuItem.title = "複製去除追蹤碼的連結";
    }
    function replaceCleanLinkMenuIcon(menuItem) {
      const icon = menuItem.querySelector("svg");
      if (!icon) return;
      icon.setAttribute("aria-hidden", "true");
      icon.setAttribute("viewBox", "0 0 512 509.84");
      icon.setAttribute("width", "24");
      icon.setAttribute("height", "24");
      icon.setAttribute("fill", "currentColor");
      icon.setAttribute("stroke", "none");
      icon.setAttribute("preserveAspectRatio", "xMidYMid meet");
      icon.innerHTML = `
            <path fill="currentColor" stroke="none" fill-rule="nonzero" d="${CLEAN_LINK_ICON_PATH}" />
        `;
      icon.querySelectorAll("path").forEach((path) => {
        path.style.setProperty("fill", "currentColor", "important");
        path.style.setProperty("stroke", "none", "important");
      });
    }
    function isVisibleMenuItem(element) {
      if (!element?.isConnected) return false;
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
    }
    function dispatchEscapeKey() {
      const target = document.activeElement instanceof Element ? document.activeElement : document.body;
      const eventInit = {
        key: "Escape",
        code: "Escape",
        keyCode: 27,
        which: 27,
        bubbles: true,
        cancelable: true,
        composed: true
      };
      target.dispatchEvent(new KeyboardEvent("keydown", eventInit));
      target.dispatchEvent(new KeyboardEvent("keyup", eventInit));
    }
    function closeNativeShareMenu(context, cleanItem) {
      dispatchEscapeKey();
      window.clearTimeout(state.nativeShareCloseTimer);
      state.nativeShareCloseTimer = window.setTimeout(() => {
        state.nativeShareCloseTimer = 0;
        if (stopped || state.pendingShareContext !== context) return;
        const menuStillVisible = isVisibleMenuItem(cleanItem) || isVisibleMenuItem(findNativeCopyLinkMenuItem(context?.menuContainer));
        const shareButton = context?.shareButton;
        if (menuStillVisible && shareButton?.isConnected) {
          state.suppressNativeShareContextUntil = Date.now() + 500;
          shareButton.click();
        }
        if (state.pendingShareContext === context) {
          clearNativeShareContext("menu_closed");
        }
      }, 120);
    }
    function injectCleanLinkMenuItem() {
      const context = state.pendingShareContext;
      if (!pruneNativeShareContext()) return false;
      if (context.cleanItem?.isConnected && context.cleanItem.parentElement === context.menuItemParent && context.menuContainer?.contains?.(context.cleanItem)) return true;
      const candidates = findNativeCopyLinkMenuItems().map((nativeItem2) => ({
        nativeItem: nativeItem2,
        menuContainer: findNativeShareMenuContainer(nativeItem2)
      })).filter(({ menuContainer: menuContainer2 }) => menuContainer2).filter(({ menuContainer: menuContainer2 }) => !context.baselineMenuContainers.has(menuContainer2));
      const { nativeItem, menuContainer } = candidates[0] || {};
      if (!nativeItem) return false;
      const cleanItem = nativeItem.cloneNode(true);
      cleanItem.classList.add(CLEAN_LINK_MENU_CLASS);
      cleanItem.removeAttribute("id");
      cleanItem.querySelectorAll("[id]").forEach((element) => element.removeAttribute("id"));
      replaceNativeCopyLinkLabel(cleanItem);
      replaceCleanLinkMenuIcon(cleanItem);
      cleanItem.addEventListener("pointerdown", stopButtonEvent, true);
      cleanItem.addEventListener("mousedown", stopButtonEvent, true);
      cleanItem.addEventListener("click", (event) => {
        blockEvent(event);
        const activationToken = createUserActivationToken(event);
        if (!pruneNativeShareContext() || state.pendingShareContext !== context || cleanItem.parentElement !== context.menuItemParent || !context.menuContainer?.contains?.(cleanItem) || !activationToken || !copyText(context.cleanUrl, activationToken)) return;
        toast("已複製無追蹤碼連結。");
        closeNativeShareMenu(context, cleanItem);
      }, true);
      context.menuContainer = menuContainer;
      context.menuItemParent = nativeItem.parentElement;
      context.nativeItem = nativeItem;
      context.cleanItem = cleanItem;
      nativeItem.before(cleanItem);
      return true;
    }
    function scheduleCleanLinkMenuInjection(attempt) {
      window.clearTimeout(state.cleanLinkMenuTimer);
      state.cleanLinkMenuTimer = window.setTimeout(() => {
        state.cleanLinkMenuTimer = 0;
        if (injectCleanLinkMenuItem()) return;
        const context = state.pendingShareContext;
        if (context && pruneNativeShareContext() && attempt < 100) {
          scheduleCleanLinkMenuInjection(attempt + 1);
        } else if (context) {
          clearNativeShareContext("injection_timeout");
        }
      }, attempt === 0 ? 0 : 80);
    }
    function handleNativeShareContextLifecycle(event) {
      const context = state.pendingShareContext;
      if (!context) return;
      if (event?.type === "keydown" && event.key === "Escape") {
        clearNativeShareContext("escape");
        return;
      }
      if (event?.type !== "pointerdown" && event?.type !== "mousedown") return;
      const target = event.target;
      if (context.menuContainer?.contains?.(target) || context.shareButton?.contains?.(target)) return;
      clearNativeShareContext("outside_click");
    }
    function bindNativeShareMenuEvents() {
      listen(document, "pointerdown", rememberNativeShareContext, true);
      listen(document, "click", rememberNativeShareContext, true);
      listen(document, "pointerdown", handleNativeShareContextLifecycle, true);
      listen(document, "mousedown", handleNativeShareContextLifecycle, true);
      listen(document, "keydown", handleNativeShareContextLifecycle, true);
    }
    function findClickableAncestor(node, boundary) {
      let current = node;
      for (let depth = 0; current && current !== boundary && depth < 8; depth += 1) {
        if (current.matches?.('[role="button"],button,a,[tabindex="0"]')) {
          return current;
        }
        const style = window.getComputedStyle(current);
        if (style.cursor === "pointer") {
          return current;
        }
        current = current.parentElement;
      }
      return node.parentElement;
    }
    function isCompactIconRect(rect) {
      return rect && rect.width >= 18 && rect.height >= 18 && rect.width <= 92 && rect.height <= 58;
    }
    function findShareIconSlot(svg, boundary) {
      let current = svg;
      let best = findClickableAncestor(svg, boundary) || svg.parentElement;
      for (let depth = 0; current && current !== boundary && depth < 8; depth += 1) {
        const parent = current.parentElement;
        if (!parent) break;
        const rect = parent.getBoundingClientRect();
        if (!isCompactIconRect(rect)) break;
        best = parent;
        current = parent;
      }
      return best;
    }
    function detailShareCandidateScore(root, item, rootPriority) {
      const rootRect = root?.getBoundingClientRect?.();
      const rect = item.slot.getBoundingClientRect();
      let score = 0;
      if (rootPriority) {
        score -= 2e4;
      }
      if (rootRect && rect.top >= rootRect.top && rect.bottom <= rootRect.bottom + 120) {
        score -= 1e4;
      }
      score += rect.top;
      score += Math.abs(rect.left - 360) * 0.15;
      score += Math.max(0, rect.top - window.innerHeight) * 10;
      score += Math.max(0, -rect.top) * 10;
      return score;
    }
    function findDetailShareButton(root) {
      const detailPostInfo = getCurrentDetailPostInfo();
      const searchRoots = [
        { node: root, rootPriority: true },
        { node: document.body, rootPriority: false }
      ].filter((item) => item.node);
      const seen = /* @__PURE__ */ new Set();
      const candidates = [];
      searchRoots.forEach(({ node: searchRoot, rootPriority }) => {
        Array.from(searchRoot.querySelectorAll("svg[aria-label], svg")).filter((svg) => {
          if (seen.has(svg)) return false;
          seen.add(svg);
          return true;
        }).filter(isShareSvg).forEach((svg) => {
          const slot = findShareIconSlot(svg, searchRoot);
          const rect = slot?.getBoundingClientRect?.();
          if (!slot || !rect || rect.width < 18 || rect.height < 18) return;
          if (rect.bottom < 0 || rect.top > window.innerHeight) return;
          const blockRoot = findPostBlockRootFromShareButton(slot);
          const blockInfo = blockRoot ? findBestPostInfoInNode(blockRoot, slot, true) : null;
          const matchesDetailPost = Boolean(
            detailPostInfo?.postId && blockInfo?.postId === detailPostInfo.postId
          );
          let score = detailShareCandidateScore(root, { slot }, rootPriority);
          score += matchesDetailPost ? -5e4 : 5e4;
          candidates.push({ svg, slot, rect, score });
        });
      });
      const shareSvg = candidates.sort((a, b) => a.score - b.score)[0];
      return shareSvg?.slot || null;
    }
    function getPostIdsInNode(node) {
      const ids = /* @__PURE__ */ new Set();
      const links = [
        ...node.matches?.('a[href*="/post/"]') ? [node] : [],
        ...Array.from(node.querySelectorAll?.('a[href*="/post/"]') || [])
      ];
      links.forEach((link) => {
        const info = parsePostInfoFromUrl(link.href || link.getAttribute?.("href"));
        if (info?.postId) ids.add(info.postId);
      });
      return ids;
    }
    function isPostBoundaryNode(node) {
      return Boolean(node?.matches?.('article,[role="article"],[data-pressable-container]'));
    }
    function hasPostBoundaryCues(node) {
      return Boolean(
        node?.matches?.('article,[role="article"]') || countShareIconsInNode(node) > 0 || node?.querySelector?.('time[datetime], a[href^="/@"], a[href*="threads.com/@"]')
      );
    }
    function isMediaOwnedByPost(element, root, expectedPostId) {
      if (!element || !root || !expectedPostId || !root.contains?.(element)) return false;
      if (element === root) return true;
      if (isInsideNestedPostBlock(element, root)) return false;
      let node = element.parentElement;
      for (let depth = 0; node && node !== root && depth < MAX_POST_CONTEXT_ANCESTORS; depth += 1) {
        if (isPostBoundaryNode(node)) {
          const postIds = getPostIdsInNode(node);
          if (postIds.size > 1) return false;
          if (postIds.size === 1 && !postIds.has(expectedPostId)) return false;
          if (postIds.size === 0 && hasPostBoundaryCues(node)) return false;
        }
        node = node.parentElement;
      }
      return node === root;
    }
    function countShareIconsInNode(node) {
      return Array.from(node.querySelectorAll?.("svg[aria-label]") || []).filter(isShareSvg).length;
    }
    function findPostBlockRootFromShareButton(shareButton) {
      const article = shareButton.closest?.('article,[role="article"]');
      if (article && countShareIconsInNode(article) === 1) return article;
      let pressableRoot = shareButton.parentElement;
      for (let depth = 0; pressableRoot && depth < 16; depth += 1, pressableRoot = pressableRoot.parentElement) {
        if (!pressableRoot.hasAttribute?.("data-pressable-container")) continue;
        const rect = pressableRoot.getBoundingClientRect();
        const hasPostIdentity = getPostIdsInNode(pressableRoot).size > 0 || Boolean(pressableRoot.querySelector("time[datetime]"));
        if (rect.width >= 220 && rect.height >= 48 && rect.width <= Math.min(window.innerWidth, 1100) && countShareIconsInNode(pressableRoot) >= 1 && hasPostIdentity) {
          return pressableRoot;
        }
      }
      let node = shareButton.parentElement;
      let best = null;
      for (let depth = 0; node && depth < 16; depth += 1, node = node.parentElement) {
        if (node === document.body || node === document.documentElement) break;
        const rect = node.getBoundingClientRect();
        if (rect.width < 220 || rect.height < 48 || rect.width > Math.min(window.innerWidth, 1100)) {
          continue;
        }
        const shareCount = countShareIconsInNode(node);
        if (shareCount > 1) break;
        if (shareCount !== 1) continue;
        const postIds = getPostIdsInNode(node);
        const hasPostIdentity = postIds.size === 1 || Boolean(node.querySelector("time[datetime]"));
        if (!hasPostIdentity) continue;
        best = node;
      }
      return best;
    }
    function createLinkToolButton(root, shareButton) {
      const linkButton = document.createElement("button");
      linkButton.type = "button";
      linkButton.className = LINK_TOOL_CLASS;
      linkButton.title = "複製這則貼文連結(去追蹤碼)";
      linkButton.setAttribute("aria-label", "複製這則貼文連結(去追蹤碼)");
      linkButton.innerHTML = `
            <svg aria-hidden="true" viewBox="0 0 512 509.84" preserveAspectRatio="xMidYMid meet">
                <path fill="currentColor" stroke="none" fill-rule="nonzero" d="${CLEAN_LINK_ICON_PATH}"></path>
            </svg>
        `;
      linkButton.addEventListener("click", (event) => {
        blockEvent(event);
        const activationToken = createUserActivationToken(event);
        if (!activationToken) return;
        const context = state.linkContextByButton.get(linkButton);
        copyPostBlockCleanLink(context?.root, context?.shareButton, activationToken);
      }, true);
      state.linkButtonByRoot.set(root, linkButton);
      state.linkButtonByShare.set(shareButton, linkButton);
      state.linkContextByButton.set(linkButton, { root, shareButton });
      state.liveLinkButtons.add(linkButton);
      return linkButton;
    }
    function createCopyToolButton(root, shareButton) {
      const copyButton = document.createElement("button");
      copyButton.type = "button";
      copyButton.className = COPY_TOOL_CLASS;
      copyButton.title = "複製這則貼文文字";
      copyButton.setAttribute("aria-label", "複製這則貼文文字");
      copyButton.innerHTML = `
            <svg aria-hidden="true" viewBox="0 0 24 24">
                <rect x="4" y="3" width="16" height="18" rx="2"></rect>
                <path d="M8 8h8M8 12h8M8 16h8"></path>
            </svg>
        `;
      copyButton.addEventListener("click", (event) => {
        blockEvent(event);
        const activationToken = createUserActivationToken(event);
        if (!activationToken) return;
        const context = state.copyContextByButton.get(copyButton);
        copyPostBlockText(context?.root, context?.actionBar, activationToken);
      }, true);
      state.copyButtonByRoot.set(root, copyButton);
      state.copyButtonByShare.set(shareButton, copyButton);
      state.copyContextByButton.set(copyButton, {
        root,
        shareButton,
        actionBar: shareButton.parentElement
      });
      state.liveCopyButtons.add(copyButton);
      return copyButton;
    }
    function cleanupCopyButtons() {
      state.liveCopyButtons.forEach((button) => button.remove());
      state.liveCopyButtons.clear();
      state.copyButtonByRoot = /* @__PURE__ */ new WeakMap();
      state.copyButtonByShare = /* @__PURE__ */ new WeakMap();
      state.copyContextByButton = /* @__PURE__ */ new WeakMap();
      state.liveLinkButtons.forEach((button) => button.remove());
      state.liveLinkButtons.clear();
      state.linkButtonByRoot = /* @__PURE__ */ new WeakMap();
      state.linkButtonByShare = /* @__PURE__ */ new WeakMap();
      state.linkContextByButton = /* @__PURE__ */ new WeakMap();
    }
    function ensureCopyButtonsForBlocks() {
      if (!document.body) return;
      const activeCopyButtons = /* @__PURE__ */ new Set();
      const activeLinkButtons = /* @__PURE__ */ new Set();
      const seenRoots = /* @__PURE__ */ new Set();
      const seenSlots = /* @__PURE__ */ new Set();
      Array.from(document.querySelectorAll("svg[aria-label]")).filter(isShareSvg).forEach((svg) => {
        const shareButton = findShareIconSlot(svg, document.body);
        if (!shareButton || seenSlots.has(shareButton)) return;
        seenSlots.add(shareButton);
        const rect = shareButton.getBoundingClientRect();
        if (!isCompactIconRect(rect)) return;
        let copyButton = state.copyButtonByShare.get(shareButton);
        const cachedContext = copyButton && state.copyContextByButton.get(copyButton);
        const root = cachedContext?.root?.isConnected ? cachedContext.root : findPostBlockRootFromShareButton(shareButton);
        if (!root || seenRoots.has(root)) return;
        seenRoots.add(root);
        let linkButton = state.linkButtonByShare.get(shareButton) || state.linkButtonByRoot.get(root);
        if (!linkButton || !linkButton.isConnected) {
          linkButton = createLinkToolButton(root, shareButton);
        } else {
          state.linkButtonByRoot.set(root, linkButton);
          state.linkButtonByShare.set(shareButton, linkButton);
          state.linkContextByButton.set(linkButton, { root, shareButton });
        }
        copyButton = copyButton || state.copyButtonByRoot.get(root);
        if (!copyButton || !copyButton.isConnected) {
          copyButton = createCopyToolButton(root, shareButton);
        } else {
          state.copyButtonByShare.set(shareButton, copyButton);
          state.copyContextByButton.set(copyButton, {
            root,
            shareButton,
            actionBar: shareButton.parentElement
          });
        }
        if (linkButton.previousElementSibling !== shareButton) {
          shareButton.after(linkButton);
        }
        if (copyButton.previousElementSibling !== linkButton) {
          linkButton.after(copyButton);
        }
        activeLinkButtons.add(linkButton);
        activeCopyButtons.add(copyButton);
      });
      state.liveCopyButtons.forEach((button) => {
        if (activeCopyButtons.has(button)) return;
        button.remove();
        state.liveCopyButtons.delete(button);
      });
      state.liveLinkButtons.forEach((button) => {
        if (activeLinkButtons.has(button)) return;
        button.remove();
        state.liveLinkButtons.delete(button);
      });
    }
    function ensureDetailFallbackBar(root) {
      if (!root) return null;
      let bar = root.querySelector?.(":scope > .tm-post-media-tool-fallback");
      if (bar) return bar;
      bar = document.createElement("div");
      bar.className = "tm-post-media-tool-fallback";
      root.appendChild(bar);
      return bar;
    }
    function isPostMediaPickerEnabled() {
      return USER_OPTIONS.enablePostMediaPicker !== false;
    }
    function cleanupDetailButton() {
      if (state.detailButton) {
        const parent = state.detailButton.parentElement;
        state.detailButton.remove();
        if (parent?.classList?.contains("tm-post-media-tool-fallback") && parent.childElementCount === 0) {
          parent.remove();
        }
        state.detailButton = null;
      }
      state.detailRoute = "";
      state.detailUiCache = null;
      state.modalItems = [];
      const modal = document.getElementById(MODAL_ID);
      if (modal) modal.remove();
    }
    function getDetailUiContext(routeKey) {
      const cached = state.detailUiCache;
      if (cached?.routeKey === routeKey && cached.root?.isConnected && cached.shareButton?.isConnected && cached.actionBar?.isConnected) {
        return cached;
      }
      const root = findDetailPostRoot();
      const shareButton = findDetailShareButton(root);
      const actionBar = shareButton?.parentElement || findDetailActionBar(root) || ensureDetailFallbackBar(root);
      const context = { routeKey, root, shareButton, actionBar };
      state.detailUiCache = context;
      return context;
    }
    function ensureDetailButton() {
      if (!document.body) return;
      if (!isDetailPostPage()) {
        cleanupDetailButton();
        state.detailRoute = "";
        return;
      }
      const routeKey = getMediaRouteKey();
      if (state.detailRoute && state.detailRoute !== routeKey) {
        cleanupDetailButton();
      }
      const { root, shareButton, actionBar } = getDetailUiContext(routeKey);
      if (!actionBar) return;
      if (isPostMediaPickerEnabled() && !state.detailButton) {
        const button = document.createElement("button");
        button.type = "button";
        button.className = POST_TOOL_CLASS;
        button.title = "Open Threads media downloader";
        button.setAttribute("aria-label", "Open Threads media downloader");
        button.innerHTML = `
                <svg aria-hidden="true" viewBox="0 0 24 24">
                    <path d="M12 4v12M8 12l4 4 4-4M4 20h16" />
                </svg>
            `;
        button.addEventListener("click", (event) => {
          blockEvent(event);
          if (!createUserActivationToken(event)) return;
          openPostMediaModal();
        }, true);
        state.detailButton = button;
      } else if (!isPostMediaPickerEnabled()) {
        cleanupDetailButton();
      }
      const linkButton = state.linkButtonByRoot.get(root) || (shareButton ? state.linkButtonByShare.get(shareButton) : null);
      const copyButton = state.copyButtonByRoot.get(root) || (shareButton ? state.copyButtonByShare.get(shareButton) : null);
      if (shareButton) {
        if (linkButton && linkButton.previousElementSibling !== shareButton) {
          shareButton.after(linkButton);
        }
        const copyAnchor = linkButton || shareButton;
        if (copyButton && copyButton.previousElementSibling !== copyAnchor) {
          copyAnchor.after(copyButton);
        }
        const downloadAnchor = copyButton || linkButton || shareButton;
        if (state.detailButton && state.detailButton.previousElementSibling !== downloadAnchor) {
          downloadAnchor.after(state.detailButton);
        }
      } else {
        if (state.detailButton && state.detailButton.parentElement !== actionBar) {
          actionBar.appendChild(state.detailButton);
        }
      }
      state.detailRoute = routeKey;
    }
    function getModalItemIdentity(item, rectKey) {
      const resolvedKey = getMediaUrlIdentity(item.resolvedUrl);
      if (resolvedKey) return `${item.type}:${resolvedKey}`;
      const previewKey = getMediaUrlIdentity(item.previewUrl);
      if (previewKey) return `${item.type}:${previewKey}`;
      return `${item.type}:rect:${rectKey}`;
    }
    function dedupeModalItems(items) {
      const structuredSlots = [];
      const itemByKey = /* @__PURE__ */ new Map();
      items.forEach((item) => {
        if (!item.resolvedUrl) return;
        if (Number.isInteger(item.structuredSlotIndex)) {
          structuredSlots.push(item);
          return;
        }
        const rect = item.element?.getBoundingClientRect?.() || { left: item.indexHint || 0, top: item.indexHint || 0, width: 0, height: 0 };
        const rectKey = [
          Math.round(rect.left / 12),
          Math.round(rect.top / 12),
          Math.round(rect.width / 12),
          Math.round(rect.height / 12)
        ].join(":");
        const key = getModalItemIdentity(item, rectKey);
        const existing = itemByKey.get(key);
        if (!existing || !existing.previewUrl && item.previewUrl) {
          itemByKey.set(key, item);
        }
      });
      return [...structuredSlots, ...itemByKey.values()].map((item, index) => {
        const { structuredSlotIndex, ...publicItem } = item;
        return { ...publicItem, index: index + 1, selected: false };
      });
    }
    function orderModalItemsFromStructuredMedia(structuredItems, fallbackItems) {
      const orderedStructuredItems = Array.from(structuredItems || []);
      const availableItems = Array.from(fallbackItems || []).filter((item) => item?.resolvedUrl);
      const usedItems = /* @__PURE__ */ new Set();
      const exactMatches = /* @__PURE__ */ new Map();
      orderedStructuredItems.forEach((structuredItem, structuredSlotIndex) => {
        const exactMatch = availableItems.find(
          (item) => !usedItems.has(item) && item.type === structuredItem.type && areMediaUrlsEquivalent(item.resolvedUrl, structuredItem?.resolvedUrl)
        );
        if (!exactMatch) return;
        usedItems.add(exactMatch);
        exactMatches.set(structuredSlotIndex, exactMatch);
      });
      const orderedItems = orderedStructuredItems.map((structuredItem, structuredSlotIndex) => {
        const exactMatch = exactMatches.get(structuredSlotIndex);
        if (!exactMatch) return { ...structuredItem, structuredSlotIndex };
        return {
          ...exactMatch,
          ...structuredItem,
          element: exactMatch.element || structuredItem.element,
          previewUrl: exactMatch.previewUrl || structuredItem.previewUrl || "",
          structuredSlotIndex
        };
      });
      return [
        ...orderedItems,
        ...availableItems.filter((item) => !usedItems.has(item))
      ];
    }
    function finalizeModalItems({ rawItems, cachedImageItems, cachedVideoItems, structuredItems = [] }) {
      const fallbackItems = [
        ...rawItems,
        ...cachedImageItems,
        ...cachedVideoItems
      ];
      const items = structuredItems.length > 0 ? orderModalItemsFromStructuredMedia(structuredItems, rawItems) : fallbackItems;
      return dedupeModalItems(items);
    }
    function uniqueElements(elements) {
      const seen = /* @__PURE__ */ new Set();
      return elements.filter((element) => {
        if (!element || seen.has(element)) return false;
        seen.add(element);
        return true;
      });
    }
    function orderMediaElementsByVisualPosition(elements) {
      return Array.from(elements || []).map((element, originalIndex) => {
        const rect = element?.getBoundingClientRect?.() || {};
        const top = Number(rect.top);
        const left = Number(rect.left);
        return {
          element,
          originalIndex,
          top: Number.isFinite(top) ? top : Infinity,
          left: Number.isFinite(left) ? left : Infinity
        };
      }).sort((a, b) => {
        const aRow = Number.isFinite(a.top) ? Math.round(a.top / 12) : Infinity;
        const bRow = Number.isFinite(b.top) ? Math.round(b.top / 12) : Infinity;
        return aRow - bRow || a.left - b.left || a.top - b.top || a.originalIndex - b.originalIndex;
      }).map(({ element }) => element);
    }
    function findVideoPreviewImage(video, images) {
      if (!video) return null;
      const videoRect = video.getBoundingClientRect();
      const cover = images.find((img) => rectsOverlap(img.getBoundingClientRect(), videoRect));
      return cover ? resolveImageUrl(cover) : null;
    }
    function collectDetailPostImages(root, postId) {
      if (!root) return [];
      const rootImages = Array.from(root.querySelectorAll("img")).filter(isLikelyDetailPostImage).filter((image) => isMediaOwnedByPost(image, root, postId));
      return uniqueElements(rootImages).sort((a, b) => {
        const aRect = a.getBoundingClientRect();
        const bRect = b.getBoundingClientRect();
        return aRect.top - bRect.top || aRect.left - bRect.left;
      });
    }
    function collectVisibleDetailPageMedia(root, postId) {
      if (!root) return [];
      const rootRect = root.getBoundingClientRect();
      const shareButton = findDetailShareButton(root);
      const actionBar = findDetailActionBar(root);
      const actionRect = (shareButton || actionBar)?.getBoundingClientRect?.();
      const bottomLimit = actionRect ? Math.min(rootRect.bottom + 16, actionRect.top + 8) : rootRect.bottom + 16;
      return Array.from(root.querySelectorAll("img, video")).filter(isDownloadableHoverMedia).filter((element) => isMediaOwnedByPost(element, root, postId)).filter((element) => {
        const rect = element.getBoundingClientRect();
        const inMainPostBand = rect.top >= rootRect.top - 24 && rect.top <= bottomLimit;
        return inMainPostBand;
      }).sort((a, b) => {
        const aRect = a.getBoundingClientRect();
        const bRect = b.getBoundingClientRect();
        return aRect.top - bRect.top || aRect.left - bRect.left;
      });
    }
    function collectDetailPostMediaItems() {
      const root = findDetailPostRoot();
      if (!root) return [];
      scanInlineScriptsForVideoUrls();
      const pagePostInfo = getCurrentDetailPostInfo();
      const postInfo = pagePostInfo ? { ...pagePostInfo, createdAt: findPostTimeInNode(root) } : null;
      const postId = normalizePostIdentity(postInfo?.postId);
      const rootRect = root.getBoundingClientRect();
      const isInMainRootBand = (element) => {
        const rect = element.getBoundingClientRect();
        return rect.top >= rootRect.top - 8 && rect.top <= rootRect.bottom + 8;
      };
      const images = collectDetailPostImages(root, postId).filter(isInMainRootBand);
      const videos = Array.from(root.querySelectorAll("video")).filter(isDownloadableHoverMedia).filter((video) => isMediaOwnedByPost(video, root, postId)).filter(isInMainRootBand);
      const standaloneVideos = videos.filter((video) => {
        const videoRect = video.getBoundingClientRect();
        return !images.some((img) => rectsOverlap(img.getBoundingClientRect(), videoRect));
      });
      const pageMedia = collectVisibleDetailPageMedia(root, postId);
      const visibleVideoElements = uniqueElements([
        ...videos,
        ...pageMedia.filter((element) => element.tagName === "VIDEO")
      ]);
      const media = orderMediaElementsByVisualPosition(
        uniqueElements([...images, ...standaloneVideos, ...pageMedia]).filter((element) => {
          if (element.tagName !== "IMG") return true;
          const imageRect = element.getBoundingClientRect();
          return !visibleVideoElements.some((video) => rectsOverlap(imageRect, video.getBoundingClientRect()));
        })
      );
      const rawItems = media.map((element, index) => {
        const isVideo = element.tagName === "VIDEO" || isVideoTargetElement(element);
        const previewUrl = element.tagName === "IMG" ? resolveImageUrl(element) : element.poster || findVideoPreviewImage(element, images) || "";
        let resolvedUrl = null;
        if (!isVideo && element.tagName === "IMG") {
          resolvedUrl = previewUrl;
        } else if (element.tagName === "IMG") {
          const postContext = findPostContext(element);
          resolvedUrl = pickMappedVideoUrlForThumbnail(element, postContext);
        } else {
          resolvedUrl = resolveVideoUrl(element, element);
        }
        return {
          type: isVideo ? "video" : "image",
          element,
          previewUrl,
          resolvedUrl,
          postInfo,
          indexHint: index
        };
      });
      const videoPreviewKeys = new Set(
        rawItems.filter((item) => item.type === "video").map((item) => getMediaUrlIdentity(item.previewUrl)).filter(Boolean)
      );
      const cachedImageItems = (postId ? state.imageUrlsByPostId.get(postId) || [] : []).slice().reverse().filter((url) => !videoPreviewKeys.has(getMediaUrlIdentity(url))).map((url, index) => ({
        type: "image",
        element: root,
        previewUrl: url,
        resolvedUrl: url,
        postInfo,
        indexHint: rawItems.length + index
      }));
      const cachedVideoItems = (postId ? state.videoUrlsByPostId.get(postId) || [] : []).slice().reverse().map((url, index) => ({
        type: "video",
        element: root,
        previewUrl: "",
        resolvedUrl: url,
        postInfo,
        indexHint: rawItems.length + cachedImageItems.length + index
      }));
      const structuredItems = (postId ? state.structuredMediaItemsByPostId.get(postId) || [] : []).map((item, index) => ({
        type: item.type,
        element: root,
        previewUrl: item.type === "image" ? item.url : "",
        resolvedUrl: item.url,
        postInfo,
        indexHint: index
      }));
      return finalizeModalItems({ rawItems, cachedImageItems, cachedVideoItems, structuredItems });
    }
    function ensurePostMediaModal() {
      let modal = document.getElementById(MODAL_ID);
      if (modal) return modal;
      modal = document.createElement("div");
      modal.id = MODAL_ID;
      modal.dataset.tmHidden = "1";
      modal.innerHTML = `
            <div class="tm-modal" role="dialog" aria-modal="true" aria-labelledby="tm-post-media-modal-title">
                <div class="tm-modal-head">
                    <div class="tm-modal-title" id="tm-post-media-modal-title">Threads Media Downloader</div>
                    <div class="tm-modal-subtitle"></div>
                    <button type="button" class="tm-close" aria-label="Close">×</button>
                </div>
                <div class="tm-actions">
                    <button type="button" data-action="download-selected">下載已選取的資源</button>
                    <button type="button" data-action="download-all">下載所有資源</button>
                </div>
                <label class="tm-select-row">
                    <input type="checkbox" data-action="select-all">
                    <span>全選</span>
                </label>
                <div class="tm-list"></div>
            </div>
        `;
      const modalControls = Object.freeze({
        selectAll: modal.querySelector("[data-action=select-all]"),
        downloadSelected: modal.querySelector("[data-action=download-selected]"),
        downloadAll: modal.querySelector("[data-action=download-all]")
      });
      modal.addEventListener("click", (event) => {
        if (event.target === modal || event.target?.classList?.contains("tm-close")) {
          closePostMediaModal();
          return;
        }
        if (isModalControlIntent(event.target, modalControls, "selectAll")) {
          setModalSelection(event.target.checked);
          return;
        }
        if (isModalControlIntent(event.target, modalControls, "downloadSelected")) {
          const activationToken = createUserActivationToken(event);
          if (activationToken) downloadModalItems(false, activationToken);
          return;
        }
        if (isModalControlIntent(event.target, modalControls, "downloadAll")) {
          const activationToken = createUserActivationToken(event);
          if (activationToken) downloadModalItems(true, activationToken);
          return;
        }
        const previewButton = event.target?.closest?.("button.tm-open");
        if (previewButton && modal.contains(previewButton)) {
          const item = state.modalItems[Number(previewButton.dataset.index)];
          if (item?.previewUrl) window.open(item.previewUrl, "_blank", "noopener,noreferrer");
        }
      }, true);
      modal.addEventListener("change", (event) => {
        if (event.target?.dataset?.index == null) return;
        const item = state.modalItems[Number(event.target.dataset.index)];
        if (item) item.selected = event.target.checked;
        syncSelectAllState();
      });
      modal.addEventListener("keydown", handlePostMediaModalKeydown, true);
      document.body.appendChild(modal);
      return modal;
    }
    function renderPostMediaModal() {
      const modal = ensurePostMediaModal();
      const postInfo = getCurrentDetailPostInfo() || { postId: "unknown" };
      const subtitle = modal.querySelector(".tm-modal-subtitle");
      const list = modal.querySelector(".tm-list");
      subtitle.textContent = `Post ID: ${postInfo.postId}`;
      list.innerHTML = "";
      if (state.modalItems.length === 0) {
        list.innerHTML = '<div class="tm-empty">目前沒有在主貼文中找到可下載的圖片或影片。</div>';
        return;
      }
      state.modalItems.forEach((item, index) => {
        const row = document.createElement("div");
        row.className = "tm-item";
        const mediaLabel = item.type === "video" ? `影片 ${index + 1}` : `相片 ${index + 1}`;
        const preview = buildModalItemPreviewMarkup(item);
        row.innerHTML = `
                <div class="tm-check-cell">
                    <input type="checkbox" data-index="${index}" ${item.selected ? "checked" : ""}>
                </div>
                <div class="tm-preview">
                    ${preview}
                    <div>- ${mediaLabel} -</div>
                </div>
                <div class="tm-open-cell">
                    <button type="button" class="tm-open" data-action="open-preview" data-index="${index}" title="開啟預覽">↗</button>
                </div>
            `;
        const videoThumbnail = row.querySelector(".tm-video-thumbnail");
        if (videoThumbnail) applyVideoThumbnailLayout(videoThumbnail, item);
        list.appendChild(row);
      });
      syncSelectAllState();
    }
    function getModalItemsSnapshot(items) {
      return (items || []).map((item) => [
        item.type,
        getMediaUrlIdentity(item.previewUrl),
        getMediaUrlIdentity(item.resolvedUrl)
      ].join(":")).join("|");
    }
    function refreshOpenPostMediaModal() {
      const modal = document.getElementById(MODAL_ID);
      if (!modal || modal.dataset.tmHidden === "1") return false;
      const previousItems = state.modalItems;
      const nextItems = collectDetailPostMediaItems();
      if (getModalItemsSnapshot(previousItems) === getModalItemsSnapshot(nextItems)) return false;
      const selectionByMedia = new Map(previousItems.map((item) => [
        `${item.type}:${getMediaUrlIdentity(item.resolvedUrl || item.previewUrl)}`,
        item.selected
      ]));
      nextItems.forEach((item) => {
        const key = `${item.type}:${getMediaUrlIdentity(item.resolvedUrl || item.previewUrl)}`;
        if (selectionByMedia.has(key)) item.selected = selectionByMedia.get(key);
      });
      state.modalItems = nextItems;
      renderPostMediaModal();
      return true;
    }
    function buildModalItemPreviewMarkup(item) {
      if (item?.type === "video") {
        const poster = validateMediaUrl(item.previewUrl, "image");
        const video = validateMediaUrl(item.resolvedUrl, "video");
        let media = "";
        if (poster.ok) {
          media = `<img src="${escapeHtml(poster.url)}" alt="">`;
        } else if (video.ok) {
          const previewUrl = new URL(video.url);
          previewUrl.hash = "t=0.1";
          media = `<video src="${escapeHtml(previewUrl.href)}" muted playsinline preload="metadata" aria-hidden="true"></video>`;
        }
        return `
                <div class="tm-video-thumbnail" data-orientation="landscape">
                    <span class="tm-video-thumbnail-fallback">影片</span>
                    ${media}
                    <span class="tm-video-play-badge" aria-hidden="true">▶</span>
                </div>
            `;
      }
      const image = validateMediaUrl(item?.previewUrl || item?.resolvedUrl, "image");
      return image.ok ? `<img src="${escapeHtml(image.url)}" alt="">` : "<div>相片</div>";
    }
    function getVideoThumbnailLayout(videoWidth, videoHeight) {
      const width = Number(videoWidth);
      const height = Number(videoHeight);
      if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
        return { orientation: "landscape", aspectRatio: "16 / 9" };
      }
      return {
        orientation: width === height ? "square" : width < height ? "portrait" : "landscape",
        aspectRatio: `${width} / ${height}`
      };
    }
    function applyVideoThumbnailLayout(thumbnail, item) {
      const apply = (width, height) => {
        const layout = getVideoThumbnailLayout(width, height);
        thumbnail.dataset.orientation = layout.orientation;
        thumbnail.style.setProperty("--tm-video-aspect-ratio", layout.aspectRatio);
      };
      const sourceVideo = item?.element?.tagName === "VIDEO" ? item.element : null;
      const previewVideo = thumbnail.querySelector("video");
      if (sourceVideo?.videoWidth > 0 && sourceVideo?.videoHeight > 0) {
        apply(sourceVideo.videoWidth, sourceVideo.videoHeight);
      }
      if (!previewVideo) return;
      const applyPreviewMetadata = () => apply(previewVideo.videoWidth, previewVideo.videoHeight);
      if (previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) {
        applyPreviewMetadata();
      } else {
        previewVideo.addEventListener("loadedmetadata", applyPreviewMetadata, { once: true });
      }
    }
    function escapeHtml(value) {
      return String(value || "").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    }
    function getPostMediaModalFocusableControls(modal) {
      return Array.from(modal?.querySelectorAll?.(
        'button:not([disabled]), input:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
      ) || []).filter(
        (control) => control?.dataset?.tmHidden !== "1" && control?.getAttribute?.("aria-hidden") !== "true"
      );
    }
    function handlePostMediaModalKeydown(event) {
      const modal = document.getElementById(MODAL_ID);
      if (!modal || modal.dataset.tmHidden === "1") return false;
      if (event?.key === "Escape") {
        event.preventDefault?.();
        event.stopPropagation?.();
        closePostMediaModal();
        return true;
      }
      if (event?.key !== "Tab") return false;
      const controls = getPostMediaModalFocusableControls(modal);
      if (!controls.length) {
        event.preventDefault?.();
        return true;
      }
      const activeIndex = controls.indexOf(document.activeElement);
      const nextIndex = event.shiftKey ? activeIndex <= 0 ? controls.length - 1 : activeIndex - 1 : activeIndex < 0 || activeIndex === controls.length - 1 ? 0 : activeIndex + 1;
      event.preventDefault?.();
      controls[nextIndex]?.focus?.();
      return true;
    }
    function openPostMediaModal() {
      if (!isPostMediaPickerEnabled()) {
        cleanupDetailButton();
        toast("文章批次下載功能目前已關閉。");
        return;
      }
      state.modalReturnFocus = document.activeElement?.isConnected ? document.activeElement : state.detailButton;
      scanInlineScriptsForVideoUrls();
      state.modalItems = collectDetailPostMediaItems();
      const modal = ensurePostMediaModal();
      renderPostMediaModal();
      modal.dataset.tmHidden = "0";
      modal.querySelector(".tm-close")?.focus?.();
    }
    function closePostMediaModal() {
      const modal = document.getElementById(MODAL_ID);
      if (modal) modal.dataset.tmHidden = "1";
      const returnFocus = state.modalReturnFocus;
      state.modalReturnFocus = null;
      if (returnFocus?.isConnected) returnFocus.focus?.();
    }
    function setModalSelection(checked) {
      state.modalItems.forEach((item) => {
        item.selected = checked;
      });
      renderPostMediaModal();
    }
    function syncSelectAllState() {
      const modal = document.getElementById(MODAL_ID);
      if (!modal) return;
      const checkbox = modal.querySelector('input[data-action="select-all"]');
      if (!checkbox) return;
      const total = state.modalItems.length;
      const selected = state.modalItems.filter((item) => item.selected).length;
      checkbox.checked = total > 0 && selected === total;
      checkbox.indeterminate = selected > 0 && selected < total;
    }
    function getModalDownloadItems(items, downloadAll) {
      return items.filter((item) => downloadAll || item.selected);
    }
    function setBatchDownloadButtonsDisabled(disabled) {
      const modal = typeof document !== "undefined" ? document.getElementById?.(MODAL_ID) : null;
      modal?.querySelectorAll?.('[data-action="download-selected"], [data-action="download-all"]').forEach((button) => {
        button.disabled = disabled;
      });
    }
    async function downloadModalItems(downloadAll, activationToken, options = {}) {
      if (!isValidUserActivationToken(activationToken)) return false;
      if (state.batchDownloadInProgress) return false;
      const sourceItems = Array.isArray(options.items) ? options.items : state.modalItems;
      const items = getModalDownloadItems(sourceItems, downloadAll).slice();
      if (items.length === 0) {
        toast("沒有選取任何資源。");
        return false;
      }
      state.batchDownloadInProgress = true;
      setBatchDownloadButtonsDisabled(true);
      const downloadFn = options.downloadFn || downloadItem;
      const resolveItem = options.resolveItem || mediaItemFromElementWithRetry;
      const delayFn = options.delayFn || ((delayMs) => new Promise((resolve) => window.setTimeout(resolve, delayMs)));
      try {
        toast(`Preparing ${items.length} media download(s)...`);
        for (const modalItem of items) {
          if (stopped || !isValidUserActivationToken(activationToken)) return false;
          const resolved = modalItem.resolvedUrl ? {
            type: modalItem.type,
            url: modalItem.resolvedUrl,
            element: modalItem.element,
            contextElement: modalItem.element,
            postInfo: modalItem.postInfo
          } : await resolveItem(modalItem.element);
          if (stopped) return false;
          if (!resolved) {
            toast(`找不到${modalItem.type === "video" ? "影片" : "圖片"} ${modalItem.index} 的下載連結。`);
            continue;
          }
          await downloadFn({
            ...resolved,
            contextElement: modalItem.element
          }, activationToken, options.downloadOptions || {});
          if (stopped) return false;
          await delayFn(320);
        }
        return true;
      } finally {
        state.batchDownloadInProgress = false;
        setBatchDownloadButtonsDisabled(false);
      }
    }
    function getLayoutRefreshIntervalMs() {
      const value = Number(USER_OPTIONS.layoutRefreshIntervalMs);
      return Number.isFinite(value) ? Math.max(0, value) : 260;
    }
    function getBackgroundScanIntervalMs() {
      const value = Number(USER_OPTIONS.backgroundScanIntervalMs);
      return Number.isFinite(value) ? Math.max(3e3, value) : 5e3;
    }
    function getMediaRouteKey() {
      return buildMediaRouteKey(location.href);
    }
    function syncMediaRouteScope() {
      const nextScope = transitionMediaRouteScope({
        routeKey: state.mediaRouteKey
      }, getMediaRouteKey());
      state.mediaRouteKey = nextScope.routeKey;
      if (nextScope.changed) {
        state.diagnostics.routeTransitions += 1;
        state.videoUrlsByPostId.clear();
        state.imageUrlsByPostId.clear();
        state.structuredMediaItemsByPostId.clear();
        state.postCounters.clear();
        state.modalItems = [];
        clearNativeShareContext("route_change");
      }
      return nextScope.changed;
    }
    function getScrollRefreshTarget(event) {
      const target = event?.target;
      if (!target || target === document || target === document.body || target === document.documentElement) {
        return document.documentElement;
      }
      return target.nodeType === Node.ELEMENT_NODE ? target : null;
    }
    function shouldIgnoreScrollRefresh(event) {
      if (USER_OPTIONS.ignoreHorizontalOnlyScroll === false || event?.type !== "scroll") return false;
      const target = getScrollRefreshTarget(event);
      if (!target) return false;
      const left = target === document.documentElement ? window.scrollX : Number(target.scrollLeft) || 0;
      const top = target === document.documentElement ? window.scrollY : Number(target.scrollTop) || 0;
      const previous = state.scrollPositions.get(target);
      state.scrollPositions.set(target, { left, top });
      if (!previous) return false;
      const deltaX = Math.abs(left - previous.left);
      const deltaY = Math.abs(top - previous.top);
      return deltaX > 0 && deltaY < 1;
    }
    function refreshHoverButtonLayout() {
      const hoverTargetIsValid = state.hoverElement && isDownloadableHoverMedia(state.hoverElement) && isPlausibleHoverRect(state.hoverElement.getBoundingClientRect());
      if (!hoverTargetIsValid) {
        hideHoverButton();
        return;
      }
      showHoverButton(state.hoverElement);
    }
    function refreshButtons(options = {}) {
      if (!document.body) return;
      state.diagnostics.fullRefreshes += 1;
      syncMediaRouteScope();
      if (options.scanNetwork !== false) {
        scanInlineScriptsForVideoUrls();
      }
      ensureCopyButtonsForBlocks();
      ensureDetailButton();
      refreshHoverButtonLayout();
      refreshOpenPostMediaModal();
    }
    function scheduleRefresh(options = {}) {
      if (options.scanNetwork !== false) state.pendingScanNetworkRefresh = true;
      if (state.scanTimer) return;
      state.scanTimer = window.setTimeout(() => {
        state.scanTimer = 0;
        const scanNetwork = state.pendingScanNetworkRefresh;
        state.pendingScanNetworkRefresh = false;
        refreshButtons({ scanNetwork });
      }, SCAN_DEBOUNCE_MS);
    }
    function runQueuedLayoutRefresh() {
      if (state.rafRefresh) return;
      state.rafRefresh = window.requestAnimationFrame(() => {
        state.rafRefresh = 0;
        state.lastLayoutRefreshAt = Date.now();
        const fullRefresh = state.pendingLayoutFullRefresh;
        state.pendingLayoutFullRefresh = false;
        if (fullRefresh) {
          refreshButtons({ scanNetwork: false });
        } else {
          refreshHoverButtonLayout();
        }
      });
    }
    function refreshButtonsSoon(event) {
      if (shouldIgnoreScrollRefresh(event)) return;
      if (event?.type === "resize") {
        state.pendingLayoutFullRefresh = true;
      }
      const interval = getLayoutRefreshIntervalMs();
      const elapsed = Date.now() - state.lastLayoutRefreshAt;
      if (elapsed >= interval) {
        window.clearTimeout(state.layoutRefreshTimer);
        state.layoutRefreshTimer = 0;
        runQueuedLayoutRefresh();
        return;
      }
      if (state.layoutRefreshTimer) return;
      state.layoutRefreshTimer = window.setTimeout(() => {
        state.layoutRefreshTimer = 0;
        runQueuedLayoutRefresh();
      }, interval - elapsed);
    }
    function rememberVideoUrl(url, postId) {
      const validated = validateMediaUrl(url, "video");
      if (!validated.ok) return;
      const normalized = validated.url;
      syncMediaRouteScope();
      if (postId) {
        const identity = normalizePostIdentity(postId);
        if (!identity) return;
        const current = state.videoUrlsByPostId.get(identity) || [];
        state.videoUrlsByPostId.delete(identity);
        state.videoUrlsByPostId.set(identity, mergeMediaUrlCache(current, normalized, 32));
        trimMapToSize(state.videoUrlsByPostId, 160);
      }
    }
    function rememberImageUrl(url, postId) {
      const validated = validateMediaUrl(url, "image");
      if (!validated.ok) return;
      const normalized = validated.url;
      if (/profile_pic|s150x150|s320x320|emoji|sprite|static|favicon|avatar/i.test(normalized)) return;
      if (!postId) return;
      const identity = normalizePostIdentity(postId);
      if (!identity) return;
      const current = state.imageUrlsByPostId.get(identity) || [];
      state.imageUrlsByPostId.delete(identity);
      state.imageUrlsByPostId.set(identity, mergeMediaUrlCache(current, normalized, 32));
      trimMapToSize(state.imageUrlsByPostId, 160);
    }
    function trimMapToSize(map, maxSize) {
      while (map.size > maxSize) {
        const oldestKey = map.keys().next().value;
        if (oldestKey === void 0) break;
        map.delete(oldestKey);
      }
    }
    function rememberStructuredMediaOrder(records) {
      const itemsByPostId = /* @__PURE__ */ new Map();
      let pendingRecordCount = 0;
      Array.from(records || []).some((record) => {
        if (pendingRecordCount >= MAX_STRUCTURED_RECORDS_PER_ROUTE) return true;
        if (!record?.postId || !record?.type || !record?.url) return;
        const postId = normalizePostIdentity(record.postId);
        if (!postId) return;
        if (!itemsByPostId.has(postId)) itemsByPostId.set(postId, []);
        itemsByPostId.get(postId).push({ type: record.type, url: record.url });
        pendingRecordCount += 1;
        return false;
      });
      itemsByPostId.forEach((nextItems, postId) => {
        const currentItems = state.structuredMediaItemsByPostId.get(postId) || [];
        if (nextItems.length >= currentItems.length) {
          state.structuredMediaItemsByPostId.delete(postId);
          state.structuredMediaItemsByPostId.set(postId, nextItems);
          trimMapToSize(state.structuredMediaItemsByPostId, 160);
        }
      });
      let overflow = Array.from(state.structuredMediaItemsByPostId.values()).reduce((total, items) => total + items.length, 0) - MAX_STRUCTURED_RECORDS_PER_ROUTE;
      while (overflow > 0 && state.structuredMediaItemsByPostId.size) {
        const oldestPostId = state.structuredMediaItemsByPostId.keys().next().value;
        const oldestItems = state.structuredMediaItemsByPostId.get(oldestPostId) || [];
        if (oldestItems.length <= overflow) {
          overflow -= oldestItems.length;
          state.structuredMediaItemsByPostId.delete(oldestPostId);
        } else {
          state.structuredMediaItemsByPostId.set(oldestPostId, oldestItems.slice(overflow));
          overflow = 0;
        }
      }
    }
    function walkJsonForVideoUrls(value, postCode) {
      const records = collectStructuredMediaUrls(value, postCode);
      rememberStructuredMediaOrder(records);
      records.forEach((record) => {
        if (record.type === "video") {
          rememberVideoUrl(record.url, record.postId);
        } else {
          rememberImageUrl(record.url, record.postId);
        }
      });
    }
    function parseJsonPayload(text) {
      if (!text || typeof text !== "string") return null;
      const trimmed = text.trim().replace(/^\s*for\s*\(\s*;\s*;\s*\)\s*;/, "").replace(/^\s*while\s*\(\s*1\s*\)\s*;/, "");
      try {
        return JSON.parse(trimmed);
      } catch (error) {
        const firstObject = trimmed.search(/[\[{]/);
        if (firstObject < 0) return null;
        const lastBrace = Math.max(trimmed.lastIndexOf("}"), trimmed.lastIndexOf("]"));
        if (lastBrace <= firstObject) return null;
        try {
          return JSON.parse(trimmed.slice(firstObject, lastBrace + 1));
        } catch (innerError) {
          return null;
        }
      }
    }
    function scanInlineScriptsForVideoUrls() {
      if (isSensitiveThreadsRoute(location.href)) return;
      Array.from(document.scripts || []).forEach((script) => {
        const text = script.textContent || "";
        if (state.scannedScriptContents.get(script) === text) return;
        state.scannedScriptContents.set(script, text);
        if (utf8ByteLength(text) > NETWORK_RESPONSE_MAX_BYTES) return;
        if (!/video_versions|playable_url|video_url|image_versions|\.mp4|\.jpe?g|\.hei[cf]|\.png|\.webp|bytestart|byteend/i.test(text)) return;
        extractVideoUrlsFromText(text, getMediaRouteKey());
      });
    }
    function extractVideoUrlsFromText(text, sourceRouteKey = getMediaRouteKey()) {
      if (!text || typeof text !== "string") return;
      state.diagnostics.networkPayloadParses += 1;
      const parsedPayload = parseJsonPayload(text);
      if (parsedPayload) {
        walkJsonForVideoUrls(parsedPayload, null);
      }
    }
    function installNetworkHooks(targetWindow) {
      const nativeFetch = targetWindow.fetch;
      let routeGeneration = 0n;
      let installedFetch = null;
      let installedOpen = null;
      let installedSend = null;
      let installedSetRequestHeader = null;
      let installedPushState = null;
      let installedReplaceState = null;
      let nativeOpen = null;
      let nativeSend = null;
      let nativeSetRequestHeader = null;
      const routeListenerDisposers = [];
      const invalidateRouteGeneration = () => {
        routeGeneration += 1n;
      };
      const history = targetWindow.history;
      const installHistoryHook = (methodName) => {
        const nativeMethod = history?.[methodName];
        if (typeof nativeMethod !== "function") return null;
        const wrappedMethod = function(...args) {
          const result = nativeMethod.apply(this, args);
          invalidateRouteGeneration();
          return result;
        };
        try {
          history[methodName] = wrappedMethod;
          return history[methodName] === wrappedMethod ? { nativeMethod, wrappedMethod } : null;
        } catch {
          return null;
        }
      };
      const listenForRouteChange = (target, type) => {
        if (typeof target?.addEventListener !== "function") return;
        try {
          target.addEventListener(type, invalidateRouteGeneration);
          routeListenerDisposers.push(() => {
            try {
              target.removeEventListener?.(type, invalidateRouteGeneration);
            } catch {
            }
          });
        } catch {
        }
      };
      installedPushState = installHistoryHook("pushState");
      installedReplaceState = installHistoryHook("replaceState");
      listenForRouteChange(targetWindow, "popstate");
      listenForRouteChange(targetWindow, "hashchange");
      listenForRouteChange(targetWindow.navigation, "navigate");
      listenForRouteChange(targetWindow.navigation, "currententrychange");
      const uninstall = () => {
        if (installedFetch && targetWindow.fetch === installedFetch) targetWindow.fetch = nativeFetch;
        routeListenerDisposers.splice(0).forEach((dispose) => dispose());
        if (installedPushState && history?.pushState === installedPushState.wrappedMethod) {
          try {
            history.pushState = installedPushState.nativeMethod;
          } catch {
          }
        }
        if (installedReplaceState && history?.replaceState === installedReplaceState.wrappedMethod) {
          try {
            history.replaceState = installedReplaceState.nativeMethod;
          } catch {
          }
        }
        const prototype = targetWindow.XMLHttpRequest?.prototype;
        if (!prototype) return;
        if (installedOpen && prototype.open === installedOpen) prototype.open = nativeOpen;
        if (installedSend && prototype.send === installedSend) prototype.send = nativeSend;
        if (installedSetRequestHeader && prototype.setRequestHeader === installedSetRequestHeader) {
          prototype.setRequestHeader = nativeSetRequestHeader;
        }
      };
      if (typeof nativeFetch === "function" && !nativeFetch.__tmTargetWrapped) {
        const wrappedFetch = function(input, init = {}) {
          let captureContext = { allowed: false, reason: "classification_failed" };
          const sourceRouteGeneration = routeGeneration;
          try {
            const routeUrl = location.href;
            const inputIsUrl = typeof input === "string" || input instanceof URL;
            const hasUninspectedRequestBody = !inputIsUrl && init.body === void 0 && input?.body != null;
            captureContext = hasUninspectedRequestBody ? { allowed: false, reason: "request_body_uninspected" } : classifyNetworkCaptureRequest({
              url: inputIsUrl ? input : input?.url,
              method: init.method || input?.method || "GET",
              headers: init.headers || input?.headers,
              body: init.body,
              routeUrl
            });
            if (captureContext.allowed) {
              captureContext = { ...captureContext, sourceRouteGeneration };
            }
          } catch (error) {
            captureContext = { allowed: false, reason: "classification_failed" };
          }
          return nativeFetch.apply(this, arguments).then((response) => {
            if (captureContext.allowed && !stopped) {
              inspectResponse(response, captureContext, {
                extractor: extractVideoUrlsFromText,
                signal: state.lifecycleController.signal,
                shouldContinue: () => !stopped && !isSensitiveThreadsRoute(location.href) && captureContext.sourceRouteGeneration === routeGeneration && captureContext.sourceRouteKey === getMediaRouteKey()
              }).catch(() => {
              });
            }
            return response;
          });
        };
        wrappedFetch.__tmTargetWrapped = true;
        targetWindow.fetch = wrappedFetch;
        installedFetch = wrappedFetch;
      }
      const xhrCtor = targetWindow.XMLHttpRequest;
      if (!xhrCtor?.prototype) return uninstall;
      nativeOpen = xhrCtor.prototype.open;
      nativeSend = xhrCtor.prototype.send;
      if (!nativeOpen.__tmTargetWrapped && !nativeSend.__tmTargetWrapped) {
        xhrCtor.prototype.open = function(method, url, ...rest) {
          if (this.__tmTargetLoadHandler) {
            this.removeEventListener?.("load", this.__tmTargetLoadHandler);
            this.__tmTargetLoadHandler = null;
          }
          this.__tmTargetFriendlyName = "";
          this.__tmTargetRequest = {
            method,
            url,
            routeUrl: location.href,
            sourceRouteGeneration: routeGeneration
          };
          return nativeOpen.call(this, method, url, ...rest);
        };
        xhrCtor.prototype.send = function(body) {
          const request = this.__tmTargetRequest || {};
          let captureContext = { allowed: false, reason: "classification_failed" };
          try {
            captureContext = classifyNetworkCaptureRequest({
              ...request,
              body,
              headers: {
                "x-fb-friendly-name": this.__tmTargetFriendlyName || ""
              }
            });
            if (captureContext.allowed) {
              captureContext = {
                ...captureContext,
                sourceRouteGeneration: request.sourceRouteGeneration
              };
            }
          } catch (error) {
            captureContext = { allowed: false, reason: "classification_failed" };
          }
          if (captureContext.allowed) {
            const loadHandler = function() {
              if (this.__tmTargetLoadHandler !== loadHandler) return;
              this.__tmTargetLoadHandler = null;
              if (stopped) return;
              inspectXhrResponse(this, captureContext, {
                extractor: extractVideoUrlsFromText,
                signal: state.lifecycleController.signal,
                shouldContinue: () => !stopped && !isSensitiveThreadsRoute(location.href) && captureContext.sourceRouteGeneration === routeGeneration && captureContext.sourceRouteKey === getMediaRouteKey()
              }).catch(() => {
              });
            };
            this.__tmTargetLoadHandler = loadHandler;
            this.addEventListener("load", loadHandler, { once: true });
          }
          return nativeSend.apply(this, arguments);
        };
        nativeSetRequestHeader = xhrCtor.prototype.setRequestHeader;
        if (typeof nativeSetRequestHeader === "function" && !nativeSetRequestHeader.__tmTargetWrapped) {
          xhrCtor.prototype.setRequestHeader = function(name, value) {
            if (String(name).toLowerCase() === "x-fb-friendly-name") {
              this.__tmTargetFriendlyName = this.__tmTargetFriendlyName ? `${this.__tmTargetFriendlyName},${value}` : value;
            }
            return nativeSetRequestHeader.call(this, name, value);
          };
          xhrCtor.prototype.setRequestHeader.__tmTargetWrapped = true;
        }
        xhrCtor.prototype.open.__tmTargetWrapped = true;
        xhrCtor.prototype.send.__tmTargetWrapped = true;
        installedOpen = xhrCtor.prototype.open;
        installedSend = xhrCtor.prototype.send;
        if (nativeSetRequestHeader) installedSetRequestHeader = xhrCtor.prototype.setRequestHeader;
      }
      return uninstall;
    }
    function initObserver() {
      if (!document.body) {
        state.initObserverTimer = window.setTimeout(initObserver, 100);
        return;
      }
      const observer = new window.MutationObserver((mutations) => {
        state.diagnostics.observerCallbacks += 1;
        const requiresRefresh = mutations.some((mutation) => {
          if (mutation.type === "attributes") return true;
          const changedNodes = [
            ...Array.from(mutation.addedNodes || []),
            ...Array.from(mutation.removedNodes || [])
          ];
          return changedNodes.some((node) => {
            const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
            if (!element) return false;
            return !element.matches?.(
              `.${POST_TOOL_CLASS}, .${COPY_TOOL_CLASS}, .${LINK_TOOL_CLASS}, .${BUTTON_CLASS}, #${TOAST_ID}, #${MODAL_ID}`
            );
          });
        });
        if (requiresRefresh) scheduleRefresh();
        if (state.pendingShareContext) {
          if (pruneNativeShareContext() && !state.cleanLinkMenuTimer) {
            scheduleCleanLinkMenuInjection(0);
          }
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ["src", "srcset"]
      });
      state.observer = observer;
      ensureHoverButton();
      scanInlineScriptsForVideoUrls();
    }
    function startBackgroundScanInterval() {
      if (state.backgroundScanIntervalId) {
        window.clearInterval(state.backgroundScanIntervalId);
      }
      state.backgroundScanIntervalId = window.setInterval(refreshButtons, getBackgroundScanIntervalMs());
    }
    function initRefreshListeners() {
      bindHoverEvents();
      if (state.uiRefreshIntervalId) {
        window.clearInterval(state.uiRefreshIntervalId);
      }
      state.uiRefreshIntervalId = window.setInterval(
        () => refreshButtons({ scanNetwork: false }),
        5e3
      );
      startBackgroundScanInterval();
    }
    const testing = Object.freeze({
      acceptPostContextCandidate,
      activateButton,
      buildModalItemPreviewMarkup,
      buildMediaRouteKey,
      classifyNetworkCaptureRequest,
      closeNativeShareMenu,
      collectStructuredMediaUrls,
      copyText,
      copyPostBlockCleanLink,
      createUserActivationToken,
      downloadItem,
      downloadModalItems,
      downloadViaBlob,
      extractVideoUrlsFromText,
      finalizeModalItems,
      findPostContext,
      findBestPostInfoInNode,
      findNativeShareMenuContainer,
      findShareSvgFromEvent,
      getPostBlockTextBoundary,
      handlePostMediaModalKeydown,
      isModalControlIntent,
      getMediaUrlIdentity,
      getModalDownloadItems,
      getVideoThumbnailLayout,
      getOwnedDetailPostFallback,
      getPerformanceSnapshot: () => Object.freeze({
        ...state.diagnostics,
        imageRouteCacheEntries: state.imageUrlsByPostId.size,
        videoRouteCacheEntries: state.videoUrlsByPostId.size,
        structuredRouteCacheEntries: state.structuredMediaItemsByPostId.size,
        structuredRouteRecordCount: Array.from(state.structuredMediaItemsByPostId.values()).reduce((total, items) => total + items.length, 0),
        listenerCount: state.listenerDisposers.length,
        startupTimerCount: state.startupTimers.length,
        pendingTimeoutCount: [
          state.hideTimer,
          state.cleanLinkMenuTimer,
          state.nativeShareCloseTimer,
          state.initObserverTimer,
          state.scanTimer,
          state.hoverScanTimer,
          state.layoutRefreshTimer
        ].filter(Boolean).length,
        animationFrameCount: Number(Boolean(state.hoverMoveRaf)) + Number(Boolean(state.rafRefresh)),
        activeIntervalCount: Number(Boolean(state.backgroundScanIntervalId)) + Number(Boolean(state.uiRefreshIntervalId)),
        observerActive: Boolean(state.observer)
      }),
      getShareContextInvalidReason,
      inspectResponse,
      inspectXhrResponse,
      installNetworkHooks,
      isInspectableResponseMime,
      isInsideNestedPostBlock,
      isMediaOwnedByPost,
      isNativeCopyLinkActionRect,
      isSecurityDownloadError,
      isTrustedUserActivation,
      mergeMediaUrlCache,
      rememberNativeShareContext,
      orderMediaElementsByVisualPosition,
      resolveImageUrl,
      scanInlineScriptsForVideoUrls,
      scheduleRefresh,
      selectPostBoundVideoUrl,
      syncMediaRouteScope,
      transitionMediaRouteScope,
      validateMediaUrl
    });
    async function start() {
      if (started) return false;
      if (stopped) throw new Error("runtime_stopped");
      started = true;
      if (IS_NODE_RUNTIME2) return true;
      state.disposeStyles = await platform.installStyles(PLUGIN_CSS);
      if (stopped) {
        state.disposeStyles?.();
        state.disposeStyles = null;
        return false;
      }
      await registerUserOptionMenu();
      if (stopped) {
        state.disposeSettingsUi?.();
        state.disposeSettingsUi = null;
        return false;
      }
      state.uninstallNetworkCapture = captureSource ? installNetworkHooks(captureSource) : null;
      if (stopped) {
        state.uninstallNetworkCapture?.();
        state.uninstallNetworkCapture = null;
        return false;
      }
      bindGlobalButtonEvents();
      bindNativeShareMenuEvents();
      initObserver();
      initRefreshListeners();
      state.startupTimers.push(window.setTimeout(refreshButtons, 800));
      state.startupTimers.push(window.setTimeout(refreshButtons, 1800));
      state.startupTimers.push(window.setTimeout(refreshButtons, 3600));
      return true;
    }
    async function stop() {
      if (stopped) return false;
      stopped = true;
      state.lifecycleController.abort();
      state.disposeSettingsUi?.();
      state.disposeStyles?.();
      state.uninstallNetworkCapture?.();
      if (!IS_NODE_RUNTIME2) {
        clearNativeShareContext("runtime_stop");
        window.clearTimeout(state.hideTimer);
        window.clearTimeout(state.cleanLinkMenuTimer);
        state.hideTimer = 0;
        state.cleanLinkMenuTimer = 0;
        state.listenerDisposers.splice(0).forEach((dispose) => dispose());
        state.observer?.disconnect?.();
        state.observer = null;
        window.clearInterval(state.backgroundScanIntervalId);
        window.clearInterval(state.uiRefreshIntervalId);
        state.backgroundScanIntervalId = 0;
        state.uiRefreshIntervalId = 0;
        window.clearTimeout(state.initObserverTimer);
        state.initObserverTimer = 0;
        state.startupTimers.splice(0).forEach((timerId) => window.clearTimeout(timerId));
        window.clearTimeout(state.scanTimer);
        window.clearTimeout(state.hoverScanTimer);
        window.clearTimeout(state.layoutRefreshTimer);
        state.scanTimer = 0;
        state.pendingScanNetworkRefresh = false;
        state.hoverScanTimer = 0;
        state.layoutRefreshTimer = 0;
        if (state.hoverMoveRaf) window.cancelAnimationFrame(state.hoverMoveRaf);
        if (state.rafRefresh) window.cancelAnimationFrame(state.rafRefresh);
        state.hoverMoveRaf = 0;
        state.rafRefresh = 0;
        state.hoverButton?.remove?.();
        state.hoverButton = null;
        state.hoverElement = null;
        state.pendingHoverPoint = null;
        cleanupDetailButton();
        state.liveButtons.forEach((button) => button.remove?.());
        state.liveButtons.clear();
        state.buttonByElement = /* @__PURE__ */ new WeakMap();
        state.elementByButton = /* @__PURE__ */ new WeakMap();
        cleanupCopyButtons();
        document.getElementById(MODAL_ID)?.remove?.();
        document.getElementById(TOAST_ID)?.remove?.();
        document.querySelectorAll(`.${CLEAN_LINK_MENU_CLASS}`).forEach((item) => item.remove());
        document.querySelectorAll(".tm-post-media-tool-fallback").forEach((item) => {
          if (item.childElementCount === 0) item.remove();
        });
      }
      state.postCounters.clear();
      state.scannedScriptContents = /* @__PURE__ */ new WeakMap();
      state.mediaRouteKey = "";
      state.captureRouteGeneration = "";
      state.modalItems = [];
      state.modalReturnFocus = null;
      state.detailUiCache = null;
      state.batchDownloadInProgress = false;
      state.videoUrlsByPostId.clear();
      state.imageUrlsByPostId.clear();
      state.structuredMediaItemsByPostId.clear();
      return true;
    }
    async function updateOptions(nextOptions) {
      Object.assign(USER_OPTIONS, normalizeOptions(nextOptions));
      if (started && !stopped && !IS_NODE_RUNTIME2) applyUserOptions();
      return Object.freeze({ ...USER_OPTIONS });
    }
    function setCaptureRouteGeneration(nextGeneration) {
      if (nextGeneration === "") {
        state.captureRouteGeneration = "";
        return true;
      }
      if (typeof nextGeneration !== "string" || !/^[a-f0-9]{16,64}$/.test(nextGeneration)) return false;
      state.captureRouteGeneration = nextGeneration;
      return true;
    }
    function ingestCapturedMedia(records, sourceRouteKey, sourceRouteGeneration) {
      if (stopped || !started || IS_NODE_RUNTIME2 || sourceRouteKey !== getMediaRouteKey() || !state.captureRouteGeneration || sourceRouteGeneration !== state.captureRouteGeneration) {
        return false;
      }
      const accepted = Array.from(records || []).filter((record) => {
        if (!record?.postId || record.type !== "image" && record.type !== "video") return false;
        return validateMediaUrl(record.url, record.type).ok;
      });
      if (!accepted.length) return false;
      rememberStructuredMediaOrder(accepted);
      accepted.forEach((record) => {
        if (record.type === "video") rememberVideoUrl(record.url, record.postId);
        else rememberImageUrl(record.url, record.postId);
      });
      scheduleRefresh({ scanNetwork: false });
      return true;
    }
    return Object.freeze({
      start,
      stop,
      updateOptions,
      setCaptureRouteGeneration,
      ingestCapturedMedia,
      testing
    });
  }

  // src/userscript/platform-adapter.js
  var OPTIONS_KEY = "threads-media-downloader-options-v1";
  function createUserscriptPlatformAdapter(environment = globalThis) {
    let disposeSettings = () => {
    };
    return {
      async loadOptions() {
        try {
          return typeof environment.GM_getValue === "function" ? environment.GM_getValue(OPTIONS_KEY, null) : null;
        } catch {
          return null;
        }
      },
      async saveOptions(options) {
        if (typeof environment.GM_setValue !== "function") return false;
        environment.GM_setValue(OPTIONS_KEY, JSON.stringify(options));
        return true;
      },
      subscribeOptions() {
        return () => {
        };
      },
      downloadMedia(details) {
        if (typeof environment.GM_download !== "function") throw new Error("unsupported");
        return environment.GM_download(details);
      },
      requestMedia(details) {
        if (typeof environment.GM_xmlhttpRequest !== "function") throw new Error("unsupported");
        return environment.GM_xmlhttpRequest(details);
      },
      async writeClipboard(text) {
        if (typeof environment.GM_setClipboard === "function") {
          environment.GM_setClipboard(text);
          return true;
        }
        await environment.navigator?.clipboard?.writeText(text);
        return true;
      },
      async installStyles(cssText) {
        if (typeof environment.GM_addStyle === "function") {
          const style2 = environment.GM_addStyle(cssText);
          let disposed2 = false;
          return () => {
            if (disposed2) return;
            disposed2 = true;
            style2?.remove?.();
          };
        }
        const style = environment.document.createElement("style");
        style.dataset.threadsPluginStyle = "1";
        style.textContent = cssText;
        environment.document.documentElement.appendChild(style);
        let disposed = false;
        return () => {
          if (disposed) return;
          disposed = true;
          style.remove();
        };
      },
      async installSettingsUi(model) {
        const disposePreviousSettings = disposeSettings;
        disposeSettings = () => {
        };
        disposePreviousSettings();
        const commandIds = [];
        if (typeof environment.GM_registerMenuCommand !== "function") return () => {
        };
        let disposed = false;
        const disposeCurrentSettings = () => {
          if (disposed) return;
          disposed = true;
          if (disposeSettings === disposeCurrentSettings) disposeSettings = () => {
          };
          if (typeof environment.GM_unregisterMenuCommand !== "function") return;
          commandIds.forEach((id) => environment.GM_unregisterMenuCommand(id));
        };
        disposeSettings = disposeCurrentSettings;
        try {
          for (const command of model.commands) {
            commandIds.push(environment.GM_registerMenuCommand(command.label, command.run));
          }
        } catch (error) {
          disposeCurrentSettings();
          throw error;
        }
        return disposeCurrentSettings;
      }
    };
  }

  // src/userscript/entry.js
  var IS_NODE_RUNTIME = typeof process !== "undefined" && process.release?.name === "node";
  async function bootstrapUserscript(environment = globalThis) {
    const platform = createUserscriptPlatformAdapter(environment);
    const initialOptions = normalizeOptions(await platform.loadOptions());
    const runtime = await createThreadsRuntime({
      platform,
      captureSource: environment.unsafeWindow || environment.window,
      document: environment.document,
      window: environment.window,
      initialOptions,
      clock: environment
    });
    await runtime.start();
    return runtime;
  }
  if (!IS_NODE_RUNTIME) {
    bootstrapUserscript().then(() => {
      console.log("[Threads Target Downloader]", "v5.1.0 loaded");
    }).catch((error) => {
      console.error("[Threads Target Downloader]", "bootstrap failed", error);
    });
  }
})();