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.

  1. // ==UserScript==
  2. // @name Mod Documentations Utility by sylin527
  3. // @namespace https://www.nexusmods.com
  4. // @match https://www.nexusmods.com/*/mods/*
  5. // @match https://www.nexusmods.com/*/articles/*
  6. // @run-at document-idle
  7. // @version 0.2.3.20250615
  8. // @license GPLv3
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_download
  12. // @grant unsafeWindow
  13. // @icon https://www.nexusmods.com/favicon.ico
  14. // @author sylin527
  15. // @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.
  16. // ==/UserScript==
  17.  
  18. //#region src/site_shared.ts
  19. function getNexusmodsUrl() {
  20. return `https://www.nexusmods.com`
  21. }
  22. function getMainContentDiv() {
  23. return document.getElementById('mainContent')
  24. }
  25. /**
  26. * base info + description tab
  27. *
  28. * Contains game id and mod id.
  29. * 在 mod url, 有 `<section id="section" class="modpage" data-game-id="1704" data-mod-id="1089">`
  30. * 在 nexusmods url, 有 `<section class="static homeindex">`
  31. */
  32. let _section = null
  33. function getSection() {
  34. !_section &&
  35. (_section = getMainContentDiv().querySelector(':scope > section'))
  36. return _section
  37. }
  38. /**
  39. * 比如 mod, article 的标题 div
  40. *
  41. * `div#pagetitle`
  42. */
  43. let _pageTitleDiv = null
  44. function getPageTitleDiv() {
  45. _pageTitleDiv ||= _pageTitleDiv = document.getElementById('pagetitle')
  46. return _pageTitleDiv
  47. }
  48. function getPageTitle() {
  49. return getPageTitleDiv().querySelector(':scope > h1').innerText
  50. }
  51. /**
  52. * 比如 mod, article 的 endorse 容器
  53. *
  54. * `div#pagetitle > ul.modactions`
  55. */
  56. let _modActionsUl = null
  57. function getModActionsUl() {
  58. _modActionsUl ||= _modActionsUl = getPageTitleDiv().querySelector(
  59. ':scope > ul.modactions',
  60. )
  61. return _modActionsUl
  62. }
  63. function getCommentContainerDiv() {
  64. return document.getElementById('comment-container')
  65. }
  66. function getCommentContainerComponent(
  67. commentContainerDiv = getCommentContainerDiv(),
  68. ) {
  69. if (!commentContainerDiv) return null
  70. const headNavDiv = commentContainerDiv.querySelector(':scope > div.head-nav')
  71. const bottomNavDiv = commentContainerDiv.querySelector(
  72. ':scope > div.bottom-nav',
  73. )
  74. const allCommentLis = commentContainerDiv.querySelectorAll(
  75. ':scope > ol > li.comment',
  76. )
  77. const stickyCommentLis = []
  78. const authorCommentLis = []
  79. const otherCommentLis = []
  80. for (const commentLi of allCommentLis) {
  81. const classList = commentLi.classList
  82. if (classList.contains('comment-sticky')) stickyCommentLis.push(commentLi)
  83. else if (classList.contains('comment-author'))
  84. authorCommentLis.push(commentLi)
  85. else otherCommentLis.push(commentLi)
  86. }
  87. return {
  88. commentContainerDiv,
  89. get commentCount() {
  90. return parseInt(
  91. document
  92. .getElementById('comment-count')
  93. .getAttribute('data-comment-count'),
  94. )
  95. },
  96. headNavDiv,
  97. bottomNavDiv,
  98. stickyCommentLis,
  99. authorCommentLis,
  100. otherCommentLis,
  101. }
  102. }
  103. function getCommentContentTextDiv(commentLi) {
  104. return commentLi.querySelector(
  105. ':scope > div.comment-content > div.comment-content-text',
  106. )
  107. }
  108.  
  109. //#endregion
  110. //#region src/api/mod_api.ts
  111. async function getFiles(gameDomainName, modId, apiKey) {
  112. const res = await fetch(
  113. `https://api.nexusmods.com/v1/games/${gameDomainName}/mods/${modId}/files.json`,
  114. { headers: { apikey: apiKey } },
  115. )
  116. return await res.json()
  117. }
  118. function generateModUrl(gameDomainName, modId) {
  119. return `https://www.nexusmods.com/${gameDomainName}/mods/${modId}`
  120. }
  121. function generateFileUrl(gameDomainName, modId, fileId) {
  122. return `${getNexusmodsUrl()}/${gameDomainName}/mods/${modId}?tab=files&file_id=${fileId}`
  123. }
  124.  
  125. //#endregion
  126. //#region src/ui.ts
  127. const { body: bodyElement, head: headElement } = document
  128. const titleElement = headElement.querySelector('title')
  129. const primaryColor = '#8197ec'
  130. const primaryHoverColor = '#a4b7ff'
  131. const highlightColor = '#d98f40'
  132. const highlightHoverColor = '#ce7f45'
  133. const mainContentMaxWidth = '1340px'
  134. function overPrimaryComponent(element) {
  135. const style = element.style
  136. element.addEventListener('mouseover', function () {
  137. style.backgroundColor = primaryHoverColor
  138. })
  139. element.addEventListener('mouseleave', function () {
  140. style.backgroundColor = primaryColor
  141. })
  142. }
  143. const containerManager = {
  144. containers: [],
  145. removeAll() {
  146. this.containers.forEach(({ element }) => element.remove())
  147. },
  148. showAll() {
  149. this.containers.forEach((container) => container.show())
  150. },
  151. hideAll() {
  152. this.containers.forEach((container) => container.hide())
  153. },
  154. add(container) {
  155. this.containers.push(container)
  156. },
  157. addBlock(element) {
  158. this.containers.push({
  159. element,
  160. show: () => (element.style.display = 'block'),
  161. hide: () => (element.style.display = 'none'),
  162. })
  163. },
  164. addInline(element) {
  165. this.containers.push({
  166. element,
  167. show: () => (element.style.display = 'inline'),
  168. hide: () => (element.style.display = 'none'),
  169. })
  170. },
  171. }
  172. function getActionContainerId() {
  173. return 'sylin527ActionContainer'
  174. }
  175. function insertActionContainerStyle() {
  176. const newStyle = document.createElement('style')
  177. headElement.appendChild(newStyle)
  178. const sheet = newStyle.sheet
  179. const containerId = getActionContainerId()
  180. /**
  181. * 设 `top: 56px` 是因 Mod page 的 `<header>` 的 `height: 56px`
  182. * 设 `background: transparent;` 以避免突兀
  183. */
  184. let ruleIndex = sheet.insertRule(`
  185. #${containerId} {
  186. display: block;
  187. position: fixed;
  188. right: 5px;
  189. top: 56px;
  190. font-size: 13px;
  191. font-weight: 400;
  192. background: transparent;
  193. z-index: 999;
  194. direction: rtl;
  195. }
  196. `)
  197. sheet.insertRule(
  198. `
  199. #${containerId} > *{
  200. display: block;
  201. margin-top: 5px;
  202. }
  203. `,
  204. ++ruleIndex,
  205. )
  206. sheet.insertRule(
  207. `
  208. #${containerId} button.action {
  209. padding: 8px;
  210. cursor: pointer;
  211. background: ${primaryColor};
  212. border-radius: 3px;
  213. border: 1px solid ${primaryHoverColor};
  214. color: #eaeaea;
  215. `,
  216. ++ruleIndex,
  217. )
  218. sheet.insertRule(
  219. `
  220. #${containerId} button.action:hover {
  221. background: ${primaryHoverColor};
  222. `,
  223. ++ruleIndex,
  224. )
  225. sheet.insertRule(
  226. `
  227. #${containerId} span.message {
  228. background-color: rgba(51, 51, 51, 0.5);
  229. color: rgb(255, 47, 151);
  230. padding: 8px;
  231. border-radius: 4px;
  232. display: inline;
  233. margin: 0 7px;
  234. visibility: hidden;
  235. }
  236. `,
  237. ++ruleIndex,
  238. )
  239. }
  240. function createActionContainer() {
  241. const containerId = getActionContainerId()
  242. let container = document.getElementById(containerId)
  243. if (null === container) {
  244. container = document.createElement('div')
  245. container.setAttribute('id', containerId)
  246. container.style.zIndex = '999'
  247. insertActionContainerStyle()
  248. }
  249. return container
  250. }
  251. let _actionContainer = null
  252. function insertActionContainer() {
  253. if (!_actionContainer) {
  254. _actionContainer = createActionContainer()
  255. bodyElement.append(_actionContainer)
  256. containerManager.addBlock(_actionContainer)
  257. }
  258. return _actionContainer
  259. }
  260. function createActionComponent(name$1) {
  261. const actionButton = document.createElement('button')
  262. actionButton.innerText = name$1
  263. actionButton.className = 'action'
  264. return actionButton
  265. }
  266. function createActionWithMessageComponent(name$1) {
  267. const containerDiv = document.createElement('div')
  268. const actionButton = createActionComponent(name$1)
  269. const messageSpan = document.createElement('span')
  270. messageSpan.className = 'message'
  271. containerDiv.append(actionButton, messageSpan)
  272. return {
  273. element: containerDiv,
  274. actionButton,
  275. messageSpan,
  276. }
  277. }
  278.  
  279. //#endregion
  280. //#region src/mod_page/tabs_shared.ts
  281. function getModUrlRegExp() {
  282. return /^((https|http):\/\/(www.)?nexusmods.com\/[a-z0-9]+\/mods\/[0-9]+)/
  283. }
  284. function isModUrl(url) {
  285. return getModUrlRegExp().test(url)
  286. }
  287. let _gameDomainName = null
  288. function getGameDomainName() {
  289. _gameDomainName ||= _gameDomainName = new URL(location.href).pathname.split(
  290. '/',
  291. )[1]
  292. return _gameDomainName
  293. }
  294. let _modId = null
  295. function getModId() {
  296. _modId ||= _modId = parseInt(getSection().getAttribute('data-mod-id'))
  297. return _modId
  298. }
  299. function getFeaturedBelowDiv() {
  300. return getSection().querySelector(
  301. ':scope > div.wrap > div:nth-of-type(2).wrap',
  302. )
  303. }
  304. let _breadcrumbUl = null
  305. function getBreadcrumbUl() {
  306. _breadcrumbUl ||= _breadcrumbUl = document.getElementById('breadcrumb')
  307. return _breadcrumbUl
  308. }
  309. let _gameName = null
  310. function getGameName() {
  311. _gameName ||= getBreadcrumbUl().querySelector(
  312. ':scope > li:nth-of-type(2)',
  313. ).innerText
  314. return _gameName
  315. }
  316. /**
  317.  
  318. * `div#feature`
  319.  
  320. *
  321.  
  322. * 如果 modder 设定了 feature, 则有 `div#feature`,
  323.  
  324. * 反之没有 `div#feature`, 有 `div#nofeature`
  325.  
  326. */
  327. let _featureDiv = null
  328. function getFeatureDiv() {
  329. _featureDiv ||= _featureDiv = document.getElementById('feature')
  330. return _featureDiv
  331. }
  332. function getModStatsUl() {
  333. return getPageTitleDiv().querySelector(':scope > ul.stats')
  334. }
  335. function getModActionsComponent() {
  336. const modActionsUl = getModActionsUl()
  337. return {
  338. element: modActionsUl,
  339. get addMediaLi() {
  340. return document.getElementById('action-media')
  341. },
  342. get trackLi() {
  343. return modActionsUl.querySelector(':scope > li[id^=action-track]')
  344. },
  345. get untrackLi() {
  346. return modActionsUl.querySelector(':scope > li[id^=action-untrack]')
  347. },
  348. get downloadLabelLi() {
  349. return modActionsUl.querySelector(':scope > li.dllabel')
  350. },
  351. get vortexLi() {
  352. return document.getElementById('action-nmm')
  353. },
  354. get manualDownloadLi() {
  355. return document.getElementById('action-manual')
  356. },
  357. }
  358. }
  359. let _modName = null
  360. function getModName() {
  361. if (!_modName) {
  362. /**
  363. * 如 `<meta property="og:title" content="Aspens Ablaze">`
  364. * Aspens Ablaze 是 mod 名
  365. */
  366. const meta = headElement.querySelector(`meta[property="og:title"]`)
  367. if (meta) _modName = meta.getAttribute('content')
  368. else
  369. _modName = getBreadcrumbUl().querySelector(
  370. ':scope > li:last-child',
  371. ).innerText
  372. }
  373. return _modName
  374. }
  375. /**
  376.  
  377. * `div#pagetitle > ul.stats.clearfix > li.stat-version > div.statitem > div.stat`
  378.  
  379. */
  380. let _modVersionDiv = null
  381. function getModVersionDiv() {
  382. _modVersionDiv ||= _modVersionDiv = getModStatsUl().querySelector(
  383. ':scope > li.stat-version > div.statitem > div.stat',
  384. )
  385. return _modVersionDiv
  386. }
  387. /**
  388.  
  389. * Mod version can be empty string???
  390.  
  391. */
  392. let _modVersion = null
  393. function getModVersion() {
  394. if (!_modVersion) {
  395. _modVersion = getModVersionDiv().innerText.trim()
  396. if (_modVersion !== '' && parseInt(_modVersion).toString() === _modVersion)
  397. _modVersion = 'v' + _modVersion
  398. }
  399. return _modVersion
  400. }
  401. function getFileInfoDiv() {
  402. return document.getElementById('fileinfo')
  403. }
  404. function getModGalleryDiv() {
  405. return document.getElementById('sidebargallery')
  406. }
  407. function getThumbnailGalleryUl() {
  408. const modGalleryDiv = getModGalleryDiv()
  409. return modGalleryDiv
  410. ? modGalleryDiv.querySelector(':scope > ul.thumbgallery')
  411. : null
  412. }
  413. function getThumbnailComponent(thumbnailLi) {
  414. return {
  415. element: thumbnailLi,
  416. get figure() {
  417. return thumbnailLi.querySelector(':scope > figure')
  418. },
  419. get anchor() {
  420. return this.figure.querySelector(':scope > a')
  421. },
  422. get img() {
  423. return this.anchor.querySelector(':scope > img')
  424. },
  425. originalImageSrc: thumbnailLi.getAttribute('data-src'),
  426. title: thumbnailLi.getAttribute('data-sub-html'),
  427. src: thumbnailLi.getAttribute(' data-exthumbimage'),
  428. }
  429. }
  430. let _modVersionWithDate = null
  431. function getModVersionWithDate() {
  432. if (!_modVersionWithDate) {
  433. const dateTimeElement = getFileInfoDiv().querySelector(
  434. ':scope > div.timestamp:nth-of-type(1) > time',
  435. )
  436. const date = new Date(
  437. parseInt(dateTimeElement.getAttribute('data-date') + '000'),
  438. )
  439. _modVersionWithDate = `${getModVersion()} (${date
  440. .getFullYear()
  441. .toString()
  442. .substring(2)}.${date.getMonth() + 1}.${date.getDate()})`
  443. }
  444. return _modVersionWithDate
  445. }
  446. function getTabsDiv() {
  447. return getFeaturedBelowDiv().querySelector(
  448. ':scope > div:nth-of-type(2) > div.tabs',
  449. )
  450. }
  451. let _modTabsUl = null
  452. function getModTabsUl() {
  453. _modTabsUl ||= _modTabsUl = getTabsDiv().querySelector(':scope > ul.modtabs')
  454. return _modTabsUl
  455. }
  456. /**
  457.  
  458. * `div.tabcontent.tabcontent-mod-page`
  459.  
  460. *
  461.  
  462. * 设 `tabContentDiv` 为 `div.tabcontent.tabcontent-mod-page`
  463.  
  464. * 切换 tab 时不会刷新 `tabContentDiv`,
  465.  
  466. * 会修改 `tabContentDiv` 的 `innerHTML`
  467.  
  468. */
  469. let _tabContentDiv = null
  470. function getTabContentDiv() {
  471. return (_tabContentDiv ||= _tabContentDiv =
  472. bodyElement.querySelector('div.tabcontent.tabcontent-mod-page'))
  473. }
  474. function getCurrentTab() {
  475. const modTabsUl = getModTabsUl()
  476. const tabSpan = modTabsUl.querySelector(
  477. ':scope > li > a.selected > span.tab-label',
  478. )
  479. return tabSpan.innerText.toLowerCase()
  480. }
  481. function getTabFromTabLi(tabLi) {
  482. const tabSpan = tabLi.querySelector(
  483. ':scope > a[data-target] > span.tab-label',
  484. )
  485. return tabSpan.innerText.toLowerCase()
  486. }
  487. function clickTabLi(callback) {
  488. const modTabsUl = getModTabsUl()
  489. const tabLis = modTabsUl.querySelectorAll(':scope > li[id^=mod-page-tab]')
  490. for (const tabLi of tabLis)
  491. tabLi.addEventListener('click', (event) => {
  492. callback(getTabFromTabLi(tabLi), event)
  493. })
  494. }
  495.  
  496. //#endregion
  497. //#region src/mod_page/files_tab.ts
  498. function isFilesTab() {
  499. return (
  500. getCurrentTab() === 'files' &&
  501. getModFilesDiv() !== null &&
  502. getArchivedFilesContainerDiv() === null
  503. )
  504. }
  505. function getModFilesDiv() {
  506. return document.getElementById('mod_files')
  507. }
  508. function getPremiumBannerDiv() {
  509. const tabContentDiv = getTabContentDiv()
  510. return tabContentDiv.querySelector('div.premium-banner.container')
  511. }
  512. function getAllSortByDivs() {
  513. const modFilesDiv = getModFilesDiv()
  514. return modFilesDiv
  515. ? modFilesDiv.querySelectorAll(
  516. 'div.file-category-header > div:nth-of-type(1)',
  517. )
  518. : null
  519. }
  520. function getAllFileHeaderDts() {
  521. const modFilesDiv = getModFilesDiv()
  522. return modFilesDiv ? modFilesDiv.querySelectorAll('dl.accordion > dt') : null
  523. }
  524. function getAllFileDescriptionDds() {
  525. const modFilesDiv = getModFilesDiv()
  526. return modFilesDiv ? modFilesDiv.querySelectorAll('dl.accordion > dd') : null
  527. }
  528. function getDownloadButtonContainerDiv(fileDescriptionDd) {
  529. return fileDescriptionDd.querySelector('div.tabbed-block:nth-of-type(2)')
  530. }
  531. function getFileId(headerDtOrDescriptionDd) {
  532. return parseInt(headerDtOrDescriptionDd.getAttribute('data-id'))
  533. }
  534. function getFileDescriptionDiv(fileDescriptionDd) {
  535. return fileDescriptionDd.querySelector('div.files-description')
  536. }
  537. function getFileDescriptionComponent(fileDescriptionDd) {
  538. const fileId = getFileId(fileDescriptionDd)
  539. const fileDescriptionDiv = getFileDescriptionDiv(fileDescriptionDd)
  540. const downloadButtonContainerDiv =
  541. getDownloadButtonContainerDiv(fileDescriptionDd)
  542. const previewFileDiv = fileDescriptionDd.querySelector(
  543. 'div.tabbed-block:last-child',
  544. )
  545. const realFilename = previewFileDiv
  546. .querySelector('a')
  547. .getAttribute('data-url')
  548. downloadButtonContainerDiv.querySelector('ul > li:last-child > a')
  549. return {
  550. fileId,
  551. fileDescriptionDiv,
  552. downloadButtonContainerDiv,
  553. previewFileDiv,
  554. realFilename,
  555. }
  556. }
  557. function getOldFilesComponent() {
  558. const element = document.getElementById('file-container-old-files')
  559. if (!element) return null
  560. const categoryHeaderDiv = element.querySelector(
  561. ':scope > div.file-category-header',
  562. )
  563. return {
  564. element,
  565. categoryHeaderDiv,
  566. get headerH2() {
  567. return categoryHeaderDiv.querySelector(':scope > h2:first-child')
  568. },
  569. get sortByContainerDiv() {
  570. return categoryHeaderDiv.querySelector(':scope > div:last-child')
  571. },
  572. }
  573. }
  574. function getFileArchiveSection() {
  575. return document.getElementById('files-tab-footer')
  576. }
  577.  
  578. //#endregion
  579. //#region src/mod_page/archived_files_tab.ts
  580. function isArchivedFilesUrl(url) {
  581. const searchParams = new URL(url).searchParams
  582. return (
  583. isModUrl(url) &&
  584. searchParams.get('tab') === 'files' &&
  585. searchParams.get('category') === 'archived'
  586. )
  587. }
  588. function getArchivedFilesContainerDiv() {
  589. return document.getElementById('file-container-archived-files')
  590. }
  591. function isArchivedFilesTab() {
  592. return (
  593. getCurrentTab() === 'files' &&
  594. getModFilesDiv() !== null &&
  595. getArchivedFilesContainerDiv() !== null
  596. )
  597. }
  598.  
  599. //#endregion
  600. //#region src/util.ts
  601. function replaceIllegalChars(pathArg) {
  602. const illegalCharReplacerMapping = {
  603. '?': '?',
  604. '*': '*',
  605. ':': ':',
  606. '<': '<',
  607. '>': '>',
  608. '"': '"',
  609. '/': ' ∕ ',
  610. '\\': ' ⧵ ',
  611. '|': '|',
  612. }
  613. pathArg = pathArg.trim()
  614. return pathArg.replace(
  615. /(\?)|(\*)|(:)|(<)|(>)|(")|(\/)|(\\)|(\|)/g,
  616. (found) => illegalCharReplacerMapping[found],
  617. )
  618. }
  619. function removeAllChildNodes(node) {
  620. while (node.hasChildNodes()) node.firstChild.remove()
  621. }
  622. function observeDirectChildNodes(targetNode, callback) {
  623. const observer = new MutationObserver((mutationList) => {
  624. callback(mutationList, observer)
  625. })
  626. observer.observe(targetNode, {
  627. childList: true,
  628. attributes: false,
  629. subtree: false,
  630. })
  631. return observer
  632. }
  633. function observeAddDirectChildNodes(targetNode, callback) {
  634. return observeDirectChildNodes(targetNode, (mutationList, observer) => {
  635. for (let index = 0; index < mutationList.length; index++) {
  636. const mutation = mutationList[index]
  637. const isAddNodesMutation = mutation.addedNodes.length > 0
  638. if (isAddNodesMutation) {
  639. callback(mutationList, observer)
  640. break
  641. }
  642. }
  643. })
  644. }
  645.  
  646. //#endregion
  647. //#region src/mod_page/description_tab.ts
  648. function isDescriptionTab() {
  649. return getCurrentTab() === 'description'
  650. }
  651. function getTabDescriptionContainerDiv() {
  652. return getTabContentDiv().querySelector(
  653. ':scope > div.container.tab-description',
  654. )
  655. }
  656. function getModHistoryDiv() {
  657. const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  658. return tabDescriptionContainerDiv
  659. ? tabDescriptionContainerDiv.querySelector(':scope > div.modhistory')
  660. : null
  661. }
  662. function getBriefOverview() {
  663. const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  664. if (!tabDescriptionContainerDiv) return null
  665. const briefOverviewP = tabDescriptionContainerDiv.querySelector(
  666. ':scope > p:nth-of-type(1)',
  667. )
  668. return briefOverviewP.innerText.trimEnd()
  669. }
  670. function getActionsUl() {
  671. const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  672. return tabDescriptionContainerDiv
  673. ? tabDescriptionContainerDiv.querySelector(':scope > ul.actions')
  674. : null
  675. }
  676. function getShareButtonAnchor() {
  677. const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  678. return tabDescriptionContainerDiv
  679. ? tabDescriptionContainerDiv.querySelector(':scope > a.button-share')
  680. : null
  681. }
  682. function getDescriptionDl() {
  683. const tabDescriptionContainerDiv = getTabDescriptionContainerDiv()
  684. return tabDescriptionContainerDiv
  685. ? tabDescriptionContainerDiv.querySelector(
  686. ':scope > div.accordionitems > dl.accordion',
  687. )
  688. : null
  689. }
  690. function getDescriptionDtDdMap() {
  691. const descriptionDl = getDescriptionDl()
  692. if (!descriptionDl) return null
  693. const descriptionDtDdMap = new Map()
  694. const children = descriptionDl.children
  695. for (let i = 0; i < children.length; i = i + 2)
  696. descriptionDtDdMap.set(children[i], children[i + 1])
  697. return descriptionDtDdMap
  698. }
  699. function getModsRequiringThisDiv() {
  700. const descriptionDtDdMap = getDescriptionDtDdMap()
  701. if (!descriptionDtDdMap) return null
  702. for (const [dt, dd] of descriptionDtDdMap)
  703. if (dt.innerText.trim().startsWith('Requirements')) {
  704. const tabbedBlockDivs = dd.querySelectorAll(':scope > div.tabbed-block')
  705. for (const tabbedBlockDiv of tabbedBlockDivs) {
  706. const text = tabbedBlockDiv.querySelector(
  707. ':scope > h3:nth-of-type(1)',
  708. ).innerText
  709. if (text === 'Mods requiring this file') return tabbedBlockDiv
  710. }
  711. }
  712. return null
  713. }
  714. function getPermissionDescriptionComponent() {
  715. const descriptionDtDdMap = getDescriptionDtDdMap()
  716. if (!descriptionDtDdMap) return null
  717. for (const [dt, dd] of descriptionDtDdMap)
  718. if (dt.innerText.trim().startsWith('Permissions and credits')) {
  719. const tabbedBlockDivs = dd.querySelectorAll(':scope > div.tabbed-block')
  720. let permissionDiv = null,
  721. authorNotesDiv = null,
  722. authorNotesContentP = null,
  723. fileCreditsDiv = null,
  724. fileCreditsContentP = null,
  725. donationDiv = null
  726. for (const tabbedBlockDiv of tabbedBlockDivs) {
  727. const partTitle = tabbedBlockDiv.querySelector(':scope > h3').innerText
  728. switch (partTitle) {
  729. case 'Credits and distribution permission': {
  730. permissionDiv = tabbedBlockDiv
  731. break
  732. }
  733. case 'Author notes': {
  734. authorNotesDiv = tabbedBlockDiv
  735. authorNotesContentP = authorNotesDiv.querySelector(':scope > p')
  736. break
  737. }
  738. case 'File credits': {
  739. fileCreditsDiv = tabbedBlockDiv
  740. fileCreditsContentP = fileCreditsDiv.querySelector(':scope > p')
  741. break
  742. }
  743. case 'Donation Points system': {
  744. donationDiv = tabbedBlockDiv
  745. break
  746. }
  747. }
  748. }
  749. return {
  750. titleDt: dt,
  751. descriptionDd: dd,
  752. permissionDiv,
  753. authorNotesDiv,
  754. authorNotesContentP,
  755. fileCreditsDiv,
  756. fileCreditsContentP,
  757. donationDiv,
  758. }
  759. }
  760. return null
  761. }
  762. function getModDescriptionContainerDiv() {
  763. return getTabContentDiv().querySelector(
  764. ':scope > div.container.mod_description_container',
  765. )
  766. }
  767.  
  768. //#endregion
  769. //#region ../../../../Workspaces/@lyne408/userscript_lib/index.ts
  770. function setValue(name$1, value) {
  771. return GM_setValue(name$1, value)
  772. }
  773. function getValue(name$1) {
  774. return GM_getValue(name$1)
  775. }
  776. function downloadFile(argObj) {
  777. argObj.saveAs = argObj.saveAs ? argObj.saveAs : false
  778. return new Promise((resolve) => {
  779. GM_download({
  780. ...argObj,
  781. onload() {
  782. resolve(Object.assign(argObj, { success: true }))
  783. },
  784. onerror(error) {
  785. resolve(
  786. Object.assign(argObj, {
  787. success: false,
  788. error,
  789. }),
  790. )
  791. },
  792. ontimeout() {
  793. resolve(
  794. Object.assign(argObj, {
  795. success: false,
  796. error: 'timeout',
  797. }),
  798. )
  799. },
  800. onprogress: argObj.onprogress,
  801. })
  802. })
  803. }
  804. async function downloadFiles(argObj) {
  805. argObj.saveAs = argObj.saveAs ? argObj.saveAs : false
  806. argObj.simultaneous = argObj.simultaneous ? argObj.simultaneous : 3
  807. const { items, simultaneous, successEach, failEach, onProgressEach } = argObj
  808. const itemsParts = []
  809. for (let i = 0; i < items.length; i = i + simultaneous)
  810. itemsParts.push(items.slice(i, i + simultaneous))
  811. const successes = []
  812. const fails = []
  813. await Promise.all(
  814. itemsParts.map(async (itemsPart) => {
  815. for (const item of itemsPart) {
  816. const { url, name: name$1 } = item
  817. const downloadResult = await downloadFile({
  818. url,
  819. name: name$1,
  820. ...argObj,
  821. onprogress: (progressRes) => {
  822. typeof onProgressEach === 'function' &&
  823. onProgressEach(item, progressRes)
  824. },
  825. })
  826. if (downloadResult.success) {
  827. successes.push(item)
  828. typeof successEach === 'function' && successEach(item)
  829. } else {
  830. fails.push(item)
  831. typeof failEach === 'function' && failEach(item, downloadResult.error)
  832. }
  833. }
  834. }),
  835. )
  836. return {
  837. successes,
  838. fails,
  839. }
  840. }
  841.  
  842. //#endregion
  843. //#region src/mod_page/tabs_shared_actions.ts
  844. function setTabsDivAsTopElement() {
  845. const modTabsUl = getModTabsUl()
  846. modTabsUl.style.height = '45px'
  847. bodyElement.classList.remove('new-head')
  848. bodyElement.style.margin = '0 auto'
  849. bodyElement.style.maxWidth = mainContentMaxWidth
  850. const tabsDivClone = getTabsDiv().cloneNode(true)
  851. removeAllChildNodes(bodyElement)
  852. bodyElement.appendChild(tabsDivClone)
  853. }
  854. function createCopyModNameAndVersionComponent() {
  855. const { actionButton, messageSpan, element } =
  856. createActionWithMessageComponent('Copy Mod Name And Version')
  857. messageSpan.innerText = 'Copied'
  858. actionButton.addEventListener('click', () => {
  859. navigator.clipboard
  860. .writeText(`${getModName()} ${getModVersionWithDate()}`)
  861. .then(
  862. () => {
  863. messageSpan.style.visibility = 'visible'
  864. setTimeout(() => (messageSpan.style.visibility = 'hidden'), 1e3)
  865. },
  866. () => console.log('%c[Error] Copy failed.', 'color: red'),
  867. )
  868. })
  869. return element
  870. }
  871. /**
  872. * @param currentTab
  873. *
  874. * 因 Firefox 保存书签时, 若书签名包含换行, 直接省略换行符
  875. *
  876. * 这里替换 brief overview 中的换行为空格
  877. */
  878. function tweakTitleInner(currentTab) {
  879. if (currentTab === 'description') {
  880. let briefOverview = getBriefOverview()
  881. briefOverview = briefOverview
  882. ? briefOverview.replaceAll(/\r\n|\n/g, ' ')
  883. : ''
  884. titleElement.innerText = `${getModName()} ${getModVersionWithDate()}: ${briefOverview}`
  885. } else
  886. titleElement.innerText = `${getModName()} ${getModVersionWithDate()} tab=${currentTab}`
  887. }
  888. function tweakTitleAfterClickingTab() {
  889. let oldTab = getCurrentTab()
  890. tweakTitleInner(oldTab)
  891. clickTabLi(async (clickedTab) => {
  892. if (oldTab !== clickedTab) {
  893. if (clickedTab === 'description' && getBriefOverview() === null)
  894. await clickedTabContentLoaded()
  895. oldTab = clickedTab
  896. tweakTitleInner(clickedTab)
  897. }
  898. })
  899. }
  900. function hideModActionsSylin527NotUse() {
  901. const { addMediaLi, downloadLabelLi, vortexLi, manualDownloadLi } =
  902. getModActionsComponent()
  903. addMediaLi && (addMediaLi.style.display = 'none')
  904. downloadLabelLi && (downloadLabelLi.style.display = 'none')
  905. manualDownloadLi && (manualDownloadLi.style.display = 'none')
  906. vortexLi && (vortexLi.style.display = 'none')
  907. }
  908. function createShowAllGalleryThumbnailsComponent() {
  909. const button = createActionComponent('Show All Thumbnails')
  910. button.addEventListener('click', () => {
  911. const thumbGalleryUl = getThumbnailGalleryUl()
  912. thumbGalleryUl.style.height = 'max-content'
  913. thumbGalleryUl.style.width = 'auto'
  914. thumbGalleryUl.style.zIndex = '99999'
  915. const thumbLis = thumbGalleryUl.querySelectorAll(':scope > li.thumb')
  916. for (const thumbLi of thumbLis) {
  917. const component = getThumbnailComponent(thumbLi)
  918. const { figure, anchor, img } = component
  919. thumbLi.style.height = 'auto'
  920. thumbLi.style.width = 'auto'
  921. thumbLi.style.marginBottom = '7px'
  922. figure.style.height = 'auto'
  923. anchor.style.top = '0'
  924. anchor.style.transform = 'unset'
  925. img.style.maxHeight = 'unset'
  926. }
  927. })
  928. return button
  929. }
  930. /**
  931. * 默认是选中的
  932. * 返回的对象的属性 checked 是一个 getter
  933. */
  934. function insertCheckboxToThumbnails() {
  935. const thumbGalleryUl = getThumbnailGalleryUl()
  936. if (!thumbGalleryUl) return null
  937. const componentsWithCheckedProperty = []
  938. const thumbLis = thumbGalleryUl.querySelectorAll(':scope > li.thumb')
  939. for (const thumbLi of thumbLis) {
  940. const component = getThumbnailComponent(thumbLi)
  941. const { figure } = component
  942. const input = document.createElement('input')
  943. input.setAttribute('type', 'checkbox')
  944. input.setAttribute(
  945. 'style',
  946. 'position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; cursor: pointer;',
  947. )
  948. input.addEventListener(
  949. 'click',
  950. (event) => {
  951. event.stopPropagation()
  952. },
  953. { capture: true },
  954. )
  955. figure.appendChild(input)
  956. componentsWithCheckedProperty.push(
  957. Object.defineProperty(component, 'checked', {
  958. get() {
  959. return input.checked
  960. },
  961. set(value) {
  962. input.checked = value
  963. },
  964. }),
  965. )
  966. }
  967. return componentsWithCheckedProperty
  968. }
  969. let hasSelectAll = false
  970. function createSelectAllImagesComponent(components) {
  971. const button = createActionComponent('Select All Images')
  972. button.addEventListener('click', () => {
  973. if (!hasSelectAll) {
  974. for (const component of components) component.checked = true
  975. hasSelectAll = true
  976. button.innerText = 'Deselect All Images'
  977. } else {
  978. for (const component of components) component.checked = false
  979. hasSelectAll = false
  980. button.innerText = 'Select All Images'
  981. }
  982. })
  983. return button
  984. }
  985. /**
  986. * @param components
  987. * @param relativeDirectory will `replaceIllegalChars()`
  988. * @param eachSuccess
  989. * @param eachFail
  990. * @returns
  991. */
  992. function downloadSelectedImages(
  993. components,
  994. relativeDirectory,
  995. eachSuccess,
  996. eachFail,
  997. ) {
  998. const allThumbnailCount = components.length
  999. const digits = allThumbnailCount.toString().length
  1000. const checkedImages = []
  1001. for (let i = 0; i < allThumbnailCount; i++) {
  1002. const { checked, originalImageSrc, title } = components[i]
  1003. if (checked) {
  1004. const extWithDot = originalImageSrc.substring(
  1005. originalImageSrc.lastIndexOf('.'),
  1006. )
  1007. const num = (i + 1).toString().padStart(digits, '0')
  1008. const name$1 = `${relativeDirectory}/${num}_${replaceIllegalChars(
  1009. title,
  1010. )}${extWithDot}`
  1011. checkedImages.push({
  1012. url: originalImageSrc,
  1013. name: name$1,
  1014. })
  1015. }
  1016. }
  1017. return downloadFiles({
  1018. items: checkedImages,
  1019. simultaneous: 3,
  1020. successEach: eachSuccess,
  1021. failEach: eachFail,
  1022. })
  1023. }
  1024. function createDownloadSelectedImagesComponent() {
  1025. const fragment = document.createDocumentFragment()
  1026. const {
  1027. actionButton: downloadButton,
  1028. messageSpan,
  1029. element: downloadDiv,
  1030. } = createActionWithMessageComponent('Download Selected Images')
  1031. fragment.append(downloadDiv)
  1032. const modGalleryDiv = getModGalleryDiv()
  1033. const hasGallery = isModUrl(location.href) && modGalleryDiv
  1034. if (!hasGallery) {
  1035. downloadButton.innerText = 'Download Selected Images (Gallery Not Found)'
  1036. downloadButton.style.display = 'none'
  1037. return fragment
  1038. }
  1039. const componentsHasCheckedProperty = insertCheckboxToThumbnails()
  1040. const selectButton = createSelectAllImagesComponent(
  1041. componentsHasCheckedProperty,
  1042. )
  1043. fragment.insertBefore(selectButton, downloadDiv)
  1044. downloadButton.addEventListener('click', () => {
  1045. messageSpan.style.visibility = 'visible'
  1046. const selectedCount = componentsHasCheckedProperty.filter(
  1047. ({ checked }) => checked,
  1048. ).length
  1049. if (selectedCount === 0) {
  1050. messageSpan.innerText
  1051. return
  1052. }
  1053. const downloadedCountSpan = document.createElement('span')
  1054. downloadedCountSpan.innerText = '0'
  1055. const failedCountSpan = document.createElement('span')
  1056. failedCountSpan.innerText = '0'
  1057. messageSpan.innerText = ''
  1058. messageSpan.append(
  1059. `Selected: ${selectedCount}`,
  1060. ' ',
  1061. 'Downloaded: ',
  1062. downloadedCountSpan,
  1063. ' ',
  1064. 'Failed: ',
  1065. failedCountSpan,
  1066. )
  1067. let downloadedCount = 0
  1068. let failedCount = 0
  1069. const relativeDirectory = `${getGameName()}/${getModName()} ${getModVersionWithDate()}`
  1070. downloadSelectedImages(
  1071. componentsHasCheckedProperty,
  1072. relativeDirectory,
  1073. () => {
  1074. downloadedCount++
  1075. downloadedCountSpan.innerText = downloadedCount.toString()
  1076. downloadedCount === selectedCount &&
  1077. (messageSpan.innerText = `Done: ${selectedCount}/${selectedCount}`)
  1078. },
  1079. () => {
  1080. failedCount++
  1081. failedCountSpan.innerText = failedCount.toString()
  1082. },
  1083. )
  1084. })
  1085. return fragment
  1086. }
  1087. function removeFeature() {
  1088. const featureDiv = getFeatureDiv()
  1089. if (!featureDiv) return
  1090. featureDiv.removeAttribute('style')
  1091. featureDiv.querySelector(':scope > div.header-img')?.remove()
  1092. featureDiv.setAttribute('id', 'nofeature')
  1093. }
  1094. function removeModGallery() {
  1095. getModGalleryDiv()?.remove()
  1096. }
  1097. function clickedTabContentLoaded() {
  1098. return new Promise((resolve) => {
  1099. observeAddDirectChildNodes(getTabContentDiv(), (mutationList, observer) => {
  1100. console.log('tabContentDiv add childNodes mutationList:', mutationList)
  1101. observer.disconnect()
  1102. resolve(0)
  1103. })
  1104. })
  1105. }
  1106. async function controlComponentDisplayAfterClickingTab(component, isShow) {
  1107. const style = component.style
  1108. async function _inner(currentTab) {
  1109. ;(await isShow(currentTab))
  1110. ? (style.display = 'block')
  1111. : (style.display = 'none')
  1112. }
  1113. await _inner(getCurrentTab())
  1114. clickTabLi(async (clickedTab) => {
  1115. await _inner(clickedTab)
  1116. })
  1117. }
  1118.  
  1119. //#endregion
  1120. //#region src/shared.ts
  1121. function isSylin527() {
  1122. const value = getValue('isSylin527')
  1123. return typeof value === 'boolean' ? value : false
  1124. }
  1125.  
  1126. //#endregion
  1127. //#region src/site_shared_actions.ts
  1128. function setSectionAsTopElement() {
  1129. bodyElement.classList.remove('new-head')
  1130. bodyElement.style.margin = '0 auto'
  1131. bodyElement.style.maxWidth = mainContentMaxWidth
  1132. const sectionBackup = getSection().cloneNode(true)
  1133. removeAllChildNodes(bodyElement)
  1134. bodyElement.appendChild(sectionBackup)
  1135. }
  1136. function getSpoilerToggleInputClassName() {
  1137. return 'sylin527_spoiler_toggle_input'
  1138. }
  1139. function getSpoilerToggleTextClassName() {
  1140. return 'sylin527_spoiler_toggle_text'
  1141. }
  1142. let hasInsertedShowSpoilerToggleStyle = false
  1143. function insertShowSpoilerToggleStyle() {
  1144. if (hasInsertedShowSpoilerToggleStyle) return
  1145. const newStyle = document.createElement('style')
  1146. headElement.appendChild(newStyle)
  1147. const sheet = newStyle.sheet
  1148. const spoilerToggleInputCN = getSpoilerToggleInputClassName()
  1149. const spoilerToggleTextCN = getSpoilerToggleTextClassName()
  1150. let ruleIndex = sheet.insertRule(`
  1151. input.${spoilerToggleInputCN},
  1152. input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN},
  1153. input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN}::after {
  1154. border: 0;
  1155. cursor: pointer;
  1156. box-sizing: border-box;
  1157. display: inline-block;
  1158. height: 27px;
  1159. width: 60px;
  1160. z-index: 999;
  1161. position: relative;
  1162. vertical-align: middle;
  1163. text-align: center;
  1164. }
  1165. `)
  1166. sheet.insertRule(
  1167. `
  1168. input.${spoilerToggleInputCN} {
  1169. margin-left: 1px;
  1170. z-index: 987654321;
  1171. opacity: 0;
  1172. }
  1173. `,
  1174. ++ruleIndex,
  1175. )
  1176. sheet.insertRule(
  1177. `
  1178. input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN} {
  1179. font-style: normal;
  1180. margin-left: -60px;
  1181. }
  1182. `,
  1183. ++ruleIndex,
  1184. )
  1185. sheet.insertRule(
  1186. `
  1187. input.${spoilerToggleInputCN} ~ i.${spoilerToggleTextCN}::after {
  1188. content: attr(unchecked_text);
  1189. background-color: ${primaryColor};
  1190. font-size: 12px;
  1191. color: #E6E6E6;
  1192. border-radius: 3px;
  1193. font-weight: 400;
  1194. line-height: 27px;
  1195. }
  1196. `,
  1197. ++ruleIndex,
  1198. )
  1199. sheet.insertRule(
  1200. `
  1201. input.${spoilerToggleInputCN}:checked ~ i.${spoilerToggleTextCN}::after {
  1202. content: attr(checked_text);
  1203. background-color: ${highlightColor};
  1204. }
  1205. `,
  1206. ++ruleIndex,
  1207. )
  1208. sheet.insertRule(
  1209. `
  1210. input.${spoilerToggleInputCN}:checked ~ div.bbc_spoiler_content {
  1211. display: none;
  1212. }
  1213. `,
  1214. ++ruleIndex,
  1215. )
  1216. sheet.insertRule(
  1217. `
  1218. div.bbc_spoiler_content {
  1219. display: block;
  1220. }
  1221. `,
  1222. ++ruleIndex,
  1223. )
  1224. hasInsertedShowSpoilerToggleStyle = true
  1225. }
  1226. function showSpoilers(container) {
  1227. insertShowSpoilerToggleStyle()
  1228. const spoilers = container.querySelectorAll('div.bbc_spoiler')
  1229. for (let i = 0; i < spoilers.length; i++) {
  1230. const spoiler = spoilers[i]
  1231. spoiler.querySelector('div.bbc_spoiler_show')?.remove()
  1232. const input = document.createElement('input')
  1233. input.className = getSpoilerToggleInputClassName()
  1234. input.setAttribute('type', 'checkbox')
  1235. const iElement = document.createElement('i')
  1236. iElement.setAttribute(
  1237. 'class',
  1238. `bbc_spoiler_show ${getSpoilerToggleTextClassName()}`,
  1239. )
  1240. iElement.setAttribute('checked_text', 'Show')
  1241. iElement.setAttribute('unchecked_text', 'Hide')
  1242. const content = spoiler.querySelector('div.bbc_spoiler_content')
  1243. spoiler.insertBefore(input, content)
  1244. spoiler.insertBefore(iElement, content)
  1245. content.removeAttribute('style')
  1246. }
  1247. }
  1248. /**
  1249. * youtube 嵌入式链接 换成 外链接
  1250. * 如 <div class="youtube_container"><iframe class="youtube_video" src="https://www.youtube.com/embed/KuO6ortp0ZY" ...></iframe></div>
  1251. * 换成 <a src="https://www.youtube.com/watch?v=KuO6ortp0ZY">https://www.youtube.com/watch?v=KuO6ortp0ZY</a>
  1252. *
  1253. * 技术需求: 替换元素, 文档位置不变
  1254. */
  1255. /**
  1256. * 获取 Youtube video iframe 的标题需要跨域, 暂不操作
  1257. * @param container
  1258. * @returns
  1259. */
  1260. function replaceYoutubeVideosToAnchor(container) {
  1261. const youtubeIframes = container.querySelectorAll('iframe.youtube_video')
  1262. if (youtubeIframes.length === 0) return
  1263. for (let i = 0; i < youtubeIframes.length; i++) {
  1264. const embedUrl = youtubeIframes[i].getAttribute('src')
  1265. const parts = embedUrl.split('/')
  1266. const videoId = parts[parts.length - 1]
  1267. const watchA = document.createElement('a')
  1268. const watchUrl = `https://www.youtube.com/watch?v=${videoId}`
  1269. watchA.style.display = 'block'
  1270. watchA.setAttribute('href', watchUrl)
  1271. watchA.innerText = watchUrl
  1272. const parent = youtubeIframes[i].parentNode
  1273. const grandparent = parent.parentNode
  1274. grandparent && grandparent.replaceChild(watchA, parent)
  1275. }
  1276. }
  1277. function replaceThumbnailUrlsToImageUrls(container) {
  1278. const imgs = container.querySelectorAll('img')
  1279. for (let i = 0; i < imgs.length; i++) {
  1280. const src = imgs[i].src
  1281. if (
  1282. src.startsWith('https://staticdelivery.nexusmods.com') &&
  1283. src.includes('thumbnails')
  1284. )
  1285. imgs[i].src = src.replace('thumbnails/', '')
  1286. }
  1287. }
  1288. function removeModActions() {
  1289. getModActionsUl().remove()
  1290. }
  1291. function simplifyDescriptionContent(contentContainerElement) {
  1292. replaceYoutubeVideosToAnchor(contentContainerElement)
  1293. replaceThumbnailUrlsToImageUrls(contentContainerElement)
  1294. showSpoilers(contentContainerElement)
  1295. }
  1296. function simplifyComment() {
  1297. const commentContainerComponent = getCommentContainerComponent()
  1298. if (!commentContainerComponent) return
  1299. const {
  1300. headNavDiv,
  1301. bottomNavDiv,
  1302. stickyCommentLis,
  1303. authorCommentLis,
  1304. otherCommentLis,
  1305. } = commentContainerComponent
  1306. headNavDiv.remove()
  1307. bottomNavDiv.remove()
  1308. for (const stickyCommentLi of stickyCommentLis) {
  1309. const commentContentTextDiv = getCommentContentTextDiv(stickyCommentLi)
  1310. simplifyDescriptionContent(commentContentTextDiv)
  1311. }
  1312. for (const authorCommentLi of authorCommentLis) {
  1313. const commentContentTextDiv = getCommentContentTextDiv(authorCommentLi)
  1314. simplifyDescriptionContent(commentContentTextDiv)
  1315. }
  1316. otherCommentLis.forEach((nonAuthorCommentLi) => nonAuthorCommentLi.remove())
  1317. }
  1318.  
  1319. //#endregion
  1320. //#region src/mod_page/files_tab_actions.ts
  1321. function addShowRealFilenameToggle() {
  1322. const modFilesDiv = getModFilesDiv()
  1323. if (!modFilesDiv) return
  1324. const input = document.createElement('input')
  1325. const toggleInputClassName = 'sylin527_real_filenames_toggle_input'
  1326. input.className = toggleInputClassName
  1327. input.setAttribute('type', 'checkbox')
  1328. input.checked = false
  1329. const i = document.createElement('i')
  1330. const toggleTextClassName = 'sylin527_real_filenames_toggle_text'
  1331. i.className = toggleTextClassName
  1332. i.setAttribute('unchecked_text', 'Hide Real Filenames')
  1333. i.setAttribute('checked_text', 'Show Real Filenames')
  1334. modFilesDiv.insertBefore(i, modFilesDiv.firstChild)
  1335. modFilesDiv.insertBefore(input, modFilesDiv.firstChild)
  1336. const style = document.createElement('style')
  1337. style.innerHTML = `
  1338. input.${toggleInputClassName},
  1339. input.${toggleInputClassName} ~ i.${toggleTextClassName},
  1340. input.${toggleInputClassName} ~ i.${toggleTextClassName}::after {
  1341. border: 0;
  1342. cursor: pointer;
  1343. box-sizing: border-box;
  1344. display: block;
  1345. height: 40px;
  1346. width: 300px;
  1347. z-index: 999;
  1348. position: relative;
  1349. }
  1350.  
  1351. /* input[type=checkbox] 全透明, 但 z-index 最大 */
  1352. input.${toggleInputClassName} {
  1353. margin: 0 auto;
  1354. z-index: 987654321;
  1355. opacity: 0;
  1356. }
  1357.  
  1358. input.${toggleInputClassName} ~ i.${toggleTextClassName} {
  1359. font-style: normal;
  1360. font-size: 18px;
  1361. text-align: center;
  1362. line-height: 40px;
  1363. border-radius: 5px;
  1364. font-weight: 400;
  1365. margin: -40px auto -60px auto;
  1366. }
  1367. /* input[type=checkbox] unchecked 时, 显示了所有的文件 */
  1368. input.${toggleInputClassName} ~ i.${toggleTextClassName}::after {
  1369. background-color: ${primaryColor};
  1370. content: attr(unchecked_text);
  1371. border-radius: 3px;
  1372. }
  1373.  
  1374. /* 因为 input[type=checkbox] 的 z-index 值最大, 所以 :hover 用在此 input 上 */
  1375. input.${toggleInputClassName}:hover ~ i.${toggleTextClassName}::after {
  1376. background-color: ${primaryHoverColor};
  1377. }
  1378.  
  1379. /* input[type=checkbox] checked 时, 隐藏了所有的文件 */
  1380. input.${toggleInputClassName}:checked ~ i.${toggleTextClassName}::after {
  1381. background-color: ${highlightColor};
  1382. content: attr(checked_text);
  1383. }
  1384.  
  1385. input.${toggleInputClassName}:hover:checked ~ i.${toggleTextClassName}::after {
  1386. background-color: ${highlightHoverColor};
  1387. }
  1388. /* 由于 SingFile 默认移除隐藏的内容, 必须先显示. 需要时在隐藏 */
  1389. input.${toggleInputClassName}:checked ~ div dd p.${getRealFilenamePClassName()} {
  1390. display: none;
  1391. }
  1392. `
  1393. document.head.appendChild(style)
  1394. }
  1395. function removePremiumBanner() {
  1396. getPremiumBannerDiv()?.remove()
  1397. }
  1398. function removeAllSortBys() {
  1399. const divs = getAllSortByDivs()
  1400. divs && Array.from(divs).forEach((sortByDiv) => sortByDiv.remove())
  1401. }
  1402. function simplifyAllFileHeaders() {
  1403. const fileHeaderDts = getAllFileHeaderDts()
  1404. if (!fileHeaderDts) return
  1405. for (const fileHeaderDt of fileHeaderDts)
  1406. fileHeaderDt.style.background = '#2d2d2d'
  1407. }
  1408. function getRealFilenamePClassName() {
  1409. return 'sylin527_real_filename_p'
  1410. }
  1411. let hasInsertedRealFilenamePStyle = false
  1412. function insertRealFilenamePStyle() {
  1413. if (hasInsertedRealFilenamePStyle) return
  1414. const newStyle = document.createElement('style')
  1415. headElement.appendChild(newStyle)
  1416. const sheet = newStyle.sheet
  1417. sheet?.insertRule(
  1418. `
  1419. p.${getRealFilenamePClassName()} {
  1420. color: #8197ec;
  1421. margin-top: 20xp;
  1422. }
  1423. `,
  1424. 0,
  1425. )
  1426. hasInsertedRealFilenamePStyle = true
  1427. }
  1428. function createRealFilenameP(realFilename, fileUrl) {
  1429. const realFilenameP = document.createElement('p')
  1430. realFilenameP.className = getRealFilenamePClassName()
  1431. const newFileUrlAnchor = document.createElement('a')
  1432. newFileUrlAnchor.href = fileUrl
  1433. newFileUrlAnchor.innerText = realFilename
  1434. realFilenameP.appendChild(newFileUrlAnchor)
  1435. return realFilenameP
  1436. }
  1437. function simplifyAllFileDescriptions$1() {
  1438. const fileDescriptionDds = getAllFileDescriptionDds()
  1439. if (!fileDescriptionDds) return
  1440. insertRealFilenamePStyle()
  1441. const gameDomainName = getGameDomainName()
  1442. const modId = getModId()
  1443. for (const fileDescriptionDd of fileDescriptionDds) {
  1444. const {
  1445. fileDescriptionDiv,
  1446. downloadButtonContainerDiv,
  1447. previewFileDiv,
  1448. realFilename,
  1449. fileId,
  1450. } = getFileDescriptionComponent(fileDescriptionDd)
  1451. simplifyDescriptionContent(fileDescriptionDiv)
  1452. downloadButtonContainerDiv.remove()
  1453. fileDescriptionDiv.append(
  1454. createRealFilenameP(
  1455. realFilename,
  1456. generateFileUrl(gameDomainName, modId, fileId),
  1457. ),
  1458. )
  1459. previewFileDiv.remove()
  1460. fileDescriptionDd.style.display = 'block'
  1461. }
  1462. addShowRealFilenameToggle()
  1463. }
  1464. function insertRemoveOldFilesComponent() {
  1465. const oldFilesComponent = getOldFilesComponent()
  1466. if (!oldFilesComponent) return null
  1467. const removeButton = document.createElement('button')
  1468. removeButton.className = 'btn inline-flex'
  1469. removeButton.style.textTransform = 'unset'
  1470. removeButton.style.verticalAlign = 'super'
  1471. removeButton.style.borderRadius = '3px'
  1472. removeButton.innerText = 'Remove'
  1473. overPrimaryComponent(removeButton)
  1474. const { element, categoryHeaderDiv, sortByContainerDiv } = oldFilesComponent
  1475. categoryHeaderDiv.insertBefore(removeButton, sortByContainerDiv)
  1476. categoryHeaderDiv.insertBefore(document.createTextNode(' '), removeButton)
  1477. removeButton.addEventListener('click', () => {
  1478. element.remove()
  1479. })
  1480. containerManager.addInline(removeButton)
  1481. }
  1482. function simplifyFilesTab() {
  1483. removePremiumBanner()
  1484. removeAllSortBys()
  1485. simplifyAllFileHeaders()
  1486. simplifyAllFileDescriptions$1()
  1487. getFileArchiveSection()?.remove()
  1488. containerManager.hideAll()
  1489. setTabsDivAsTopElement()
  1490. }
  1491. function createSimplifyFilesTabComponent() {
  1492. const button = createActionComponent('Simplify Files Tab')
  1493. button.addEventListener('click', () => {
  1494. simplifyFilesTab()
  1495. })
  1496. isFilesTab()
  1497. ? insertRemoveOldFilesComponent()
  1498. : (button.style.display = 'none')
  1499. controlComponentDisplayAfterClickingTab(button, async (clickedTab) => {
  1500. const bFilesTab =
  1501. clickedTab === 'files' &&
  1502. (await clickedTabContentLoaded()) === 0 &&
  1503. isFilesTab()
  1504. bFilesTab && insertRemoveOldFilesComponent()
  1505. return bFilesTab
  1506. })
  1507. return button
  1508. }
  1509.  
  1510. //#endregion
  1511. //#region src/mod_page/archived_files_tab_actions.ts
  1512. function getApiKey() {
  1513. return getValue('apikey')
  1514. }
  1515. /**
  1516. * If not configure `apikey` value, return null
  1517. * @param gameDomainName
  1518. * @param modId
  1519. * @returns
  1520. */
  1521. async function getArchivedFileIdRealFilenameMap(gameDomainName, modId) {
  1522. const apiKey = getApiKey()
  1523. if (!apiKey || apiKey === '') return null
  1524. const resultJson = await getFiles(gameDomainName, modId, apiKey)
  1525. const { files } = resultJson
  1526. const map = new Map()
  1527. for (const { file_id, category_id, file_name } of files)
  1528. category_id === 7 && map.set(file_id, file_name)
  1529. return map
  1530. }
  1531. async function simplifyAllFileDescriptions() {
  1532. const fileDescriptionDds = getAllFileDescriptionDds()
  1533. if (!fileDescriptionDds) return
  1534. insertRealFilenamePStyle()
  1535. const gameDomainName = getGameDomainName()
  1536. const modId = getModId()
  1537. const oldFileIdRealFilenameMap = await getArchivedFileIdRealFilenameMap(
  1538. gameDomainName,
  1539. modId,
  1540. )
  1541. for (const fileDescriptionDd of fileDescriptionDds) {
  1542. const fileDescriptionDiv = getFileDescriptionDiv(fileDescriptionDd)
  1543. simplifyDescriptionContent(fileDescriptionDiv)
  1544. getDownloadButtonContainerDiv(fileDescriptionDd).remove()
  1545. const fileId = getFileId(fileDescriptionDd)
  1546. const realFilename = oldFileIdRealFilenameMap
  1547. ? oldFileIdRealFilenameMap.get(fileId)
  1548. : 'File Link'
  1549. fileDescriptionDiv.append(
  1550. createRealFilenameP(
  1551. realFilename,
  1552. generateFileUrl(gameDomainName, modId, fileId),
  1553. ),
  1554. )
  1555. fileDescriptionDd.style.display = 'block'
  1556. }
  1557. addShowRealFilenameToggle()
  1558. }
  1559. function tweakTitleIfArchivedFilesTab() {
  1560. isArchivedFilesUrl(location.href) &&
  1561. (titleElement.innerText = `${getModName()} ${getModVersionWithDate()} tab=archived_files`)
  1562. }
  1563. function createSimplifyArchivedFilesTabComponent() {
  1564. const button = createActionComponent('Simplify Archived Files Tab')
  1565. button.addEventListener('click', async () => {
  1566. removePremiumBanner()
  1567. removeAllSortBys()
  1568. simplifyAllFileHeaders()
  1569. await simplifyAllFileDescriptions()
  1570. containerManager.hideAll()
  1571. setTabsDivAsTopElement()
  1572. })
  1573. !isArchivedFilesTab() && (button.style.display = 'none')
  1574. controlComponentDisplayAfterClickingTab(
  1575. button,
  1576. async (clickedTab) =>
  1577. clickedTab === 'files' &&
  1578. (await clickedTabContentLoaded()) === 0 &&
  1579. isArchivedFilesTab(),
  1580. )
  1581. return button
  1582. }
  1583.  
  1584. //#endregion
  1585. //#region src/article_page/article_page.ts
  1586. function getArticleUrlRegExp() {
  1587. return /^((https|http):\/\/(www.)?nexusmods.com\/[a-z0-9]+\/articles\/[0-9]+)/
  1588. }
  1589. function isArticleUrl(url) {
  1590. return getArticleUrlRegExp().test(url)
  1591. }
  1592. function getArticleContainerDiv() {
  1593. return getSection().querySelector('div.container')
  1594. }
  1595. function getArticleElement() {
  1596. return getArticleContainerDiv().querySelector(':scope > article')
  1597. }
  1598.  
  1599. //#endregion
  1600. //#region src/article_page/article_page_actions.ts
  1601. function simplifyArticlePage() {
  1602. simplifyDescriptionContent(getArticleElement())
  1603. titleElement.innerText = getPageTitle()
  1604. removeModActions()
  1605. setSectionAsTopElement()
  1606. }
  1607. function createSimplifyArticlePageComponent() {
  1608. const button = createActionComponent('Simplify Article Page')
  1609. button.addEventListener('click', () => {
  1610. simplifyArticlePage()
  1611. containerManager.hideAll()
  1612. })
  1613. return button
  1614. }
  1615.  
  1616. //#endregion
  1617. //#region src/mod_page/description_tab_actions.ts
  1618. function showAllDescriptionDds() {
  1619. const dtDdMap = getDescriptionDtDdMap()
  1620. if (!dtDdMap || dtDdMap.size === 0) return
  1621. const newStyle = document.createElement('style')
  1622. headElement.appendChild(newStyle)
  1623. const sheet = newStyle.sheet
  1624. const accordionToggle = 'sylin527_show_accordion_toggle'
  1625. let ruleIndex = sheet.insertRule(`
  1626. input.${accordionToggle} {
  1627. cursor: pointer;
  1628. display: block;
  1629. height: 43.5px;
  1630. margin: -44.5px 0 1px 0;
  1631. width: 100%;
  1632. z-index: 999;
  1633. position: relative;
  1634. opacity: 0;
  1635. }
  1636. `)
  1637. sheet.insertRule(
  1638. `
  1639. input.${accordionToggle}:checked ~ dd{
  1640. display: none;
  1641. }
  1642. `,
  1643. ++ruleIndex,
  1644. )
  1645. for (const [dt, dd] of dtDdMap) {
  1646. dt.style.background = '#2d2d2d'
  1647. dd.style.display = 'block'
  1648. dd.removeAttribute('style')
  1649. const newPar = document.createElement('div')
  1650. const toggle = document.createElement('input')
  1651. toggle.setAttribute('class', accordionToggle)
  1652. toggle.setAttribute('type', 'checkbox')
  1653. dd.parentElement.insertBefore(toggle, dd)
  1654. newPar.append(dt, toggle, dd)
  1655. getDescriptionDl()?.append(newPar)
  1656. }
  1657. }
  1658. function simplifyTabDescription() {
  1659. getActionsUl()?.remove()
  1660. getModHistoryDiv()?.remove()
  1661. getShareButtonAnchor()?.remove()
  1662. const descriptionDl = getDescriptionDl()
  1663. if (descriptionDl) {
  1664. getModsRequiringThisDiv()?.remove()
  1665. const permissionDescriptionComponent = getPermissionDescriptionComponent()
  1666. if (permissionDescriptionComponent) {
  1667. const { authorNotesContentP, fileCreditsContentP } =
  1668. permissionDescriptionComponent
  1669. authorNotesContentP && simplifyDescriptionContent(authorNotesContentP)
  1670. fileCreditsContentP && simplifyDescriptionContent(fileCreditsContentP)
  1671. }
  1672. showAllDescriptionDds()
  1673. }
  1674. }
  1675. function simplifyModDescription() {
  1676. const modDescriptionContainerDiv = getModDescriptionContainerDiv()
  1677. if (modDescriptionContainerDiv)
  1678. simplifyDescriptionContent(modDescriptionContainerDiv)
  1679. }
  1680. function setLocationToModUrlIfDescriptionTab() {
  1681. const modUrl = generateModUrl(getGameDomainName(), getModId())
  1682. getCurrentTab() === 'description' && history.replaceState(null, '', modUrl)
  1683. clickTabLi(async (clickedTab) => {
  1684. clickedTab === 'description' &&
  1685. (await clickedTabContentLoaded()) === 0 &&
  1686. history.replaceState(null, '', modUrl)
  1687. })
  1688. }
  1689.  
  1690. //#endregion
  1691. //#region src/mod_page/file_tab.ts
  1692. function isFileUrl(url) {
  1693. const searchParams = new URL(url).searchParams
  1694. return (
  1695. isModUrl(url) &&
  1696. searchParams.get('tab') === 'files' &&
  1697. searchParams.has('file_id')
  1698. )
  1699. }
  1700. function getFileIdFromUrl(url) {
  1701. const fileId = new URL(url).searchParams.get('file_id')
  1702. return fileId ? parseInt(fileId) : null
  1703. }
  1704.  
  1705. //#endregion
  1706. //#region src/mod_page/file_tab_actions.ts
  1707. function tweakTitleIfFileTab() {
  1708. isFileUrl(location.href) &&
  1709. (titleElement.innerText = `${getModName()} ${getModVersionWithDate()} file=${getFileIdFromUrl(
  1710. location.href,
  1711. )}`)
  1712. }
  1713.  
  1714. //#endregion
  1715. //#region src/mod_page/forum_tab.ts
  1716. function getModTopicsDiv() {
  1717. return document.getElementById('tab-modtopics')
  1718. }
  1719. function getTopicsTabH2() {
  1720. return document.getElementById('topics_tab_h2')
  1721. }
  1722. function isForumTab() {
  1723. return getCurrentTab() === 'forum' && getTopicsTabH2() !== null
  1724. }
  1725. function getTopicTable() {
  1726. return document.getElementById('mod_forum_topics')
  1727. }
  1728. function getAllTopicAnchors() {
  1729. const topicTable = getTopicTable()
  1730. if (!topicTable) return null
  1731. const topicAnchorsOfTHead = topicTable.tHead.querySelectorAll(
  1732. ':scope > tr > td.table-topic > a.go-to-topic',
  1733. )
  1734. const topicAnchorsOfTBody = topicTable.tBodies[0].querySelectorAll(
  1735. ':scope > tr > td.table-topic > a.go-to-topic',
  1736. )
  1737. return Array.from(topicAnchorsOfTHead).concat(Array.from(topicAnchorsOfTBody))
  1738. }
  1739. function clickTopicAnchor(callback) {
  1740. const allTopicAnchors = getAllTopicAnchors()
  1741. if (allTopicAnchors)
  1742. for (const topicAnchor of allTopicAnchors)
  1743. topicAnchor.addEventListener('click', (event) => {
  1744. callback(topicAnchor, event)
  1745. })
  1746. }
  1747.  
  1748. //#endregion
  1749. //#region src/mod_page/forum_topic_tab.ts
  1750. function getTopicTitle() {
  1751. const h2 = document.getElementById('comment-count')
  1752. if (!h2) return ''
  1753. const titleWithCommentCount = h2.innerText
  1754. const lastLeftParenthesisIndex = titleWithCommentCount.lastIndexOf('(')
  1755. return titleWithCommentCount.substring(0, lastLeftParenthesisIndex - 1)
  1756. }
  1757. function isForumTopicTab() {
  1758. return getCurrentTab() === 'forum' && getTopicsTabH2() === null
  1759. }
  1760.  
  1761. //#endregion
  1762. //#region src/mod_page/forum_tab_actions.ts
  1763. function modTopicsDivAddedDirectChildNodes() {
  1764. return new Promise((resolve) => {
  1765. observeAddDirectChildNodes(getModTopicsDiv(), (mutationList, observer) => {
  1766. console.log('modTopicsDiv add childNodes mutationList:', mutationList)
  1767. observer.disconnect()
  1768. resolve(0)
  1769. })
  1770. })
  1771. }
  1772.  
  1773. //#endregion
  1774. //#region src/mod_page/posts_tab.ts
  1775. function isPostsTab() {
  1776. return getCurrentTab() === 'posts'
  1777. }
  1778.  
  1779. //#endregion
  1780. //#region src/mod_page/posts_tab_actions.ts
  1781. function hasStickyOrAuthorComments() {
  1782. const commentContainerComponent = getCommentContainerComponent()
  1783. if (!commentContainerComponent) return false
  1784. const { authorCommentLis, stickyCommentLis } = commentContainerComponent
  1785. return authorCommentLis.length + stickyCommentLis.length > 0
  1786. }
  1787. function createSimplifyPostsTabComponent() {
  1788. const button = createActionComponent('Simplify Posts Tab')
  1789. button.addEventListener('click', () => {
  1790. simplifyComment()
  1791. containerManager.hideAll()
  1792. setTabsDivAsTopElement()
  1793. })
  1794. ;(!isPostsTab() || (isPostsTab() && !hasStickyOrAuthorComments())) &&
  1795. (button.style.display = 'none')
  1796. controlComponentDisplayAfterClickingTab(
  1797. button,
  1798. async (clickedTab) =>
  1799. clickedTab === 'posts' &&
  1800. (await clickedTabContentLoaded()) === 0 &&
  1801. hasStickyOrAuthorComments(),
  1802. )
  1803. return button
  1804. }
  1805.  
  1806. //#endregion
  1807. //#region src/mod_page/forum_topic_tab_actions.ts
  1808. function createSimplifyForumTopicTabComponent() {
  1809. const button = createActionComponent('Simplify Forum Topic Tab')
  1810. button.addEventListener('click', () => {
  1811. titleElement.innerText = replaceIllegalChars(getTopicTitle())
  1812. simplifyComment()
  1813. containerManager.hideAll()
  1814. setTabsDivAsTopElement()
  1815. })
  1816. ;(!isForumTopicTab() ||
  1817. (isForumTopicTab() && !hasStickyOrAuthorComments())) &&
  1818. (button.style.display = 'none')
  1819. function _addClickTopicAnchorEvent() {
  1820. clickTopicAnchor(async () => {
  1821. await modTopicsDivAddedDirectChildNodes()
  1822. isForumTopicTab() &&
  1823. hasStickyOrAuthorComments() &&
  1824. (button.style.display = 'block')
  1825. })
  1826. }
  1827. isForumTab() && _addClickTopicAnchorEvent()
  1828. clickTabLi(async (clickedTab) => {
  1829. button.style.display = 'none'
  1830. clickedTab === 'forum' &&
  1831. (await clickedTabContentLoaded()) === 0 &&
  1832. isForumTab() &&
  1833. _addClickTopicAnchorEvent()
  1834. })
  1835. return button
  1836. }
  1837.  
  1838. //#endregion
  1839. //#region src/mod_page/mod_page_actions.ts
  1840. function simplifyModPage() {
  1841. removeFeature()
  1842. removeModActions()
  1843. removeModGallery()
  1844. simplifyTabDescription()
  1845. simplifyModDescription()
  1846. titleElement.innerText = `${getModName()} ${getModVersionWithDate()}`
  1847. containerManager.hideAll()
  1848. setSectionAsTopElement()
  1849. }
  1850. function createSimplifyModPageComponent() {
  1851. const button = createActionComponent('Simplify Mod Page')
  1852. button.addEventListener('click', () => {
  1853. simplifyModPage()
  1854. })
  1855. !isDescriptionTab() && (button.style.display = 'none')
  1856. /**
  1857. * 似乎 mod page loaded (description tab) 加载之后,
  1858. * `description tab <li>` 还是被 Nexusmods 的 JavaScript 代码 `click` 了一下,
  1859. * 但没有刷新 tab content, 也就没有 childList MutationRecord.
  1860. * 这时候再 `await clickedTabContentLoaded()` 就得不到返回值了.
  1861. * 会导致首次点击其它的 tab, `Simplify Mod Page` button 还是显示.
  1862. */
  1863. clickTabLi(async (clickedTab) => {
  1864. button.style.display = 'none'
  1865. clickedTab === 'description' &&
  1866. (await clickedTabContentLoaded()) === 0 &&
  1867. isDescriptionTab() &&
  1868. (button.style.display = 'block')
  1869. })
  1870. return button
  1871. }
  1872.  
  1873. //#endregion
  1874. //#region src/userscripts/userscripts_shared.ts
  1875. const isProduction = true
  1876. function getAuthor() {
  1877. return 'sylin527'
  1878. }
  1879.  
  1880. //#endregion
  1881. //#region src/userscripts/mod_documentation_utility/userscript.header.ts
  1882. const name = `Mod Documentations Utility by ${getAuthor()}${
  1883. isProduction ? '' : ' Development Version'
  1884. }`
  1885. const version = `0.2.3.20250615`
  1886.  
  1887. //#endregion
  1888. //#region src/userscripts/mod_documentation_utility/userscript.main.ts
  1889. /**
  1890.  
  1891. * 仅初始化 `apikey` 为 `''`
  1892.  
  1893. *
  1894.  
  1895. * 没有初始化 `isSylin527`
  1896.  
  1897. */
  1898. function initStorage() {
  1899. const apiKey = getApiKey()
  1900. !apiKey && setValue('apikey', '')
  1901. }
  1902. function initModDocumentationUtility() {
  1903. initStorage()
  1904. const href = location.href
  1905. const actionContainer = insertActionContainer()
  1906. if (isModUrl(href)) {
  1907. tweakTitleAfterClickingTab()
  1908. setLocationToModUrlIfDescriptionTab()
  1909. actionContainer.append(
  1910. createCopyModNameAndVersionComponent(),
  1911. createShowAllGalleryThumbnailsComponent(),
  1912. )
  1913. if (isSylin527()) {
  1914. actionContainer.appendChild(createDownloadSelectedImagesComponent())
  1915. hideModActionsSylin527NotUse()
  1916. }
  1917. actionContainer.append(
  1918. createSimplifyModPageComponent(),
  1919. createSimplifyFilesTabComponent(),
  1920. createSimplifyArchivedFilesTabComponent(),
  1921. createSimplifyPostsTabComponent(),
  1922. createSimplifyForumTopicTabComponent(),
  1923. )
  1924. tweakTitleIfFileTab()
  1925. tweakTitleIfArchivedFilesTab()
  1926. } else if (isArticleUrl(href))
  1927. actionContainer.appendChild(createSimplifyArticlePageComponent())
  1928. }
  1929. function main() {
  1930. initModDocumentationUtility()
  1931. const scriptInfo = `Load userscript: ${name} ${version}`
  1932. console.log('%c [Info] ' + scriptInfo, 'color: green')
  1933. console.log('%c [Info] URL: ' + location.href, 'color: green')
  1934. }
  1935. main()
  1936.  
  1937. //#endregion