Mod Documentations Utility by sylin527

Help to save the mod documentations to local disk. Simplify mod page, files tab, posts tab, forum tab, article page. Download mod gallery. ($browser_download_path\$game\$mod_version_date) Show requirements, changelogs, file descriptions and spoilers, replace thumbnails to original, replace embedded YouTube videos to links, remove unnecessary contents. After saving those pages by SingleFile, you can show/hide requirements, changelogs, spoilers, real file names downloaded, etc.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Mod Documentations Utility by sylin527
// @description Help to save the mod documentations to local disk.    Simplify mod page, files tab, posts tab, forum tab, article page.    Download mod gallery. ($browser_download_path\$game\$mod_version_date)    Show requirements, changelogs, file descriptions and spoilers, replace thumbnails to original, replace embedded YouTube videos to links, remove unnecessary contents.    After saving those pages by SingleFile, you can show/hide requirements, changelogs, spoilers, real file names downloaded, etc.
// @version 0.2.6.20260430
// @author sylin527
// @namespace https://www.nexusmods.com
// @runAt document-idle
// @match https://www.nexusmods.com/*/mods/*
// @match https://www.nexusmods.com/*/articles/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_download
// @grant unsafeWindow
// @icon https://www.nexusmods.com/favicon.ico
// @license GPL-3.0
// ==/UserScript==
//#region src/website.ts
const websiteUrl = "https://www.nexusmods.com";
const apiV1Url = "https://api.nexusmods.com/v1";

//#endregion
//#region src/api/mod_api.ts
/**
* @param gameDomainName 如: `skyrimspecialedition`
* @param modId
* @param apikey
* @returns
*/
async function getFiles(gameDomainName, modId, apiKey) {
	return await (await fetch(`${apiV1Url}/games/${gameDomainName}/mods/${modId}/files.json`, { headers: { apikey: apiKey } })).json();
}
function generateModUrl(gameDomainName, modId) {
	return `${websiteUrl}/${gameDomainName}/mods/${modId}`;
}
function generateFileUrl(gameDomainName, modId, fileId) {
	return `${websiteUrl}/${gameDomainName}/mods/${modId}?tab=files&file_id=${fileId}`;
}
const widgetsUrl = `${websiteUrl}/Core/Libs/Common/Widgets`;

//#endregion
//#region src/site_shared.ts
/**
* `header#head`
* @returns
*/
function getHeader() {
	return document.getElementById("head");
}
/**
* `div#mainContent`
* @returns
*/
function getMainContentDiv() {
	return document.getElementById("mainContent");
}
/**
* base info + description tab
*
* Contains game id and mod id.
* 在 mod url, 有 `<section id="section" class="modpage" data-game-id="1704" data-mod-id="1089">`
* 在 nexusmods url, 有 `<section class="static homeindex">`
*/
let _section = null;
function getSection() {
	!_section && (_section = getMainContentDiv().querySelector(":scope > section"));
	return _section;
}
/**
* 比如 mod, article 的标题 div
*
* `div#pagetitle`
*/
let _pageTitleDiv = null;
function getPageTitleDiv() {
	_pageTitleDiv ||= _pageTitleDiv = document.getElementById("pagetitle");
	return _pageTitleDiv;
}
/**
* 比如 mod, article 的标题
*
* `div#pagetitle > h1`
*/
function getPageTitle() {
	return getPageTitleDiv().querySelector(":scope > h1").innerText;
}
/**
* 比如 mod, article 的 endorse 容器
*
* `div#pagetitle > ul.modactions`
*/
let _modActionsUl = null;
function getModActionsUl() {
	_modActionsUl ||= _modActionsUl = getPageTitleDiv().querySelector(":scope > ul.modactions");
	return _modActionsUl;
}
function getCommentContainerDiv() {
	return document.getElementById("comment-container");
}
function getCommentContainerComponent(commentContainerDiv = getCommentContainerDiv()) {
	if (!commentContainerDiv) return null;
	const headNavDiv = commentContainerDiv.querySelector(":scope > div.head-nav");
	const bottomNavDiv = commentContainerDiv.querySelector(":scope > div.bottom-nav");
	const allCommentLis = commentContainerDiv.querySelectorAll(":scope > ol > li.comment");
	const stickyCommentLis = [];
	const authorCommentLis = [];
	const otherCommentLis = [];
	for (const commentLi of allCommentLis) {
		const classList = commentLi.classList;
		if (classList.contains("comment-sticky")) stickyCommentLis.push(commentLi);
		else if (classList.contains("comment-author")) authorCommentLis.push(commentLi);
		else otherCommentLis.push(commentLi);
	}
	return {
		commentContainerDiv,
		get commentCount() {
			return parseInt(document.getElementById("comment-count").getAttribute("data-comment-count"));
		},
		headNavDiv,
		bottomNavDiv,
		stickyCommentLis,
		authorCommentLis,
		otherCommentLis
	};
}
function getCommentContentTextDiv(commentLi) {
	return commentLi.querySelector(":scope > div.comment-content > div.comment-content-text");
}
/**
* `footer#rj-footer`
*/
function getFooter() {
	return document.getElementById("rj-footer");
}
function getBackToTop() {
	return document.getElementById("rj-back-to-top");
}

//#endregion
//#region src/ui.ts
const { body: bodyElement, head: headElement } = document;
/**
* title element cache
*/
const titleElement = headElement.querySelector("title");
const primaryColor = "#8197ec";
const primaryHoverColor = "#a4b7ff";
const highlightColor = "#d98f40";
const highlightHoverColor = "#ce7f45";
const containerManager = {
	containers: [],
	removeAll() {
		this.containers.forEach(({ element }) => element.remove());
	},
	showAll() {
		this.containers.forEach((container) => container.show());
	},
	hideAll() {
		this.containers.forEach((container) => container.hide());
	},
	add(container) {
		this.containers.push(container);
	},
	addBlock(element) {
		this.containers.push({
			element,
			show: () => element.style.display = "block",
			hide: () => element.style.display = "none"
		});
	},
	addInline(element) {
		this.containers.push({
			element,
			show: () => element.style.display = "inline",
			hide: () => element.style.display = "none"
		});
	}
};
function getActionContainerId() {
	return "sylin527ActionContainer";
}
function insertActionContainerStyle() {
	const newStyle = document.createElement("style");
	headElement.appendChild(newStyle);
	const sheet = newStyle.sheet;
	const containerId = getActionContainerId();
	/**
	* 设 `top: 56px` 是因 Mod page 的 `<header>` 的 `height: 56px`
	* 设 `background: transparent;` 以避免突兀
	*/
	let ruleIndex = sheet.insertRule(`
#${containerId} {
  display: block;
  position: fixed;
  right: 5px;
  top: 56px;
  font-size: 13px;
  font-weight: 400;
  background: transparent;
  z-index: 999;
  direction: rtl;
}
`);
	sheet.insertRule(`
#${containerId} div.action-container {
  height: 42px;
  padding: 3px 0;
}
    `, ++ruleIndex);
	sheet.insertRule(`
#${containerId} button.action-btn {
  padding: 8px;
  height: 36px;
  font-size: 13px;
  margin: 6px 0px;
  line-height: unset;
  display: inline-block;
}
    `, ++ruleIndex);
	sheet.insertRule(`
#${containerId} span.action-msg {
  background-color: rgba(51, 51, 51, 0.5);
  color: rgb(255, 47, 151);
  padding: 8px;
  border-radius: 4px;
  display: inline-block;
  margin: 0 7px;
  visibility: hidden;
}
`, ++ruleIndex);
}
function createActionContainer() {
	const containerId = getActionContainerId();
	let container = document.getElementById(containerId);
	if (null === container) {
		container = document.createElement("div");
		container.setAttribute("id", containerId);
		container.style.zIndex = "999";
		insertActionContainerStyle();
	}
	return container;
}
let _actionContainer = null;
function insertActionContainer() {
	if (!_actionContainer) {
		_actionContainer = createActionContainer();
		bodyElement.append(_actionContainer);
		containerManager.addBlock(_actionContainer);
	}
	return _actionContainer;
}
function showActionContainer() {
	if (_actionContainer) _actionContainer.style.display = "block";
}
function createActionComponent(name$1) {
	const containerDiv = document.createElement("div");
	containerDiv.className = "action-container";
	const actionButton = document.createElement("button");
	actionButton.innerText = name$1;
	actionButton.className = "rj-btn-secondary action-btn";
	containerDiv.append(actionButton);
	return {
		element: containerDiv,
		actionButton
	};
}
function createActionWithMessageComponent(name$1) {
	const { element, actionButton } = createActionComponent(name$1);
	const messageSpan = document.createElement("span");
	messageSpan.className = "action-msg";
	element.append(actionButton, messageSpan);
	return {
		element,
		actionButton,
		messageSpan
	};
}

//#endregion
//#region src/mod_page/tabs_shared.ts
/**
* 不描述 description, files, images 等 tab 专有的信息.
* 仅描述 tabs shared 的信息, 如 game name, mod name, version, gallery 等等.
*/
function getModUrlRegExp() {
	return /^((https|http):\/\/(www.)?nexusmods.com\/[a-z0-9]+\/mods\/[0-9]+)/;
}
function isModUrl(url) {
	return getModUrlRegExp().test(url);
}
let _gameDomainName = null;
function getGameDomainName() {
	_gameDomainName ||= _gameDomainName = new URL(location.href).pathname.split("/")[1];
	return _gameDomainName;
}
let _modId = null;
function getModId() {
	_modId ||= _modId = parseInt(getSection().getAttribute("data-mod-id"));
	return _modId;
}
function getFeaturedBelowDiv() {
	return getSection().querySelector(":scope > div.wrap > div:nth-of-type(2).wrap");
}
let _breadcrumbUl = null;
function getBreadcrumbUl() {
	_breadcrumbUl ||= _breadcrumbUl = document.getElementById("breadcrumb");
	return _breadcrumbUl;
}
let _gameName = null;
function getGameName() {
	_gameName ||= getBreadcrumbUl().querySelector(":scope > li:nth-of-type(2)").innerText;
	return _gameName;
}
/**
* `div#feature`
*
* 如果 modder 设定了 feature, 则有 `div#feature`,
* 反之没有 `div#feature`, 有 `div#nofeature`
*/
let _featureDiv = null;
function getFeatureDiv() {
	_featureDiv ||= _featureDiv = document.getElementById("feature");
	return _featureDiv;
}
function getModStatsUl() {
	return getPageTitleDiv().querySelector(":scope > ul.stats");
}
function getModActionsComponent() {
	const modActionsUl = getModActionsUl();
	return {
		element: modActionsUl,
		get addMediaLi() {
			return document.getElementById("action-media");
		},
		get trackLi() {
			return modActionsUl.querySelector(":scope > li[id^=action-track]");
		},
		get untrackLi() {
			return modActionsUl.querySelector(":scope > li[id^=action-untrack]");
		},
		get downloadLabelLi() {
			return modActionsUl.querySelector(":scope > li.dllabel");
		},
		get vortexLi() {
			return document.getElementById("action-nmm");
		},
		get manualDownloadLi() {
			return document.getElementById("action-manual");
		}
	};
}
let _modName = null;
function getModName() {
	if (!_modName) {
		/**
		* 如 `<meta property="og:title" content="Aspens Ablaze">`
		* Aspens Ablaze 是 mod 名
		*/
		const meta = headElement.querySelector(`meta[property="og:title"]`);
		if (meta) _modName = meta.getAttribute("content");
		else _modName = getBreadcrumbUl().querySelector(":scope > li:last-child").innerText;
	}
	return _modName;
}
/**
* `div#pagetitle > ul.stats.clearfix > li.stat-version > div.statitem > div.stat`
*/
let _modVersionDiv = null;
function getModVersionDiv() {
	_modVersionDiv ||= _modVersionDiv = getModStatsUl().querySelector(":scope > li.stat-version > div.statitem > div.stat");
	return _modVersionDiv;
}
/**
* Mod version can be empty string???
*/
let _modVersion = null;
function getModVersion() {
	if (!_modVersion) {
		_modVersion = getModVersionDiv().innerText.trim();
		if (_modVersion !== "" && parseInt(_modVersion).toString() === _modVersion) _modVersion = "v" + _modVersion;
	}
	return _modVersion;
}
function getFileInfoDiv() {
	return document.getElementById("fileinfo");
}
/**
* `div#sidebargallery`
* @returns
*/
function getModGalleryDiv() {
	return document.getElementById("sidebargallery");
}
function getThumbnailGalleryUl() {
	const modGalleryDiv = getModGalleryDiv();
	return modGalleryDiv ? modGalleryDiv.querySelector(":scope > ul.thumbgallery") : null;
}
function getThumbnailComponent(thumbnailLi) {
	return {
		element: thumbnailLi,
		get figure() {
			return thumbnailLi.querySelector(":scope > figure");
		},
		get anchor() {
			return this.figure.querySelector(":scope > a");
		},
		get img() {
			return this.anchor.querySelector(":scope > img");
		},
		originalImageSrc: thumbnailLi.getAttribute("data-src"),
		title: thumbnailLi.getAttribute("data-sub-html"),
		src: thumbnailLi.getAttribute(" data-exthumbimage")
	};
}
let _modVersionWithDate = null;
function getModVersionWithDate() {
	if (!_modVersionWithDate) {
		const dateTimeElement = getFileInfoDiv().querySelector(":scope > div.timestamp:nth-of-type(1) > time");
		const date = new Date(parseInt(dateTimeElement.getAttribute("data-date") + "000"));
		_modVersionWithDate = `${getModVersion()} (${date.getFullYear().toString().substring(2)}.${date.getMonth() + 1}.${date.getDate()})`;
	}
	return _modVersionWithDate;
}
function getTabsDiv() {
	return getFeaturedBelowDiv().querySelector(":scope > div:nth-of-type(2) > div.tabs");
}
let _modTabsUl = null;
function getModTabsUl() {
	_modTabsUl ||= _modTabsUl = getTabsDiv().querySelector(":scope > ul.modtabs");
	return _modTabsUl;
}
/**
* `div.tabcontent.tabcontent-mod-page`
*
* 设 `tabContentDiv` 为 `div.tabcontent.tabcontent-mod-page`
* 切换 tab 时不会刷新 `tabContentDiv`,
* 会修改 `tabContentDiv` 的 `innerHTML`
*/
let _tabContentDiv = null;
function getTabContentDiv() {
	return _tabContentDiv ||= _tabContentDiv = bodyElement.querySelector("div.tabcontent.tabcontent-mod-page");
}
function getCurrentTab() {
	return getModTabsUl().querySelector(":scope > li > a.selected > span.tab-label").innerText.toLowerCase();
}
function getTabFromTabLi(tabLi) {
	return tabLi.querySelector(":scope > a[data-target] > span.tab-label").innerText.toLowerCase();
}
/**
* 用于点击 tab 时, 知道点击了哪个 tab.
*
* 可用于控制对应的 action component 的显示/隐藏.
*
* @param callback
*/
function clickTabLi(callback) {
	const tabLis = getModTabsUl().querySelectorAll(":scope > li[id^=mod-page-tab]");
	for (const tabLi of tabLis) tabLi.addEventListener("click", (event) => {
		callback(getTabFromTabLi(tabLi), event);
	});
}

//#endregion
//#region src/mod_page/files_tab.ts
/**
* https://www.nexusmods.com/skyrimspecialedition/mods/14449?tab=files
* https://www.nexusmods.com/skyrimspecialedition/mods/14449/?tab=files 可能是历史原因, 导致多了一个 '/'
* 可能是历史原因, 甚至有时候切换 tab 时, url 不变...
*/
/**
* 暂且这样判断吧
* @returns
*/
function isFilesTab() {
	return getCurrentTab() === "files" && getModFilesDiv() !== null && getArchivedFilesContainerDiv() === null;
}
/**
* `div#mod_files`
*/
function getModFilesDiv() {
	return document.getElementById("mod_files");
}
/**
* 获取 `div.premium-banner.container`
*/
function getPremiumBannerDiv() {
	return getTabContentDiv().querySelector("div.premium-banner.container");
}
function getAllSortByDivs() {
	const modFilesDiv = getModFilesDiv();
	return modFilesDiv ? modFilesDiv.querySelectorAll("div.file-category-header > div:nth-of-type(1)") : null;
}
function getAllFileHeaderDts() {
	const modFilesDiv = getModFilesDiv();
	return modFilesDiv ? modFilesDiv.querySelectorAll("dl.accordion > dt") : null;
}
function getAllFileDescriptionDds() {
	const modFilesDiv = getModFilesDiv();
	return modFilesDiv ? modFilesDiv.querySelectorAll("dl.accordion > dd") : null;
}
function getDownloadButtonContainerDiv(fileDescriptionDd) {
	return fileDescriptionDd.querySelector("div.tabbed-block:nth-of-type(2)");
}
/**
* @param headerDtOrDescriptionDd header `<dt>` 或 description `<dd>` 都有属性 `data-id`
* @returns
*/
function getFileId(headerDtOrDescriptionDd) {
	return parseInt(headerDtOrDescriptionDd.getAttribute("data-id"));
}
function getFileDescriptionDiv(fileDescriptionDd) {
	return fileDescriptionDd.querySelector("div.files-description");
}
function getFileDescriptionComponent(fileDescriptionDd) {
	const fileId = getFileId(fileDescriptionDd);
	const fileDescriptionDiv = getFileDescriptionDiv(fileDescriptionDd);
	const downloadButtonContainerDiv = getDownloadButtonContainerDiv(fileDescriptionDd);
	const previewFileDiv = fileDescriptionDd.querySelector("div.tabbed-block:last-child");
	const realFilename = previewFileDiv.querySelector("a").getAttribute("data-url");
	downloadButtonContainerDiv.querySelector("ul > li:last-child > a");
	return {
		fileId,
		fileDescriptionDiv,
		downloadButtonContainerDiv,
		previewFileDiv,
		realFilename
	};
}
function getOldFilesComponent() {
	const element = document.getElementById("file-container-old-files");
	if (!element) return null;
	const categoryHeaderDiv = element.querySelector(":scope > div.file-category-header");
	return {
		element,
		categoryHeaderDiv,
		get headerH2() {
			return categoryHeaderDiv.querySelector(":scope > h2:first-child");
		},
		get sortByContainerDiv() {
			return categoryHeaderDiv.querySelector(":scope > div:last-child");
		}
	};
}
function getFileArchiveSection() {
	return document.getElementById("files-tab-footer");
}

//#endregion
//#region src/mod_page/archived_files_tab.ts
/**
* eg. `https://www.nexusmods.com/skyrimspecialedition/mods/2182/?tab=files&category=archived`
*/
function isArchivedFilesUrl(url) {
	const searchParams = new URL(url).searchParams;
	return isModUrl(url) && searchParams.get("tab") === "files" && searchParams.get("category") === "archived";
}
function getArchivedFilesContainerDiv() {
	return document.getElementById("file-container-archived-files");
}
/**
* 暂且这样判断吧
* @returns
*/
function isArchivedFilesTab() {
	return getCurrentTab() === "files" && getModFilesDiv() !== null && getArchivedFilesContainerDiv() !== null;
}

//#endregion
//#region src/mod_page/description_tab.ts
function isDescriptionTab() {
	return getCurrentTab() === "description";
}
/**
* 容器 of 官方预设的描述模板
* tab 内容未加载完成时, 返回 null
*/
function getTabDescriptionContainerDiv() {
	return getTabContentDiv().querySelector(":scope > div.container.tab-description");
}
/**
* `briefOverview` cache
*/
function getBriefOverview() {
	const tabDescriptionContainerDiv = getTabDescriptionContainerDiv();
	if (!tabDescriptionContainerDiv) return null;
	return tabDescriptionContainerDiv.querySelector(":scope > p:nth-of-type(1)").innerText.trimEnd();
}
/**
*  Has "Share", "Report Abuse" buttons.
* @returns
*/
function getActionsUl() {
	const tabDescriptionContainerDiv = getTabDescriptionContainerDiv();
	return tabDescriptionContainerDiv ? tabDescriptionContainerDiv.querySelector(":scope > ul.actions") : null;
}
function getDescriptionDl() {
	const tabDescriptionContainerDiv = getTabDescriptionContainerDiv();
	return tabDescriptionContainerDiv ? tabDescriptionContainerDiv.querySelector(":scope > div.accordionitems > dl.accordion") : null;
}
function getDescriptionDtDdMap() {
	const descriptionDl = getDescriptionDl();
	if (!descriptionDl) return null;
	const descriptionDtDdMap = /* @__PURE__ */ new Map();
	const ddDts = Array.from(descriptionDl.children).filter((child) => child.tagName === "DT" || child.tagName === "DD");
	for (let i = 0; i < ddDts.length; i = i + 2) descriptionDtDdMap.set(ddDts[i], ddDts[i + 1]);
	return descriptionDtDdMap;
}
function getPermissionDescriptionComponent() {
	const descriptionDtDdMap = getDescriptionDtDdMap();
	if (!descriptionDtDdMap) return null;
	for (const [dt, dd] of descriptionDtDdMap) if (dt.innerText.trim().startsWith("Permissions and credits")) {
		const tabbedBlockDivs = dd.querySelectorAll(":scope > div.tabbed-block");
		let permissionDiv = null, authorNotesDiv = null, authorNotesContentP = null, fileCreditsDiv = null, fileCreditsContentP = null, donationDiv = null;
		for (const tabbedBlockDiv of tabbedBlockDivs) switch (tabbedBlockDiv.querySelector(":scope > h3").innerText) {
			case "Credits and distribution permission":
				permissionDiv = tabbedBlockDiv;
				break;
			case "Author notes":
				authorNotesDiv = tabbedBlockDiv;
				authorNotesContentP = authorNotesDiv.querySelector(":scope > p");
				break;
			case "File credits":
				fileCreditsDiv = tabbedBlockDiv;
				fileCreditsContentP = fileCreditsDiv.querySelector(":scope > p");
				break;
			case "Donation Points system":
				donationDiv = tabbedBlockDiv;
				break;
		}
		return {
			titleDt: dt,
			descriptionDd: dd,
			permissionDiv,
			authorNotesDiv,
			authorNotesContentP,
			fileCreditsDiv,
			fileCreditsContentP,
			donationDiv
		};
	}
	return null;
}
/**
* 容器 of 作者自定义的描述
* tab 内容未加载完成时, 返回 null
*/
function getModDescriptionContainerDiv() {
	return getTabContentDiv().querySelector(":scope > div.container.mod_description_container");
}

//#endregion
//#region src/util.ts
/**
* cache codes of other file
* @see `file:///D:/Workspaces/@lyne408/ecmascript_lib/src/path_util.ts`
*
* @param pathArg
* @returns
*/
function replaceIllegalChars(pathArg) {
	const replacerMap = {
		"?": "?",
		"*": "*",
		":": ":",
		"<": "<",
		">": ">",
		"\"": """,
		"/": " ∕ ",
		"\\": " ⧵ ",
		"|": "|"
	};
	pathArg = pathArg.trim();
	return pathArg.replace(/(\?)|(\*)|(:)|(<)|(>)|(")|(\/)|(\\)|(\|)/g, (found) => replacerMap[found]);
}
/**
* cache codes of other file
* @param targetNode
* @param callback
* @returns
*/
function observeDirectChildNodes(targetNode, callback) {
	const observer = new MutationObserver((mutationList) => {
		callback(mutationList, observer);
	});
	observer.observe(targetNode, {
		childList: true,
		attributes: false,
		subtree: false
	});
	return observer;
}
/**
* cache codes of other file
* @param targetNode
* @param callback
* @returns
*/
function observeAddDirectChildNodes(targetNode, callback) {
	return observeDirectChildNodes(targetNode, (mutationList, observer) => {
		for (let index = 0; index < mutationList.length; index++) if (mutationList[index].addedNodes.length > 0) {
			callback(mutationList, observer);
			break;
		}
	});
}

//#endregion
//#region ../../../../Workspaces/@lyne408/userscript_lib/src/value.ts
function setValue(name$1, value) {
	return GM_setValue(name$1, value);
}
function getValue(name$1) {
	return GM_getValue(name$1);
}

//#endregion
//#region ../../../../Workspaces/@lyne408/userscript_lib/src/download.ts
/**
* `@grant GM_download`
* `saveAs = false`
* @param argObj
* @returns
*/
const downloadFile = async (argObj) => {
	argObj.saveAs = argObj.saveAs ? argObj.saveAs : false;
	return new Promise((resolve) => {
		GM_download({
			...argObj,
			onload() {
				resolve(Object.assign(argObj, { success: true }));
			},
			onerror(error) {
				resolve(Object.assign(argObj, {
					success: false,
					error
				}));
			},
			ontimeout() {
				resolve(Object.assign(argObj, {
					success: false,
					error: "timeout"
				}));
			},
			onprogress: argObj.onprogress
		});
	});
};
/**
* 适合下载到多个目录
*
* `@grant GM_download`
*
* simultaneous = 3, saveAs = false, directory = './'
*
* [lyne408] 下载 URL.createObjectURL() 的返回值 (如 `URL.createObjectURL(blob)`) 可能会遇到 error,
* 如 error `{ id: "fa04ee91-710e-4905-9b85-a9f016552f3d", error: "not_whitelisted" }`.
* not_whitelisted - the requested file extension is not whitelisted.
* 曾遇到不允许下载 JSON, HTML 文件. 在 Tampmonkey 的 Settings -> Download BETA -> Whitelisted File Extensions 里添加.
* @param items
* @param simultaneous
* @param eachSuccess
* @param eachFail
* @returns
*/
const downloadFiles = async (argObj) => {
	argObj.saveAs = argObj.saveAs ? argObj.saveAs : false;
	argObj.simultaneous = argObj.simultaneous ? argObj.simultaneous : 3;
	const { items, simultaneous, successEach, failEach, onProgressEach } = argObj;
	const itemsParts = [];
	for (let i = 0; i < items.length; i = i + simultaneous) itemsParts.push(items.slice(i, i + simultaneous));
	const successes = [];
	const fails = [];
	await Promise.all(itemsParts.map(async (itemsPart) => {
		for (const item of itemsPart) {
			const { url, name: name$1 } = item;
			const downloadResult = await downloadFile({
				url,
				name: name$1,
				...argObj,
				onprogress: (progressRes) => {
					typeof onProgressEach === "function" && onProgressEach(item, progressRes);
				}
			});
			if (downloadResult.success) {
				successes.push(item);
				typeof successEach === "function" && successEach(item);
			} else {
				fails.push(item);
				typeof failEach === "function" && failEach(item, downloadResult.error);
			}
		}
	}));
	return {
		successes,
		fails
	};
};

//#endregion
//#region ../../../../Workspaces/@lyne408/ecmascript_lib/src/time_util.ts
function delay(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

//#endregion
//#region src/mod_page/tabs_shared_actions.ts
function createCopyModNameAndVersionComponent() {
	const { actionButton, messageSpan, element } = createActionWithMessageComponent("Copy Mod Name And Version");
	messageSpan.innerText = "Copied";
	actionButton.addEventListener("click", () => {
		navigator.clipboard.writeText(`${getModName()} ${getModVersionWithDate()}`).then(() => {
			messageSpan.style.visibility = "visible";
			setTimeout(() => messageSpan.style.visibility = "hidden", 1e3);
		}, () => console.log("%c[Error] Copy failed.", "color: red"));
	});
	return element;
}
/**
* @param currentTab
*
* 因 Firefox 保存书签时, 若书签名包含换行, 直接省略换行符
*
* 这里替换 brief overview 中的换行为空格
*/
function tweakTitleInner(currentTab) {
	if (currentTab === "description") {
		let briefOverview = getBriefOverview();
		briefOverview = briefOverview ? briefOverview.replaceAll(/\r\n|\n/g, " ") : "";
		titleElement.innerText = `${getModName()} ${getModVersionWithDate()}: ${briefOverview}`;
	} else titleElement.innerText = `${getModName()} ${getModVersionWithDate()} tab=${currentTab}`;
}
/**
* tweak title after open mod urls (mod page or any tabs) or click tabs
*/
function tweakTitleAfterClickingTab() {
	let oldTab = getCurrentTab();
	tweakTitleInner(oldTab);
	clickTabLi(async (clickedTab) => {
		if (oldTab !== clickedTab) {
			if (clickedTab === "description" && getBriefOverview() === null) await clickedTabContentLoaded();
			oldTab = clickedTab;
			tweakTitleInner(clickedTab);
		}
	});
}
function hideModActionsSylin527NotUse() {
	const { addMediaLi, downloadLabelLi, vortexLi, manualDownloadLi } = getModActionsComponent();
	addMediaLi && (addMediaLi.style.display = "none");
	downloadLabelLi && (downloadLabelLi.style.display = "none");
	manualDownloadLi && (manualDownloadLi.style.display = "none");
	vortexLi && (vortexLi.style.display = "none");
}
/**
* 从逻辑来讲, 应该是 UI 逻辑分离, 暂时懒得写了
* 如果有 thumb gallery, 添加按钮, 并 bind event
* 点击按钮的逻辑: 页面最宽为 body 宽度, 所有 thumbnails 都以原图显示在页面
*
* 作者不上传 mod 图片时, 应该没有 `#sidebargallery > ul.thumbgallery`
*
* 如果没有 gallery, 返回 null
*/
function createShowAllGalleryThumbnailsComponent() {
	let hasShowAll = false;
	const { actionButton, element } = createActionComponent("Show All Thumbnails");
	actionButton.addEventListener("click", async () => {
		if (hasShowAll) {
			window.dispatchEvent(new Event("resize"));
			await delay(500);
			hasShowAll = false;
			actionButton.innerText = "Show All Thumbnails";
			return;
		}
		const thumbGalleryUl = getThumbnailGalleryUl();
		if (!thumbGalleryUl) return;
		thumbGalleryUl.style.height = "max-content";
		thumbGalleryUl.style.width = "auto";
		thumbGalleryUl.style.zIndex = "99999";
		const thumbLis = thumbGalleryUl.querySelectorAll(":scope > li.thumb");
		for (const thumbLi of thumbLis) {
			const { figure, anchor, img } = getThumbnailComponent(thumbLi);
			thumbLi.style.height = "auto";
			thumbLi.style.width = "auto";
			thumbLi.style.marginBottom = "7px";
			figure.style.height = "auto";
			anchor.style.top = "0";
			anchor.style.transform = "unset";
			img.style.maxHeight = "unset";
		}
		hasShowAll = true;
		actionButton.innerText = "Hide Extra Thumbnails";
	});
	return element;
}
/**
* 默认是选中的
* 返回的对象的属性 checked 是一个 getter
*/
function insertCheckboxToThumbnails() {
	const thumbGalleryUl = getThumbnailGalleryUl();
	if (!thumbGalleryUl) return null;
	const componentsWithCheckedProperty = [];
	const thumbLis = thumbGalleryUl.querySelectorAll(":scope > li.thumb");
	for (const thumbLi of thumbLis) {
		const component = getThumbnailComponent(thumbLi);
		const { figure } = component;
		const input = document.createElement("input");
		input.setAttribute("type", "checkbox");
		input.setAttribute("style", "position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; cursor: pointer;");
		input.addEventListener("click", (event) => {
			event.stopPropagation();
		}, { capture: true });
		figure.appendChild(input);
		componentsWithCheckedProperty.push(Object.defineProperty(component, "checked", {
			get() {
				return input.checked;
			},
			set(value) {
				input.checked = value;
			}
		}));
	}
	return componentsWithCheckedProperty;
}
let hasSelectAll = false;
function createSelectAllImagesComponent(components) {
	const { actionButton, element } = createActionComponent("Select All Images");
	actionButton.addEventListener("click", () => {
		if (!hasSelectAll) {
			for (const component of components) component.checked = true;
			hasSelectAll = true;
			actionButton.innerText = "Deselect All Images";
		} else {
			for (const component of components) component.checked = false;
			hasSelectAll = false;
			actionButton.innerText = "Select All Images";
		}
	});
	return element;
}
/**
* @param components
* @param relativeDirectory  will `replaceIllegalChars()`
* @param eachSuccess
* @param eachFail
* @returns
*/
function downloadSelectedImages(components, relativeDirectory, eachSuccess, eachFail) {
	const allThumbnailCount = components.length;
	const digits = allThumbnailCount.toString().length;
	const checkedImages = [];
	for (let i = 0; i < allThumbnailCount; i++) {
		const { checked, originalImageSrc, title } = components[i];
		if (checked) {
			const extWithDot = originalImageSrc.substring(originalImageSrc.lastIndexOf("."));
			const name$1 = `${relativeDirectory}/${(i + 1).toString().padStart(digits, "0")}_${replaceIllegalChars(title)}${extWithDot}`;
			checkedImages.push({
				url: originalImageSrc,
				name: name$1
			});
		}
	}
	return downloadFiles({
		items: checkedImages,
		simultaneous: 3,
		successEach: eachSuccess,
		failEach: eachFail
	});
}
function createDownloadSelectedImagesComponent() {
	const fragment = document.createDocumentFragment();
	const { actionButton: downloadButton, messageSpan, element: downloadDiv } = createActionWithMessageComponent("Download Selected Images");
	fragment.append(downloadDiv);
	const modGalleryDiv = getModGalleryDiv();
	if (!(isModUrl(location.href) && modGalleryDiv)) {
		downloadButton.innerText = "Download Selected Images (Gallery Not Found)";
		downloadButton.style.display = "none";
		return fragment;
	}
	const componentsHasCheckedProperty = insertCheckboxToThumbnails();
	const selectButton = createSelectAllImagesComponent(componentsHasCheckedProperty);
	fragment.insertBefore(selectButton, downloadDiv);
	downloadButton.addEventListener("click", () => {
		messageSpan.style.visibility = "visible";
		const selectedCount = componentsHasCheckedProperty.filter(({ checked }) => checked).length;
		if (selectedCount === 0) {
			messageSpan.innerText;
			return;
		}
		const downloadedCountSpan = document.createElement("span");
		downloadedCountSpan.innerText = "0";
		const failedCountSpan = document.createElement("span");
		failedCountSpan.innerText = "0";
		messageSpan.innerText = "";
		messageSpan.append(`Selected: ${selectedCount}`, " ", "Downloaded: ", downloadedCountSpan, " ", "Failed: ", failedCountSpan);
		let downloadedCount = 0;
		let failedCount = 0;
		downloadSelectedImages(componentsHasCheckedProperty, `${getGameName()}/${getModName()} ${getModVersionWithDate()}`, () => {
			downloadedCount++;
			downloadedCountSpan.innerText = downloadedCount.toString();
			downloadedCount === selectedCount && (messageSpan.innerText = `Done: ${selectedCount}/${selectedCount}`);
		}, () => {
			failedCount++;
			failedCountSpan.innerText = failedCount.toString();
		});
	});
	return fragment;
}
/**
* 如果有 div#feature, 清除其 style 属性, 更改其 id 为 nofeature 以使用 nofeature 样式.
*
* 对比 div#feature, div#nofeature 样式为: 清除背景, 减小 height.
*
* 2023-10-20 最新的 SSE mod page 变化, 需要额外清除 header img
*/
function removeFeature() {
	const featureDiv = getFeatureDiv();
	if (!featureDiv) return;
	featureDiv.removeAttribute("style");
	featureDiv.querySelector(":scope > div.header-img")?.remove();
	featureDiv.setAttribute("id", "nofeature");
}
/**
* 不改变页面结构, 仅移除不需部分
* @param element
*/
function mainContentDivAsTopElement() {
	bodyElement.style.marginTop = "0";
	getHeader()?.remove();
	removeFeature();
	getModGalleryDiv()?.remove();
	getFooter()?.remove();
	getBackToTop()?.remove();
}
function removeModGallery() {
	getModGalleryDiv()?.remove();
}
/**
* 点击 tab 时, Nexusmods 先删除原 tabContentDiv.querySelectorAll("div"), 再新增
*
* 点 tab 后, 有一个 MutationRecord[], 一般含有两个 MutationRecord.
* 第一个 MutationRecord 是删除 tabContentDiv 下的原 tab 的 childNodes
* 第二个 MutationRecord 是增加新 tab 的 的childNodes 到 ContentDiv
*/
function clickedTabContentLoaded() {
	return new Promise((resolve) => {
		observeAddDirectChildNodes(getTabContentDiv(), (mutationList, observer) => {
			console.log("tabContentDiv add childNodes mutationList:", mutationList);
			observer.disconnect();
			resolve(0);
		});
	});
}
async function controlComponentDisplayAfterClickingTab(component, isShow) {
	const style = component.style;
	async function _inner(currentTab) {
		await isShow(currentTab) ? style.display = "block" : style.display = "none";
	}
	await _inner(getCurrentTab());
	clickTabLi(async (clickedTab) => {
		await _inner(clickedTab);
	});
}

//#endregion
//#region src/site_shared_actions.ts
/**
* 从 Userscript 的 Storage 里获取
* @returns
*/
function isSylin527() {
	const value = getValue("isSylin527");
	return typeof value === "boolean" ? value : false;
}
function getSpoilerToggleInputClassName() {
	return "sylin527_spoiler_toggle_input";
}
function getSpoilerToggleTextClassName() {
	return "sylin527_spoiler_toggle_text";
}
let hasInsertedShowSpoilerToggleStyle = false;
function insertShowSpoilerToggleStyle() {
	if (hasInsertedShowSpoilerToggleStyle) return;
	const newStyle = document.createElement("style");
	headElement.appendChild(newStyle);
	const sheet = newStyle.sheet;
	const spoilerToggleInputCN = getSpoilerToggleInputClassName();
	const spoilerToggleTextCN = getSpoilerToggleTextClassName();
	let ruleIndex = sheet.insertRule(`
    input.${spoilerToggleInputCN},
    input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN},
    input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN}::after {
      border: 0;
      cursor: pointer;
      box-sizing: border-box;
      display: inline-block;
      height: 27px;
      width: 60px;
      z-index: 999;
      position: relative;
      vertical-align: middle;
      text-align: center;
    }
    `);
	sheet.insertRule(`
    input.${spoilerToggleInputCN} {
      margin-left: 1px;
      z-index: 987654321;
      opacity: 0;
    }
    `, ++ruleIndex);
	sheet.insertRule(`
    input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN} {
      font-style: normal;
      margin: 0;
      padding: 0;
      margin-left: -60px;
    }
    `, ++ruleIndex);
	sheet.insertRule(`
    input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN}::after {
      content: attr(unchecked_text);
      background-color: ${primaryColor};
      font-size: 12px;
      color: #E6E6E6;
      border-radius: 3px;
      font-weight: 400;
      line-height: 27px;
      margin: 0;
      padding: 0;
    }
    `, ++ruleIndex);
	sheet.insertRule(`
    input.${spoilerToggleInputCN}:checked ~ i.${spoilerToggleTextCN}::after {
      content: attr(checked_text);
      background-color: ${highlightColor};
    }
    `, ++ruleIndex);
	sheet.insertRule(`
    input.${spoilerToggleInputCN}:checked ~ div.bbc_spoiler_content {
      display: none;
    }
    `, ++ruleIndex);
	sheet.insertRule(`
    div.bbc_spoiler_content {
      display: block;
    }
    `, ++ruleIndex);
	hasInsertedShowSpoilerToggleStyle = true;
}
function showSpoilers(container) {
	insertShowSpoilerToggleStyle();
	const spoilers = container.querySelectorAll("div.bbc_spoiler");
	for (let i = 0; i < spoilers.length; i++) {
		const spoiler = spoilers[i];
		spoiler.querySelector("div.bbc_spoiler_show")?.remove();
		const input = document.createElement("input");
		input.className = getSpoilerToggleInputClassName();
		input.setAttribute("type", "checkbox");
		const iElement = document.createElement("i");
		iElement.setAttribute("class", `bbc_spoiler_show ${getSpoilerToggleTextClassName()}`);
		iElement.setAttribute("checked_text", "Show");
		iElement.setAttribute("unchecked_text", "Hide");
		const content = spoiler.querySelector("div.bbc_spoiler_content");
		spoiler.insertBefore(input, content);
		spoiler.insertBefore(iElement, content);
		content.removeAttribute("style");
	}
}
/**
* youtube 嵌入式链接 换成 外链接
* 如 <div class="youtube_container"><iframe class="youtube_video" src="https://www.youtube.com/embed/KuO6ortp0ZY" ...></iframe></div>
* 	换成 <a src="https://www.youtube.com/watch?v=KuO6ortp0ZY">https://www.youtube.com/watch?v=KuO6ortp0ZY</a>
*
* 技术需求: 替换元素, 文档位置不变
*/
/**
* 获取 Youtube video iframe 的标题需要跨域, 暂不操作
* @param container
* @returns
*/
function replaceYoutubeVideosToAnchor(container) {
	const youtubeIframes = Array.from(container.querySelectorAll("iframe.youtube_video"));
	if (youtubeIframes.length === 0) return;
	for (const iframe of youtubeIframes) {
		const parts = iframe.getAttribute("src").split("/");
		const videoId = parts[parts.length - 1];
		const watchA = document.createElement("a");
		const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
		watchA.style.display = "block";
		watchA.setAttribute("href", watchUrl);
		watchA.innerText = watchUrl;
		const parent = iframe.parentNode;
		const grandparent = parent.parentNode;
		grandparent && grandparent.replaceChild(watchA, parent);
	}
}
function replaceThumbnailUrlsToImageUrls(container) {
	const imgs = container.querySelectorAll("img");
	for (let i = 0; i < imgs.length; i++) {
		const src = imgs[i].src;
		if (src.startsWith("https://staticdelivery.nexusmods.com") && src.includes("thumbnails")) imgs[i].src = src.replace("thumbnails/", "");
	}
}
function removeModActions() {
	getModActionsUl().remove();
}
function simplifyDescriptionContent(contentContainerElement) {
	replaceYoutubeVideosToAnchor(contentContainerElement);
	replaceThumbnailUrlsToImageUrls(contentContainerElement);
	showSpoilers(contentContainerElement);
}
function simplifyComments() {
	const commentContainerComponent = getCommentContainerComponent();
	if (!commentContainerComponent) return;
	const { stickyCommentLis, authorCommentLis } = commentContainerComponent;
	for (const stickyCommentLi of stickyCommentLis) simplifyDescriptionContent(getCommentContentTextDiv(stickyCommentLi));
	for (const authorCommentLi of authorCommentLis) simplifyDescriptionContent(getCommentContentTextDiv(authorCommentLi));
}
function removeAdAndPremiumTips() {
	document.querySelectorAll(`
#pw-oop-flex_container,
.ads-top,
.ads-bottom,
.premium-banner,
#pw-oop-left_rail,
#pw-oop-right_rail,
.ads-bottom,
.gg-overlay,
.gg-overlay-reset,
#pw-oop-bottom_rail,
#adBanner,
#adBlockingBanner,
div[id^='google_ads_iframe_'],
div:has(> div[id^='google_ads_iframe_']),
#nonPremiumBanner,
#freeTrialBanner,
.new-new-premium-banner,
.new-new-premium-banner__inner,
div:has(> a[href='https://users.nexusmods.com/account/billing/premium'])
`).forEach((element) => {
		element.remove();
	});
}

//#endregion
//#region src/mod_page/files_tab_actions.ts
function addShowRealFilenameToggle() {
	const modFilesDiv = getModFilesDiv();
	if (!modFilesDiv) return;
	const input = document.createElement("input");
	const toggleInputClassName = "sylin527_real_filenames_toggle_input";
	input.className = toggleInputClassName;
	input.setAttribute("type", "checkbox");
	input.checked = false;
	const i = document.createElement("i");
	const toggleTextClassName = "sylin527_real_filenames_toggle_text";
	i.className = toggleTextClassName;
	i.setAttribute("unchecked_text", "Hide Real Filenames");
	i.setAttribute("checked_text", "Show Real Filenames");
	modFilesDiv.insertBefore(i, modFilesDiv.firstChild);
	modFilesDiv.insertBefore(input, modFilesDiv.firstChild);
	const style = document.createElement("style");
	style.innerHTML = `
  input.${toggleInputClassName},
  input.${toggleInputClassName} ~ i.${toggleTextClassName},
  input.${toggleInputClassName} ~ i.${toggleTextClassName}::after {
    border: 0;
    cursor: pointer;
    box-sizing: border-box;
    display: block;
    height: 40px;
    width: 300px;
    z-index: 999;
    position: relative;
  }

  /* input[type=checkbox] 全透明, 但 z-index 最大 */
  input.${toggleInputClassName} {
    margin: 0 auto;
    z-index: 987654321;
    opacity: 0;
  }

  input.${toggleInputClassName} ~ i.${toggleTextClassName} {
    font-style: normal;
    font-size: 18px;
    text-align: center;
    line-height: 40px;
    border-radius: 5px;
    font-weight: 400;
    margin: -40px auto -60px auto;
  }
 
  /* input[type=checkbox] unchecked 时, 显示了所有的文件 */
  input.${toggleInputClassName} ~ i.${toggleTextClassName}::after {
    background-color: ${primaryColor};
    content: attr(unchecked_text);
    border-radius: 3px;
  }

  /* 因为 input[type=checkbox] 的 z-index 值最大, 所以 :hover 用在此 input 上 */
  input.${toggleInputClassName}:hover ~ i.${toggleTextClassName}::after {
    background-color: ${primaryHoverColor};
  }

  /* input[type=checkbox] checked 时, 隐藏了所有的文件 */
  input.${toggleInputClassName}:checked ~ i.${toggleTextClassName}::after {
    background-color: ${highlightColor};
    content: attr(checked_text);
  }

  input.${toggleInputClassName}:hover:checked ~ i.${toggleTextClassName}::after {
    background-color: ${highlightHoverColor};
  }
 
  /* 由于 SingFile 默认移除隐藏的内容, 必须先显示. 需要时在隐藏 */
  input.${toggleInputClassName}:checked ~ div dd p.${getRealFilenamePClassName()} {
    display: none;
  }
  `;
	document.head.appendChild(style);
}
function removePremiumBanner() {
	getPremiumBannerDiv()?.remove();
}
function removeAllSortBys() {
	const divs = getAllSortByDivs();
	divs && Array.from(divs).forEach((sortByDiv) => sortByDiv.remove());
}
function simplifyAllFileHeaders() {
	const fileHeaderDts = getAllFileHeaderDts();
	if (!fileHeaderDts) return;
	for (const fileHeaderDt of fileHeaderDts) fileHeaderDt.style.background = "#2d2d2d";
}
function getRealFilenamePClassName() {
	return "sylin527_real_filename_p";
}
let hasInsertedRealFilenamePStyle = false;
function insertRealFilenamePStyle() {
	if (hasInsertedRealFilenamePStyle) return;
	const newStyle = document.createElement("style");
	headElement.appendChild(newStyle);
	newStyle.sheet?.insertRule(`
    p.${getRealFilenamePClassName()} {
      color: #8197ec;
      margin-top: 20xp;
    }
    `, 0);
	hasInsertedRealFilenamePStyle = true;
}
function createRealFilenameP(realFilename, fileUrl) {
	const realFilenameP = document.createElement("p");
	realFilenameP.className = getRealFilenamePClassName();
	const newFileUrlAnchor = document.createElement("a");
	newFileUrlAnchor.href = fileUrl;
	newFileUrlAnchor.innerText = realFilename;
	realFilenameP.appendChild(newFileUrlAnchor);
	return realFilenameP;
}
/**
* Simplify File Description
*/
function simplifyAllFileDescriptions$1() {
	const fileDescriptionDds = getAllFileDescriptionDds();
	if (!fileDescriptionDds) return;
	insertRealFilenamePStyle();
	const gameDomainName = getGameDomainName();
	const modId = getModId();
	for (const fileDescriptionDd of fileDescriptionDds) {
		const { fileDescriptionDiv, downloadButtonContainerDiv, previewFileDiv, realFilename, fileId } = getFileDescriptionComponent(fileDescriptionDd);
		simplifyDescriptionContent(fileDescriptionDiv);
		downloadButtonContainerDiv.remove();
		fileDescriptionDiv.append(createRealFilenameP(realFilename, generateFileUrl(gameDomainName, modId, fileId)));
		previewFileDiv.remove();
		fileDescriptionDd.style.display = "block";
	}
	addShowRealFilenameToggle();
}
function insertRemoveOldFilesComponent() {
	const oldFilesComponent = getOldFilesComponent();
	if (!oldFilesComponent) return null;
	const removeButton = document.createElement("button");
	removeButton.className = "rj-btn-secondary";
	removeButton.innerText = "Remove";
	const { element, categoryHeaderDiv, sortByContainerDiv } = oldFilesComponent;
	categoryHeaderDiv.insertBefore(removeButton, sortByContainerDiv);
	categoryHeaderDiv.insertBefore(document.createTextNode(" "), removeButton);
	removeButton.addEventListener("click", () => {
		element.remove();
	});
	containerManager.addInline(removeButton);
}
function simplifyFilesTab() {
	removeAdAndPremiumTips();
	removePremiumBanner();
	removeAllSortBys();
	simplifyAllFileHeaders();
	simplifyAllFileDescriptions$1();
	getFileArchiveSection()?.remove();
	mainContentDivAsTopElement();
}
function createSimplifyFilesTabComponent() {
	const { actionButton, element } = createActionComponent("Simplify Files Tab");
	actionButton.addEventListener("click", () => {
		simplifyFilesTab();
		containerManager.hideAll();
	});
	isFilesTab() ? insertRemoveOldFilesComponent() : element.style.display = "none";
	controlComponentDisplayAfterClickingTab(element, async (clickedTab) => {
		showActionContainer();
		const bFilesTab = clickedTab === "files" && await clickedTabContentLoaded() === 0 && isFilesTab();
		bFilesTab && insertRemoveOldFilesComponent();
		return bFilesTab;
	});
	return element;
}

//#endregion
//#region src/mod_page/archived_files_tab_actions.ts
/**
* 无默认值
* @returns
*/
function getApiKey() {
	return getValue("apikey");
}
/**
* If not configure `apikey` value, return null
* @param gameDomainName
* @param modId
* @returns
*/
async function getArchivedFileIdRealFilenameMap(gameDomainName, modId) {
	const apiKey = getApiKey();
	if (!apiKey || apiKey === "") return null;
	const { files } = await getFiles(gameDomainName, modId, apiKey);
	const map = /* @__PURE__ */ new Map();
	for (const { file_id, category_id, file_name } of files) category_id === 7 && map.set(file_id, file_name);
	return map;
}
/**
* Simplify File Description
*/
async function simplifyAllFileDescriptions() {
	const fileDescriptionDds = getAllFileDescriptionDds();
	if (!fileDescriptionDds) return;
	insertRealFilenamePStyle();
	const gameDomainName = getGameDomainName();
	const modId = getModId();
	const oldFileIdRealFilenameMap = await getArchivedFileIdRealFilenameMap(gameDomainName, modId);
	for (const fileDescriptionDd of fileDescriptionDds) {
		const fileDescriptionDiv = getFileDescriptionDiv(fileDescriptionDd);
		simplifyDescriptionContent(fileDescriptionDiv);
		getDownloadButtonContainerDiv(fileDescriptionDd).remove();
		const fileId = getFileId(fileDescriptionDd);
		const realFilename = oldFileIdRealFilenameMap ? oldFileIdRealFilenameMap.get(fileId) : "File Link";
		fileDescriptionDiv.append(createRealFilenameP(realFilename, generateFileUrl(gameDomainName, modId, fileId)));
		fileDescriptionDd.style.display = "block";
	}
	addShowRealFilenameToggle();
}
function tweakTitleIfArchivedFilesTab() {
	isArchivedFilesUrl(location.href) && (titleElement.innerText = `${getModName()} ${getModVersionWithDate()} tab=archived_files`);
}
function createSimplifyArchivedFilesTabComponent() {
	const { actionButton, element } = createActionComponent("Simplify Archived Files Tab");
	actionButton.addEventListener("click", async () => {
		removePremiumBanner();
		removeAdAndPremiumTips();
		removeAllSortBys();
		simplifyAllFileHeaders();
		await simplifyAllFileDescriptions();
		containerManager.hideAll();
	});
	!isArchivedFilesTab() && (element.style.display = "none");
	controlComponentDisplayAfterClickingTab(element, async (clickedTab) => {
		showActionContainer();
		return clickedTab === "files" && await clickedTabContentLoaded() === 0 && isArchivedFilesTab();
	});
	return element;
}

//#endregion
//#region src/article_page/article_page.ts
function getArticleUrlRegExp() {
	return /^((https|http):\/\/(www.)?nexusmods.com\/[a-z0-9]+\/articles\/[0-9]+)/;
}
function isArticleUrl(url) {
	return getArticleUrlRegExp().test(url);
}
function getArticleContainerDiv() {
	return getSection().querySelector("div.container");
}
function getArticleElement() {
	return getArticleContainerDiv().querySelector(":scope > article");
}
function getArticleInformation() {
	const fileInfoDiv = document.getElementById("fileinfo");
	if (!fileInfoDiv) return null;
	const sideItems = fileInfoDiv.querySelectorAll(":scope > div.sideitem");
	for (const sideItem of sideItems) sideItem.querySelector("time");
	const addedOnTime = sideItems[0]?.querySelector(":scope > time");
	const addedOn = addedOnTime ? addedOnTime.dateTime : "";
	const editedOnTime = sideItems[1]?.querySelector(":scope > time");
	const editedOn = editedOnTime ? editedOnTime.dateTime : "";
	const authorLink = sideItems[2]?.querySelector(":scope > a");
	const authorName = authorLink?.innerText.trim() || "";
	const authorUserIdMatch = authorLink?.href.match(/\/users\/(\d+)/);
	return {
		addedOn,
		editedOn,
		author: {
			name: authorName,
			userId: authorUserIdMatch ? parseInt(authorUserIdMatch[1]) : 0
		}
	};
}

//#endregion
//#region ../../../../Workspaces/@lyne408/ecmascript_lib/src/date_util.ts
/**
* 高精度日期格式化函数
* 支持:年(Y)、月(M)、日(D)、小时(H/h)、分钟(m)、秒(s)、毫秒 (SSS), 微秒 (SSSSSS)
* @param date - 输入日期:Date对象、时间戳(number) 或 ISO字符串
* @param format - 格式化模板,如 "YYYY-MM-DD HH:mm:ss.SSSSSS"
* @returns 格式化后的字符串
*/
function formatDate(date, format = "YYYY-MM-DD HH:mm:ss.SSS") {
	const d = new Date(date);
	if (isNaN(d.getTime())) throw new Error("Invalid date");
	const ms = d.getMilliseconds().toString().padStart(3, "0");
	let us = "000";
	if (typeof date === "string") {
		const microMatch = date.match(/\.\d{3}(\d{1,3})/);
		if (microMatch) us = microMatch[1].padEnd(3, "0");
	}
	const precisionStr = ms + us;
	const year = d.getFullYear();
	const hours = d.getHours();
	const matches = {
		YYYY: year,
		YY: String(year).slice(-2),
		MM: String(d.getMonth() + 1).padStart(2, "0"),
		M: d.getMonth() + 1,
		DD: String(d.getDate()).padStart(2, "0"),
		D: d.getDate(),
		HH: String(hours).padStart(2, "0"),
		H: hours,
		hh: String(hours % 12 || 12).padStart(2, "0"),
		h: hours % 12 || 12,
		mm: String(d.getMinutes()).padStart(2, "0"),
		m: d.getMinutes(),
		ss: String(d.getSeconds()).padStart(2, "0"),
		s: d.getSeconds(),
		A: hours >= 12 ? "PM" : "AM",
		a: hours >= 12 ? "pm" : "am"
	};
	return format.replace(/YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a|S+/g, (match) => {
		if (match.startsWith("S")) return precisionStr.substring(0, match.length);
		return String(matches[match] ?? match);
	});
}
/**
* 'YY.M.D'
* @param date
* @returns
*/
function formatToShortDate(date) {
	return formatDate(date, "YY.M.D");
}

//#endregion
//#region src/article_page/article_page_actions.ts
function simplifyArticlePage() {
	removeAdAndPremiumTips();
	simplifyComments();
	simplifyDescriptionContent(getArticleElement());
	const info = getArticleInformation();
	if (info && info.editedOn) {
		const shortDate = formatToShortDate(new Date(info.editedOn));
		titleElement.innerText = `Article - ${getPageTitle()} ${shortDate}`;
	} else titleElement.innerText = `Article - ${getPageTitle()}`;
	removeModActions();
	mainContentDivAsTopElement();
}
function createSimplifyArticlePageComponent() {
	const { actionButton, element } = createActionComponent("Simplify Article Page");
	actionButton.addEventListener("click", () => {
		simplifyArticlePage();
		containerManager.hideAll();
	});
	return element;
}

//#endregion
//#region src/mod_page/description_tab_actions.ts
function showAllDescriptionDds() {
	const dtDdMap = getDescriptionDtDdMap();
	if (!dtDdMap || dtDdMap.size === 0) return;
	const newStyle = document.createElement("style");
	headElement.appendChild(newStyle);
	const sheet = newStyle.sheet;
	const accordionToggle = "sylin527_show_accordion_toggle";
	let ruleIndex = sheet.insertRule(`
    input.${accordionToggle} {
      cursor: pointer;
      display: block;
      height: 43.5px;
      margin: -44.5px 0 1px 0;
      width: 100%;
      z-index: 999;
      position: relative;
      opacity: 0;
    }
  `);
	sheet.insertRule(`
    input.${accordionToggle}:checked ~ dd{
      display: none;
    }
  `, ++ruleIndex);
	for (const [dt, dd] of dtDdMap) {
		dt.style.background = "#2d2d2d";
		dd.style.display = "block";
		dd.removeAttribute("style");
		const newPar = document.createElement("div");
		const toggle = document.createElement("input");
		toggle.setAttribute("class", accordionToggle);
		toggle.setAttribute("type", "checkbox");
		dd.parentElement.insertBefore(toggle, dd);
		newPar.append(dt, toggle, dd);
		getDescriptionDl()?.append(newPar);
	}
}
function simplifyTabDescription() {
	getActionsUl()?.remove();
	if (getDescriptionDl()) {
		const permissionDescriptionComponent = getPermissionDescriptionComponent();
		if (permissionDescriptionComponent) {
			const { authorNotesContentP, fileCreditsContentP } = permissionDescriptionComponent;
			authorNotesContentP && simplifyDescriptionContent(authorNotesContentP);
			fileCreditsContentP && simplifyDescriptionContent(fileCreditsContentP);
		}
		showAllDescriptionDds();
	}
}
function simplifyModDescription() {
	const modDescriptionContainerDiv = getModDescriptionContainerDiv();
	if (modDescriptionContainerDiv) simplifyDescriptionContent(modDescriptionContainerDiv);
}
/**
* 目的: 点击 description tab 后, url 为 mod url, 而不是 description tab url
* 便于书签管理
*/
function setLocationToModUrlIfDescriptionTab() {
	const modUrl = generateModUrl(getGameDomainName(), getModId());
	getCurrentTab() === "description" && history.replaceState(null, "", modUrl);
	clickTabLi(async (clickedTab) => {
		showActionContainer();
		clickedTab === "description" && await clickedTabContentLoaded() === 0 && history.replaceState(null, "", modUrl);
	});
}

//#endregion
//#region src/mod_page/file_tab.ts
function isFileUrl(url) {
	const searchParams = new URL(url).searchParams;
	return isModUrl(url) && searchParams.get("tab") === "files" && searchParams.has("file_id");
}
function getFileIdFromUrl(url) {
	const fileId = new URL(url).searchParams.get("file_id");
	return fileId ? parseInt(fileId) : null;
}

//#endregion
//#region src/mod_page/file_tab_actions.ts
function tweakTitleIfFileTab() {
	isFileUrl(location.href) && (titleElement.innerText = `${getModName()} ${getModVersionWithDate()} file=${getFileIdFromUrl(location.href)}`);
}

//#endregion
//#region src/mod_page/forum_tab.ts
function getModTopicsDiv() {
	return document.getElementById("tab-modtopics");
}
/**
* ForumTab
* @returns
*/
function getTopicsTabH2() {
	return document.getElementById("topics_tab_h2");
}
function isForumTab() {
	return getCurrentTab() === "forum" && getTopicsTabH2() !== null;
}
function getTopicTable() {
	return document.getElementById("mod_forum_topics");
}
function getAllTopicAnchors() {
	const topicTable = getTopicTable();
	if (!topicTable) return null;
	const topicAnchorsOfTHead = topicTable.tHead.querySelectorAll(":scope > tr > td.table-topic > a.go-to-topic");
	const topicAnchorsOfTBody = topicTable.tBodies[0].querySelectorAll(":scope > tr > td.table-topic > a.go-to-topic");
	return Array.from(topicAnchorsOfTHead).concat(Array.from(topicAnchorsOfTBody));
}
function clickTopicAnchor(callback) {
	const allTopicAnchors = getAllTopicAnchors();
	if (allTopicAnchors) for (const topicAnchor of allTopicAnchors) topicAnchor.addEventListener("click", (event) => {
		callback(topicAnchor, event);
	});
}

//#endregion
//#region src/mod_page/forum_topic_tab.ts
/**
* `<h2 id="comment-count">Intentional clipping setup for No Grass In Objects (1 comment)</h2>`
*/
function getTopicTitle() {
	const h2 = document.getElementById("comment-count");
	if (!h2) return "";
	const titleWithCommentCount = h2.innerText;
	const lastLeftParenthesisIndex = titleWithCommentCount.lastIndexOf("(");
	return titleWithCommentCount.substring(0, lastLeftParenthesisIndex - 1);
}
/**
* 可以通过打开 ForumTopicUrl 来进入 ForumTopicTab
* 切换 ForumTopicTab 与 ForumTab 不需要重载页面
* @returns
*/
function isForumTopicTab() {
	return getCurrentTab() === "forum" && getTopicsTabH2() === null;
}

//#endregion
//#region src/mod_page/forum_tab_actions.ts
/**
* 点击 topicAnchor 后, Nexusmods 修改 modTopicsDiv 的 ChildNodes,
* 没有修改 `div.tabcontent.tabcontent-mod-page` 的 ChildNodes
* @returns
*/
function modTopicsDivAddedDirectChildNodes() {
	return new Promise((resolve) => {
		observeAddDirectChildNodes(getModTopicsDiv(), (mutationList, observer) => {
			observer.disconnect();
			resolve(0);
		});
	});
}

//#endregion
//#region src/mod_page/posts_tab.ts
function isPostsTab() {
	return getCurrentTab() === "posts";
}

//#endregion
//#region src/mod_page/posts_tab_actions.ts
function hasStickyOrAuthorComments() {
	const commentContainerComponent = getCommentContainerComponent();
	if (!commentContainerComponent) return false;
	const { authorCommentLis, stickyCommentLis } = commentContainerComponent;
	return authorCommentLis.length + stickyCommentLis.length > 0;
}
function createSimplifyPostsTabComponent() {
	const { actionButton, element } = createActionComponent("Simplify Posts Tab");
	actionButton.addEventListener("click", () => {
		removeAdAndPremiumTips();
		simplifyComments();
		containerManager.hideAll();
		mainContentDivAsTopElement();
	});
	if (!isPostsTab() || isPostsTab() && !hasStickyOrAuthorComments()) element.style.display = "none";
	controlComponentDisplayAfterClickingTab(element, async (clickedTab) => {
		showActionContainer();
		return clickedTab === "posts" && await clickedTabContentLoaded() === 0 && hasStickyOrAuthorComments();
	});
	return element;
}

//#endregion
//#region src/mod_page/forum_topic_tab_actions.ts
function createSimplifyForumTopicTabComponent() {
	const { actionButton, element } = createActionComponent("Simplify Forum Topic Tab");
	actionButton.addEventListener("click", () => {
		removeAdAndPremiumTips();
		titleElement.innerText = replaceIllegalChars(getTopicTitle());
		simplifyComments();
		containerManager.hideAll();
		mainContentDivAsTopElement();
	});
	(!isForumTopicTab() || isForumTopicTab() && !hasStickyOrAuthorComments()) && (element.style.display = "none");
	function _addClickTopicAnchorEvent() {
		clickTopicAnchor(async () => {
			await modTopicsDivAddedDirectChildNodes();
			isForumTopicTab() && hasStickyOrAuthorComments() && (element.style.display = "block");
		});
	}
	isForumTab() && _addClickTopicAnchorEvent();
	clickTabLi(async (clickedTab) => {
		element.style.display = "none";
		showActionContainer();
		clickedTab === "forum" && await clickedTabContentLoaded() === 0 && isForumTab() && _addClickTopicAnchorEvent();
	});
	return element;
}

//#endregion
//#region src/mod_page/mod_page_actions.ts
/**
* mod url page (has description tab)
* @returns
*/
function createSimplifyModPageComponent() {
	const { actionButton, element } = createActionComponent("Simplify Mod Page");
	actionButton.addEventListener("click", () => {
		removeAdAndPremiumTips();
		removeFeature();
		removeModActions();
		removeModGallery();
		simplifyTabDescription();
		simplifyModDescription();
		titleElement.innerText = `${getModName()} ${getModVersionWithDate()}`;
		containerManager.hideAll();
		mainContentDivAsTopElement();
	});
	!isDescriptionTab() && (element.style.display = "none");
	/**
	* 似乎 mod page loaded (description tab) 加载之后,
	* `description tab <li>` 还是被 Nexusmods 的 JavaScript 代码 `click` 了一下,
	* 但没有刷新 tab content, 也就没有 childList MutationRecord.
	* 这时候再 `await clickedTabContentLoaded()` 就得不到返回值了.
	* 会导致首次点击其它的 tab, `Simplify Mod Page` button 还是显示.
	*/
	clickTabLi(async (clickedTab) => {
		element.style.display = "none";
		showActionContainer();
		clickedTab === "description" && await clickedTabContentLoaded() === 0 && isDescriptionTab() && (element.style.display = "block");
	});
	return element;
}

//#endregion
//#region src/userscripts/userscripts_shared.ts
const author = "sylin527";

//#endregion
//#region src/userscripts/mod_documentation_utility/userscript.header.ts
const name = `Mod Documentations Utility by ${author}`;
const version = "0.2.6.20260430";

//#endregion
//#region src/userscripts/mod_documentation_utility/userscript.main.ts
/**
* 仅初始化 `apikey` 为 `''`
*
* 没有初始化 `isSylin527`
*/
function initStorage() {
	!getApiKey() && setValue("apikey", "");
}
function init() {
	initStorage();
	const href = location.href;
	const actionContainer = insertActionContainer();
	if (isModUrl(href)) {
		tweakTitleAfterClickingTab();
		setLocationToModUrlIfDescriptionTab();
		actionContainer.append(createCopyModNameAndVersionComponent(), createShowAllGalleryThumbnailsComponent(), createDownloadSelectedImagesComponent());
		if (isSylin527()) hideModActionsSylin527NotUse();
		actionContainer.append(createSimplifyModPageComponent(), createSimplifyFilesTabComponent(), createSimplifyArchivedFilesTabComponent(), createSimplifyPostsTabComponent(), createSimplifyForumTopicTabComponent());
		tweakTitleIfFileTab();
		tweakTitleIfArchivedFilesTab();
	} else if (isArticleUrl(href)) actionContainer.appendChild(createSimplifyArticlePageComponent());
}
function main() {
	init();
	const scriptInfo = `Load userscript: ${name} ${version}`;
	console.log("%c [Info] " + scriptInfo, "color: green");
	console.log("%c [Info] URL: " + location.href, "color: green");
}
main();

//#endregion