Curseforge QOL Fixes

Various Quality of Life improvements to the Curseforge website

当前为 2020-07-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Curseforge QOL Fixes
  3. // @version 0.18
  4. // @description Various Quality of Life improvements to the Curseforge website
  5. // @author comp500
  6. // @namespace https://infra.link/
  7. // @match https://www.curseforge.com/*
  8. // @homepageURL https://github.com/comp500/Curseforge-Userscripts/
  9. // @supportURL https://github.com/comp500/Curseforge-Userscripts/issues/
  10. // @source https://github.com/comp500/Curseforge-Userscripts/
  11. // @run-at document-end
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17.  
  18. // Add a search box
  19. let searchBoxContainer = document.createElement("div");
  20. searchBoxContainer.className = "flex mr-4 items-center";
  21. // Get the current assets path
  22. let styleSheet = Array.from(document.styleSheets).find((s) => /\/Content\/([\d\-]+)\//.test(s.href));
  23. let assetsPath = styleSheet == null ? "2-0-7179-35052" : /\/Content\/([\d\-]+)\//.exec(styleSheet.href)[1];
  24. searchBoxContainer.innerHTML = `<form action="/minecraft/mc-mods/search" method="get" novalidate="novalidate" autocomplete="false" style="width:100%">
  25. <div class="flex flex-col h-full justify-between">
  26. <div class="input input--icon" style="color: #000">
  27. <i class="search textgray-900 flex items-center justify-center">
  28. <svg class="icon" viewBox="0 0 20 20" width="16" height="16"><use xlink:href="/Content/${assetsPath}/Skins/CurseForge/images/twitch/Object/Search.svg#Object/Search"></use></svg>
  29. </i>
  30. <input type="text" name="search" id="cfqolTopbarSearch" placeholder="Search Mods">
  31. </div>
  32. </div></form>`;
  33. let insertLocation = document.querySelector(".curseforge-header .ml-auto > div");
  34. if (insertLocation != null && insertLocation.firstChild != null) {
  35. // @Inject(method = "the navbar", at = @At("HEAD"))
  36. insertLocation.insertBefore(searchBoxContainer, insertLocation.firstChild);
  37.  
  38. // Make the search box magically grow
  39. let searchBox = searchBoxContainer.querySelector("#cfqolTopbarSearch");
  40. if (searchBox != null) {
  41. // Fix stupid flexboxes - set to flex-grow 1 flex-shrink 0
  42. searchBoxContainer.style.flex = "1 0";
  43. searchBoxContainer.parentNode.parentNode.style.flex = "1 0";
  44.  
  45. let navLinksContainer = document.querySelector(".top-nav__nav-link").parentNode;
  46. navLinksContainer.style.transition = "opacity 0.4s, max-width 0.3s";
  47. navLinksContainer.style.overflowX = "hidden";
  48. searchBox.style.transition = "";
  49. searchBox.addEventListener("focus", (e) => {
  50. navLinksContainer.style.opacity = 0;
  51. navLinksContainer.style.maxWidth = "0";
  52. });
  53. searchBox.addEventListener("blur", (e) => {
  54. navLinksContainer.style.opacity = 1;
  55. navLinksContainer.style.maxWidth = "2000px";
  56. });
  57. // Make the search icon focus the search box
  58. let searchIcon = searchBoxContainer.querySelector(".search");
  59. if (searchIcon != null) {
  60. searchIcon.addEventListener("click", (e) => {
  61. searchBox.focus();
  62. });
  63. }
  64. }
  65. }
  66.  
  67. // Hide useless links, to save space
  68. let uselessLinks = ["Minecraft Forums"];
  69. Array.from(document.querySelectorAll(".top-nav__nav-link"))
  70. .filter((n) => uselessLinks.includes(n.innerText))
  71. .forEach((n) => n.parentNode.removeChild(n));
  72.  
  73. // Add an "All Files" tab for mods
  74. let modPathMatches = /\/minecraft\/mc-mods\/([a-z][\da-z\-_]{0,127})/.exec(document.location.pathname);
  75. let filesTab = document.getElementById("nav-files");
  76. if (modPathMatches != null && modPathMatches.length == 2 && filesTab != null) {
  77. let modSlug = modPathMatches[1];
  78. let modAllFiles = document.createElement("li");
  79. let isAllFilesPage = /\/minecraft\/mc-mods\/[a-z][\da-z\-_]{0,127}\/files\/all/.test(
  80. document.location.pathname
  81. );
  82. if (isAllFilesPage) {
  83. modAllFiles.className =
  84. "border-b-2 border-primary-500 b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  85. filesTab.className = "b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  86. } else {
  87. modAllFiles.className = "b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  88. }
  89. modAllFiles.innerHTML = `<a href="/minecraft/mc-mods/${modSlug}/files/all" class="text-${
  90. isAllFilesPage ? "primary" : "gray"
  91. }-500 hover:no-underline">
  92. <span class="b-list-label">
  93. All Files
  94. </span>
  95. </a>`;
  96. filesTab.parentNode.insertBefore(modAllFiles, filesTab.nextSibling);
  97. }
  98.  
  99. // Add an "All Files" tab for modpacks
  100. let packPathMatches = /\/minecraft\/modpacks\/([a-z][\da-z\-_]{0,127})/.exec(document.location.pathname);
  101. if (packPathMatches != null && packPathMatches.length == 2 && filesTab != null) {
  102. let packSlug = packPathMatches[1];
  103. let packAllFiles = document.createElement("li");
  104. let isAllFilesPage = /\/minecraft\/modpacks\/[a-z][\da-z\-_]{0,127}\/files\/all/.test(
  105. document.location.pathname
  106. );
  107. if (isAllFilesPage) {
  108. packAllFiles.className =
  109. "border-b-2 border-primary-500 b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  110. filesTab.className = "b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  111. } else {
  112. packAllFiles.className = "b-list-item p-nav-item px-2 pb-1/10 -mb-1/10 text-gray-500";
  113. }
  114. packAllFiles.innerHTML = `<a href="/minecraft/modpacks/${packSlug}/files/all" class="text-${
  115. isAllFilesPage ? "primary" : "gray"
  116. }-500 hover:no-underline">
  117. <span class="b-list-label">
  118. All Files
  119. </span>
  120. </a>`;
  121. filesTab.parentNode.insertBefore(packAllFiles, filesTab.nextSibling);
  122. }
  123.  
  124. // Add pagination to the bottom of the page in dependency lists
  125. let dependenciesPage = document.querySelector(".project-dependencies-page > div");
  126. if (dependenciesPage != null) {
  127. let paginationTop = document.querySelector(".project-dependencies-page > div .pagination-top");
  128. if (paginationTop != null) {
  129. dependenciesPage.appendChild(paginationTop.parentNode.cloneNode(true)).classList.remove("mb-4");
  130. }
  131. }
  132.  
  133. // Skip download countdowns
  134. let downloadScript = Array.from(document.scripts).find(
  135. (s) => s.innerText != null && s.innerText.includes("PublicProjectDownload.countdown")
  136. );
  137. if (downloadScript != null && downloadScript.innerText != null) {
  138. let matches = downloadScript.innerText.match(/countdown\("(.+)"\)/);
  139. if (matches != null && matches[1] != null) {
  140. // Break the existing script
  141. let countdownEl = document.querySelector("span[data-countdown-seconds]");
  142. if (countdownEl != null) {
  143. // UNSAFE if grant != none! For some reason jQuery stores data in itself rather than attrs?!
  144. jQuery.removeData(countdownEl, "countdown-seconds");
  145. }
  146.  
  147. let downloadText = document.querySelector("p[data-countdown-timer]");
  148. if (downloadText != null) {
  149. downloadText.innerText = "Downloading now...";
  150. }
  151.  
  152. window.location.href = matches[1];
  153. }
  154. }
  155. /**
  156. * Link redirections
  157. */
  158.  
  159. const linkList = Array.from(document.getElementsByTagName("a"));
  160.  
  161. // Better method for skipping, if links contain file ID already
  162. let regexDownloadLink = /^https:\/\/www.curseforge.com\/.*\/download\/\d+$/;
  163. linkList
  164. .filter((a) => regexDownloadLink.test(a.href))
  165. .forEach((a) => {
  166. a.href = a.href + "/file";
  167. });
  168.  
  169. // Change the Browse link and the default Minecraft tab (from other links) to /minecraft/mc-mods
  170. let regexBrowse = /^http:\/\/bit.ly\/2Lzpfsl|https:\/\/www.curseforge.com\/minecraft\/?$/;
  171. linkList
  172. .filter((a) => regexBrowse.test(a.href))
  173. .forEach((a) => {
  174. a.href = "https://www.curseforge.com/minecraft/mc-mods";
  175. });
  176. // Change the default member tab to projects
  177. let regexMemberLink = /^https:\/\/www.curseforge.com\/members\/[^\/]+\/?$/;
  178. linkList
  179. .filter((a) => regexMemberLink.test(a.href))
  180. .forEach((a) => {
  181. a.href = a.href + (a.href.endsWith("/") ? "" : "/") + "projects";
  182. });
  183. // Redirect linkout URLs
  184. let regexLinkoutLink = /^https:\/\/www.curseforge.com\/linkout/;
  185. linkList
  186. .filter(a => regexLinkoutLink.test(a.href))
  187. .forEach(a => {
  188. let url = new URL(a.href);
  189. a.href = decodeURIComponent(url.searchParams.get("remoteUrl"));
  190. });
  191.  
  192. // Readd download buttons for modpacks
  193. Array.from(document.querySelectorAll("a.button"))
  194. .filter((l) => l.pathname.startsWith("/minecraft/modpacks") && l.href.endsWith("?client=y"))
  195. .map((link) => {
  196. let newHref = link.href.slice(0, -9);
  197. if (regexDownloadLink.test(newHref)) {
  198. newHref = newHref + "/file";
  199. }
  200.  
  201. if (link.classList.contains("button--icon-only")) {
  202. // All Files list
  203.  
  204. let newLink = link.cloneNode(true);
  205. newLink.href = newHref;
  206. newLink.classList.add("button--hollow");
  207. newLink.classList.add("mr-2");
  208.  
  209. let icon = newLink.querySelector(".icon");
  210. if (icon != null) {
  211. icon.parentNode.innerHTML = `<svg class="icon icon-fixed-width icon-margin" viewBox="0 0 20 20" width="18" height="18">
  212. <use xlink:href="/Content/${assetsPath}/Skins/CurseForge/images/twitch/Action/Download.svg#Action/Download"></use>
  213. </svg>`;
  214. }
  215. if (link.parentNode != null) {
  216. link.parentNode.insertBefore(newLink, link);
  217. }
  218. } else if (link.querySelector(".button__text") != null) {
  219. // Full text buttons
  220.  
  221. let newButton = link.parentNode.cloneNode(true);
  222. // "Main file" buttons have ml-2 on the right button, while the rest have px-1 on both
  223. if (link.parentNode.classList == null || !link.parentNode.classList.contains("px-1")) {
  224. link.parentNode.classList.add("ml-2");
  225. }
  226. let newLink = newButton.querySelector("a.button");
  227. newLink.classList.add("button--hollow");
  228. newLink.href = newHref;
  229.  
  230. // Buttons at the top of the page have mr-1 on the install icon, and no icon on the Download button
  231. let svgEl = newLink.querySelector("svg.mr-1");
  232. if (svgEl == null) {
  233. newLink.innerHTML = `<span class="button__text">
  234. <svg class="icon icon-margin" viewBox="0 0 20 20" width="18" height="18">
  235. <use xlink:href="/Content/${assetsPath}/Skins/CurseForge/images/twitch/Action/Download.svg#Action/Download"></use>
  236. </svg> Download
  237. </span>`;
  238. } else {
  239. svgEl.parentNode.removeChild(svgEl);
  240. let newText = newLink.querySelector(".button__text");
  241. if (newText != null) {
  242. newText.innerText = "Download";
  243. }
  244. }
  245. link.parentNode.parentNode.insertBefore(newButton, link.parentNode);
  246. }
  247. });
  248.  
  249. // Minecraft version-specific files list
  250. Array.from(document.querySelectorAll(".cf-recentfiles-credits-wrapper"))
  251. .filter((w) => w.firstChild == null || (w.childNodes.length == 1 && w.firstChild.nodeType != Node.ELEMENT_NODE))
  252. .forEach((wrapper) => {
  253. let link = wrapper.parentNode.querySelector("a");
  254.  
  255. if (link != null) {
  256. let newHref = link.href.replace("files", "download");
  257. if (regexDownloadLink.test(newHref)) {
  258. newHref = newHref + "/file";
  259. }
  260. wrapper.innerHTML = `<a href="${newHref}" class="button button--icon-only button--sidebar">
  261. <span class="button__text">
  262. <svg class="icon icon-fixed-width icon-margin" viewBox="0 0 20 20" width="16" height="16"><use xlink:href="/Content/${assetsPath}/Skins/CurseForge/images/twitch/Action/Download.svg#Action/Download"></use></svg>
  263. </span>
  264. </a>`;
  265. }
  266. });
  267. })();