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. 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.

// ==UserScript==
// @name        Mod Documentations Utility by sylin527
// @namespace   https://www.nexusmods.com
// @match       https://www.nexusmods.com/*/mods/*
// @match       https://www.nexusmods.com/*/articles/*
// @run-at      document-idle
// @version     0.2.3.20250615
// @license     GPLv3
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_download
// @grant       unsafeWindow
// @icon        https://www.nexusmods.com/favicon.ico
// @author      sylin527
// @description Help to save the mod documentations to local disk.    Simplify mod page, files tab, posts tab, forum tab, article page.    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.
// ==/UserScript==

//#region src/site_shared.ts
function getNexusmodsUrl() {
  return `https://www.nexusmods.com`
}
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
}
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',
  )
}

//#endregion
//#region src/api/mod_api.ts
async function getFiles(gameDomainName, modId, apiKey) {
  const res = await fetch(
    `https://api.nexusmods.com/v1/games/${gameDomainName}/mods/${modId}/files.json`,
    { headers: { apikey: apiKey } },
  )
  return await res.json()
}
function generateModUrl(gameDomainName, modId) {
  return `https://www.nexusmods.com/${gameDomainName}/mods/${modId}`
}
function generateFileUrl(gameDomainName, modId, fileId) {
  return `${getNexusmodsUrl()}/${gameDomainName}/mods/${modId}?tab=files&file_id=${fileId}`
}

//#endregion
//#region src/ui.ts
const { body: bodyElement, head: headElement } = document
const titleElement = headElement.querySelector('title')
const primaryColor = '#8197ec'
const primaryHoverColor = '#a4b7ff'
const highlightColor = '#d98f40'
const highlightHoverColor = '#ce7f45'
const mainContentMaxWidth = '1340px'
function overPrimaryComponent(element) {
  const style = element.style
  element.addEventListener('mouseover', function () {
    style.backgroundColor = primaryHoverColor
  })
  element.addEventListener('mouseleave', function () {
    style.backgroundColor = primaryColor
  })
}
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} > *{
      display: block;
      margin-top: 5px;
    }
    `,
    ++ruleIndex,
  )
  sheet.insertRule(
    `
    #${containerId} button.action {
    padding: 8px;
    cursor: pointer;
    background: ${primaryColor};
    border-radius: 3px;
    border: 1px solid ${primaryHoverColor};
    color: #eaeaea;
    `,
    ++ruleIndex,
  )
  sheet.insertRule(
    `
    #${containerId} button.action:hover {
    background: ${primaryHoverColor};
    `,
    ++ruleIndex,
  )
  sheet.insertRule(
    `
    #${containerId} span.message {
      background-color: rgba(51, 51, 51, 0.5);
      color: rgb(255, 47, 151);
      padding: 8px;
      border-radius: 4px;
      display: inline;
      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 createActionComponent(name$1) {
  const actionButton = document.createElement('button')
  actionButton.innerText = name$1
  actionButton.className = 'action'
  return actionButton
}
function createActionWithMessageComponent(name$1) {
  const containerDiv = document.createElement('div')
  const actionButton = createActionComponent(name$1)
  const messageSpan = document.createElement('span')
  messageSpan.className = 'message'
  containerDiv.append(actionButton, messageSpan)
  return {
    element: containerDiv,
    actionButton,
    messageSpan,
  }
}

//#endregion
//#region src/mod_page/tabs_shared.ts
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')
}
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() {
  const modTabsUl = getModTabsUl()
  const tabSpan = modTabsUl.querySelector(
    ':scope > li > a.selected > span.tab-label',
  )
  return tabSpan.innerText.toLowerCase()
}
function getTabFromTabLi(tabLi) {
  const tabSpan = tabLi.querySelector(
    ':scope > a[data-target] > span.tab-label',
  )
  return tabSpan.innerText.toLowerCase()
}
function clickTabLi(callback) {
  const modTabsUl = getModTabsUl()
  const tabLis = modTabsUl.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
function isFilesTab() {
  return (
    getCurrentTab() === 'files' &&
    getModFilesDiv() !== null &&
    getArchivedFilesContainerDiv() === null
  )
}
function getModFilesDiv() {
  return document.getElementById('mod_files')
}
function getPremiumBannerDiv() {
  const tabContentDiv = getTabContentDiv()
  return tabContentDiv.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)')
}
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
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')
}
function isArchivedFilesTab() {
  return (
    getCurrentTab() === 'files' &&
    getModFilesDiv() !== null &&
    getArchivedFilesContainerDiv() !== null
  )
}

//#endregion
//#region src/util.ts
function replaceIllegalChars(pathArg) {
  const illegalCharReplacerMapping = {
    '?': '?',
    '*': '*',
    ':': ':',
    '<': '<',
    '>': '>',
    '"': '"',
    '/': ' ∕ ',
    '\\': ' ⧵ ',
    '|': '|',
  }
  pathArg = pathArg.trim()
  return pathArg.replace(
    /(\?)|(\*)|(:)|(<)|(>)|(")|(\/)|(\\)|(\|)/g,
    (found) => illegalCharReplacerMapping[found],
  )
}
function removeAllChildNodes(node) {
  while (node.hasChildNodes()) node.firstChild.remove()
}
function observeDirectChildNodes(targetNode, callback) {
  const observer = new MutationObserver((mutationList) => {
    callback(mutationList, observer)
  })
  observer.observe(targetNode, {
    childList: true,
    attributes: false,
    subtree: false,
  })
  return observer
}
function observeAddDirectChildNodes(targetNode, callback) {
  return observeDirectChildNodes(targetNode, (mutationList, observer) => {
    for (let index = 0; index < mutationList.length; index++) {
      const mutation = mutationList[index]
      const isAddNodesMutation = mutation.addedNodes.length > 0
      if (isAddNodesMutation) {
        callback(mutationList, observer)
        break
      }
    }
  })
}

//#endregion
//#region src/mod_page/description_tab.ts
function isDescriptionTab() {
  return getCurrentTab() === 'description'
}
function getTabDescriptionContainerDiv() {
  return getTabContentDiv().querySelector(
    ':scope > div.container.tab-description',
  )
}
function getModHistoryDiv() {
  const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  return tabDescriptionContainerDiv
    ? tabDescriptionContainerDiv.querySelector(':scope > div.modhistory')
    : null
}
function getBriefOverview() {
  const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  if (!tabDescriptionContainerDiv) return null
  const briefOverviewP = tabDescriptionContainerDiv.querySelector(
    ':scope > p:nth-of-type(1)',
  )
  return briefOverviewP.innerText.trimEnd()
}
function getActionsUl() {
  const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  return tabDescriptionContainerDiv
    ? tabDescriptionContainerDiv.querySelector(':scope > ul.actions')
    : null
}
function getShareButtonAnchor() {
  const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  return tabDescriptionContainerDiv
    ? tabDescriptionContainerDiv.querySelector(':scope > a.button-share')
    : 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 = new Map()
  const children = descriptionDl.children
  for (let i = 0; i < children.length; i = i + 2)
    descriptionDtDdMap.set(children[i], children[i + 1])
  return descriptionDtDdMap
}
function getModsRequiringThisDiv() {
  const descriptionDtDdMap = getDescriptionDtDdMap()
  if (!descriptionDtDdMap) return null
  for (const [dt, dd] of descriptionDtDdMap)
    if (dt.innerText.trim().startsWith('Requirements')) {
      const tabbedBlockDivs = dd.querySelectorAll(':scope > div.tabbed-block')
      for (const tabbedBlockDiv of tabbedBlockDivs) {
        const text = tabbedBlockDiv.querySelector(
          ':scope > h3:nth-of-type(1)',
        ).innerText
        if (text === 'Mods requiring this file') return tabbedBlockDiv
      }
    }
  return null
}
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) {
        const partTitle = tabbedBlockDiv.querySelector(':scope > h3').innerText
        switch (partTitle) {
          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
}
function getModDescriptionContainerDiv() {
  return getTabContentDiv().querySelector(
    ':scope > div.container.mod_description_container',
  )
}

//#endregion
//#region ../../../../Workspaces/@lyne408/userscript_lib/index.ts
function setValue(name$1, value) {
  return GM_setValue(name$1, value)
}
function getValue(name$1) {
  return GM_getValue(name$1)
}
function downloadFile(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,
    })
  })
}
async function downloadFiles(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 src/mod_page/tabs_shared_actions.ts
function setTabsDivAsTopElement() {
  const modTabsUl = getModTabsUl()
  modTabsUl.style.height = '45px'
  bodyElement.classList.remove('new-head')
  bodyElement.style.margin = '0 auto'
  bodyElement.style.maxWidth = mainContentMaxWidth
  const tabsDivClone = getTabsDiv().cloneNode(true)
  removeAllChildNodes(bodyElement)
  bodyElement.appendChild(tabsDivClone)
}
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}`
}
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')
}
function createShowAllGalleryThumbnailsComponent() {
  const button = createActionComponent('Show All Thumbnails')
  button.addEventListener('click', () => {
    const thumbGalleryUl = getThumbnailGalleryUl()
    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 component = getThumbnailComponent(thumbLi)
      const { figure, anchor, img } = component
      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'
    }
  })
  return button
}
/**
 * 默认是选中的
 * 返回的对象的属性 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 button = createActionComponent('Select All Images')
  button.addEventListener('click', () => {
    if (!hasSelectAll) {
      for (const component of components) component.checked = true
      hasSelectAll = true
      button.innerText = 'Deselect All Images'
    } else {
      for (const component of components) component.checked = false
      hasSelectAll = false
      button.innerText = 'Select All Images'
    }
  })
  return button
}
/**
 * @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 num = (i + 1).toString().padStart(digits, '0')
      const name$1 = `${relativeDirectory}/${num}_${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()
  const hasGallery = isModUrl(location.href) && modGalleryDiv
  if (!hasGallery) {
    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
    const relativeDirectory = `${getGameName()}/${getModName()} ${getModVersionWithDate()}`
    downloadSelectedImages(
      componentsHasCheckedProperty,
      relativeDirectory,
      () => {
        downloadedCount++
        downloadedCountSpan.innerText = downloadedCount.toString()
        downloadedCount === selectedCount &&
          (messageSpan.innerText = `Done: ${selectedCount}/${selectedCount}`)
      },
      () => {
        failedCount++
        failedCountSpan.innerText = failedCount.toString()
      },
    )
  })
  return fragment
}
function removeFeature() {
  const featureDiv = getFeatureDiv()
  if (!featureDiv) return
  featureDiv.removeAttribute('style')
  featureDiv.querySelector(':scope > div.header-img')?.remove()
  featureDiv.setAttribute('id', 'nofeature')
}
function removeModGallery() {
  getModGalleryDiv()?.remove()
}
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/shared.ts
function isSylin527() {
  const value = getValue('isSylin527')
  return typeof value === 'boolean' ? value : false
}

//#endregion
//#region src/site_shared_actions.ts
function setSectionAsTopElement() {
  bodyElement.classList.remove('new-head')
  bodyElement.style.margin = '0 auto'
  bodyElement.style.maxWidth = mainContentMaxWidth
  const sectionBackup = getSection().cloneNode(true)
  removeAllChildNodes(bodyElement)
  bodyElement.appendChild(sectionBackup)
}
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-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;
    }
    `,
    ++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 = container.querySelectorAll('iframe.youtube_video')
  if (youtubeIframes.length === 0) return
  for (let i = 0; i < youtubeIframes.length; i++) {
    const embedUrl = youtubeIframes[i].getAttribute('src')
    const parts = embedUrl.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 = youtubeIframes[i].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 simplifyComment() {
  const commentContainerComponent = getCommentContainerComponent()
  if (!commentContainerComponent) return
  const {
    headNavDiv,
    bottomNavDiv,
    stickyCommentLis,
    authorCommentLis,
    otherCommentLis,
  } = commentContainerComponent
  headNavDiv.remove()
  bottomNavDiv.remove()
  for (const stickyCommentLi of stickyCommentLis) {
    const commentContentTextDiv = getCommentContentTextDiv(stickyCommentLi)
    simplifyDescriptionContent(commentContentTextDiv)
  }
  for (const authorCommentLi of authorCommentLis) {
    const commentContentTextDiv = getCommentContentTextDiv(authorCommentLi)
    simplifyDescriptionContent(commentContentTextDiv)
  }
  otherCommentLis.forEach((nonAuthorCommentLi) => nonAuthorCommentLi.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)
  const sheet = newStyle.sheet
  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
}
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 = 'btn inline-flex'
  removeButton.style.textTransform = 'unset'
  removeButton.style.verticalAlign = 'super'
  removeButton.style.borderRadius = '3px'
  removeButton.innerText = 'Remove'
  overPrimaryComponent(removeButton)
  const { element, categoryHeaderDiv, sortByContainerDiv } = oldFilesComponent
  categoryHeaderDiv.insertBefore(removeButton, sortByContainerDiv)
  categoryHeaderDiv.insertBefore(document.createTextNode(' '), removeButton)
  removeButton.addEventListener('click', () => {
    element.remove()
  })
  containerManager.addInline(removeButton)
}
function simplifyFilesTab() {
  removePremiumBanner()
  removeAllSortBys()
  simplifyAllFileHeaders()
  simplifyAllFileDescriptions$1()
  getFileArchiveSection()?.remove()
  containerManager.hideAll()
  setTabsDivAsTopElement()
}
function createSimplifyFilesTabComponent() {
  const button = createActionComponent('Simplify Files Tab')
  button.addEventListener('click', () => {
    simplifyFilesTab()
  })
  isFilesTab()
    ? insertRemoveOldFilesComponent()
    : (button.style.display = 'none')
  controlComponentDisplayAfterClickingTab(button, async (clickedTab) => {
    const bFilesTab =
      clickedTab === 'files' &&
      (await clickedTabContentLoaded()) === 0 &&
      isFilesTab()
    bFilesTab && insertRemoveOldFilesComponent()
    return bFilesTab
  })
  return button
}

//#endregion
//#region src/mod_page/archived_files_tab_actions.ts
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 resultJson = await getFiles(gameDomainName, modId, apiKey)
  const { files } = resultJson
  const map = new Map()
  for (const { file_id, category_id, file_name } of files)
    category_id === 7 && map.set(file_id, file_name)
  return map
}
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 button = createActionComponent('Simplify Archived Files Tab')
  button.addEventListener('click', async () => {
    removePremiumBanner()
    removeAllSortBys()
    simplifyAllFileHeaders()
    await simplifyAllFileDescriptions()
    containerManager.hideAll()
    setTabsDivAsTopElement()
  })
  !isArchivedFilesTab() && (button.style.display = 'none')
  controlComponentDisplayAfterClickingTab(
    button,
    async (clickedTab) =>
      clickedTab === 'files' &&
      (await clickedTabContentLoaded()) === 0 &&
      isArchivedFilesTab(),
  )
  return button
}

//#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')
}

//#endregion
//#region src/article_page/article_page_actions.ts
function simplifyArticlePage() {
  simplifyDescriptionContent(getArticleElement())
  titleElement.innerText = getPageTitle()
  removeModActions()
  setSectionAsTopElement()
}
function createSimplifyArticlePageComponent() {
  const button = createActionComponent('Simplify Article Page')
  button.addEventListener('click', () => {
    simplifyArticlePage()
    containerManager.hideAll()
  })
  return button
}

//#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()
  getModHistoryDiv()?.remove()
  getShareButtonAnchor()?.remove()
  const descriptionDl = getDescriptionDl()
  if (descriptionDl) {
    getModsRequiringThisDiv()?.remove()
    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)
}
function setLocationToModUrlIfDescriptionTab() {
  const modUrl = generateModUrl(getGameDomainName(), getModId())
  getCurrentTab() === 'description' && history.replaceState(null, '', modUrl)
  clickTabLi(async (clickedTab) => {
    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')
}
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
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)
}
function isForumTopicTab() {
  return getCurrentTab() === 'forum' && getTopicsTabH2() === null
}

//#endregion
//#region src/mod_page/forum_tab_actions.ts
function modTopicsDivAddedDirectChildNodes() {
  return new Promise((resolve) => {
    observeAddDirectChildNodes(getModTopicsDiv(), (mutationList, observer) => {
      console.log('modTopicsDiv add childNodes mutationList:', mutationList)
      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 button = createActionComponent('Simplify Posts Tab')
  button.addEventListener('click', () => {
    simplifyComment()
    containerManager.hideAll()
    setTabsDivAsTopElement()
  })
  ;(!isPostsTab() || (isPostsTab() && !hasStickyOrAuthorComments())) &&
    (button.style.display = 'none')
  controlComponentDisplayAfterClickingTab(
    button,
    async (clickedTab) =>
      clickedTab === 'posts' &&
      (await clickedTabContentLoaded()) === 0 &&
      hasStickyOrAuthorComments(),
  )
  return button
}

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

//#endregion
//#region src/mod_page/mod_page_actions.ts
function simplifyModPage() {
  removeFeature()
  removeModActions()
  removeModGallery()
  simplifyTabDescription()
  simplifyModDescription()
  titleElement.innerText = `${getModName()} ${getModVersionWithDate()}`
  containerManager.hideAll()
  setSectionAsTopElement()
}
function createSimplifyModPageComponent() {
  const button = createActionComponent('Simplify Mod Page')
  button.addEventListener('click', () => {
    simplifyModPage()
  })
  !isDescriptionTab() && (button.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) => {
    button.style.display = 'none'
    clickedTab === 'description' &&
      (await clickedTabContentLoaded()) === 0 &&
      isDescriptionTab() &&
      (button.style.display = 'block')
  })
  return button
}

//#endregion
//#region src/userscripts/userscripts_shared.ts
const isProduction = true
function getAuthor() {
  return 'sylin527'
}

//#endregion
//#region src/userscripts/mod_documentation_utility/userscript.header.ts
const name = `Mod Documentations Utility by ${getAuthor()}${
  isProduction ? '' : ' Development Version'
}`
const version = `0.2.3.20250615`

//#endregion
//#region src/userscripts/mod_documentation_utility/userscript.main.ts
/**

* 仅初始化 `apikey` 为 `''`

*

* 没有初始化 `isSylin527`

*/
function initStorage() {
  const apiKey = getApiKey()
  !apiKey && setValue('apikey', '')
}
function initModDocumentationUtility() {
  initStorage()
  const href = location.href
  const actionContainer = insertActionContainer()
  if (isModUrl(href)) {
    tweakTitleAfterClickingTab()
    setLocationToModUrlIfDescriptionTab()
    actionContainer.append(
      createCopyModNameAndVersionComponent(),
      createShowAllGalleryThumbnailsComponent(),
    )
    if (isSylin527()) {
      actionContainer.appendChild(createDownloadSelectedImagesComponent())
      hideModActionsSylin527NotUse()
    }
    actionContainer.append(
      createSimplifyModPageComponent(),
      createSimplifyFilesTabComponent(),
      createSimplifyArchivedFilesTabComponent(),
      createSimplifyPostsTabComponent(),
      createSimplifyForumTopicTabComponent(),
    )
    tweakTitleIfFileTab()
    tweakTitleIfArchivedFilesTab()
  } else if (isArticleUrl(href))
    actionContainer.appendChild(createSimplifyArticlePageComponent())
}
function main() {
  initModDocumentationUtility()
  const scriptInfo = `Load userscript: ${name} ${version}`
  console.log('%c [Info] ' + scriptInfo, 'color: green')
  console.log('%c [Info] URL: ' + location.href, 'color: green')
}
main()

//#endregion