Modrinthify

Redirect curseforge.com mod pages to modrinth.com when possible

  1. // ==UserScript==
  2. // @name Modrinthify
  3. // @namespace Violentmonkey Scripts
  4. // @match *://*.curseforge.com/minecraft/*
  5. // @grant none
  6. // @version 1.6.0
  7. // @author devBoi76
  8. // @license MIT
  9. // @description Redirect curseforge.com mod pages to modrinth.com when possible
  10. // ==/UserScript==
  11.  
  12. /* jshint esversion: 6 */
  13.  
  14. function htmlToElements(html) {
  15. var t = document.createElement('template')
  16. t.innerHTML = html
  17. return t.content
  18. }
  19.  
  20. function similarity(s1, s2) {
  21. var longer = s1;
  22. var shorter = s2;
  23. if (s1.length < s2.length) {
  24. longer = s2;
  25. shorter = s1;
  26. }
  27. var longerLength = longer.length;
  28. if (longerLength == 0) {
  29. return 1.0;
  30. }
  31. return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);
  32. }
  33. function editDistance(s1, s2) {
  34. s1 = s1.toLowerCase();
  35. s2 = s2.toLowerCase();
  36.  
  37. var costs = new Array();
  38. for (var i = 0; i <= s1.length; i++) {
  39. var lastValue = i;
  40. for (var j = 0; j <= s2.length; j++) {
  41. if (i == 0)
  42. costs[j] = j;
  43. else {
  44. if (j > 0) {
  45. var newValue = costs[j - 1];
  46. if (s1.charAt(i - 1) != s2.charAt(j - 1))
  47. newValue = Math.min(Math.min(newValue, lastValue),
  48. costs[j]) + 1;
  49. costs[j - 1] = lastValue;
  50. lastValue = newValue;
  51. }
  52. }
  53. }
  54. if (i > 0)
  55. costs[s2.length] = lastValue;
  56. }
  57. return costs[s2.length];
  58. }
  59.  
  60. const new_design_button = '<a id="modrinth-body" href="REDIRECT" target="_blank" style="overflow: hidden; margin-top: -1px; display: flex" > <style>#modrinth-body:hover {background-color: #4d4d4d;}#modrinth-body{background-color: #333; height: 36px; text-decoration: none; font-weight: 600; font-family: sans-serif; color: #e5e5e5; --mr-green: #30b27b; transition: background-color 0.15s ease;}#modrinth-body > div{display: flex; align-items: center;}#modrinthify-redirect{display: flex; height: 100%; align-items: center; background-color: var(--mr-green); font-weight: 600; padding-inline: 0.5rem;}#modrinthify-redirect > svg{height: 30px; margin-right: 0.5rem; fill: #e5e5e5;}</style> <div> <img style="display: inline-block; height: 36px; width: 36px" src="ICON_SOURCE"/> <div style="display: inline-block; margin-inline: 1rem; font-weight: 600" > MOD_NAME </div><div id="modrinthify-redirect" data-tooltip="Get on Modrinth" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.73 141.73" aria-hidden="true" > <g> <path d="M159.07,89.29A70.94,70.94,0,1,0,20,63.52H32A58.78,58.78,0,0,1,145.23,49.93l-11.66,3.12a46.54,46.54,0,0,0-29-26.52l-2.15,12.13a34.31,34.31,0,0,1,2.77,63.26l3.19,11.9a46.52,46.52,0,0,0,28.33-49l11.62-3.1A57.94,57.94,0,0,1,147.27,85Z" transform="translate(-19.79)" fill-rule="evenodd" ></path> <path transform="translate(-19.79)" d="M108.92,139.3A70.93,70.93,0,0,1,19.79,76h12a59.48,59.48,0,0,0,1.78,9.91,58.73,58.73,0,0,0,3.63,9.91l10.68-6.41a46.58,46.58,0,0,1,44.72-65L90.43,36.54A34.38,34.38,0,0,0,57.36,79.75C57.67,80.88,58,82,58.43,83l13.66-8.19L68,63.93l12.9-13.25,16.31-3.51L101.9,53l-7.52,7.61-6.55,2.06-4.69,4.82,2.3,6.38s4.64,4.94,4.65,4.94l6.57-1.74,4.67-5.13,10.2-3.24,3,6.84L104.05,88.43,86.41,94l-7.92-8.81L64.7,93.48a34.44,34.44,0,0,0,28.72,11.59L96.61,117A46.6,46.6,0,0,1,54.13,99.83l-10.64,6.38a58.81,58.81,0,0,0,99.6-9.77l11.8,4.29A70.77,70.77,0,0,1,108.92,139.3Z" ></path> </g> </svg> Get on Modrinth </div></div></a>'
  61.  
  62. const new_design_donation = `<a id="donate-button" target="_blank" href="REDIRECT" data-tooltip="Support the Author" > <style>#donate-button{background-color: #ff5e5b; font-weight: 600; text-decoration: none; display: flex; align-items: center; padding-right: 0.5rem;}#donate-button img{height: 100%; width: 36px;}</style> <img src="https://i.ibb.co/Y2Xgd4Q/kofilogo.png"> Support the Author </a>`;
  63.  
  64. const svg = '<svg class="h-full absolute" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 777 141.73" aria-hidden="true" class="text-logo"><g><path d="M159.07,89.29A70.94,70.94,0,1,0,20,63.52H32A58.78,58.78,0,0,1,145.23,49.93l-11.66,3.12a46.54,46.54,0,0,0-29-26.52l-2.15,12.13a34.31,34.31,0,0,1,2.77,63.26l3.19,11.9a46.52,46.52,0,0,0,28.33-49l11.62-3.1A57.94,57.94,0,0,1,147.27,85Z" transform="translate(-19.79)" fill="var(--color-brand)" fill-rule="evenodd"></path><path transform="translate(-19.79)" fill="var(--color-brand)" d="M108.92,139.3A70.93,70.93,0,0,1,19.79,76h12a59.48,59.48,0,0,0,1.78,9.91,58.73,58.73,0,0,0,3.63,9.91l10.68-6.41a46.58,46.58,0,0,1,44.72-65L90.43,36.54A34.38,34.38,0,0,0,57.36,79.75C57.67,80.88,58,82,58.43,83l13.66-8.19L68,63.93l12.9-13.25,16.31-3.51L101.9,53l-7.52,7.61-6.55,2.06-4.69,4.82,2.3,6.38s4.64,4.94,4.65,4.94l6.57-1.74,4.67-5.13,10.2-3.24,3,6.84L104.05,88.43,86.41,94l-7.92-8.81L64.7,93.48a34.44,34.44,0,0,0,28.72,11.59L96.61,117A46.6,46.6,0,0,1,54.13,99.83l-10.64,6.38a58.81,58.81,0,0,0,99.6-9.77l11.8,4.29A70.77,70.77,0,0,1,108.92,139.3Z"></path></g></svg>'
  65.  
  66. const HTML = ` \
  67. <div id="modrinthify-redirect" class="button" style="background-color: #30B27B;\
  68. border-top-right-radius: 0;\
  69. border-bottom-right-radius: 0;\
  70. font-weight: 600;"\
  71. data-tooltip="Get on Modrinth">\
  72. <figure class="icon icon-margin relative w-5 h-4" >\
  73. ${svg}
  74. </figure> \
  75. Get on Modrinth\
  76. </div> \
  77. `
  78.  
  79. const DONATE_HTML = `\
  80. <a target="_blank" href="REDIRECT" class="button" style="background-color: #FF5E5B;\
  81. border-top-left-radius: 0;\
  82. border-bottom-left-radius: 0;\
  83. font-weight: 600;"\
  84. data-tooltip="Support the Author"> \
  85. <figure class="icon icon-margin relative w-5 h-4" >\
  86. <img src="https://i.ibb.co/Y2Xgd4Q/kofilogo.png">\
  87. </figure>\
  88. Support the Author
  89. </a> \
  90. `
  91.  
  92. const REGEX = /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gmi
  93.  
  94. const MOD_PAGE_HTML = `<a id="modrinth-body" href="REDIRECT" target="_blank" class="box flex" style="overflow: hidden; height: max-content; margin-top: -1px;"><div>\
  95. <img style="display:inline-block; height: 3rem" src="ICON_SOURCE">\
  96. <div class="mx-2 font-bold" style="display: inline-block">MOD_NAME</div>\
  97. <div style="display: inline-block">BUTTON_HTML</div></div></a>`
  98.  
  99. const SEARCH_PAGE_HTML = `<a href="REDIRECT" target="_blank" class="box flex" style="overflow: hidden"><div>\
  100. <img style="display:inline-block; height: 3rem" src="ICON_SOURCE">\
  101. <div class="mx-2 font-bold" style="display: inline-block">MOD_NAME</div>\
  102. <div style="display: inline-block">BUTTON_HTML</div></div></a>`
  103.  
  104. let query = "head title"
  105. const tab_title = document.querySelector(query).innerText
  106. let mod_name = undefined
  107. let mod_name_noloader = undefined
  108. let page = undefined
  109.  
  110. function main() {
  111. console.log("main()")
  112. const url = document.URL.split("/")
  113. page = url[4]
  114. const is_new_design = document.querySelector("#__next") != null
  115. const is_search = is_new_design ? (url[4].split("?")[0] == "search") : (url[5].startsWith("search") && url[5].split("?").length >= 2)
  116. if (is_search) {
  117. if (is_new_design) {
  118. search_query = document.querySelector(".search-input-field").value
  119. }
  120. else {
  121. search_query = document.querySelector(".mt-6 > h2:nth-child(1)").textContent.match(/Search results for '(.*)'/)[1]
  122. }
  123. } else {
  124. if (is_new_design) {
  125. // search_query = document.querySelector(".project-header > h1:nth-child(2)").innerText
  126. search_query = document.querySelector("head > title:nth-child(2)").innerText.split(" - ")[0]
  127. } else {
  128. search_query = document.querySelector("head meta[property='og:title']").getAttribute("content")
  129. }
  130. }
  131. mod_name = search_query
  132. mod_name_noloader = mod_name.replace(REGEX, "")
  133. if (is_search && is_new_design) {
  134. page_re = /.*&class=(.*?)&.*/
  135. page = (page.match(page_re) || ["", "all"])[1]
  136. }
  137. console.log(page)
  138. api_facets = ""
  139. switch (page) {
  140. //=Mods===============
  141. case "mc-mods":
  142. api_facets =`facets=[["categories:'forge'","categories:'fabric'","categories:'quilt'","categories:'liteloader'","categories:'modloader'","categories:'rift'"],["project_type:mod"]]`
  143. break
  144. //=Server=Plugins=====
  145. case "mc-addons":
  146. return
  147. case "customization":
  148. api_facets = `facets=[["project_type:shader"]]`
  149. break
  150. case "bukkit-plugins":
  151. api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]`
  152. break
  153. //=Resource=Packs=====
  154. case "texture-packs":
  155. api_facets = `facets=[["project_type:resourcepack"]]`
  156. break
  157. //=Modpacks===========
  158. case "modpacks":
  159. api_facets = `facets=[["project_type:modpack"]]`
  160. break
  161. case "all":
  162. api_facets = ``
  163. break
  164. }
  165. fetch(`https://api.modrinth.com/v2/search?limit=3&query=${mod_name_noloader}&${api_facets}`, {method: "GET", mode: "cors"})
  166. .then(response => response.json())
  167. .then(resp => {
  168. let bd = document.querySelector("#modrinth-body")
  169. if (bd) {bd.remove()}
  170. if (page == undefined) {
  171. return
  172. }
  173. if (resp.hits.length == 0) {
  174. return
  175. }
  176. let max_sim = 0
  177. let max_hit = undefined
  178. for (const hit of resp.hits) {
  179. if (similarity(hit.title.trim(), mod_name) > max_sim) {
  180. max_sim = similarity(hit.title.trim(), mod_name.trim())
  181. max_hit = hit
  182. }
  183. if (similarity(hit.title.trim(), mod_name_noloader) > max_sim) {
  184. max_sim = similarity(hit.title.trim(), mod_name_noloader.trim())
  185. max_hit = hit
  186. }
  187. }
  188. if (max_sim <= 0.7) {
  189. return
  190. }
  191. // Add the buttons
  192. if (is_search) {
  193. if (is_new_design) {
  194. // query = ".results-count"
  195. query = ".search-tags"
  196. let s = document.querySelector(query)
  197. let buttonElement = htmlToElements(new_design_button
  198. .replace("ICON_SOURCE", max_hit.icon_url)
  199. .replace("MOD_NAME", max_hit.title.trim())
  200. .replace("REDIRECT", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`)
  201. .replace("BUTTON_HTML", HTML))
  202. buttonElement.childNodes[0].style.marginLeft = "auto"
  203. s.appendChild(buttonElement)
  204. } else {
  205. query = ".mt-6 > div:nth-child(3)"
  206. let s = document.querySelector(query)
  207. let buttonElement = htmlToElements(SEARCH_PAGE_HTML
  208. .replace("ICON_SOURCE", max_hit.icon_url)
  209. .replace("MOD_NAME", max_hit.title.trim())
  210. .replace("REDIRECT", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`)
  211. .replace("BUTTON_HTML", HTML))
  212. s.appendChild(buttonElement)
  213. }
  214. } else {
  215. if (is_new_design) {
  216. query = ".actions"
  217. let s = document.querySelector(query)
  218. let buttonElement = htmlToElements(new_design_button
  219. .replace("ICON_SOURCE", max_hit.icon_url)
  220. .replace("MOD_NAME", max_hit.title.trim())
  221. .replace("REDIRECT", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`))
  222. s.appendChild(buttonElement)
  223. } else {
  224. query = "div.-mx-1:nth-child(1)"
  225. let s = document.querySelector(query)
  226. let buttonElement = htmlToElements(MOD_PAGE_HTML
  227. .replace("ICON_SOURCE", max_hit.icon_url)
  228. .replace("MOD_NAME", max_hit.title.trim())
  229. .replace("REDIRECT", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`)
  230. .replace("BUTTON_HTML", HTML))
  231. s.appendChild(buttonElement)
  232. }
  233. }
  234. // Add donation button if present
  235. fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, {method: "GET", mode: "cors"})
  236. .then(response_p => response_p.json())
  237. .then(resp_p => {
  238. if (document.querySelector("#donate-button")) { return }
  239. if (resp_p.donation_urls.length > 0) {
  240. let redir = document.getElementById("modrinth-body")
  241. if (is_new_design) {
  242. redir.innerHTML += new_design_donation.replace("REDIRECT", resp_p.donation_urls[0].url)
  243. if (is_search) {
  244. redir.style.marginRight = "-195.5px"
  245. } else {
  246. redir.style.marginRight = "-195.5px"
  247. }
  248. } else {
  249. let donations = resp_p.donation_urls
  250. let dbutton = document.createElement("div")
  251. dbutton.innerHTML = DONATE_HTML.replace("REDIRECT", donations[0].url)
  252. dbutton.style.display = "inline-block"
  253. let redir = document.getElementById("modrinthify-redirect")
  254. redir.after(dbutton)
  255. if (!is_search) {
  256. redir.parentNode.parentNode.parentNode.style.marginRight = "-150px"
  257. }
  258. }
  259. }
  260. })
  261. })
  262. }
  263.  
  264. main()
  265.  
  266. // document.querySelector(".classes-list").childNodes.forEach( (el) => {
  267. // el.childNodes[0].addEventListener("click", main)
  268. // })
  269.  
  270. let lastURL = document.URL
  271. new MutationObserver( () => {
  272. let url = document.URL
  273. if (url != lastURL) {
  274. lastURL = url
  275. main()
  276. }
  277. }).observe(document, {subtree: true, childList: true})
  278.  
  279. // document.querySelector(".search-input-field").addEventListener("keydown", (event) => {
  280. // if (event.key == "Enter") {
  281. // main()
  282. // }
  283. // })