Threads Plugin

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
    });
  }
})();