# x_storages

mount_path: /local
driver: Local
status: work
modified: 2026-08-13 12:00:38.384109309+00:00
order_by: name
order_direction: asc
extract_folder: front
webdav_policy: native_proxy
addition:
  {"root_folder_path":"/local","directory_size":false,"thumbnail":false,"thumb_cache_folder":"","thumb_concurrency":"16","video_thumb_pos":"20%","show_hidden":true,"mkdir_perm":"777","recycle_bin_path":"delete permanently"}


mount_path: /nas
driver: Local
status: work
modified: 2026-08-13 11:52:29.535316342+00:00
order_by: name
order_direction: asc
extract_folder: front
webdav_policy: native_proxy
addition:
  {"root_folder_path":"/local/nas","directory_size":false,"thumbnail":false,"thumb_cache_folder":"","thumb_concurrency":"16","video_thumb_pos":"20%","show_hidden":true,"mkdir_perm":"777","recycle_bin_path":"delete permanently"}


mount_path: /baidunetdisk
driver: BaiduNetdisk
status: work
modified: 2026-08-13 11:40:52.495023462+00:00
extract_folder: front
webdav_policy: native_proxy
addition:
  {"root_folder_path":"/","order_by":"name","order_direction":"asc","download_api":"official","use_online_api":true,"api_url_address":"https://api.oplist.org/baiduyun/renewapi","client_id":"","client_secret":"","custom_crack_ua":"netdisk","AccessToken":"121.f54397fe2bf4c245f1351ddfecffd71c.YDfe2LHt6CKosz3SaKSHpLGxLJre2mbfiknhXCQ.xhEcow","refresh_token":"122.3ad2da1407bd620cdc8d46caa1488120.Yll7qZmIA7fGDlYeIk3onTx0_YI3axGfYmV9azL.2ghLcw","upload_thread":"3","upload_timeout":60,"upload_api":"https://d.pcs.baidu.com","use_dynamic_upload_api":true,"custom_upload_part_size":0,"low_bandwith_upload_mode":false,"only_list_video_file":false}


mount_path: /xunlei
driver: Thunder
status: work
modified: 2026-08-13 11:45:55.952163941+00:00
order_by: name
order_direction: asc
extract_folder: front
webdav_policy: 302_redirect
addition:
  {"root_folder_id":"","username":"18116521688","password":"200184LMQ","captcha_token":"ck0.VaX0tp2wTrL-3_yqAkXdadMkzthEQCOdYw9wDbQy9MGlrmTjR3gzqqYMkGKN2jlmksn8H8byrmmWW0yBx4lHQ_jCJcYkqlsCAtGGr7Nw4En2se9NCTssaS0Z7AYtXlg14k1hzRXyNMugaehxFAi6qVtPeyJKKjae74eD3v0V_7UG4tdgUfN3Fm_Beg1k23id-NhZs63zsFmfPHj9cDvaZ90vLJ6DbaD6SsS6UqIyUVQsuTG3pJ5qLG9MIkd-EeON7ijdVYLJYlMpcZ5dMFUdXEPxb7fkT1a6ONy6guEFmQY.CmUIn5HG1_8zEhBYcDZ2c3h6XzdJWVZ3MkJCGgs4LjMxLjAuOTcyNiIbY29tLnh1bmxlaS5kb3dubG9hZHByb3ZpZGVyKiBiNzViZDQ3NDI5NDZkNTA1MWYwNzI0MjhhNTAwNDBjZBKAAat5kzFhePWIeB_65fIM1mkuOOA6GybeRe0h22MIzzkq0G49hayawzoPjk1ziPHS9MT7_8RY6RMHi0cADIsgvUaUfgxUeUfoVuaMZ_N4nR7FeLwPma1LvT5GuRyVf6hGbSIiFFGj17zre3QkR01L-iZH7PkON2CGH_kA9xoz9XL4","credit_key":"","device_id":"b75bd4742946d5051f072428a50040cd","space":""}


# x_setting_items

key: text_types
type: text
value:
  txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm


key: iframe_previews
type: text
value:
  {
  	"doc,docx,xls,xlsx,ppt,pptx": {
  		"Microsoft":"https://view.officeapps.live.com/op/view.aspx?src=$e_url",
  		"Google":"https://docs.google.com/gview?url=$e_url&embedded=true"
  	},
  	"pdf": {
  		"PDF.js":"https://res.oplist.org/pdf.js/web/viewer.html?file=$e_url"
  	},
  	"epub": {
  		"EPUB.js":"https://res.oplist.org/epub.js/viewer.html?url=$e_url"
  	}
  }


key: customize_head
type: text
value:
  <script>
  (() => {
      "use strict";

      const LOAD_FLAG =
          "__OPENLIST_COPYPARTY_LOADED__";

      if (window[LOAD_FLAG]) {
          return;
      }

      window[LOAD_FLAG] = true;

      const CONFIG = {
          version: "2026-08-13.12",

          /*
           * OpenList Web 路径到 Copyparty Web 路径的映射。
           *
           * /local -> 宿主机 /home/mickey     -> Copyparty /
           * /nas   -> 宿主机 /home/mickey/nas -> Copyparty /nas
           */
          pathMappings: [
              {
                  openlistPrefix: "/local",
                  copypartyPrefix: "/"
              },
              {
                  openlistPrefix: "/nas",
                  copypartyPrefix: "/nas"
              }
          ],

          // Copyparty 对外端口
          copypartyPort: "2100",

          /*
           * 可选：按 OpenList 的完整 Origin 指定 Copyparty 反代地址。
           * Origin 必须包含协议；非标准端口也必须写在这里。
           * 未匹配时自动回退为 当前协议://当前主机:2100。
           *
           * 示例：
           * "https://openlist.example.com":
           *     "https://download.example.com",
           * "https://openlist.example.com:8443":
           *     "https://download.example.com:9443"
           */
          copypartyBaseByOpenListOrigin: {
              "https://openlist-speedtest.mingqian.space:1443":
                  "https://copyparty-speedtest.mingqian.space:1443",
              "https://openlist-speedtest.mingqian.online":
                  "https://copyparty-speedtest.mingqian.online",
          },

          // 是否保留 Console 调试输出
          debug: true
      };

      const FOLDER_SIZE_MARKS = new Set([
          "",
          "-",
          // 中文原文：全角破折号“—” [ZH]
          "\u2014",
          // 中文原文：短破折号“–” [ZH]
          "\u2013"
      ]);

      let lastContextTarget = null;

      function log(...args) {
          if (CONFIG.debug) {
              console.log(
                  "[OpenList-Copyparty]",
                  ...args
              );
          }
      }

      function describeElement(element) {
          if (!(element instanceof Element)) {
              return null;
          }

          return {
              tag: element.tagName,
              id: element.id || "",
              className:
                  typeof element.className === "string"
                      ? element.className
                      : element.getAttribute("class") || "",
              text: (
                  element.textContent || ""
              )
                  .trim()
                  .replace(/\s+/g, " ")
                  .slice(0, 120)
          };
      }

      function getPathMapping(path) {
          return CONFIG.pathMappings.find(
              mapping =>
                  path === mapping.openlistPrefix ||
                  path.startsWith(
                      mapping.openlistPrefix + "/"
                  )
          ) || null;
      }

      function isManagedPath(path) {
          return getPathMapping(path) !== null;
      }

      function normalizeCopypartyBase(value) {
          try {
              const url = new URL(value);

              if (![
                  "http:",
                  "https:"
              ].includes(url.protocol)) {
                  throw new Error(
                      "Only HTTP and HTTPS endpoints are supported"
                  );
              }

              const path = url.pathname
                  .replace(/\/+$/, "");

              return url.origin + path;
          } catch (error) {
              log(
                  "Invalid configured Copyparty base:",
                  value,
                  error
              );

              return null;
          }
      }

      function getCopypartyEndpoint() {
          const openlistOrigin =
              window.location.origin;
          const configured =
              CONFIG.copypartyBaseByOpenListOrigin[
                  openlistOrigin
              ];
          const configuredBase = configured
              ? normalizeCopypartyBase(configured)
              : null;

          if (configuredBase) {
              return {
                  base: configuredBase,
                  source: "origin-map",
                  openlistOrigin,
                  matchedOrigin: openlistOrigin
              };
          }

          return {
              base:
                  window.location.protocol +
                  "//" +
                  window.location.hostname +
                  ":" +
                  CONFIG.copypartyPort,
              source: "same-host-port-fallback",
              openlistOrigin,
              matchedOrigin: null
          };
      }

      function getCopypartyBase() {
          return getCopypartyEndpoint().base;
      }

      function encodePath(path) {
          return path
              .split("/")
              .map(part => encodeURIComponent(part))
              .join("/");
      }

      function getOpenListPath(href) {
          try {
              const url = new URL(
                  href,
                  window.location.origin
              );

              return decodeURIComponent(
                  url.pathname
              );
          } catch (error) {
              log(
                  "Failed to parse OpenList path:",
                  href,
                  error
              );

              return null;
          }
      }

      function getCopypartyRawPath(openlistPath) {
          const mapping =
              getPathMapping(openlistPath);

          if (!mapping) {
              return null;
          }

          const relative = openlistPath.slice(
              mapping.openlistPrefix.length
          );

          const prefix =
              mapping.copypartyPrefix === "/"
                  ? ""
                  : mapping.copypartyPrefix
                      .replace(/\/+$/, "");

          const path =
              prefix + relative;

          return path || "/";
      }

      function getCopypartyPath(openlistPath) {
          const path =
              getCopypartyRawPath(openlistPath);

          return path === null
              ? null
              : encodePath(path);
      }

      function buildCopypartyDownloadUrl(
          openlistPath,
          itemType
      ) {
          let path = getCopypartyPath(
              openlistPath
          );

          if (path === null) {
              return null;
          }

          if (
              itemType === "folder" &&
              !path.endsWith("/")
          ) {
              path += "/";
          }

          return (
              getCopypartyBase() +
              path +
              (
                  itemType === "folder"
                      ? "?zip"
                      : "?dl"
              )
          );
      }

      function normalizeMenuText(element) {
          return (
              element &&
              element.textContent ||
              ""
          )
              .trim()
              .replace(/\s+/g, "");
      }

      function stopOpenListAction(event) {
          event.preventDefault();
          event.stopPropagation();
          event.stopImmediatePropagation();
      }

      function closeOpenListMenus(
          reason,
          sourceElement
      ) {
          /*
           * Hope MenuContent is rendered in a portal and points to its own
           * trigger through aria-labelledby. Capture that relation before
           * the intercepted click finishes, then toggle the exact trigger.
           */
          const hopeMenus = [];
          let content =
              sourceElement instanceof Element
                  ? sourceElement.closest(
                      ".hope-menu__content"
                  )
                  : null;

          while (content) {
              const triggerId = content.getAttribute(
                  "aria-labelledby"
              );
              const trigger = triggerId
                  ? document.getElementById(triggerId)
                  : null;

              hopeMenus.push({
                  content,
                  triggerId,
                  trigger
              });

              content = trigger
                  ? trigger.closest(
                      ".hope-menu__content"
                  )
                  : null;
          }

          const dispatchClick = element => {
              if (!(element instanceof EventTarget)) {
                  return false;
              }

              return element.dispatchEvent(
                  new MouseEvent("click", {
                      view: window,
                      button: 0,
                      bubbles: true,
                      cancelable: true,
                      composed: true
                  })
              );
          };

          const dismiss = phase => {
              const eventOptions = {
                  bubbles: true,
                  cancelable: true,
                  composed: true
              };
              const target =
                  document.activeElement instanceof
                      HTMLElement
                      ? document.activeElement
                      : document.body;

              if (target) {
                  target.dispatchEvent(
                      new KeyboardEvent("keydown", {
                          ...eventOptions,
                          key: "Escape",
                          code: "Escape",
                          keyCode: 27,
                          which: 27
                      })
                  );
                  target.blur();
              }

              if (document.body) {
                  /*
                   * Do not synthesize pointerdown/mousedown here. Some
                   * browser extensions assume those events are trusted and
                   * call getSelection() on a missing frame, polluting the
                   * console with unrelated content-script exceptions.
                   */
                  document.body.dispatchEvent(
                      new MouseEvent("click", {
                          ...eventOptions,
                          button: 0
                      })
                  );
              }

              log("Menu dismissal dispatched:", {
                  reason,
                  phase
              });
          };

          dismiss("immediate");

          setTimeout(() => {
              let triggerClicks = 0;

              for (const menu of hopeMenus) {
                  if (
                      menu.trigger &&
                      menu.trigger.isConnected &&
                      menu.trigger.getAttribute(
                          "aria-expanded"
                      ) !== "false"
                  ) {
                      dispatchClick(menu.trigger);
                      triggerClicks += 1;
                  }
              }

              dismiss("after-click");
              log("Associated Hope menu triggers closed:", {
                  reason,
                  menuCount: hopeMenus.length,
                  triggerClicks,
                  menus: hopeMenus.map(menu => ({
                      triggerId: menu.triggerId,
                      triggerTag:
                          menu.trigger &&
                          menu.trigger.tagName,
                      triggerClass:
                          menu.trigger &&
                          menu.trigger.getAttribute &&
                          menu.trigger.getAttribute("class"),
                      expanded:
                          menu.trigger &&
                          menu.trigger.getAttribute(
                              "aria-expanded"
                          )
                  }))
              });
          }, 0);

          setTimeout(() => {
              let completedTransitions = 0;

              for (const menu of hopeMenus) {
                  if (
                      !menu.content.isConnected ||
                      !menu.trigger
                  ) {
                      continue;
                  }

                  if (
                      menu.trigger.getAttribute(
                          "aria-expanded"
                      ) === "true"
                  ) {
                      dispatchClick(menu.trigger);
                  }

                  menu.content.dispatchEvent(
                      new Event("transitionend", {
                          bubbles: true
                      })
                  );
                  menu.content.dispatchEvent(
                      new Event("animationend", {
                          bubbles: true
                      })
                  );
                  completedTransitions += 1;
              }

              dismiss("hope-transition-fallback");
              log("Hope menu transitions completed:", {
                  reason,
                  count: completedTransitions
              });
          }, 120);
      }

      function getListItemAnchor(element) {
          const anchor = element.closest(
              "a[href]"
          );

          if (!anchor) {
              return null;
          }

          const href =
              anchor.getAttribute("href");
          const path = href
              ? getOpenListPath(href)
              : null;

          /*
           * OpenList 使用 CSS Modules，版本升级后列表项的 class 名
           * 可能改变，不能只依赖 visselect-item/listitem。受管路径负责
           * 排除设置、登录等普通链接，行内结构负责确认它是文件列表项。
           */
          const hasKnownClass =
              anchor.classList.contains(
                  "visselect-item"
              ) ||
              anchor.classList.contains(
                  "listitem"
              );
          const hasItemStructure = Boolean(
              anchor.querySelector("p.name") ||
              anchor.querySelector("p.size") ||
              anchor.querySelector(
                  'input[type="checkbox"]'
              ) ||
              anchor.querySelector(
                  '[class*="name-box"]'
              ) ||
              anchor.querySelector(
                  '[class*="checkbox"]'
              )
          );

          if (
              !path ||
              !isManagedPath(path) ||
              (!hasKnownClass && !hasItemStructure)
          ) {
              return null;
          }

          return anchor;
      }

      function inspectAnchor(anchor) {
          if (!(anchor instanceof Element)) {
              return null;
          }

          const href =
              anchor.getAttribute("href");
          const path = href
              ? getOpenListPath(href)
              : null;
          const checkbox = anchor.querySelector(
              'input[type="checkbox"]'
          );

          return {
              href,
              path,
              managed: Boolean(
                  path && isManagedPath(path)
              ),
              className:
                  anchor.getAttribute("class") || "",
              hasName: Boolean(
                  anchor.querySelector("p.name")
              ),
              hasSize: Boolean(
                  anchor.querySelector("p.size")
              ),
              hasCheckbox: Boolean(checkbox),
              checkboxChecked: Boolean(
                  checkbox && checkbox.checked
              ),
              checkboxDataChecked:
                  checkbox &&
                  checkbox.getAttribute(
                      "data-checked"
                  ),
              accepted: Boolean(
                  getListItemAnchor(anchor)
              ),
              type: getItemType(anchor)
          };
      }

      /*
       * 根据实际调试 DOM 判断类型：
       *
       * 文件夹：
       * p.size = "-"，SVG viewBox = "0 0 512 512"
       *
       * 文件：
       * p.size = "50.00M" 等真实大小，
       * SVG viewBox = "0 0 16 16"
       */
      function getItemType(anchor) {
          if (!anchor) {
              return "unknown";
          }

          const sizeElement =
              anchor.querySelector("p.size");

          const sizeText = sizeElement
              ? (
                  sizeElement.textContent || ""
              ).trim()
              : null;

          let hasFolderIcon = false;
          let hasFileIcon = false;

          for (
              const icon of
              anchor.querySelectorAll("svg")
          ) {
              const viewBox = (
                  icon.getAttribute("viewBox") ||
                  ""
              )
                  .trim()
                  .replace(/\s+/g, " ");

              if (viewBox === "0 0 512 512") {
                  hasFolderIcon = true;
              }

              if (viewBox === "0 0 16 16") {
                  hasFileIcon = true;
              }
          }

          log(
              "Item type detected:",
              {
                  href:
                      anchor.getAttribute("href"),
                  sizeText,
                  hasFolderIcon,
                  hasFileIcon
              }
          );

          if (
              sizeText !== null &&
              !FOLDER_SIZE_MARKS.has(sizeText)
          ) {
              return "file";
          }

          if (
              hasFolderIcon &&
              sizeText !== null &&
              FOLDER_SIZE_MARKS.has(sizeText)
          ) {
              return "folder";
          }

          if (hasFileIcon) {
              return "file";
          }

          if (hasFolderIcon) {
              return "folder";
          }

          if (
              sizeText !== null &&
              FOLDER_SIZE_MARKS.has(sizeText)
          ) {
              return "folder";
          }

          return "unknown";
      }

      function getSelectedItems() {
          const result = [];
          const seenPaths = new Set();

          const candidates = new Set();
          const checkedSelectors = [
              'input[type="checkbox"]:checked',
              'input[type="checkbox"][data-checked]',
              'label[data-checked] input[type="checkbox"]',
              '[role="checkbox"][aria-checked="true"] input[type="checkbox"]',
              'a[href].selected',
              'a[href][aria-selected="true"]'
          ];

          for (const selector of checkedSelectors) {
              for (
                  const element of
                  document.querySelectorAll(selector)
              ) {
                  candidates.add(element);
              }
          }

          for (const candidate of candidates) {
              const anchor =
                  getListItemAnchor(candidate);

              if (!anchor) {
                  continue;
              }

              const href =
                  anchor.getAttribute("href");

              const path = href
                  ? getOpenListPath(href)
                  : null;

              if (
                  !path ||
                  seenPaths.has(path)
              ) {
                  continue;
              }

              seenPaths.add(path);

              result.push({
                  anchor,
                  href: path,
                  type: getItemType(anchor)
              });
          }

          log(
              "Selected items:",
              {
                  candidateCount:
                      candidates.size,
                  items: result.map(item => ({
                      href: item.href,
                      type: item.type
                  }))
              }
          );

          return result;
      }

      function getSimpleItem(item) {
          if (!item) {
              return null;
          }

          return {
              href: item.href,
              type: item.type,
              copypartyPath:
                  getCopypartyRawPath(item.href)
          };
      }

      function showNotice(
          message,
          isError = false
      ) {
          const old = document.getElementById(
              "openlist-copyparty-notice"
          );

          if (old) {
              old.remove();
          }

          const notice =
              document.createElement("div");

          notice.id =
              "openlist-copyparty-notice";

          notice.textContent = message;
          notice.setAttribute("role", "status");

          Object.assign(
              notice.style,
              {
                  position: "fixed",
                  top: "24px",
                  left: "50%",
                  transform: "translateX(-50%)",
                  zIndex: "99999999",
                  padding: "10px 18px",
                  borderRadius: "8px",
                  background: isError
                      ? "rgba(190,35,35,0.92)"
                      : "rgba(0,0,0,0.82)",
                  color: "#fff",
                  fontSize: "14px",
                  fontWeight: "500",
                  boxShadow:
                      "0 4px 16px rgba(0,0,0,.25)",
                  pointerEvents: "none",
                  transition: "opacity .2s ease"
              }
          );

          document.body.appendChild(notice);

          setTimeout(() => {
              notice.style.opacity = "0";
          }, 1800);

          setTimeout(() => {
              notice.remove();
          }, 2100);
      }

      function startSingleDownload(
          event,
          item
      ) {
          const validation = {
              hasItem: Boolean(item),
              href: item && item.href,
              type: item && item.type,
              managedPath: Boolean(
                  item &&
                  isManagedPath(item.href)
              ),
              supportedType: Boolean(
                  item &&
                  [
                      "file",
                      "folder"
                  ].includes(item.type)
              )
          };

          log(
              "Single-download validation:",
              validation
          );

          if (
              !validation.hasItem ||
              !validation.managedPath ||
              !validation.supportedType
          ) {
              log(
                  "Single download not intercepted; check target and item type"
              );

              return false;
          }

          const downloadUrl =
              buildCopypartyDownloadUrl(
                  item.href,
                  item.type
              );

          if (!downloadUrl) {
              log(
                  "Failed to build Copyparty URL:",
                  getSimpleItem(item)
              );

              return false;
          }

          stopOpenListAction(event);
          closeOpenListMenus(
              "single-download",
              event.target
          );

          // 中文原文：文件夹“已调用 Copyparty 打包文件夹”；文件“已调用 Copyparty 下载文件”。[ZH]
          showNotice(
              item.type === "folder"
                  ? "\u5df2\u8c03\u7528 Copyparty \u6253\u5305\u6587\u4ef6\u5939"
                  : "\u5df2\u8c03\u7528 Copyparty \u4e0b\u8f7d\u6587\u4ef6"
          );

          log(
              "Copyparty single download:",
              {
                  href: item.href,
                  type: item.type,
                  url: downloadUrl
              }
          );

          /*
           * 必须在用户点击事件内立即发起下载。延迟跳转会丢失
           * user-activation，并给 OpenList 原生下载留下抢先执行的机会。
           */
          window.location.assign(downloadUrl);

          return true;
      }

      function rememberContextTarget(
          event,
          source
      ) {
          const element = event.target;

          if (!(element instanceof Element)) {
              log(
                  `${source}: event.target is not an Element`
              );
              return false;
          }

          const rawAnchor =
              element.closest("a[href]");

          const anchor =
              getListItemAnchor(element);

          log(
              `${source} target diagnosis:`,
              {
                  button: event.button,
                  target: describeElement(element),
                  closestHref:
                      rawAnchor &&
                      rawAnchor.getAttribute("href"),
                  acceptedAnchor: Boolean(anchor),
                  previousTarget:
                      getSimpleItem(lastContextTarget)
              }
          );

          /*
           * 级联菜单本身也可能产生 contextmenu 事件。
           * 这里保留之前的有效文件目标，不能将其清空。
           */
          if (!anchor) {
              return false;
          }

          const href =
              anchor.getAttribute("href");

          const path = href
              ? getOpenListPath(href)
              : null;

          if (!path) {
              log(
                  `${source}: failed to parse href`,
                  href
              );
              return false;
          }

          lastContextTarget = {
              anchor,
              href: path,
              type: getItemType(anchor)
          };

          log(
              `${source} target remembered:`,
              getSimpleItem(lastContextTarget)
          );

          return true;
      }

      function resolveContextDownloadTarget() {
          if (
              lastContextTarget &&
              isManagedPath(
                  lastContextTarget.href
              ) &&
              [
                  "file",
                  "folder"
              ].includes(lastContextTarget.type)
          ) {
              return lastContextTarget;
          }

          const selectedItems =
              getSelectedItems();

          if (selectedItems.length === 1) {
              log(
                  "Using the only selected item as context target:",
                  getSimpleItem(selectedItems[0])
              );

              return selectedItems[0];
          }

          log(
              "Unable to resolve context-menu target:",
              {
                  lastContextTarget:
                      getSimpleItem(
                          lastContextTarget
                      ),
                  selectedItems:
                      selectedItems.map(
                          getSimpleItem
                      )
              }
          );

          return null;
      }

      function getCommonDirectory(paths) {
          const parentParts = paths.map(path => {
              const parts = path
                  .split("/")
                  .filter(Boolean);

              return parts.slice(0, -1);
          });

          const common = parentParts[0]
              ? [...parentParts[0]]
              : [];

          while (common.length) {
              const matches = parentParts.every(
                  parts => common.every(
                      (part, index) =>
                          parts[index] === part
                  )
              );

              if (matches) {
                  break;
              }

              common.pop();
          }

          return common;
      }

      /*
       * 完全采用 Copyparty 自身 Web UI 的多选 ZIP 协议：
       * multipart/form-data + act=zip + files=换行分隔的 URL 编码路径。
       */
      function submitCopypartySelection(items) {
          const relativePaths = items.map(item =>
              getCopypartyRawPath(item.href)
                  .replace(/^\/+/, "")
          );

          if (relativePaths.some(path => !path)) {
              throw new Error(
                  "Storage root cannot be packaged with other items"
              );
          }

          const commonDirectory =
              getCommonDirectory(relativePaths);

          const selectedPaths = relativePaths.map(
              path => {
                  const parts = path
                      .split("/")
                      .filter(Boolean)
                      .slice(commonDirectory.length);

                  return parts
                      .map(part =>
                          encodeURIComponent(part)
                      )
                      .join("/");
              }
          );

          let actionPath =
              "/" + commonDirectory.join("/");

          actionPath = encodePath(actionPath);

          if (!actionPath.endsWith("/")) {
              actionPath += "/";
          }

          const form =
              document.createElement("form");

          form.method = "post";
          /*
           * 不使用 _blank：部分 Chromium 构建会把新下载上下文的
           * POST Origin 序列化为 null，触发 Copyparty 的 CSRF/CORS
           * 防护。附件响应不会替换当前 OpenList 页面。
           */
          form.target = "_self";
          form.enctype = "multipart/form-data";
          form.action =
              getCopypartyBase() +
              actionPath +
              "?zip";
          form.style.display = "none";

          const actionInput =
              document.createElement("input");

          actionInput.name = "act";
          actionInput.value = "zip";

          const filesInput =
              document.createElement("textarea");

          filesInput.name = "files";
          filesInput.value =
              selectedPaths.join("\n");

          form.append(
              actionInput,
              filesInput
          );

          document.body.appendChild(form);

          log(
              "Copyparty multi-select ZIP form:",
              {
                  action: form.action,
                  files: selectedPaths
              }
          );

          form.submit();

          setTimeout(() => {
              form.remove();
          }, 1000);
      }

      function startPackageDownload(
          event,
          items
      ) {
          if (!items.length) {
              log(
                  "No file-list items selected; preserving OpenList behavior"
              );

              return false;
          }

          const unsupportedItem = items.find(
              item =>
                  !isManagedPath(item.href) ||
                  ![
                      "file",
                      "folder"
                  ].includes(item.type)
          );

          if (unsupportedItem) {
              log(
                  "Selection contains unmanaged paths or unknown item types:",
                  unsupportedItem
              );

              return false;
          }

          if (
              items.length === 1 &&
              items[0].type === "folder"
          ) {
              return startSingleDownload(
                  event,
                  items[0]
              );
          }

          stopOpenListAction(event);
          closeOpenListMenus(
              "multi-select-download",
              event.target
          );

          try {
              submitCopypartySelection(items);

              // 中文原文：已调用 Copyparty 打包 N 个项目。[ZH]
              showNotice(
                  `\u5df2\u8c03\u7528 Copyparty \u6253\u5305 ${items.length} \u4e2a\u9879\u76ee`
              );

              return true;
          } catch (error) {
              log("Copyparty multi-select ZIP failed:", error);

              // 中文原文：创建打包下载失败。[ZH]
              showNotice(
                  "\u521b\u5efa\u6253\u5305\u4e0b\u8f7d\u5931\u8d25",
                  true
              );

              return false;
          }
      }

      document.addEventListener(
          "pointerdown",
          event => {
              if (event.button === 2) {
                  rememberContextTarget(
                      event,
                      "pointerdown"
                  );
              }
          },
          true
      );

      document.addEventListener(
          "contextmenu",
          event => {
              rememberContextTarget(
                  event,
                  "contextmenu"
              );
          },
          true
      );

      /*
       * 必须监听 window 的捕获阶段。OpenList 的 Solid 菜单使用委托
       * 点击处理；若本处理器也挂在 document，即使调用
       * stopImmediatePropagation，也可能排在已注册的原生处理器之后。
       * window capture 会在事件到达 document 前完成 Copyparty 接管。
       */
      window.addEventListener(
          "click",
          event => {
              const element = event.target;

              if (!(element instanceof Element)) {
                  return;
              }

              const topMenuText = element.closest(
                  ".hope-menu__item__text"
              );

              const contextMenuContent =
                  element.closest(
                      ".solid-contextmenu__item__content"
                  );

              // 中文原文：打包下载（同时匹配顶部菜单和右键子菜单）。[ZH]
              const isPackageDownload =
                  normalizeMenuText(topMenuText) ===
                      "\u6253\u5305\u4e0b\u8f7d" ||
                  normalizeMenuText(
                      contextMenuContent
                  ) === "\u6253\u5305\u4e0b\u8f7d";

              if (isPackageDownload) {
                  log(
                      "Package-download action captured:",
                      contextMenuContent
                          ? "context submenu"
                          : "top menu"
                  );

                  const selectedItems =
                      getSelectedItems();

                  log(
                      "Package-download arguments:",
                      selectedItems.map(
                          getSimpleItem
                      )
                  );

                  const intercepted =
                      startPackageDownload(
                      event,
                      selectedItems
                  );

                  log(
                      "Package-download interception result:",
                      {
                          intercepted,
                          eventPhase:
                              event.eventPhase,
                          currentTarget:
                              event.currentTarget === window
                                  ? "window"
                                  : "other",
                          defaultPrevented:
                              event.defaultPrevented
                      }
                  );

                  return;
              }

              if (!contextMenuContent) {
                  return;
              }

              // 中文原文：下载。[ZH]
              if (
                  normalizeMenuText(
                      contextMenuContent
                  ) !== "\u4e0b\u8f7d"
              ) {
                  return;
              }

              const target =
                  resolveContextDownloadTarget();

              log(
                  "Context-menu download captured; target:",
                  getSimpleItem(target)
              );

              const intercepted =
                  startSingleDownload(
                  event,
                  target
              );

              log(
                  "Context-download interception result:",
                  {
                      intercepted,
                      eventPhase:
                          event.eventPhase,
                      currentTarget:
                          event.currentTarget === window
                              ? "window"
                              : "other",
                      defaultPrevented:
                          event.defaultPrevented
                  }
              );
          },
          true
      );

      window.OpenListCopypartyDebug = {
          config: CONFIG,

          state() {
              const state = {
                  version: CONFIG.version,
                  pageOrigin:
                      window.location.origin,
                  copypartyBase:
                      getCopypartyBase(),
                  copypartyEndpoint:
                      getCopypartyEndpoint(),
                  lastContextTarget:
                      getSimpleItem(
                          lastContextTarget
                      ),
                  selectedItems:
                      getSelectedItems().map(
                          getSimpleItem
                      )
              };

              console.log(
                  "[OpenList-Copyparty] Debug state",
                  state
              );

              return state;
          },

          url(openlistPath, type = "folder") {
              const result =
                  buildCopypartyDownloadUrl(
                      openlistPath,
                      type
                  );

              console.log(
                  "[OpenList-Copyparty] URL test",
                  {
                      openlistPath,
                      type,
                      result
                  }
              );

              return result;
          },

          scan() {
              const items = Array.from(
                  document.querySelectorAll(
                      "a[href]"
                  )
              )
                  .map(inspectAnchor)
                  .filter(item => item.managed);

              console.table(items);
              console.log(
                  "[OpenList-Copyparty] Page item scan:",
                  items
              );

              return items;
          },

          async probe() {
              const url =
                  getCopypartyBase() + "/?ls";

              console.log(
                  "[OpenList-Copyparty] Connectivity test started",
                  url
              );

              try {
                  const response = await fetch(
                      url,
                      {
                          method: "GET",
                          mode: "cors",
                          cache: "no-store"
                      }
                  );

                  const result = {
                      ok: response.ok,
                      status: response.status,
                      statusText:
                          response.statusText,
                      contentType:
                          response.headers.get(
                              "content-type"
                          )
                  };

                  console.log(
                      "[OpenList-Copyparty] Connectivity test result",
                      result
                  );

                  return result;
              } catch (error) {
                  console.error(
                      "[OpenList-Copyparty] Connectivity test failed",
                      error
                  );

                  throw error;
              }
          }
      };

      // 中文原文：OpenList 调用 Copyparty 下载脚本已加载。[ZH]
      log(
          "OpenList \u8c03\u7528 Copyparty \u4e0b\u8f7d\u811a\u672c\u5df2\u52a0\u8f7d",
          {
              version: CONFIG.version,
              pathMappings:
                  CONFIG.pathMappings,
              copypartyBase:
                  getCopypartyBase(),
              copypartyEndpoint:
                  getCopypartyEndpoint()
          }
      );
  })();
  </script>


key: privacy_regs
type: text
value:
  (?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])
  ([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)
  (?U)access_token=(.*)&


key: share_summary_content
type: text
value:
  @{{creator}} shared {{#each files}}{{#if @first}}"{{filename this}}"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}


key: ignore_system_files
value: false
type: bool
help:
  When enabled, ignores common system files during upload (.DS_Store, desktop.ini, Thumbs.db, and files starting with ._)


key: token
type: string
value:
  openlist-888617d6-2685-470e-a7c4-bfa9ec64336deTmcBV7Y1ipFD7lKMryF7Stctc8e8S9KmBU9sEHsDqc1EMafgMQ9fMqHxCoo3a1z


key: multipart_chunk_size
value: 10
type: number
help:
  chunk size of multipart upload in MB (positive integer), keep it under your CDN's request body limit; each active session buffers up to 8 chunks on the server's disk


