├── LICENSE ├── README.md ├── autogen_chrome ├── background.js ├── html │ ├── chrome.css │ ├── main.css │ ├── main.html │ ├── notifications.js │ ├── preferences.html │ └── preferences.js ├── icons │ ├── favicon.png │ └── kofilogo.png ├── main.js ├── manifest.json └── spigot.js ├── chrome ├── html │ ├── chrome.css │ ├── main.css │ ├── main.html │ └── preferences.html ├── icons │ ├── favicon.png │ └── kofilogo.png └── manifest.json ├── convert_chrome.py ├── firefox ├── background.js ├── html │ ├── chrome.css │ ├── main.css │ ├── main.html │ ├── notifications.js │ ├── preferences.html │ └── preferences.js ├── icons │ ├── favicon.png │ └── kofilogo.png ├── main.js ├── manifest.json └── spigot.js └── greasyfork ├── spigot.js └── userscript.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 devBoi76 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modrinthify 2 | 3 | When looking at Minecraft mods, modpacks, plugins and resource packs on curseforge.com, Modrinthify automatically searches modrinth.com for the mod and shows you a redirect button if it finds it. If you click on that button you will be taken to the project's Modrinth project page 4 | 5 | The extension will also show the creator's donation page whenever present 6 | 7 | The extension is available for [firefox](https://addons.mozilla.org/en-US/firefox/addon/modrinthify/), [chrome](https://chrome.google.com/webstore/detail/modrinthify/gjjlcbppchpjacimpkjhoancdbdmpcoc) and [as a userscript](https://greasyfork.org/en/scripts/445993-modrinthify), [spigot userscript](https://greasyfork.org/en/scripts/451067-modrinthify-spigot) 8 | 9 | ![newcf_one_e_comp](https://user-images.githubusercontent.com/77896685/219877614-1e336385-f63d-4f54-a1ec-bd9b64b30c9f.png) 10 | ![newcf_two_e_comp](https://user-images.githubusercontent.com/77896685/219877618-370e0ece-397c-4168-a154-6cc8b7fb10e7.png) 11 | 12 | Redirects on spigotmc.org: 13 | 14 | ![image](https://user-images.githubusercontent.com/77896685/189420503-ba50a9d4-69f4-4772-8f50-7530b84f014d.png) 15 | 16 | As of version 1.5 the extension also has Modrinth notifications integration! 17 | *note: this works only on firefox and chrome, not with userscripts* 18 | 19 | ![ext_popup_firefox](https://user-images.githubusercontent.com/77896685/190860461-d2ed396e-3b11-4e61-bc1f-1c3f6bb9b2d2.png) 20 | ![ext_popup_chrome](https://user-images.githubusercontent.com/77896685/190860463-91f1db9d-e8cf-4fd0-a46d-641ceb78d721.png) 21 | 22 | --- 23 | 24 | 25 | The source code is split into 4 folders, `firefox`, `chrome`, `chrome_autogen` and `greasyfork`, each of them contains the source code used for the platform 26 | 27 | The `chrome` folder contains non-javascript files that can't be easily converted from the firefox version 28 | 29 | The `chrome_autogen` folder is the output folder for `convert_chrome.py`, which automatically converts firefox javascript files to be compatible with chrome 30 | 31 | Feel free to post any issues or suggestions you might have on the issues page 32 | 33 | The chrome web store listing might update with a slight delay because of their long review times 34 | -------------------------------------------------------------------------------- /autogen_chrome/background.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction = chrome.action 2 | 3 | const API_BASE = "https://api.modrinth.com/v2/user/"; 4 | 5 | async function fetchNotifs(user, token) { 6 | let h = new Headers({ 7 | Authorization: token, 8 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 9 | }); 10 | let resp = await fetch(API_BASE + user + "/notifications", { 11 | headers: h, 12 | }); 13 | 14 | if (resp.status != 200) { 15 | return { 16 | status: resp.status, 17 | notifications: undefined, 18 | }; 19 | } 20 | let json = await resp.json(); 21 | return { 22 | status: 200, 23 | notifications: json, 24 | }; 25 | } 26 | 27 | async function browserAlarmListener(e) { 28 | if (e.name == "check-notifications") { 29 | let s = await chrome.storage.sync.get([ 30 | "user", 31 | "token", 32 | "notif_enable", 33 | ]); 34 | 35 | if (!s.notif_enable) { 36 | chrome.browserAction.setBadgeText({ text: "" }); 37 | return; 38 | } 39 | 40 | let token = s.token; 41 | let user = s.user; 42 | let resp = await fetchNotifs(user, token); 43 | 44 | if (resp.status == 401) { 45 | chrome.storage.sync.set({ 46 | notif_enable: false, 47 | issue_connecting: 401, 48 | }); 49 | chrome.browserAction.setBadgeText({ text: "ERR" }); 50 | return; 51 | } else if (resp.status == 404) { 52 | chrome.storage.sync.set({ 53 | notif_enable: false, 54 | issue_connecting: 404, 55 | }); 56 | chrome.browserAction.setBadgeText({ text: "ERR" }); 57 | return; 58 | } 59 | 60 | let parsed = resp.notifications; 61 | let n_old = 0; 62 | let n_updated = 0; 63 | last_checked = (await chrome.storage.sync.get(["last_checked"])) 64 | .last_checked; 65 | for (let i = 0; i < parsed.length; i++) { 66 | let el = parsed[i]; 67 | let date_created = 68 | el.body.type == "legacy_markdown" 69 | ? el.created 70 | : el.date_published; 71 | if (last_checked > Date.parse(date_created)) { 72 | n_old += 1; 73 | } else { 74 | n_updated += 1; 75 | } 76 | } 77 | 78 | if (n_updated > 0) { 79 | chrome.browserAction.setBadgeText({ text: n_updated.toString() }); 80 | } else { 81 | chrome.browserAction.setBadgeText({ text: "" }); 82 | } 83 | } 84 | } 85 | 86 | async function setAlarm() { 87 | let check_delay = parseFloat( 88 | (await chrome.storage.sync.get(["check_delay"])).check_delay || 10, 89 | ); 90 | 91 | if (check_delay == 0) { 92 | return; 93 | } 94 | 95 | chrome.alarms.create("check-notifications", { 96 | delayInMinutes: 0.05, 97 | periodInMinutes: parseFloat(check_delay), 98 | }); 99 | 100 | chrome.alarms.onAlarm.addListener(browserAlarmListener); 101 | } 102 | 103 | setAlarm(); 104 | -------------------------------------------------------------------------------- /autogen_chrome/html/chrome.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | width:300px; 4 | height: calc(420px); 5 | line-height: 1.3; 6 | position: relative; 7 | color: var(--text); 8 | background-color: var(--page-bg); 9 | font-size: 100%; 10 | } 11 | 12 | *::-webkit-scrollbar { 13 | width: 8px; 14 | } 15 | 16 | ::-webkit-scrollbar-track { 17 | background: var(--page-bg); 18 | } 19 | 20 | ::-webkit-scrollbar-thumb { 21 | background: var(--divider); 22 | border-radius: 4px; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb:hover { 26 | background: #babfc5; 27 | } 28 | 29 | #icon-clear-a-container::before { 30 | top: -1.05rem; 31 | bottom: 0.2rem; 32 | } -------------------------------------------------------------------------------- /autogen_chrome/html/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --divider: #c8cdd3; 3 | --bg: #e5e7eb; 4 | --bg_darker: #dddfe3; 5 | --faded: #4b5563; 6 | --text: black; 7 | --accent: #1E925B; 8 | --page-bg: white; 9 | --clear: #cb2245; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | :root { 14 | --divider: #474b54; 15 | --bg: #26292f; 16 | --bg_darker: #2e3137; 17 | --faded: #b0bac5; 18 | --accent: #1bd96a; 19 | --text: #ecf9fb; 20 | --page-bg: #16181c; 21 | --clear: #ff496e; 22 | } 23 | } 24 | body[data-theme="light"] { 25 | --divider: #c8cdd3; 26 | --bg: #e5e7eb; 27 | --bg_darker: #dddfe3; 28 | --faded: #4b5563; 29 | --text: black; 30 | --accent: #1E925B; 31 | --page-bg: white; 32 | --clear: #cb2245; 33 | } 34 | body[data-theme="dark"]{ 35 | --divider: #474b54; 36 | --bg: #26292f; 37 | --bg_darker: #2e3137; 38 | --faded: #b0bac5; 39 | --accent: #1bd96a; 40 | --text: #ecf9fb; 41 | --page-bg: #16181c; 42 | --clear: #ff496e; 43 | } 44 | 45 | body { 46 | font-family: sans-serif; 47 | width:300px; 48 | height: calc(420px); 49 | line-height: 1.3; 50 | position: relative; 51 | color: var(--text); 52 | background-color: var(--page-bg); 53 | } 54 | 55 | 56 | h3 { 57 | padding-bottom: 6px; 58 | margin-block: 0; 59 | } 60 | 61 | h4 { 62 | margin: 0; 63 | } 64 | 65 | p { 66 | margin: 0 67 | } 68 | 69 | a { 70 | display: block; 71 | } 72 | 73 | hr { 74 | border: 1px dashed var(--divider); 75 | } 76 | 77 | #main { 78 | width: 300px; 79 | background-color: var(--page-bg); 80 | } 81 | 82 | #notifications { 83 | overflow-y: scroll; 84 | margin-bottom: 0.5rem; 85 | } 86 | 87 | .title { 88 | display: flex; 89 | justify-content: space-between; 90 | margin-bottom: 0.5rem; 91 | padding-bottom: 0.5rem; 92 | user-select: none; 93 | position: sticky; 94 | top: 0; 95 | inset-inline: 0.5rem; 96 | padding-top: 0.5rem; 97 | margin-top: -0.5rem; 98 | background-color: var(--page-bg); 99 | z-index: 1; 100 | } 101 | .title svg { 102 | height: 100%; 103 | justify-self: center; 104 | align-self: center; 105 | cursor: pointer; 106 | color: var(--faded); 107 | transition: 0.15s color; 108 | margin-inline: 0.2rem; 109 | } 110 | 111 | .title svg:hover { 112 | color: var(--text); 113 | } 114 | 115 | .notification { 116 | background-color: var(--bg); 117 | border-radius: 0.5rem; 118 | margin-top: 0.25rem; 119 | overflow: hidden; 120 | } 121 | 122 | .header { 123 | padding: 0.5rem 0.5rem 0.2rem; 124 | border-bottom: 2px solid var(--divider); 125 | background-color: var(--bg); 126 | position: relative; 127 | } 128 | .header p { 129 | color: var(--faded); 130 | } 131 | 132 | .subheader { 133 | display: flex; 134 | justify-content: space-between; 135 | } 136 | 137 | .clear-all-button { 138 | color: var(--page-bg); 139 | user-select: none; 140 | font-weight: 900; 141 | cursor: pointer; 142 | padding: 0.2rem 0.5rem; 143 | position: absolute; 144 | right: 0; 145 | top: 0; 146 | opacity: 0; 147 | background-color: var(--clear); 148 | border-bottom-left-radius: 0.5rem; 149 | } 150 | 151 | .clear-all-button:hover { 152 | text-decoration: 2px underline; 153 | } 154 | 155 | .header:hover .clear-all-button { 156 | opacity: 1; 157 | } 158 | 159 | .version { 160 | padding: 0.2rem 0.5rem; 161 | color: var(--accent); 162 | font-weight: 600; 163 | position: relative; 164 | display: flex; 165 | justify-content: space-between; 166 | } 167 | .version:nth-child(2n+1) { 168 | background-color: var(--bg_darker); 169 | } 170 | .version::after { 171 | content: attr(after_text); 172 | margin-left: 0.5rem; 173 | border-left: 1px solid var(--divider); 174 | padding-left: 0.5rem; 175 | color: var(--faded); 176 | float: right; 177 | } 178 | .version:hover::after { 179 | content: "clear"; 180 | color: transparent; 181 | font-weight: 900; 182 | font-size: 1rem; 183 | } 184 | .being-cleared, .being-cleared * { 185 | opacity: 0.5; 186 | text-decoration: none !important; 187 | user-select: none !important; 188 | cursor: progress !important; 189 | } 190 | .clear-hitbox { 191 | position: absolute; 192 | right: 0; 193 | top: 0; 194 | padding: 0.2rem 0.5rem; 195 | font-weight: 900; 196 | color: transparent; 197 | cursor: pointer; 198 | user-select: none; 199 | border-top-left-radius: 0.5rem; 200 | border-bottom-left-radius: 0.5rem; 201 | } 202 | .clear-hitbox:hover { 203 | text-decoration: underline 2px; 204 | } 205 | .version:hover > .clear-hitbox { 206 | color: var(--clear); 207 | background-color: var(--clear); 208 | color: var(--page-bg); 209 | } 210 | .version-link { 211 | color: var(--accent); 212 | text-decoration: none; 213 | user-select: none; 214 | } 215 | .version-link:hover { 216 | text-decoration: underline 2px; 217 | } 218 | .version > .version-link { 219 | flex-grow: 1; 220 | } 221 | .hideable { 222 | display: none; 223 | } 224 | .more { 225 | padding: 0.2rem 0.5rem; 226 | background-color: var(--bg_darker); 227 | color: var(--faded); 228 | font-weight: 600; 229 | transition: 0.15s background-color; 230 | cursor:pointer; 231 | user-select: none; 232 | } 233 | .more:hover { 234 | background-color: var(--bg); 235 | } 236 | .version + .more { 237 | border-top: 2px solid var(--divider); 238 | } 239 | 240 | @keyframes spin { 241 | from { 242 | transform:rotate3d(0,0,1,0deg); 243 | } 244 | to { 245 | transform:rotate3d(0,0,1,-360deg); 246 | } 247 | } 248 | 249 | .spinning { 250 | animation-name: spin; 251 | animation-duration: 300ms; 252 | animation-iteration-count: infinite; 253 | animation-timing-function: ease-in-out; 254 | color: var(--accent) !important; 255 | } 256 | 257 | #icon-clear-a-container { 258 | display: inline; 259 | white-space: nowrap; 260 | cursor: pointer; 261 | } 262 | 263 | #icon-clear-a-container::before { 264 | left: -5rem; 265 | background-color: var(--clear); 266 | color: var(--page-bg); 267 | top: -0.75rem; 268 | padding: 0.2rem 0.5rem; 269 | padding-right: 0.5rem; 270 | border-bottom-left-radius: 0.5rem; 271 | border-top-left-radius: 0.5rem; 272 | padding-right: 1.2rem; 273 | font-weight: 900; 274 | z-index: -1; 275 | bottom: 0.35rem; 276 | text-decoration: underline 2px; 277 | } 278 | 279 | [data-tooltip] { 280 | position: relative; 281 | } 282 | [data-tooltip]::before { 283 | position : absolute; 284 | content : attr(data-tooltip); 285 | display: none; 286 | } 287 | 288 | [data-tooltip]:hover::before { 289 | display: block; 290 | } 291 | 292 | #icon-clear-a { 293 | border-radius: 100%; 294 | padding-inline: 0.2rem; 295 | margin-inline: -0.2rem; 296 | transition: none; 297 | } 298 | #icon-clear-a:hover, #icon-clear-a-container:hover > #icon-clear-a { 299 | background-color: var(--clear); 300 | color: var(--page-bg); 301 | } 302 | 303 | .title a { 304 | text-decoration: none; 305 | color: var(--text); 306 | display: flex; 307 | } 308 | 309 | .title a:hover { 310 | box-shadow: inset 0 -6px 0 0px var(--page-bg), inset 0 -8px 0 0px var(--accent); 311 | color: var(--accent); 312 | } 313 | .title a:not(:hover) > h3 { 314 | box-shadow: inset 0 -6px 0 0px var(--page-bg), inset 0 -8px 0 0px var(--divider); 315 | } 316 | 317 | .title > a > svg { 318 | color: inherit !important; 319 | margin-top: -6px; 320 | opacity: 0; 321 | transition: none; 322 | } 323 | 324 | .title > a:hover > svg { 325 | position: inherit; 326 | opacity: 1; 327 | } 328 | 329 | /* Settings CSS */ 330 | 331 | #settings { 332 | background-color: var(--page-bg); 333 | display:none; 334 | width: 300px; 335 | } 336 | 337 | .inline-link { 338 | color: var(--accent) !important; 339 | display: inline !important; 340 | text-decoration: underline 2px !important; 341 | } 342 | 343 | ul { 344 | margin-block: 0.2rem; 345 | } 346 | 347 | button { 348 | cursor: pointer; 349 | background-color: var(--accent); 350 | float:right; 351 | } 352 | input:not([type="radio"]), button { 353 | background-color: var(--bg); 354 | color: var(--text); 355 | border: none; 356 | padding: 0.5rem; 357 | border-radius: 0.5rem; 358 | transition: 0.15s background-color; 359 | font-weight: 600; 360 | } 361 | input:invalid { 362 | outline: 4px solid #db316255; 363 | } 364 | input:hover, button:hover { 365 | background-color: var(--bg_darker); 366 | } 367 | 368 | input[type="text"], input[type="password"] { 369 | width: calc(100% - 1rem); 370 | margin-block: 0.5rem; 371 | } 372 | 373 | input[type="checkbox"], input[type="radio"], label { 374 | user-select: none; 375 | } 376 | 377 | input[type="number"] { 378 | width: 3rem; 379 | margin-inline: 0.5rem; 380 | } 381 | 382 | .radio { 383 | display:inline-block; 384 | } 385 | 386 | .error { 387 | background-color: #db316255; 388 | color: var(--text); 389 | padding: 0.5rem; 390 | border-radius: 0.5rem; 391 | margin-block: 0.5rem; 392 | } 393 | 394 | #close-icon { 395 | display: flex; 396 | 397 | border-radius: 0.5rem; 398 | align-items: center; 399 | cursor: pointer; 400 | font-weight: 600; 401 | transition: 0.15s background-color, 0.15s color; 402 | color: var(--faded); 403 | } 404 | #close-icon * { 405 | transition: 0.15s background-color, 0.15s color; 406 | } 407 | 408 | #close-icon:hover *, #close-icon:hover { 409 | color: var(--accent) !important; 410 | } 411 | -------------------------------------------------------------------------------- /autogen_chrome/html/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |

Notifications

14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |

Notifications

40 | 41 | 42 |
43 |
44 | 45 |
46 | Save 47 | 48 |
49 |
50 |
51 | 52 |
53 |

Theme:

54 |
55 | 56 |
58 | 59 |
61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 |
70 |

71 | Create a token from your settings. Make sure to allow: 72 |

78 |

79 | 82 |
83 |
84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /autogen_chrome/html/notifications.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction = chrome.action 2 | 3 | const API_BASE = "https://api.modrinth.com/v2"; 4 | const LINK_BASE = "https://modrinth.com"; 5 | 6 | const MINUTE = 60 * 1000; 7 | const HOUR = 3600 * 1000; 8 | const DAY = 86400 * 1000; 9 | 10 | const N_VERSIONS = 3; 11 | 12 | const version_regex = /The project,? .*,? has released a new version: (.*)/m; 13 | 14 | function timeStringForUnix(unix) { 15 | if (unix > DAY) { 16 | time_string = Math.round(unix / DAY) + "d"; 17 | } else if (unix > HOUR) { 18 | time_string = Math.round(unix / HOUR) + "h"; 19 | } else if (unix > MINUTE) { 20 | time_string = Math.round(unix / MINUTE) + "m"; 21 | } else { 22 | time_string = Math.round(unix / 1000) + "s"; 23 | } 24 | return time_string; 25 | } 26 | 27 | async function fetchNotifs(user, token) { 28 | let h = new Headers({ 29 | Authorization: token, 30 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 31 | }); 32 | let resp = await fetch(API_BASE + "/user/" + user + "/notifications", { 33 | headers: h, 34 | }); 35 | 36 | if (resp.status != 200) { 37 | return { 38 | status: resp.status, 39 | notifications: undefined, 40 | }; 41 | } 42 | let json = await resp.json(); 43 | return { 44 | status: 200, 45 | notifications: json, 46 | }; 47 | } 48 | 49 | async function toggleSeeOld() { 50 | let hideable = document.querySelectorAll(".notification.hideable"); 51 | 52 | let n_old = document.querySelectorAll( 53 | ".notification.hideable .version", 54 | ).length; 55 | 56 | this.setAttribute( 57 | "is_expanded", 58 | !(this.getAttribute("is_expanded") == "true"), 59 | ); 60 | 61 | if (this.getAttribute("is_expanded") == "true") { 62 | for (const el of hideable) { 63 | el.style.display = "block"; 64 | } 65 | this.innerText = "- Less"; 66 | } else { 67 | for (const el of hideable) { 68 | el.style.display = "none"; 69 | } 70 | this.innerText = `+${n_old} Old`; 71 | } 72 | } 73 | 74 | function toggleExpandNotif() { 75 | let hideable = this.parentElement.querySelectorAll(".version.hideable"); 76 | 77 | this.setAttribute( 78 | "is_expanded", 79 | !(this.getAttribute("is_expanded") == "true"), 80 | ); 81 | 82 | if (this.getAttribute("is_expanded") == "true") { 83 | for (const el of hideable) { 84 | el.style.display = "flex"; 85 | } 86 | this.innerText = "- Less"; 87 | } else { 88 | for (const el of hideable) { 89 | el.style.display = "none"; 90 | } 91 | this.innerText = `+${hideable.length} More`; 92 | } 93 | } 94 | 95 | function build_notification( 96 | auth_token, 97 | project_info, 98 | versions_info, 99 | isOldType, 100 | startHidden = false, 101 | ) { 102 | let notification = document.createElement("div"); 103 | notification.className = "notification"; 104 | 105 | let s = ""; 106 | let s1 = (versions_info || []).length; 107 | let badge_t = (versions_info || []).length; 108 | if (versions_info && versions_info.length > 1) { 109 | s = "s"; 110 | } else if (versions_info == null) { 111 | s = "s: ∞"; 112 | s1 = "Too many! "; 113 | badge_t = "∞"; 114 | } 115 | 116 | if (versions_info == null) { 117 | } 118 | 119 | const title = isOldType 120 | ? project_info.replaceAll("**", "") 121 | : project_info.title + " has been updated!"; 122 | 123 | notification.innerHTML = `

${title}

\ 124 |

${s1} new version${s}:

Clear
`; 125 | notification.setAttribute("data-notification-count", badge_t); 126 | 127 | document.querySelector("#notifications").appendChild(notification); 128 | if (versions_info == null) return; 129 | 130 | let version_ids = []; 131 | 132 | let i = 0; 133 | for (const v of versions_info) { 134 | i++; 135 | let version = document.createElement("div"); 136 | version.className = "version"; 137 | if (i > N_VERSIONS) { 138 | version.classList = "version hideable"; 139 | } 140 | let version_link = document.createElement("a"); 141 | version_link.className = "version-link"; 142 | 143 | const link = isOldType 144 | ? v.link 145 | : `/mod/${v.project_id}/version/${v.id}`; 146 | version_link.href = LINK_BASE + link; 147 | version_link.target = "_blank"; 148 | 149 | const version_number = isOldType 150 | ? v.text.match(version_regex)[1] 151 | : v.version_number; 152 | version_link.innerText = version_number; 153 | version.appendChild(version_link); 154 | version.setAttribute("data-notification-id", v.id); 155 | version_ids.push(v.id); 156 | 157 | let clear_hitbox = document.createElement("div"); 158 | clear_hitbox.className = "clear-hitbox"; 159 | clear_hitbox.innerText = "Clear"; 160 | 161 | version.appendChild(clear_hitbox); 162 | clear_hitbox.addEventListener("click", async (ev) => { 163 | let el = ev.currentTarget; 164 | el.parentElement.classList.add("being-cleared"); 165 | let h = new Headers({ 166 | Authorization: auth_token, 167 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 168 | }); 169 | let resp = await fetch( 170 | API_BASE + 171 | `/notification/${el.parentElement.getAttribute("data-notification-id")}`, 172 | { 173 | headers: h, 174 | method: "DELETE", 175 | }, 176 | ); 177 | if (resp.status == 204) { 178 | let notification_el = el.parentElement.parentElement; 179 | let n_left = parseInt( 180 | notification_el.getAttribute("data-notification-count"), 181 | ); 182 | if (n_left - 1 > N_VERSIONS) { 183 | let to_display = notification_el.querySelector(".hideable"); 184 | to_display.classList.remove("hideable"); 185 | 186 | let more_button = notification_el.querySelector(".more"); 187 | let mb_is_closed = 188 | more_button.getAttribute("is_expanded") == "false"; 189 | 190 | // Update "more" text 191 | if (mb_is_closed) { 192 | more_button.innerText = `+${n_left - N_VERSIONS - 1} More`; 193 | } 194 | 195 | notification_el.setAttribute( 196 | "data-notification-count", 197 | n_left - 1, 198 | ); 199 | el.parentElement.remove(); 200 | } else if (n_left - 1 == 0) { 201 | notification_el.remove(); 202 | } else { 203 | el.parentElement.remove(); 204 | } 205 | } else { 206 | el.parentElement.classList.remove("being-cleared"); 207 | } 208 | }); 209 | 210 | const date_published = isOldType ? v.created : v.date_published; 211 | let time_passed = Date.now() - Date.parse(date_published); 212 | 213 | let time_string = timeStringForUnix(time_passed); 214 | 215 | version.setAttribute("after_text", time_string); 216 | if (startHidden) { 217 | notification.classList.add("hideable"); 218 | } 219 | notification.appendChild(version); 220 | notification 221 | .querySelector(".clear-all-button") 222 | .addEventListener("click", async (ev) => { 223 | let query_string = '["' + version_ids.join('","') + '"]'; 224 | let h = new Headers({ 225 | Authorization: auth_token, 226 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 227 | }); 228 | let notif = ev.currentTarget.parentElement.parentElement; 229 | notif.classList.add("being-cleared"); 230 | 231 | let resp = await fetch( 232 | API_BASE + `/notifications?ids=` + query_string, 233 | { 234 | headers: h, 235 | method: "DELETE", 236 | }, 237 | ); 238 | if (resp.status == 204) { 239 | notif.remove(); 240 | } 241 | }); 242 | } 243 | if (i > N_VERSIONS) { 244 | let more = document.createElement("p"); 245 | more.classList = "more"; 246 | more.setAttribute("is_expanded", false); 247 | more.addEventListener("click", toggleExpandNotif); 248 | more.innerText = `+${i - N_VERSIONS} More`; 249 | notification.appendChild(more); 250 | } 251 | } 252 | 253 | async function updateNotifs(ignore_last_checked) { 254 | ignore_last_checked == ignore_last_checked || false; 255 | 256 | let notif_enable = (await chrome.storage.sync.get("notif_enable")) 257 | .notif_enable; 258 | let last_status = (await chrome.storage.sync.get(["issue_connecting"])) 259 | .issue_connecting; 260 | 261 | // "Notifications disabled" 262 | 263 | if (document.querySelector("#notif-enable-popup")) { 264 | document.querySelector("#notif-enable-popup").remove(); 265 | } 266 | 267 | if (!notif_enable) { 268 | document.querySelectorAll("#notifications *").forEach((el) => { 269 | if (el.id != "up-to-date-notif") { 270 | el.remove(); 271 | } 272 | }); 273 | let notif_enable_popup = document.createElement("div"); 274 | notif_enable_popup.classList = "notification"; 275 | notif_enable_popup.id = "notif-enable-popup"; 276 | notif_enable_popup.innerHTML = `

Notifications are disabled

Go to settings to enable them

`; 277 | document 278 | .querySelector("#notifications") 279 | .appendChild(notif_enable_popup); 280 | chrome.browserAction.setBadgeText({ text: "" }); 281 | return; 282 | } 283 | 284 | if (last_status != 0) { 285 | document.querySelectorAll("#notifications *").forEach((el) => { 286 | if (el.id != "up-to-date-notif") { 287 | el.remove(); 288 | } 289 | }); 290 | restoreSettings(); 291 | return; 292 | } 293 | 294 | document.querySelector("#refresh-icon-a").classList = "spinning"; 295 | 296 | let global_user = (await chrome.storage.sync.get("user")).user; 297 | 298 | let global_auth_token = (await chrome.storage.sync.get("token")).token; 299 | let resp = await fetchNotifs(global_user, global_auth_token); 300 | 301 | if (resp.status == 401) { 302 | chrome.storage.sync.set({ issue_connecting: 401 }); 303 | chrome.browserAction.setBadgeText({ text: "ERR" }); 304 | document.querySelector("#refresh-icon-a").classList = ""; 305 | restoreSettings(); 306 | return; 307 | } else if (resp.status == 404) { 308 | chrome.storage.sync.set({ issue_connecting: 404 }); 309 | chrome.browserAction.setBadgeText({ text: "ERR" }); 310 | document.querySelector("#refresh-icon-a").classList = ""; 311 | restoreSettings(); 312 | return; 313 | } 314 | 315 | let parsed = resp.notifications; 316 | 317 | // New notification type logic 318 | 319 | // To mass fetch version strings 320 | let version_ids = new Array(); 321 | let project_ids = new Set(); 322 | 323 | let notif_ids = new Array(); 324 | 325 | for (let i = 0; i < parsed.length; i++) { 326 | notif = parsed[i]; 327 | if (notif.body) { 328 | if (notif.body.type != "project_update") { 329 | continue; 330 | } 331 | 332 | version_ids.push(notif.body.version_id); 333 | project_ids.add(notif.body.project_id); 334 | notif_ids.push(notif.id); 335 | } 336 | } 337 | 338 | // fetch titles and add notifications 339 | 340 | // NOTE: Project and version result has the same order as the query parameters 341 | 342 | let pids_query = '["' + Array.from(project_ids).join('","') + '"]'; 343 | let verids_query = '["' + version_ids.join('","') + '"]'; 344 | pro_json = fetch(API_BASE + "/projects?ids=" + pids_query).then( 345 | (response) => { 346 | return response.json(); 347 | }, 348 | ); 349 | ver_json = fetch(API_BASE + "/versions?ids=" + verids_query).then( 350 | (response) => { 351 | if (response.ok == false) return null; 352 | return response.json(); 353 | }, 354 | ); 355 | 356 | let result = await Promise.all([pro_json, ver_json]); 357 | let projects = result[0]; 358 | projects.sort((a, b) => { 359 | unixA = Date.parse(a.updated); 360 | unixB = Date.parse(b.updated); 361 | if (unixA > unixB) { 362 | return -1; 363 | } else { 364 | return 1; 365 | } 366 | }); 367 | let versions = result[1]; 368 | 369 | // Map project_id => [version_info] 370 | let project_id_to_version_info = new Map(); 371 | for (let i = 0; i < (versions || []).length; i++) { 372 | let v = versions[i]; 373 | let a = project_id_to_version_info.get(v.project_id) || []; 374 | a.push(v); 375 | project_id_to_version_info.set(v.project_id, a); 376 | } 377 | // Map project_id => project_info 378 | let project_id_to_project_info = new Map(); 379 | for (let i = 0; i < projects.length; i++) { 380 | project_id_to_project_info[projects[i].id] = projects[i]; 381 | } 382 | 383 | // Old notification type logic 384 | 385 | let updated = new Map(); 386 | let old = new Map(); 387 | let n_old = 0; 388 | let n_updated = 0; 389 | 390 | for (let i = 0; i < parsed.length; i++) { 391 | let el = parsed[i]; 392 | let last_checked = (await chrome.storage.sync.get(["last_checked"])) 393 | .last_checked; 394 | let created_date = 395 | el.body.type == "legacy_markdown" ? el.created : el.date_published; 396 | notif_ids.push(el.id); 397 | if (last_checked > Date.parse(created_date)) { 398 | if (el.body.type == "legacy_markdown") { 399 | let a = old.get(el.title) || []; 400 | a.push(el); 401 | old.set(el.title, a); 402 | } 403 | n_old += 1; 404 | } else { 405 | if (el.body.type == "legacy_markdown") { 406 | let a = updated.get(el.title) || []; 407 | a.push(el); 408 | updated.set(el.title, a); 409 | } 410 | n_updated += 1; 411 | } 412 | } 413 | // end 414 | 415 | // Update "Clear all" button with new IDs 416 | let cback = async (ev) => { 417 | let query_string = '["' + notif_ids.join('","') + '"]'; 418 | let h = new Headers({ 419 | Authorization: global_auth_token, 420 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 421 | }); 422 | let resp = await fetch( 423 | API_BASE + `/notifications?ids=` + query_string, 424 | { 425 | headers: h, 426 | method: "DELETE", 427 | }, 428 | ); 429 | document 430 | .querySelectorAll(".notification") 431 | .forEach((el) => el.classList.add("being-cleared")); 432 | if (resp.status == 204) { 433 | document 434 | .querySelectorAll(".notification") 435 | .forEach((el) => el.remove()); 436 | updateNotifs(false); 437 | } else { 438 | document 439 | .querySelectorAll(".notification") 440 | .forEach((el) => el.classList.remove("being-cleared")); 441 | } 442 | }; 443 | document.querySelector("#icon-clear-a").removeEventListener("click", cback); 444 | document.querySelector("#icon-clear-a").addEventListener("click", cback); 445 | 446 | if (document.querySelector("#no-notifs")) { 447 | document.querySelector("#no-notifs").remove(); 448 | } 449 | 450 | if (n_updated > 0) { 451 | chrome.browserAction.setBadgeText({ text: n_updated.toString() }); 452 | } 453 | if (n_updated == 0 && n_old == 0) { 454 | let no_notifs = document.createElement("div"); 455 | no_notifs.classList = "notification"; 456 | no_notifs.id = "no-notifs"; 457 | no_notifs.innerHTML = `

No new notifications

`; 458 | document.querySelector("#notifications").appendChild(no_notifs); 459 | chrome.browserAction.setBadgeText({ text: "" }); 460 | 461 | document.querySelector("#refresh-icon-a").classList = ""; 462 | return; 463 | } 464 | // Do this here to minimize blank time 465 | document.querySelectorAll("#notifications *").forEach((el) => { 466 | if (el.id != "up-to-date-notif") { 467 | el.remove(); 468 | } 469 | }); 470 | 471 | if (versions == null) { 472 | warning = document.createElement("h1"); 473 | warning.innerHTML = 474 | "Clear your notifications! The modrinth API has reached an internal limit and has returned an invalid response."; 475 | warning.style = "color: red; font-weight: 900;"; 476 | document.querySelector("#notifications").appendChild(warning); 477 | } 478 | 479 | for (project_info of projects) { 480 | build_notification( 481 | global_auth_token, 482 | project_info, 483 | project_id_to_version_info.get(project_info.id), 484 | false, 485 | false, 486 | ); 487 | } 488 | 489 | updated.forEach((new_vers, title) => { 490 | build_notification(global_auth_token, title, new_vers, true, false); 491 | }); 492 | 493 | if (n_old > 0) { 494 | if (document.querySelector("#up-to-date-notif")) { 495 | document.querySelector("#up-to-date-notif").remove(); 496 | } 497 | let old_separator = document.createElement("hr"); 498 | document.querySelector("#notifications").appendChild(old_separator); 499 | let up_to_date = document.createElement("div"); 500 | up_to_date.classList = "notification"; 501 | up_to_date.id = "up-to-date-notif"; 502 | up_to_date.innerHTML = `

See older notifications

+${n_old} Old

`; 503 | document.querySelector("#notifications").appendChild(up_to_date); 504 | document 505 | .querySelector("#more-old") 506 | .addEventListener("click", toggleSeeOld); 507 | 508 | // Place hidden old notifs 509 | old.forEach((new_vers, title) => { 510 | build_notification(global_auth_token, title, new_vers, true, true); 511 | }); 512 | } 513 | document.querySelector("#refresh-icon-a").classList = ""; 514 | } 515 | 516 | async function saveOptions(e) { 517 | if (e) { 518 | e.preventDefault(); 519 | } 520 | let form = document.querySelector("form"); 521 | 522 | const data = new FormData(form); 523 | 524 | let theme = data.get("theme") || "auto"; 525 | 526 | const notif_enable = data.get("notif-enable") == "on"; 527 | const check_delay = data.get("notif-check-delay"); 528 | 529 | const token = data.get("token").trim(); 530 | 531 | if (notif_enable == true && (token == "" || token == undefined)) { 532 | document.querySelector(".error").innerText = 533 | "Please fill out these fields to enable notifications"; 534 | document.querySelector(".error").style.display = "block"; 535 | document.querySelector("#token").style.outline = "4px solid #db316255"; 536 | 537 | chrome.storage.sync.set({ 538 | check_delay: check_delay, 539 | theme: theme, 540 | notif_enable: false, 541 | issue_connecting: 0, 542 | }); 543 | 544 | return false; 545 | } else if (notif_enable == false) { 546 | if (token == undefined) { 547 | token = ""; 548 | } 549 | await chrome.storage.sync.set({ 550 | token: token, 551 | }); 552 | } 553 | 554 | let old_token = await chrome.storage.sync.get("token"); 555 | 556 | if (old_token.token != token && notif_enable == true) { 557 | let close_icon = document.querySelector("#close-icon svg"); 558 | close_icon.classList = "spinning"; 559 | let h = new Headers({ 560 | Authorization: token, 561 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}`, 562 | }); 563 | 564 | let resp = await fetch(API_BASE + "/user", { 565 | headers: h, 566 | }); 567 | close_icon.classList = ""; 568 | if (resp.status == 401) { 569 | document.querySelector(".error").innerText = 570 | "Invalid API authorization token"; 571 | document.querySelector(".error").style.display = "block"; 572 | document.querySelector("#token").style.outline = 573 | "4px solid #db316255"; 574 | 575 | chrome.storage.sync.set({ 576 | check_delay: check_delay, 577 | theme: theme, 578 | notif_enable: false, 579 | issue_connecting: 0, 580 | }); 581 | return false; 582 | } 583 | 584 | let resp_json = await resp.json(); 585 | chrome.storage.sync.set({ 586 | token: token, 587 | user: resp_json.username, 588 | }); 589 | } 590 | 591 | await chrome.storage.sync.set({ 592 | check_delay: check_delay, 593 | theme: theme, 594 | notif_enable: notif_enable, 595 | issue_connecting: 0, 596 | }); 597 | document.querySelector(".error").style.display = "none"; 598 | return true; 599 | } 600 | 601 | async function restoreSettings() { 602 | document.querySelector("#settings").style.display = "block"; 603 | document.querySelector("#main").style.display = "none"; 604 | 605 | let a = await chrome.storage.sync.get([ 606 | "token", 607 | "user", 608 | "notif_enable", 609 | "issue_connecting", 610 | "theme", 611 | "check_delay", 612 | ]); 613 | 614 | document.querySelector("#notif-enable").checked = a.notif_enable || false; 615 | document.querySelector("#token").value = a.token || ""; 616 | 617 | a.theme = a.theme || "auto"; 618 | document.querySelector(`#${a.theme}-theme`).checked = "on"; 619 | document.querySelector("#notif-check-delay").value = a.check_delay || 10; 620 | 621 | if (a.issue_connecting != 0) { 622 | if (a.issue_connecting == 401) { 623 | document.querySelector(".error").innerText = 624 | "There has been an issue connecting to Modrinth:\nError 401 Unauthorized\n(Invalid token)"; 625 | document.querySelector(".error").style.display = "block"; 626 | document.querySelector("#token").style.outline = 627 | "4px solid #db316255"; 628 | } else if (a.issue_connecting == 404) { 629 | document.querySelector(".error").innerText = 630 | "There has been an issue connecting to Modrinth:\nError 404 User not found\n(Invalid token)"; 631 | document.querySelector(".error").style.display = "block"; 632 | } 633 | } 634 | } 635 | 636 | async function closeSettings() { 637 | document.querySelector(".error").style.display = "none"; 638 | document.querySelector("#token").style.outline = "none"; 639 | let do_close = await saveOptions(); 640 | if (do_close) { 641 | document.querySelector("#main").style.display = "block"; 642 | document.querySelector("#settings").style.display = "none"; 643 | changeTheme(); 644 | updateNotifs(false); 645 | } 646 | } 647 | 648 | async function updateLastChecked() { 649 | // Remove notifications 650 | unix = Date.now(); 651 | chrome.storage.sync.set({ 652 | last_checked: unix, 653 | }); 654 | updateNotifs(false); 655 | } 656 | 657 | function updateNotifsNoVar() { 658 | updateNotifs(false); 659 | } 660 | 661 | async function getLastChecked() { 662 | let a = await chrome.storage.sync.get(["last_checked"]).last_checked; 663 | return a; 664 | } 665 | document 666 | .querySelector("#settings-icon") 667 | .addEventListener("click", restoreSettings); 668 | document.querySelector("#close-icon").addEventListener("click", closeSettings); 669 | document.querySelector("form").addEventListener("submit", saveOptions); 670 | document 671 | .querySelector("#refresh-icon-a") 672 | .addEventListener("click", updateNotifsNoVar); 673 | // document.querySelector("#icon-clear-a").addEventListener("click", updateLastChecked) 674 | document.addEventListener("DOMContentLoaded", updateNotifsNoVar); 675 | 676 | function changeTheme() { 677 | chrome.storage.sync.get("theme").then((resp) => { 678 | document.querySelector("body").setAttribute("data-theme", resp.theme); 679 | }); 680 | } 681 | changeTheme(); 682 | -------------------------------------------------------------------------------- /autogen_chrome/html/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 50 | 252 | 323 | 324 |
325 |
326 | 327 |

Notifications

328 | 329 | 330 |
331 |
332 | 333 |
334 | Save 335 | 336 |
337 |
338 |
339 | 340 |
341 |

Theme:

342 |
343 | 344 |
346 | 347 |
349 | 350 |
351 |
352 | 353 | 354 |
355 |
356 | 357 |
358 |

359 | Create a token from your settings. Make sure to allow: 360 |

366 |

367 | 370 |
371 |
372 | 373 | 374 | 375 | -------------------------------------------------------------------------------- /autogen_chrome/html/preferences.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction = chrome.action 2 | 3 | async function closeSettings() { 4 | document.querySelector(".error").style.display = "none" 5 | document.querySelector("#token").style.outline = "none" 6 | let do_close = await saveOptions() 7 | if (do_close) { 8 | changeTheme() 9 | // updateNotifs(false) 10 | } 11 | } 12 | function changeTheme() { 13 | 14 | chrome.storage.sync.get("theme").then((resp) => { 15 | document.querySelector("body").setAttribute("data-theme", resp.theme) 16 | }) 17 | 18 | } 19 | async function saveOptions(e) { 20 | if (e) { 21 | e.preventDefault(); 22 | } 23 | let form = document.querySelector("form") 24 | 25 | const data = new FormData(form) 26 | 27 | let theme = data.get("theme") || "auto" 28 | 29 | const notif_enable = data.get("notif-enable") == "on"; 30 | const check_delay = data.get("notif-check-delay"); 31 | 32 | const token = data.get("token").trim(); 33 | // console.log(token) 34 | 35 | if (notif_enable == true && (token == "" || token == undefined)) { 36 | document.querySelector(".error").innerText = "Please fill out these fields to enable notifications" 37 | document.querySelector(".error").style.display = "block" 38 | document.querySelector("#token").style.outline = "4px solid #db316255" 39 | 40 | chrome.storage.sync.set({ 41 | check_delay: check_delay, 42 | theme: theme, 43 | notif_enable: false, 44 | issue_connecting: 0, 45 | }) 46 | 47 | return false 48 | } else if (notif_enable == false) { 49 | if (token == undefined) { 50 | token = "" 51 | } 52 | await chrome.storage.sync.set({ 53 | token: token 54 | }) 55 | } 56 | 57 | let old_token = await chrome.storage.sync.get("token") 58 | // console.log(old_token, '"', token) 59 | 60 | if (old_token.token != token && notif_enable == true) { 61 | 62 | let close_icon = document.querySelector("#close-icon svg") 63 | close_icon.classList = "spinning" 64 | let h = new Headers({ 65 | "Authorization": token, 66 | "User-Agent": `devBoi76/modrinthify/${chrome.runtime.getManifest().version}` 67 | }) 68 | 69 | let resp = await fetch(API_BASE+"/user", { 70 | headers: h 71 | }) 72 | close_icon.classList = "" 73 | if (resp.status == 401) { 74 | document.querySelector(".error").innerText = "Invalid authorization token" 75 | document.querySelector(".error").style.display = "block" 76 | document.querySelector("#token").style.outline = "4px solid #db316255" 77 | 78 | chrome.storage.sync.set({ 79 | check_delay: check_delay, 80 | theme: theme, 81 | notif_enable: false, 82 | issue_connecting: 0, 83 | }) 84 | return false 85 | } 86 | 87 | let resp_json = await resp.json() 88 | chrome.storage.sync.set({ 89 | token: token, 90 | user: resp_json.username 91 | }) 92 | } 93 | 94 | await chrome.storage.sync.set({ 95 | check_delay: check_delay, 96 | theme: theme, 97 | notif_enable: notif_enable, 98 | issue_connecting: 0, 99 | }) 100 | document.querySelector(".error").style.display = "none" 101 | return true 102 | } 103 | 104 | async function restoreSettings() { 105 | // document.querySelector("#settings").style.display = "block" 106 | // document.querySelector("#main").style.display = "none" 107 | 108 | let a = await chrome.storage.sync.get(["token", "user", "notif_enable", "issue_connecting", "theme", "check_delay"]) 109 | 110 | document.querySelector("#notif-enable").checked = a.notif_enable || false; 111 | document.querySelector("#token").value = a.token || ""; 112 | 113 | a.theme = a.theme || "auto" 114 | document.querySelector(`#${a.theme}-theme`).checked = "on" 115 | document.querySelector("#notif-check-delay").value = a.check_delay || 5 116 | 117 | if (a.issue_connecting != 0) { 118 | if (a.issue_connecting == 401) { 119 | document.querySelector(".error").innerText = "There has been an issue connecting to Modrinth:\nError 401 Unauthorized\n(Invalid token)" 120 | document.querySelector(".error").style.display = "block" 121 | document.querySelector("#token").style.outline = "4px solid #db316255" 122 | } 123 | else if (a.issue_connecting == 404) { 124 | document.querySelector(".error").innerText = "There has been an issue connecting to Modrinth:\nError 404 User not found\n(Invalid token)" 125 | document.querySelector(".error").style.display = "block" 126 | 127 | } 128 | } 129 | 130 | } 131 | 132 | document.querySelector("#close-icon").addEventListener("click", closeSettings); 133 | document.querySelector("form").addEventListener("submit", saveOptions); 134 | document.addEventListener("DOMContentLoaded", restoreSettings) 135 | 136 | changeTheme(); -------------------------------------------------------------------------------- /autogen_chrome/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/autogen_chrome/icons/favicon.png -------------------------------------------------------------------------------- /autogen_chrome/icons/kofilogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/autogen_chrome/icons/kofilogo.png -------------------------------------------------------------------------------- /autogen_chrome/main.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction = chrome.action 2 | 3 | function htmlToElements(html) { 4 | var t = document.createElement("template"); 5 | t.innerHTML = html; 6 | return t.content; 7 | } 8 | 9 | function similarity(s1, s2) { 10 | var longer = s1; 11 | var shorter = s2; 12 | if (s1.length < s2.length) { 13 | longer = s2; 14 | shorter = s1; 15 | } 16 | var longerLength = longer.length; 17 | if (longerLength == 0) { 18 | return 1.0; 19 | } 20 | return ( 21 | (longerLength - editDistance(longer, shorter)) / 22 | parseFloat(longerLength) 23 | ); 24 | } 25 | 26 | function editDistance(s1, s2) { 27 | s1 = s1.toLowerCase(); 28 | s2 = s2.toLowerCase(); 29 | 30 | var costs = new Array(); 31 | for (var i = 0; i <= s1.length; i++) { 32 | var lastValue = i; 33 | for (var j = 0; j <= s2.length; j++) { 34 | if (i == 0) costs[j] = j; 35 | else { 36 | if (j > 0) { 37 | var newValue = costs[j - 1]; 38 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 39 | newValue = 40 | Math.min(Math.min(newValue, lastValue), costs[j]) + 41 | 1; 42 | costs[j - 1] = lastValue; 43 | lastValue = newValue; 44 | } 45 | } 46 | } 47 | if (i > 0) costs[s2.length] = lastValue; 48 | } 49 | return costs[s2.length]; 50 | } 51 | 52 | const new_design_button = 53 | '
MOD_NAME
Get on Modrinth
'; 54 | 55 | const new_design_donation = ``; 56 | 57 | const svg = 58 | ''; 59 | 60 | const HTML = ` \ 61 |
\ 66 |
\ 67 | ${svg} 68 |
\ 69 | Get on Modrinth\ 70 |
\ 71 | `; 72 | 73 | const DONATE_HTML = `\ 74 | \ 79 |
\ 80 | \ 81 |
\ 82 | Support the Author 83 |
\ 84 | `; 85 | 86 | const REGEX = 87 | /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gim; 88 | 89 | const MOD_PAGE_HTML = `
\ 90 | \ 91 |
MOD_NAME
\ 92 |
BUTTON_HTML
`; 93 | 94 | const SEARCH_PAGE_HTML = `
\ 95 | \ 96 |
MOD_NAME
\ 97 |
BUTTON_HTML
`; 98 | 99 | let query = "head title"; 100 | const tab_title = document.querySelector(query).innerText; 101 | let mod_name = undefined; 102 | let mod_name_noloader = undefined; 103 | let page = undefined; 104 | 105 | function main() { 106 | const url = document.URL.split("/"); 107 | page = url[4]; 108 | 109 | const is_new_design = !location.hostname.startsWith("old.curseforge.com"); 110 | 111 | const is_search = is_new_design 112 | ? url[4].split("?")[0] == "search" 113 | : url[5].startsWith("search") && url[5].split("?").length >= 2; 114 | 115 | if (is_search) { 116 | if (is_new_design) { 117 | search_query = document.querySelector(".search-input-field").value; 118 | } else { 119 | search_query = document 120 | .querySelector(".mt-6 > h2:nth-child(1)") 121 | .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.title.split(" - Minecraft ")[0]; 127 | } else { 128 | search_query = document 129 | .querySelector("head meta[property='og:title']") 130 | .getAttribute("content"); 131 | } 132 | } 133 | 134 | mod_name = search_query; 135 | mod_name_noloader = mod_name.replace(REGEX, ""); 136 | 137 | if (is_search && is_new_design) { 138 | page_re = /.*&class=(.*?)&.*/; 139 | page = (page.match(page_re) || ["", "all"])[1]; 140 | } 141 | 142 | api_facets = ""; 143 | switch (page) { 144 | //=Mods=============== 145 | case "mc-mods": 146 | api_facets = `facets=[["categories:'forge'","categories:'fabric'","categories:'quilt'","categories:'liteloader'","categories:'modloader'","categories:'rift'"],["project_type:mod"]]`; 147 | break; 148 | //=Server=Plugins===== 149 | case "mc-addons": 150 | return; 151 | case "customization": 152 | api_facets = `facets=[["project_type:shader"]]`; 153 | break; 154 | case "bukkit-plugins": 155 | api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]`; 156 | break; 157 | //=Resource=Packs===== 158 | case "texture-packs": 159 | api_facets = `facets=[["project_type:resourcepack"]]`; 160 | break; 161 | //=Modpacks=========== 162 | case "modpacks": 163 | api_facets = `facets=[["project_type:modpack"]]`; 164 | break; 165 | case "all": 166 | api_facets = ``; 167 | break; 168 | } 169 | 170 | fetch( 171 | `https://api.modrinth.com/v2/search?limit=3&query=${mod_name_noloader}&${api_facets}`, 172 | { method: "GET", mode: "cors" }, 173 | ) 174 | .then((response) => response.json()) 175 | .then((resp) => { 176 | let bd = document.querySelector("#modrinth-body"); 177 | if (bd) { 178 | bd.remove(); 179 | } 180 | 181 | if (page == undefined) { 182 | return; 183 | } 184 | 185 | if (resp.hits.length == 0) { 186 | return; 187 | } 188 | 189 | let max_sim = 0; 190 | let max_hit = undefined; 191 | 192 | for (const hit of resp.hits) { 193 | if (similarity(hit.title.trim(), mod_name) > max_sim) { 194 | max_sim = similarity(hit.title.trim(), mod_name.trim()); 195 | max_hit = hit; 196 | } 197 | if (similarity(hit.title.trim(), mod_name_noloader) > max_sim) { 198 | max_sim = similarity( 199 | hit.title.trim(), 200 | mod_name_noloader.trim(), 201 | ); 202 | max_hit = hit; 203 | } 204 | } 205 | 206 | if (max_sim <= 0.7) { 207 | return; 208 | } 209 | // Add the buttons 210 | 211 | if (is_search) { 212 | if (is_new_design) { 213 | // query = ".results-count" 214 | query = ".search-tags"; 215 | let s = document.querySelector(query); 216 | let buttonElement = htmlToElements( 217 | new_design_button 218 | .replace("ICON_SOURCE", max_hit.icon_url) 219 | .replace("MOD_NAME", max_hit.title.trim()) 220 | .replace( 221 | "REDIRECT", 222 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 223 | ) 224 | .replace("BUTTON_HTML", HTML), 225 | ); 226 | buttonElement.childNodes[0].style.marginLeft = "auto"; 227 | s.appendChild(buttonElement); 228 | } else { 229 | query = ".mt-6 > div:nth-child(3)"; 230 | let s = document.querySelector(query); 231 | let buttonElement = htmlToElements( 232 | SEARCH_PAGE_HTML.replace( 233 | "ICON_SOURCE", 234 | max_hit.icon_url, 235 | ) 236 | .replace("MOD_NAME", max_hit.title.trim()) 237 | .replace( 238 | "REDIRECT", 239 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 240 | ) 241 | .replace("BUTTON_HTML", HTML), 242 | ); 243 | s.appendChild(buttonElement); 244 | } 245 | } else { 246 | if (is_new_design) { 247 | query = ".actions"; 248 | let s = document.querySelector(query); 249 | let buttonElement = htmlToElements( 250 | new_design_button 251 | .replace("ICON_SOURCE", max_hit.icon_url) 252 | .replace("MOD_NAME", max_hit.title.trim()) 253 | .replace( 254 | "REDIRECT", 255 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 256 | ), 257 | ); 258 | s.appendChild(buttonElement); 259 | } else { 260 | query = "div.-mx-1:nth-child(1)"; 261 | let s = document.querySelector(query); 262 | let buttonElement = htmlToElements( 263 | MOD_PAGE_HTML.replace("ICON_SOURCE", max_hit.icon_url) 264 | .replace("MOD_NAME", max_hit.title.trim()) 265 | .replace( 266 | "REDIRECT", 267 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 268 | ) 269 | .replace("BUTTON_HTML", HTML), 270 | ); 271 | s.appendChild(buttonElement); 272 | } 273 | } 274 | // Add donation button if present 275 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, { 276 | method: "GET", 277 | mode: "cors", 278 | }) 279 | .then((response_p) => response_p.json()) 280 | .then((resp_p) => { 281 | if (document.querySelector("#donate-button")) { 282 | return; 283 | } 284 | if (resp_p.donation_urls.length > 0) { 285 | let redir = document.getElementById("modrinth-body"); 286 | 287 | if (is_new_design) { 288 | redir.innerHTML += new_design_donation.replace( 289 | "REDIRECT", 290 | resp_p.donation_urls[0].url, 291 | ); 292 | if (is_search) { 293 | redir.style.marginRight = "-195.5px"; 294 | } else { 295 | redir.style.marginRight = "-195.5px"; 296 | } 297 | } else { 298 | let donations = resp_p.donation_urls; 299 | let dbutton = document.createElement("div"); 300 | dbutton.innerHTML = DONATE_HTML.replace( 301 | "REDIRECT", 302 | donations[0].url, 303 | ); 304 | dbutton.style.display = "inline-block"; 305 | let redir = document.getElementById( 306 | "modrinthify-redirect", 307 | ); 308 | redir.after(dbutton); 309 | if (!is_search) { 310 | redir.parentNode.parentNode.parentNode.style.marginRight = 311 | "-150px"; 312 | } 313 | } 314 | } 315 | }); 316 | }); 317 | } 318 | 319 | main(); 320 | 321 | // document.querySelector(".classes-list").childNodes.forEach( (el) => { 322 | // el.childNodes[0].addEventListener("click", main) 323 | // }) 324 | 325 | let lastURL = document.URL; 326 | new MutationObserver(() => { 327 | let url = document.URL; 328 | if (url != lastURL) { 329 | lastURL = url; 330 | main(); 331 | } 332 | }).observe(document, { subtree: true, childList: true }); 333 | 334 | // document.querySelector(".search-input-field").addEventListener("keydown", (event) => { 335 | // if (event.key == "Enter") { 336 | // main() 337 | // } 338 | // }) 339 | -------------------------------------------------------------------------------- /autogen_chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Modrinthify", 4 | "version": "1.7.2", 5 | 6 | "description": "Redirect Curseforge mod pages to Modrinth and show Modrinth notifications", 7 | 8 | "icons": { 9 | "48": "icons/favicon.png" 10 | }, 11 | "host_permissions": ["https://api.modrinth.com/v2/*"], 12 | "permissions": ["storage", "alarms"], 13 | "content_scripts": [ 14 | { 15 | "matches": ["*://*.curseforge.com/minecraft/*"], 16 | "js": ["main.js"] 17 | }, 18 | { 19 | "matches": ["*://*.spigotmc.org/*"], 20 | "js": ["spigot.js"] 21 | } 22 | ], 23 | "web_accessible_resources": [ 24 | { 25 | "resources": ["icons/kofilogo.png"], 26 | "matches": [ 27 | "https://*.curseforge.com/*", 28 | "https://*.spigotmc.org/*" 29 | ] 30 | } 31 | ], 32 | "action": { 33 | "default_icon": "icons/favicon.png", 34 | "default_title": "Modrinthify", 35 | "default_popup": "html/main.html" 36 | }, 37 | "background": { 38 | "service_worker": "background.js" 39 | }, 40 | "options_ui": { 41 | "page": "html/preferences.html" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /autogen_chrome/spigot.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction = chrome.action 2 | 3 | function similarity(s1, s2) { 4 | var longer = s1; 5 | var shorter = s2; 6 | if (s1.length < s2.length) { 7 | longer = s2; 8 | shorter = s1; 9 | } 10 | var longerLength = longer.length; 11 | if (longerLength == 0) { 12 | return 1.0; 13 | } 14 | return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); 15 | } 16 | 17 | function editDistance(s1, s2) { 18 | s1 = s1.toLowerCase(); 19 | s2 = s2.toLowerCase(); 20 | 21 | var costs = new Array(); 22 | for (var i = 0; i <= s1.length; i++) { 23 | var lastValue = i; 24 | for (var j = 0; j <= s2.length; j++) { 25 | if (i == 0) 26 | costs[j] = j; 27 | else { 28 | if (j > 0) { 29 | var newValue = costs[j - 1]; 30 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 31 | newValue = Math.min(Math.min(newValue, lastValue), 32 | costs[j]) + 1; 33 | costs[j - 1] = lastValue; 34 | lastValue = newValue; 35 | } 36 | } 37 | } 38 | if (i > 0) 39 | costs[s2.length] = lastValue; 40 | } 41 | return costs[s2.length]; 42 | } 43 | 44 | const svg = '' 45 | 46 | const HTML = ` 47 |
\ 52 |
\ 53 | ${svg} 54 |
\ 55 | Get on Modrinth\ 56 |
\ 57 | ` 58 | 59 | const DONATE_HTML = `\ 60 |
Support the Author 61 |
62 | ` 63 | 64 | const REGEX = /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gmi 65 | PLUGIN_PAGE_HTML = ` \ 82 |
MR_NAME
83 |
84 | Get on Modrinth 85 |
86 |
` 87 | 88 | const SEARCH_PAGE_HTML = `
\ 89 | \ 90 |
MOD_NAME
\ 91 |
BUTTON_HTML
` 92 | 93 | function main() { 94 | const api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]` 95 | 96 | const url = document.URL.split("/") 97 | 98 | let is_search = false 99 | let is_project_page = false 100 | if (url[3] == "search") { 101 | is_search = true 102 | } else if (url[3] == "resources") { 103 | is_project_page = true 104 | } else return 105 | 106 | let query = "" 107 | if (is_search) { 108 | query = document.querySelector(".titleBar > h1:nth-child(1)").textContent.match(/Search Results for Query: (.*)/)[1] 109 | 110 | } else if (is_project_page) { 111 | query = document.querySelector(".resourceInfo > h1:nth-child(3)").firstChild.textContent 112 | } 113 | 114 | fetch(`https://api.modrinth.com/v2/search?limit=3&query=${query}&${api_facets}`, {method: "GET", mode: "cors"}) 115 | .then(response => response.json()) 116 | .then(resp => { 117 | 118 | if (resp.hits.length == 0) { 119 | return 120 | } 121 | 122 | let max_sim = 0 123 | let max_hit = undefined 124 | 125 | for (const hit of resp.hits) { 126 | if (similarity(hit.title.trim(), query) > max_sim) { 127 | max_sim = similarity(hit.title.trim(), query) 128 | max_hit = hit 129 | } 130 | } 131 | if (max_sim <= 0.7) { 132 | return 133 | } 134 | 135 | if (is_search) { 136 | let s = document.querySelector("div.pageNavLinkGroup:nth-child(2)") 137 | let div = document.createElement("div") 138 | div.outerHTML = s.innerHTML = PLUGIN_PAGE_HTML 139 | .replace("MR_ICON", max_hit.icon_url) 140 | .replace("MR_NAME", max_hit.title.trim()) 141 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 142 | .replace("BUTTON_HTML", HTML) + s.innerHTML 143 | 144 | s.after(div) 145 | 146 | } else if (is_project_page) { 147 | let s = document.querySelector(".innerContent") 148 | 149 | s.innerHTML = PLUGIN_PAGE_HTML 150 | .replace("MR_ICON", max_hit.icon_url) 151 | .replace("MR_NAME", max_hit.title.trim()) 152 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 153 | .replace("BUTTON_HTML", HTML) + s.innerHTML 154 | } 155 | 156 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, {method: "GET", mode: "cors"}) 157 | .then(response_p => response_p.json()) 158 | .then(resp_p => { 159 | if (resp_p.donation_urls.length > 0) { 160 | document.querySelector("#modrinth-body div").innerHTML += DONATE_HTML.replace("REDIRECT", resp_p.donation_urls[0].url) 161 | // if (!is_search) { 162 | // redir.parentNode.parentNode.parentNode.style.marginRight = "-150px" 163 | // } 164 | } 165 | }) 166 | 167 | 168 | }) 169 | 170 | 171 | } 172 | 173 | main() 174 | -------------------------------------------------------------------------------- /chrome/html/chrome.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | width:300px; 4 | height: calc(420px); 5 | line-height: 1.3; 6 | position: relative; 7 | color: var(--text); 8 | background-color: var(--page-bg); 9 | font-size: 100%; 10 | } 11 | 12 | *::-webkit-scrollbar { 13 | width: 8px; 14 | } 15 | 16 | ::-webkit-scrollbar-track { 17 | background: var(--page-bg); 18 | } 19 | 20 | ::-webkit-scrollbar-thumb { 21 | background: var(--divider); 22 | border-radius: 4px; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb:hover { 26 | background: #babfc5; 27 | } 28 | 29 | #icon-clear-a-container::before { 30 | top: -1.05rem; 31 | bottom: 0.2rem; 32 | } -------------------------------------------------------------------------------- /chrome/html/main.css: -------------------------------------------------------------------------------- 1 | ../../firefox/html/main.css -------------------------------------------------------------------------------- /chrome/html/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |

Notifications

14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |

Notifications

40 | 41 | 42 |
43 |
44 | 45 |
46 | Save 47 | 48 |
49 |
50 |
51 | 52 |
53 |

Theme:

54 |
55 | 56 |
58 | 59 |
61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 |
70 |

71 | Create a token from your settings. Make sure to allow: 72 |

78 |

79 | 82 |
83 |
84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /chrome/html/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 50 | 252 | 323 | 324 |
325 |
326 | 327 |

Notifications

328 | 329 | 330 |
331 |
332 | 333 |
334 | Save 335 | 336 |
337 |
338 |
339 | 340 |
341 |

Theme:

342 |
343 | 344 |
346 | 347 |
349 | 350 |
351 |
352 | 353 | 354 |
355 |
356 | 357 |
358 |

359 | Create a token from your settings. Make sure to allow: 360 |

366 |

367 | 370 |
371 |
372 | 373 | 374 | 375 | -------------------------------------------------------------------------------- /chrome/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/chrome/icons/favicon.png -------------------------------------------------------------------------------- /chrome/icons/kofilogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/chrome/icons/kofilogo.png -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Modrinthify", 4 | "version": "1.7.2", 5 | 6 | "description": "Redirect Curseforge mod pages to Modrinth and show Modrinth notifications", 7 | 8 | "icons": { 9 | "48": "icons/favicon.png" 10 | }, 11 | "host_permissions": ["https://api.modrinth.com/v2/*"], 12 | "permissions": ["storage", "alarms"], 13 | "content_scripts": [ 14 | { 15 | "matches": ["*://*.curseforge.com/minecraft/*"], 16 | "js": ["main.js"] 17 | }, 18 | { 19 | "matches": ["*://*.spigotmc.org/*"], 20 | "js": ["spigot.js"] 21 | } 22 | ], 23 | "web_accessible_resources": [ 24 | { 25 | "resources": ["icons/kofilogo.png"], 26 | "matches": [ 27 | "https://*.curseforge.com/*", 28 | "https://*.spigotmc.org/*" 29 | ] 30 | } 31 | ], 32 | "action": { 33 | "default_icon": "icons/favicon.png", 34 | "default_title": "Modrinthify", 35 | "default_popup": "html/main.html" 36 | }, 37 | "background": { 38 | "service_worker": "background.js" 39 | }, 40 | "options_ui": { 41 | "page": "html/preferences.html" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /convert_chrome.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import shutil 3 | import os 4 | 5 | IN_FOLDER = "firefox" 6 | OUT_FOLDER = "autogen_chrome" 7 | 8 | OUT_ADDITIONS = "chrome.browserAction = chrome.action\n\n" 9 | 10 | ff_path = os.getcwd() + "/" + IN_FOLDER 11 | cr_path = os.getcwd() + "/chrome" 12 | auto_cr_path = os.getcwd() + "/" + OUT_FOLDER 13 | 14 | files = [] 15 | 16 | copy_files = [] 17 | 18 | # r=root, d=directories, f = files 19 | for r, d, f in os.walk(ff_path): 20 | for file in f: 21 | if file.endswith(".js"): 22 | files.append(os.path.join(r, file)) 23 | else: 24 | copy_files.append(os.path.join(r, file)) 25 | 26 | print("TRANSFORM JS\n") 27 | 28 | for f in files: 29 | print("TRANSFORM", f) 30 | 31 | with open(f, "r") as ff_file: 32 | autogen_cr_filename = f.replace("firefox", OUT_FOLDER) 33 | os.makedirs(os.path.dirname(autogen_cr_filename), exist_ok=True) 34 | 35 | auto_cr_file = open(autogen_cr_filename, "w+") 36 | auto_cr_file.write(OUT_ADDITIONS) 37 | ff_text = ff_file.read() 38 | auto_cr_file.write(ff_text.replace("browser.", "chrome.")) 39 | auto_cr_file.close() 40 | 41 | print("\nCOPY SHARED\n") 42 | 43 | for f in copy_files: 44 | print("COPY", f) 45 | autogen_cr_filename = f.replace("firefox", OUT_FOLDER) 46 | cr_filename = f.replace("firefox", "chrome") 47 | os.makedirs(os.path.dirname(autogen_cr_filename), exist_ok=True) 48 | print(f"cp {cr_filename} {autogen_cr_filename}") 49 | os.system(f"cp {cr_filename} {autogen_cr_filename}") 50 | 51 | print("\nZIP EXTENSIONS\n") 52 | print(f"{os.getcwd()}/firefox.zip") 53 | shutil.make_archive(f"{os.getcwd()}/firefox", "zip", root_dir=f"{os.getcwd()}/firefox", base_dir=f"{os.getcwd()}/firefox") 54 | 55 | print(f"{os.getcwd()}/chrome.zip") 56 | shutil.make_archive(f"{os.getcwd()}/chrome", "zip", root_dir=f"{os.getcwd()}/autogen_chrome", base_dir=f"{os.getcwd()}/autogen_chrome") 57 | -------------------------------------------------------------------------------- /firefox/background.js: -------------------------------------------------------------------------------- 1 | const API_BASE = "https://api.modrinth.com/v2/user/"; 2 | 3 | async function fetchNotifs(user, token) { 4 | let h = new Headers({ 5 | Authorization: token, 6 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 7 | }); 8 | let resp = await fetch(API_BASE + user + "/notifications", { 9 | headers: h, 10 | }); 11 | 12 | if (resp.status != 200) { 13 | return { 14 | status: resp.status, 15 | notifications: undefined, 16 | }; 17 | } 18 | let json = await resp.json(); 19 | return { 20 | status: 200, 21 | notifications: json, 22 | }; 23 | } 24 | 25 | async function browserAlarmListener(e) { 26 | if (e.name == "check-notifications") { 27 | let s = await browser.storage.sync.get([ 28 | "user", 29 | "token", 30 | "notif_enable", 31 | ]); 32 | 33 | if (!s.notif_enable) { 34 | chrome.browserAction.setBadgeText({ text: "" }); 35 | return; 36 | } 37 | 38 | let token = s.token; 39 | let user = s.user; 40 | let resp = await fetchNotifs(user, token); 41 | 42 | if (resp.status == 401) { 43 | browser.storage.sync.set({ 44 | notif_enable: false, 45 | issue_connecting: 401, 46 | }); 47 | browser.browserAction.setBadgeText({ text: "ERR" }); 48 | return; 49 | } else if (resp.status == 404) { 50 | browser.storage.sync.set({ 51 | notif_enable: false, 52 | issue_connecting: 404, 53 | }); 54 | browser.browserAction.setBadgeText({ text: "ERR" }); 55 | return; 56 | } 57 | 58 | let parsed = resp.notifications; 59 | let n_old = 0; 60 | let n_updated = 0; 61 | last_checked = (await browser.storage.sync.get(["last_checked"])) 62 | .last_checked; 63 | for (let i = 0; i < parsed.length; i++) { 64 | let el = parsed[i]; 65 | let date_created = 66 | el.body.type == "legacy_markdown" 67 | ? el.created 68 | : el.date_published; 69 | if (last_checked > Date.parse(date_created)) { 70 | n_old += 1; 71 | } else { 72 | n_updated += 1; 73 | } 74 | } 75 | 76 | if (n_updated > 0) { 77 | browser.browserAction.setBadgeText({ text: n_updated.toString() }); 78 | } else { 79 | browser.browserAction.setBadgeText({ text: "" }); 80 | } 81 | } 82 | } 83 | 84 | async function setAlarm() { 85 | let check_delay = parseFloat( 86 | (await browser.storage.sync.get(["check_delay"])).check_delay || 10, 87 | ); 88 | 89 | if (check_delay == 0) { 90 | return; 91 | } 92 | 93 | browser.alarms.create("check-notifications", { 94 | delayInMinutes: 0.05, 95 | periodInMinutes: parseFloat(check_delay), 96 | }); 97 | 98 | browser.alarms.onAlarm.addListener(browserAlarmListener); 99 | } 100 | 101 | setAlarm(); 102 | -------------------------------------------------------------------------------- /firefox/html/chrome.css: -------------------------------------------------------------------------------- 1 | /* actual file in chrome directory. This is so it gets copied over */ -------------------------------------------------------------------------------- /firefox/html/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --divider: #c8cdd3; 3 | --bg: #e5e7eb; 4 | --bg_darker: #dddfe3; 5 | --faded: #4b5563; 6 | --text: black; 7 | --accent: #1E925B; 8 | --page-bg: white; 9 | --clear: #cb2245; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | :root { 14 | --divider: #474b54; 15 | --bg: #26292f; 16 | --bg_darker: #2e3137; 17 | --faded: #b0bac5; 18 | --accent: #1bd96a; 19 | --text: #ecf9fb; 20 | --page-bg: #16181c; 21 | --clear: #ff496e; 22 | } 23 | } 24 | body[data-theme="light"] { 25 | --divider: #c8cdd3; 26 | --bg: #e5e7eb; 27 | --bg_darker: #dddfe3; 28 | --faded: #4b5563; 29 | --text: black; 30 | --accent: #1E925B; 31 | --page-bg: white; 32 | --clear: #cb2245; 33 | } 34 | body[data-theme="dark"]{ 35 | --divider: #474b54; 36 | --bg: #26292f; 37 | --bg_darker: #2e3137; 38 | --faded: #b0bac5; 39 | --accent: #1bd96a; 40 | --text: #ecf9fb; 41 | --page-bg: #16181c; 42 | --clear: #ff496e; 43 | } 44 | 45 | body { 46 | font-family: sans-serif; 47 | width:300px; 48 | height: calc(420px); 49 | line-height: 1.3; 50 | position: relative; 51 | color: var(--text); 52 | background-color: var(--page-bg); 53 | } 54 | 55 | 56 | h3 { 57 | padding-bottom: 6px; 58 | margin-block: 0; 59 | } 60 | 61 | h4 { 62 | margin: 0; 63 | } 64 | 65 | p { 66 | margin: 0 67 | } 68 | 69 | a { 70 | display: block; 71 | } 72 | 73 | hr { 74 | border: 1px dashed var(--divider); 75 | } 76 | 77 | #main { 78 | width: 300px; 79 | background-color: var(--page-bg); 80 | } 81 | 82 | #notifications { 83 | overflow-y: scroll; 84 | margin-bottom: 0.5rem; 85 | } 86 | 87 | .title { 88 | display: flex; 89 | justify-content: space-between; 90 | margin-bottom: 0.5rem; 91 | padding-bottom: 0.5rem; 92 | user-select: none; 93 | position: sticky; 94 | top: 0; 95 | inset-inline: 0.5rem; 96 | padding-top: 0.5rem; 97 | margin-top: -0.5rem; 98 | background-color: var(--page-bg); 99 | z-index: 1; 100 | } 101 | .title svg { 102 | height: 100%; 103 | justify-self: center; 104 | align-self: center; 105 | cursor: pointer; 106 | color: var(--faded); 107 | transition: 0.15s color; 108 | margin-inline: 0.2rem; 109 | } 110 | 111 | .title svg:hover { 112 | color: var(--text); 113 | } 114 | 115 | .notification { 116 | background-color: var(--bg); 117 | border-radius: 0.5rem; 118 | margin-top: 0.25rem; 119 | overflow: hidden; 120 | } 121 | 122 | .header { 123 | padding: 0.5rem 0.5rem 0.2rem; 124 | border-bottom: 2px solid var(--divider); 125 | background-color: var(--bg); 126 | position: relative; 127 | } 128 | .header p { 129 | color: var(--faded); 130 | } 131 | 132 | .subheader { 133 | display: flex; 134 | justify-content: space-between; 135 | } 136 | 137 | .clear-all-button { 138 | color: var(--page-bg); 139 | user-select: none; 140 | font-weight: 900; 141 | cursor: pointer; 142 | padding: 0.2rem 0.5rem; 143 | position: absolute; 144 | right: 0; 145 | top: 0; 146 | opacity: 0; 147 | background-color: var(--clear); 148 | border-bottom-left-radius: 0.5rem; 149 | } 150 | 151 | .clear-all-button:hover { 152 | text-decoration: 2px underline; 153 | } 154 | 155 | .header:hover .clear-all-button { 156 | opacity: 1; 157 | } 158 | 159 | .version { 160 | padding: 0.2rem 0.5rem; 161 | color: var(--accent); 162 | font-weight: 600; 163 | position: relative; 164 | display: flex; 165 | justify-content: space-between; 166 | } 167 | .version:nth-child(2n+1) { 168 | background-color: var(--bg_darker); 169 | } 170 | .version::after { 171 | content: attr(after_text); 172 | margin-left: 0.5rem; 173 | border-left: 1px solid var(--divider); 174 | padding-left: 0.5rem; 175 | color: var(--faded); 176 | float: right; 177 | } 178 | .version:hover::after { 179 | content: "clear"; 180 | color: transparent; 181 | font-weight: 900; 182 | font-size: 1rem; 183 | } 184 | .being-cleared, .being-cleared * { 185 | opacity: 0.5; 186 | text-decoration: none !important; 187 | user-select: none !important; 188 | cursor: progress !important; 189 | } 190 | .clear-hitbox { 191 | position: absolute; 192 | right: 0; 193 | top: 0; 194 | padding: 0.2rem 0.5rem; 195 | font-weight: 900; 196 | color: transparent; 197 | cursor: pointer; 198 | user-select: none; 199 | border-top-left-radius: 0.5rem; 200 | border-bottom-left-radius: 0.5rem; 201 | } 202 | .clear-hitbox:hover { 203 | text-decoration: underline 2px; 204 | } 205 | .version:hover > .clear-hitbox { 206 | color: var(--clear); 207 | background-color: var(--clear); 208 | color: var(--page-bg); 209 | } 210 | .version-link { 211 | color: var(--accent); 212 | text-decoration: none; 213 | user-select: none; 214 | } 215 | .version-link:hover { 216 | text-decoration: underline 2px; 217 | } 218 | .version > .version-link { 219 | flex-grow: 1; 220 | } 221 | .hideable { 222 | display: none; 223 | } 224 | .more { 225 | padding: 0.2rem 0.5rem; 226 | background-color: var(--bg_darker); 227 | color: var(--faded); 228 | font-weight: 600; 229 | transition: 0.15s background-color; 230 | cursor:pointer; 231 | user-select: none; 232 | } 233 | .more:hover { 234 | background-color: var(--bg); 235 | } 236 | .version + .more { 237 | border-top: 2px solid var(--divider); 238 | } 239 | 240 | @keyframes spin { 241 | from { 242 | transform:rotate3d(0,0,1,0deg); 243 | } 244 | to { 245 | transform:rotate3d(0,0,1,-360deg); 246 | } 247 | } 248 | 249 | .spinning { 250 | animation-name: spin; 251 | animation-duration: 300ms; 252 | animation-iteration-count: infinite; 253 | animation-timing-function: ease-in-out; 254 | color: var(--accent) !important; 255 | } 256 | 257 | #icon-clear-a-container { 258 | display: inline; 259 | white-space: nowrap; 260 | cursor: pointer; 261 | } 262 | 263 | #icon-clear-a-container::before { 264 | left: -5rem; 265 | background-color: var(--clear); 266 | color: var(--page-bg); 267 | top: -0.75rem; 268 | padding: 0.2rem 0.5rem; 269 | padding-right: 0.5rem; 270 | border-bottom-left-radius: 0.5rem; 271 | border-top-left-radius: 0.5rem; 272 | padding-right: 1.2rem; 273 | font-weight: 900; 274 | z-index: -1; 275 | bottom: 0.35rem; 276 | text-decoration: underline 2px; 277 | } 278 | 279 | [data-tooltip] { 280 | position: relative; 281 | } 282 | [data-tooltip]::before { 283 | position : absolute; 284 | content : attr(data-tooltip); 285 | display: none; 286 | } 287 | 288 | [data-tooltip]:hover::before { 289 | display: block; 290 | } 291 | 292 | #icon-clear-a { 293 | border-radius: 100%; 294 | padding-inline: 0.2rem; 295 | margin-inline: -0.2rem; 296 | transition: none; 297 | } 298 | #icon-clear-a:hover, #icon-clear-a-container:hover > #icon-clear-a { 299 | background-color: var(--clear); 300 | color: var(--page-bg); 301 | } 302 | 303 | .title a { 304 | text-decoration: none; 305 | color: var(--text); 306 | display: flex; 307 | } 308 | 309 | .title a:hover { 310 | box-shadow: inset 0 -6px 0 0px var(--page-bg), inset 0 -8px 0 0px var(--accent); 311 | color: var(--accent); 312 | } 313 | .title a:not(:hover) > h3 { 314 | box-shadow: inset 0 -6px 0 0px var(--page-bg), inset 0 -8px 0 0px var(--divider); 315 | } 316 | 317 | .title > a > svg { 318 | color: inherit !important; 319 | margin-top: -6px; 320 | opacity: 0; 321 | transition: none; 322 | } 323 | 324 | .title > a:hover > svg { 325 | position: inherit; 326 | opacity: 1; 327 | } 328 | 329 | /* Settings CSS */ 330 | 331 | #settings { 332 | background-color: var(--page-bg); 333 | display:none; 334 | width: 300px; 335 | } 336 | 337 | .inline-link { 338 | color: var(--accent) !important; 339 | display: inline !important; 340 | text-decoration: underline 2px !important; 341 | } 342 | 343 | ul { 344 | margin-block: 0.2rem; 345 | } 346 | 347 | button { 348 | cursor: pointer; 349 | background-color: var(--accent); 350 | float:right; 351 | } 352 | input:not([type="radio"]), button { 353 | background-color: var(--bg); 354 | color: var(--text); 355 | border: none; 356 | padding: 0.5rem; 357 | border-radius: 0.5rem; 358 | transition: 0.15s background-color; 359 | font-weight: 600; 360 | } 361 | input:invalid { 362 | outline: 4px solid #db316255; 363 | } 364 | input:hover, button:hover { 365 | background-color: var(--bg_darker); 366 | } 367 | 368 | input[type="text"], input[type="password"] { 369 | width: calc(100% - 1rem); 370 | margin-block: 0.5rem; 371 | } 372 | 373 | input[type="checkbox"], input[type="radio"], label { 374 | user-select: none; 375 | } 376 | 377 | input[type="number"] { 378 | width: 3rem; 379 | margin-inline: 0.5rem; 380 | } 381 | 382 | .radio { 383 | display:inline-block; 384 | } 385 | 386 | .error { 387 | background-color: #db316255; 388 | color: var(--text); 389 | padding: 0.5rem; 390 | border-radius: 0.5rem; 391 | margin-block: 0.5rem; 392 | } 393 | 394 | #close-icon { 395 | display: flex; 396 | 397 | border-radius: 0.5rem; 398 | align-items: center; 399 | cursor: pointer; 400 | font-weight: 600; 401 | transition: 0.15s background-color, 0.15s color; 402 | color: var(--faded); 403 | } 404 | #close-icon * { 405 | transition: 0.15s background-color, 0.15s color; 406 | } 407 | 408 | #close-icon:hover *, #close-icon:hover { 409 | color: var(--accent) !important; 410 | } 411 | -------------------------------------------------------------------------------- /firefox/html/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |

Notifications

13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |

Notifications

39 | 40 | 41 |
42 |
43 | 44 |
45 | Save 46 | 47 |
48 |
49 |
50 | 51 |
52 |

Theme:

53 |
54 | 55 |
57 | 58 |
60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 |

70 | Create a token from your settings. Make sure to allow: 71 |

77 |

78 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /firefox/html/notifications.js: -------------------------------------------------------------------------------- 1 | const API_BASE = "https://api.modrinth.com/v2"; 2 | const LINK_BASE = "https://modrinth.com"; 3 | 4 | const MINUTE = 60 * 1000; 5 | const HOUR = 3600 * 1000; 6 | const DAY = 86400 * 1000; 7 | 8 | const N_VERSIONS = 3; 9 | 10 | const version_regex = /The project,? .*,? has released a new version: (.*)/m; 11 | 12 | function timeStringForUnix(unix) { 13 | if (unix > DAY) { 14 | time_string = Math.round(unix / DAY) + "d"; 15 | } else if (unix > HOUR) { 16 | time_string = Math.round(unix / HOUR) + "h"; 17 | } else if (unix > MINUTE) { 18 | time_string = Math.round(unix / MINUTE) + "m"; 19 | } else { 20 | time_string = Math.round(unix / 1000) + "s"; 21 | } 22 | return time_string; 23 | } 24 | 25 | async function fetchNotifs(user, token) { 26 | let h = new Headers({ 27 | Authorization: token, 28 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 29 | }); 30 | let resp = await fetch(API_BASE + "/user/" + user + "/notifications", { 31 | headers: h, 32 | }); 33 | 34 | if (resp.status != 200) { 35 | return { 36 | status: resp.status, 37 | notifications: undefined, 38 | }; 39 | } 40 | let json = await resp.json(); 41 | return { 42 | status: 200, 43 | notifications: json, 44 | }; 45 | } 46 | 47 | async function toggleSeeOld() { 48 | let hideable = document.querySelectorAll(".notification.hideable"); 49 | 50 | let n_old = document.querySelectorAll( 51 | ".notification.hideable .version", 52 | ).length; 53 | 54 | this.setAttribute( 55 | "is_expanded", 56 | !(this.getAttribute("is_expanded") == "true"), 57 | ); 58 | 59 | if (this.getAttribute("is_expanded") == "true") { 60 | for (const el of hideable) { 61 | el.style.display = "block"; 62 | } 63 | this.innerText = "- Less"; 64 | } else { 65 | for (const el of hideable) { 66 | el.style.display = "none"; 67 | } 68 | this.innerText = `+${n_old} Old`; 69 | } 70 | } 71 | 72 | function toggleExpandNotif() { 73 | let hideable = this.parentElement.querySelectorAll(".version.hideable"); 74 | 75 | this.setAttribute( 76 | "is_expanded", 77 | !(this.getAttribute("is_expanded") == "true"), 78 | ); 79 | 80 | if (this.getAttribute("is_expanded") == "true") { 81 | for (const el of hideable) { 82 | el.style.display = "flex"; 83 | } 84 | this.innerText = "- Less"; 85 | } else { 86 | for (const el of hideable) { 87 | el.style.display = "none"; 88 | } 89 | this.innerText = `+${hideable.length} More`; 90 | } 91 | } 92 | 93 | function build_notification( 94 | auth_token, 95 | project_info, 96 | versions_info, 97 | isOldType, 98 | startHidden = false, 99 | ) { 100 | let notification = document.createElement("div"); 101 | notification.className = "notification"; 102 | 103 | let s = ""; 104 | let s1 = (versions_info || []).length; 105 | let badge_t = (versions_info || []).length; 106 | if (versions_info && versions_info.length > 1) { 107 | s = "s"; 108 | } else if (versions_info == null) { 109 | s = "s: ∞"; 110 | s1 = "Too many! "; 111 | badge_t = "∞"; 112 | } 113 | 114 | if (versions_info == null) { 115 | } 116 | 117 | const title = isOldType 118 | ? project_info.replaceAll("**", "") 119 | : project_info.title + " has been updated!"; 120 | 121 | notification.innerHTML = `

${title}

\ 122 |

${s1} new version${s}:

Clear
`; 123 | notification.setAttribute("data-notification-count", badge_t); 124 | 125 | document.querySelector("#notifications").appendChild(notification); 126 | if (versions_info == null) return; 127 | 128 | let version_ids = []; 129 | 130 | let i = 0; 131 | for (const v of versions_info) { 132 | i++; 133 | let version = document.createElement("div"); 134 | version.className = "version"; 135 | if (i > N_VERSIONS) { 136 | version.classList = "version hideable"; 137 | } 138 | let version_link = document.createElement("a"); 139 | version_link.className = "version-link"; 140 | 141 | const link = isOldType 142 | ? v.link 143 | : `/mod/${v.project_id}/version/${v.id}`; 144 | version_link.href = LINK_BASE + link; 145 | version_link.target = "_blank"; 146 | 147 | const version_number = isOldType 148 | ? v.text.match(version_regex)[1] 149 | : v.version_number; 150 | version_link.innerText = version_number; 151 | version.appendChild(version_link); 152 | version.setAttribute("data-notification-id", v.id); 153 | version_ids.push(v.id); 154 | 155 | let clear_hitbox = document.createElement("div"); 156 | clear_hitbox.className = "clear-hitbox"; 157 | clear_hitbox.innerText = "Clear"; 158 | 159 | version.appendChild(clear_hitbox); 160 | clear_hitbox.addEventListener("click", async (ev) => { 161 | let el = ev.currentTarget; 162 | el.parentElement.classList.add("being-cleared"); 163 | let h = new Headers({ 164 | Authorization: auth_token, 165 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 166 | }); 167 | let resp = await fetch( 168 | API_BASE + 169 | `/notification/${el.parentElement.getAttribute("data-notification-id")}`, 170 | { 171 | headers: h, 172 | method: "DELETE", 173 | }, 174 | ); 175 | if (resp.status == 204) { 176 | let notification_el = el.parentElement.parentElement; 177 | let n_left = parseInt( 178 | notification_el.getAttribute("data-notification-count"), 179 | ); 180 | if (n_left - 1 > N_VERSIONS) { 181 | let to_display = notification_el.querySelector(".hideable"); 182 | to_display.classList.remove("hideable"); 183 | 184 | let more_button = notification_el.querySelector(".more"); 185 | let mb_is_closed = 186 | more_button.getAttribute("is_expanded") == "false"; 187 | 188 | // Update "more" text 189 | if (mb_is_closed) { 190 | more_button.innerText = `+${n_left - N_VERSIONS - 1} More`; 191 | } 192 | 193 | notification_el.setAttribute( 194 | "data-notification-count", 195 | n_left - 1, 196 | ); 197 | el.parentElement.remove(); 198 | } else if (n_left - 1 == 0) { 199 | notification_el.remove(); 200 | } else { 201 | el.parentElement.remove(); 202 | } 203 | } else { 204 | el.parentElement.classList.remove("being-cleared"); 205 | } 206 | }); 207 | 208 | const date_published = isOldType ? v.created : v.date_published; 209 | let time_passed = Date.now() - Date.parse(date_published); 210 | 211 | let time_string = timeStringForUnix(time_passed); 212 | 213 | version.setAttribute("after_text", time_string); 214 | if (startHidden) { 215 | notification.classList.add("hideable"); 216 | } 217 | notification.appendChild(version); 218 | notification 219 | .querySelector(".clear-all-button") 220 | .addEventListener("click", async (ev) => { 221 | let query_string = '["' + version_ids.join('","') + '"]'; 222 | let h = new Headers({ 223 | Authorization: auth_token, 224 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 225 | }); 226 | let notif = ev.currentTarget.parentElement.parentElement; 227 | notif.classList.add("being-cleared"); 228 | 229 | let resp = await fetch( 230 | API_BASE + `/notifications?ids=` + query_string, 231 | { 232 | headers: h, 233 | method: "DELETE", 234 | }, 235 | ); 236 | if (resp.status == 204) { 237 | notif.remove(); 238 | } 239 | }); 240 | } 241 | if (i > N_VERSIONS) { 242 | let more = document.createElement("p"); 243 | more.classList = "more"; 244 | more.setAttribute("is_expanded", false); 245 | more.addEventListener("click", toggleExpandNotif); 246 | more.innerText = `+${i - N_VERSIONS} More`; 247 | notification.appendChild(more); 248 | } 249 | } 250 | 251 | async function updateNotifs(ignore_last_checked) { 252 | ignore_last_checked == ignore_last_checked || false; 253 | 254 | let notif_enable = (await browser.storage.sync.get("notif_enable")) 255 | .notif_enable; 256 | let last_status = (await browser.storage.sync.get(["issue_connecting"])) 257 | .issue_connecting; 258 | 259 | // "Notifications disabled" 260 | 261 | if (document.querySelector("#notif-enable-popup")) { 262 | document.querySelector("#notif-enable-popup").remove(); 263 | } 264 | 265 | if (!notif_enable) { 266 | document.querySelectorAll("#notifications *").forEach((el) => { 267 | if (el.id != "up-to-date-notif") { 268 | el.remove(); 269 | } 270 | }); 271 | let notif_enable_popup = document.createElement("div"); 272 | notif_enable_popup.classList = "notification"; 273 | notif_enable_popup.id = "notif-enable-popup"; 274 | notif_enable_popup.innerHTML = `

Notifications are disabled

Go to settings to enable them

`; 275 | document 276 | .querySelector("#notifications") 277 | .appendChild(notif_enable_popup); 278 | browser.browserAction.setBadgeText({ text: "" }); 279 | return; 280 | } 281 | 282 | if (last_status != 0) { 283 | document.querySelectorAll("#notifications *").forEach((el) => { 284 | if (el.id != "up-to-date-notif") { 285 | el.remove(); 286 | } 287 | }); 288 | restoreSettings(); 289 | return; 290 | } 291 | 292 | document.querySelector("#refresh-icon-a").classList = "spinning"; 293 | 294 | let global_user = (await browser.storage.sync.get("user")).user; 295 | 296 | let global_auth_token = (await browser.storage.sync.get("token")).token; 297 | let resp = await fetchNotifs(global_user, global_auth_token); 298 | 299 | if (resp.status == 401) { 300 | browser.storage.sync.set({ issue_connecting: 401 }); 301 | browser.browserAction.setBadgeText({ text: "ERR" }); 302 | document.querySelector("#refresh-icon-a").classList = ""; 303 | restoreSettings(); 304 | return; 305 | } else if (resp.status == 404) { 306 | browser.storage.sync.set({ issue_connecting: 404 }); 307 | browser.browserAction.setBadgeText({ text: "ERR" }); 308 | document.querySelector("#refresh-icon-a").classList = ""; 309 | restoreSettings(); 310 | return; 311 | } 312 | 313 | let parsed = resp.notifications; 314 | 315 | // New notification type logic 316 | 317 | // To mass fetch version strings 318 | let version_ids = new Array(); 319 | let project_ids = new Set(); 320 | 321 | let notif_ids = new Array(); 322 | 323 | for (let i = 0; i < parsed.length; i++) { 324 | notif = parsed[i]; 325 | if (notif.body) { 326 | if (notif.body.type != "project_update") { 327 | continue; 328 | } 329 | 330 | version_ids.push(notif.body.version_id); 331 | project_ids.add(notif.body.project_id); 332 | notif_ids.push(notif.id); 333 | } 334 | } 335 | 336 | // fetch titles and add notifications 337 | 338 | // NOTE: Project and version result has the same order as the query parameters 339 | 340 | let pids_query = '["' + Array.from(project_ids).join('","') + '"]'; 341 | let verids_query = '["' + version_ids.join('","') + '"]'; 342 | pro_json = fetch(API_BASE + "/projects?ids=" + pids_query).then( 343 | (response) => { 344 | return response.json(); 345 | }, 346 | ); 347 | ver_json = fetch(API_BASE + "/versions?ids=" + verids_query).then( 348 | (response) => { 349 | if (response.ok == false) return null; 350 | return response.json(); 351 | }, 352 | ); 353 | 354 | let result = await Promise.all([pro_json, ver_json]); 355 | let projects = result[0]; 356 | projects.sort((a, b) => { 357 | unixA = Date.parse(a.updated); 358 | unixB = Date.parse(b.updated); 359 | if (unixA > unixB) { 360 | return -1; 361 | } else { 362 | return 1; 363 | } 364 | }); 365 | let versions = result[1]; 366 | 367 | // Map project_id => [version_info] 368 | let project_id_to_version_info = new Map(); 369 | for (let i = 0; i < (versions || []).length; i++) { 370 | let v = versions[i]; 371 | let a = project_id_to_version_info.get(v.project_id) || []; 372 | a.push(v); 373 | project_id_to_version_info.set(v.project_id, a); 374 | } 375 | // Map project_id => project_info 376 | let project_id_to_project_info = new Map(); 377 | for (let i = 0; i < projects.length; i++) { 378 | project_id_to_project_info[projects[i].id] = projects[i]; 379 | } 380 | 381 | // Old notification type logic 382 | 383 | let updated = new Map(); 384 | let old = new Map(); 385 | let n_old = 0; 386 | let n_updated = 0; 387 | 388 | for (let i = 0; i < parsed.length; i++) { 389 | let el = parsed[i]; 390 | let last_checked = (await browser.storage.sync.get(["last_checked"])) 391 | .last_checked; 392 | let created_date = 393 | el.body.type == "legacy_markdown" ? el.created : el.date_published; 394 | notif_ids.push(el.id); 395 | if (last_checked > Date.parse(created_date)) { 396 | if (el.body.type == "legacy_markdown") { 397 | let a = old.get(el.title) || []; 398 | a.push(el); 399 | old.set(el.title, a); 400 | } 401 | n_old += 1; 402 | } else { 403 | if (el.body.type == "legacy_markdown") { 404 | let a = updated.get(el.title) || []; 405 | a.push(el); 406 | updated.set(el.title, a); 407 | } 408 | n_updated += 1; 409 | } 410 | } 411 | // end 412 | 413 | // Update "Clear all" button with new IDs 414 | let cback = async (ev) => { 415 | let query_string = '["' + notif_ids.join('","') + '"]'; 416 | let h = new Headers({ 417 | Authorization: global_auth_token, 418 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 419 | }); 420 | let resp = await fetch( 421 | API_BASE + `/notifications?ids=` + query_string, 422 | { 423 | headers: h, 424 | method: "DELETE", 425 | }, 426 | ); 427 | document 428 | .querySelectorAll(".notification") 429 | .forEach((el) => el.classList.add("being-cleared")); 430 | if (resp.status == 204) { 431 | document 432 | .querySelectorAll(".notification") 433 | .forEach((el) => el.remove()); 434 | updateNotifs(false); 435 | } else { 436 | document 437 | .querySelectorAll(".notification") 438 | .forEach((el) => el.classList.remove("being-cleared")); 439 | } 440 | }; 441 | document.querySelector("#icon-clear-a").removeEventListener("click", cback); 442 | document.querySelector("#icon-clear-a").addEventListener("click", cback); 443 | 444 | if (document.querySelector("#no-notifs")) { 445 | document.querySelector("#no-notifs").remove(); 446 | } 447 | 448 | if (n_updated > 0) { 449 | browser.browserAction.setBadgeText({ text: n_updated.toString() }); 450 | } 451 | if (n_updated == 0 && n_old == 0) { 452 | let no_notifs = document.createElement("div"); 453 | no_notifs.classList = "notification"; 454 | no_notifs.id = "no-notifs"; 455 | no_notifs.innerHTML = `

No new notifications

`; 456 | document.querySelector("#notifications").appendChild(no_notifs); 457 | browser.browserAction.setBadgeText({ text: "" }); 458 | 459 | document.querySelector("#refresh-icon-a").classList = ""; 460 | return; 461 | } 462 | // Do this here to minimize blank time 463 | document.querySelectorAll("#notifications *").forEach((el) => { 464 | if (el.id != "up-to-date-notif") { 465 | el.remove(); 466 | } 467 | }); 468 | 469 | if (versions == null) { 470 | warning = document.createElement("h1"); 471 | warning.innerHTML = 472 | "Clear your notifications! The modrinth API has reached an internal limit and has returned an invalid response."; 473 | warning.style = "color: red; font-weight: 900;"; 474 | document.querySelector("#notifications").appendChild(warning); 475 | } 476 | 477 | for (project_info of projects) { 478 | build_notification( 479 | global_auth_token, 480 | project_info, 481 | project_id_to_version_info.get(project_info.id), 482 | false, 483 | false, 484 | ); 485 | } 486 | 487 | updated.forEach((new_vers, title) => { 488 | build_notification(global_auth_token, title, new_vers, true, false); 489 | }); 490 | 491 | if (n_old > 0) { 492 | if (document.querySelector("#up-to-date-notif")) { 493 | document.querySelector("#up-to-date-notif").remove(); 494 | } 495 | let old_separator = document.createElement("hr"); 496 | document.querySelector("#notifications").appendChild(old_separator); 497 | let up_to_date = document.createElement("div"); 498 | up_to_date.classList = "notification"; 499 | up_to_date.id = "up-to-date-notif"; 500 | up_to_date.innerHTML = `

See older notifications

+${n_old} Old

`; 501 | document.querySelector("#notifications").appendChild(up_to_date); 502 | document 503 | .querySelector("#more-old") 504 | .addEventListener("click", toggleSeeOld); 505 | 506 | // Place hidden old notifs 507 | old.forEach((new_vers, title) => { 508 | build_notification(global_auth_token, title, new_vers, true, true); 509 | }); 510 | } 511 | document.querySelector("#refresh-icon-a").classList = ""; 512 | } 513 | 514 | async function saveOptions(e) { 515 | if (e) { 516 | e.preventDefault(); 517 | } 518 | let form = document.querySelector("form"); 519 | 520 | const data = new FormData(form); 521 | 522 | let theme = data.get("theme") || "auto"; 523 | 524 | const notif_enable = data.get("notif-enable") == "on"; 525 | const check_delay = data.get("notif-check-delay"); 526 | 527 | const token = data.get("token").trim(); 528 | 529 | if (notif_enable == true && (token == "" || token == undefined)) { 530 | document.querySelector(".error").innerText = 531 | "Please fill out these fields to enable notifications"; 532 | document.querySelector(".error").style.display = "block"; 533 | document.querySelector("#token").style.outline = "4px solid #db316255"; 534 | 535 | browser.storage.sync.set({ 536 | check_delay: check_delay, 537 | theme: theme, 538 | notif_enable: false, 539 | issue_connecting: 0, 540 | }); 541 | 542 | return false; 543 | } else if (notif_enable == false) { 544 | if (token == undefined) { 545 | token = ""; 546 | } 547 | await browser.storage.sync.set({ 548 | token: token, 549 | }); 550 | } 551 | 552 | let old_token = await browser.storage.sync.get("token"); 553 | 554 | if (old_token.token != token && notif_enable == true) { 555 | let close_icon = document.querySelector("#close-icon svg"); 556 | close_icon.classList = "spinning"; 557 | let h = new Headers({ 558 | Authorization: token, 559 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}`, 560 | }); 561 | 562 | let resp = await fetch(API_BASE + "/user", { 563 | headers: h, 564 | }); 565 | close_icon.classList = ""; 566 | if (resp.status == 401) { 567 | document.querySelector(".error").innerText = 568 | "Invalid API authorization token"; 569 | document.querySelector(".error").style.display = "block"; 570 | document.querySelector("#token").style.outline = 571 | "4px solid #db316255"; 572 | 573 | browser.storage.sync.set({ 574 | check_delay: check_delay, 575 | theme: theme, 576 | notif_enable: false, 577 | issue_connecting: 0, 578 | }); 579 | return false; 580 | } 581 | 582 | let resp_json = await resp.json(); 583 | browser.storage.sync.set({ 584 | token: token, 585 | user: resp_json.username, 586 | }); 587 | } 588 | 589 | await browser.storage.sync.set({ 590 | check_delay: check_delay, 591 | theme: theme, 592 | notif_enable: notif_enable, 593 | issue_connecting: 0, 594 | }); 595 | document.querySelector(".error").style.display = "none"; 596 | return true; 597 | } 598 | 599 | async function restoreSettings() { 600 | document.querySelector("#settings").style.display = "block"; 601 | document.querySelector("#main").style.display = "none"; 602 | 603 | let a = await browser.storage.sync.get([ 604 | "token", 605 | "user", 606 | "notif_enable", 607 | "issue_connecting", 608 | "theme", 609 | "check_delay", 610 | ]); 611 | 612 | document.querySelector("#notif-enable").checked = a.notif_enable || false; 613 | document.querySelector("#token").value = a.token || ""; 614 | 615 | a.theme = a.theme || "auto"; 616 | document.querySelector(`#${a.theme}-theme`).checked = "on"; 617 | document.querySelector("#notif-check-delay").value = a.check_delay || 10; 618 | 619 | if (a.issue_connecting != 0) { 620 | if (a.issue_connecting == 401) { 621 | document.querySelector(".error").innerText = 622 | "There has been an issue connecting to Modrinth:\nError 401 Unauthorized\n(Invalid token)"; 623 | document.querySelector(".error").style.display = "block"; 624 | document.querySelector("#token").style.outline = 625 | "4px solid #db316255"; 626 | } else if (a.issue_connecting == 404) { 627 | document.querySelector(".error").innerText = 628 | "There has been an issue connecting to Modrinth:\nError 404 User not found\n(Invalid token)"; 629 | document.querySelector(".error").style.display = "block"; 630 | } 631 | } 632 | } 633 | 634 | async function closeSettings() { 635 | document.querySelector(".error").style.display = "none"; 636 | document.querySelector("#token").style.outline = "none"; 637 | let do_close = await saveOptions(); 638 | if (do_close) { 639 | document.querySelector("#main").style.display = "block"; 640 | document.querySelector("#settings").style.display = "none"; 641 | changeTheme(); 642 | updateNotifs(false); 643 | } 644 | } 645 | 646 | async function updateLastChecked() { 647 | // Remove notifications 648 | unix = Date.now(); 649 | browser.storage.sync.set({ 650 | last_checked: unix, 651 | }); 652 | updateNotifs(false); 653 | } 654 | 655 | function updateNotifsNoVar() { 656 | updateNotifs(false); 657 | } 658 | 659 | async function getLastChecked() { 660 | let a = await browser.storage.sync.get(["last_checked"]).last_checked; 661 | return a; 662 | } 663 | document 664 | .querySelector("#settings-icon") 665 | .addEventListener("click", restoreSettings); 666 | document.querySelector("#close-icon").addEventListener("click", closeSettings); 667 | document.querySelector("form").addEventListener("submit", saveOptions); 668 | document 669 | .querySelector("#refresh-icon-a") 670 | .addEventListener("click", updateNotifsNoVar); 671 | // document.querySelector("#icon-clear-a").addEventListener("click", updateLastChecked) 672 | document.addEventListener("DOMContentLoaded", updateNotifsNoVar); 673 | 674 | function changeTheme() { 675 | browser.storage.sync.get("theme").then((resp) => { 676 | document.querySelector("body").setAttribute("data-theme", resp.theme); 677 | }); 678 | } 679 | changeTheme(); 680 | -------------------------------------------------------------------------------- /firefox/html/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 50 | 252 | 323 | 324 |
325 |
326 | 327 |

Notifications

328 | 329 | 330 |
331 |
332 | 333 |
334 | Save 335 | 336 |
337 |
338 |
339 | 340 |
341 |

Theme:

342 |
343 | 344 |
346 | 347 |
349 | 350 |
351 |
352 | 353 | 354 |
355 |
356 | 357 |
358 |

359 | Create a token from your settings. Make sure to allow: 360 |

366 |

367 | 370 |
371 |
372 | 373 | 374 | 375 | -------------------------------------------------------------------------------- /firefox/html/preferences.js: -------------------------------------------------------------------------------- 1 | async function closeSettings() { 2 | document.querySelector(".error").style.display = "none" 3 | document.querySelector("#token").style.outline = "none" 4 | let do_close = await saveOptions() 5 | if (do_close) { 6 | changeTheme() 7 | // updateNotifs(false) 8 | } 9 | } 10 | function changeTheme() { 11 | 12 | browser.storage.sync.get("theme").then((resp) => { 13 | document.querySelector("body").setAttribute("data-theme", resp.theme) 14 | }) 15 | 16 | } 17 | async function saveOptions(e) { 18 | if (e) { 19 | e.preventDefault(); 20 | } 21 | let form = document.querySelector("form") 22 | 23 | const data = new FormData(form) 24 | 25 | let theme = data.get("theme") || "auto" 26 | 27 | const notif_enable = data.get("notif-enable") == "on"; 28 | const check_delay = data.get("notif-check-delay"); 29 | 30 | const token = data.get("token").trim(); 31 | // console.log(token) 32 | 33 | if (notif_enable == true && (token == "" || token == undefined)) { 34 | document.querySelector(".error").innerText = "Please fill out these fields to enable notifications" 35 | document.querySelector(".error").style.display = "block" 36 | document.querySelector("#token").style.outline = "4px solid #db316255" 37 | 38 | browser.storage.sync.set({ 39 | check_delay: check_delay, 40 | theme: theme, 41 | notif_enable: false, 42 | issue_connecting: 0, 43 | }) 44 | 45 | return false 46 | } else if (notif_enable == false) { 47 | if (token == undefined) { 48 | token = "" 49 | } 50 | await browser.storage.sync.set({ 51 | token: token 52 | }) 53 | } 54 | 55 | let old_token = await browser.storage.sync.get("token") 56 | // console.log(old_token, '"', token) 57 | 58 | if (old_token.token != token && notif_enable == true) { 59 | 60 | let close_icon = document.querySelector("#close-icon svg") 61 | close_icon.classList = "spinning" 62 | let h = new Headers({ 63 | "Authorization": token, 64 | "User-Agent": `devBoi76/modrinthify/${browser.runtime.getManifest().version}` 65 | }) 66 | 67 | let resp = await fetch(API_BASE+"/user", { 68 | headers: h 69 | }) 70 | close_icon.classList = "" 71 | if (resp.status == 401) { 72 | document.querySelector(".error").innerText = "Invalid authorization token" 73 | document.querySelector(".error").style.display = "block" 74 | document.querySelector("#token").style.outline = "4px solid #db316255" 75 | 76 | browser.storage.sync.set({ 77 | check_delay: check_delay, 78 | theme: theme, 79 | notif_enable: false, 80 | issue_connecting: 0, 81 | }) 82 | return false 83 | } 84 | 85 | let resp_json = await resp.json() 86 | browser.storage.sync.set({ 87 | token: token, 88 | user: resp_json.username 89 | }) 90 | } 91 | 92 | await browser.storage.sync.set({ 93 | check_delay: check_delay, 94 | theme: theme, 95 | notif_enable: notif_enable, 96 | issue_connecting: 0, 97 | }) 98 | document.querySelector(".error").style.display = "none" 99 | return true 100 | } 101 | 102 | async function restoreSettings() { 103 | // document.querySelector("#settings").style.display = "block" 104 | // document.querySelector("#main").style.display = "none" 105 | 106 | let a = await browser.storage.sync.get(["token", "user", "notif_enable", "issue_connecting", "theme", "check_delay"]) 107 | 108 | document.querySelector("#notif-enable").checked = a.notif_enable || false; 109 | document.querySelector("#token").value = a.token || ""; 110 | 111 | a.theme = a.theme || "auto" 112 | document.querySelector(`#${a.theme}-theme`).checked = "on" 113 | document.querySelector("#notif-check-delay").value = a.check_delay || 5 114 | 115 | if (a.issue_connecting != 0) { 116 | if (a.issue_connecting == 401) { 117 | document.querySelector(".error").innerText = "There has been an issue connecting to Modrinth:\nError 401 Unauthorized\n(Invalid token)" 118 | document.querySelector(".error").style.display = "block" 119 | document.querySelector("#token").style.outline = "4px solid #db316255" 120 | } 121 | else if (a.issue_connecting == 404) { 122 | document.querySelector(".error").innerText = "There has been an issue connecting to Modrinth:\nError 404 User not found\n(Invalid token)" 123 | document.querySelector(".error").style.display = "block" 124 | 125 | } 126 | } 127 | 128 | } 129 | 130 | document.querySelector("#close-icon").addEventListener("click", closeSettings); 131 | document.querySelector("form").addEventListener("submit", saveOptions); 132 | document.addEventListener("DOMContentLoaded", restoreSettings) 133 | 134 | changeTheme(); -------------------------------------------------------------------------------- /firefox/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/firefox/icons/favicon.png -------------------------------------------------------------------------------- /firefox/icons/kofilogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devBoi76/modrinthify/af1c5530be54fc6d8d6b45fd92c246ade38cc6d3/firefox/icons/kofilogo.png -------------------------------------------------------------------------------- /firefox/main.js: -------------------------------------------------------------------------------- 1 | function htmlToElements(html) { 2 | var t = document.createElement("template"); 3 | t.innerHTML = html; 4 | return t.content; 5 | } 6 | 7 | function similarity(s1, s2) { 8 | var longer = s1; 9 | var shorter = s2; 10 | if (s1.length < s2.length) { 11 | longer = s2; 12 | shorter = s1; 13 | } 14 | var longerLength = longer.length; 15 | if (longerLength == 0) { 16 | return 1.0; 17 | } 18 | return ( 19 | (longerLength - editDistance(longer, shorter)) / 20 | parseFloat(longerLength) 21 | ); 22 | } 23 | 24 | function editDistance(s1, s2) { 25 | s1 = s1.toLowerCase(); 26 | s2 = s2.toLowerCase(); 27 | 28 | var costs = new Array(); 29 | for (var i = 0; i <= s1.length; i++) { 30 | var lastValue = i; 31 | for (var j = 0; j <= s2.length; j++) { 32 | if (i == 0) costs[j] = j; 33 | else { 34 | if (j > 0) { 35 | var newValue = costs[j - 1]; 36 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 37 | newValue = 38 | Math.min(Math.min(newValue, lastValue), costs[j]) + 39 | 1; 40 | costs[j - 1] = lastValue; 41 | lastValue = newValue; 42 | } 43 | } 44 | } 45 | if (i > 0) costs[s2.length] = lastValue; 46 | } 47 | return costs[s2.length]; 48 | } 49 | 50 | const new_design_button = 51 | '
MOD_NAME
Get on Modrinth
'; 52 | 53 | const new_design_donation = ``; 54 | 55 | const svg = 56 | ''; 57 | 58 | const HTML = ` \ 59 |
\ 64 |
\ 65 | ${svg} 66 |
\ 67 | Get on Modrinth\ 68 |
\ 69 | `; 70 | 71 | const DONATE_HTML = `\ 72 | \ 77 |
\ 78 | \ 79 |
\ 80 | Support the Author 81 |
\ 82 | `; 83 | 84 | const REGEX = 85 | /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gim; 86 | 87 | const MOD_PAGE_HTML = `
\ 88 | \ 89 |
MOD_NAME
\ 90 |
BUTTON_HTML
`; 91 | 92 | const SEARCH_PAGE_HTML = `
\ 93 | \ 94 |
MOD_NAME
\ 95 |
BUTTON_HTML
`; 96 | 97 | let query = "head title"; 98 | const tab_title = document.querySelector(query).innerText; 99 | let mod_name = undefined; 100 | let mod_name_noloader = undefined; 101 | let page = undefined; 102 | 103 | function main() { 104 | const url = document.URL.split("/"); 105 | page = url[4]; 106 | 107 | const is_new_design = !location.hostname.startsWith("old.curseforge.com"); 108 | 109 | const is_search = is_new_design 110 | ? url[4].split("?")[0] == "search" 111 | : url[5].startsWith("search") && url[5].split("?").length >= 2; 112 | 113 | if (is_search) { 114 | if (is_new_design) { 115 | search_query = document.querySelector(".search-input-field").value; 116 | } else { 117 | search_query = document 118 | .querySelector(".mt-6 > h2:nth-child(1)") 119 | .textContent.match(/Search results for '(.*)'/)[1]; 120 | } 121 | } else { 122 | if (is_new_design) { 123 | // search_query = document.querySelector(".project-header > h1:nth-child(2)").innerText 124 | search_query = document.title.split(" - Minecraft ")[0]; 125 | } else { 126 | search_query = document 127 | .querySelector("head meta[property='og:title']") 128 | .getAttribute("content"); 129 | } 130 | } 131 | 132 | mod_name = search_query; 133 | mod_name_noloader = mod_name.replace(REGEX, ""); 134 | 135 | if (is_search && is_new_design) { 136 | page_re = /.*&class=(.*?)&.*/; 137 | page = (page.match(page_re) || ["", "all"])[1]; 138 | } 139 | 140 | api_facets = ""; 141 | switch (page) { 142 | //=Mods=============== 143 | case "mc-mods": 144 | api_facets = `facets=[["categories:'forge'","categories:'fabric'","categories:'quilt'","categories:'liteloader'","categories:'modloader'","categories:'rift'"],["project_type:mod"]]`; 145 | break; 146 | //=Server=Plugins===== 147 | case "mc-addons": 148 | return; 149 | case "customization": 150 | api_facets = `facets=[["project_type:shader"]]`; 151 | break; 152 | case "bukkit-plugins": 153 | api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]`; 154 | break; 155 | //=Resource=Packs===== 156 | case "texture-packs": 157 | api_facets = `facets=[["project_type:resourcepack"]]`; 158 | break; 159 | //=Modpacks=========== 160 | case "modpacks": 161 | api_facets = `facets=[["project_type:modpack"]]`; 162 | break; 163 | case "all": 164 | api_facets = ``; 165 | break; 166 | } 167 | 168 | fetch( 169 | `https://api.modrinth.com/v2/search?limit=3&query=${mod_name_noloader}&${api_facets}`, 170 | { method: "GET", mode: "cors" }, 171 | ) 172 | .then((response) => response.json()) 173 | .then((resp) => { 174 | let bd = document.querySelector("#modrinth-body"); 175 | if (bd) { 176 | bd.remove(); 177 | } 178 | 179 | if (page == undefined) { 180 | return; 181 | } 182 | 183 | if (resp.hits.length == 0) { 184 | return; 185 | } 186 | 187 | let max_sim = 0; 188 | let max_hit = undefined; 189 | 190 | for (const hit of resp.hits) { 191 | if (similarity(hit.title.trim(), mod_name) > max_sim) { 192 | max_sim = similarity(hit.title.trim(), mod_name.trim()); 193 | max_hit = hit; 194 | } 195 | if (similarity(hit.title.trim(), mod_name_noloader) > max_sim) { 196 | max_sim = similarity( 197 | hit.title.trim(), 198 | mod_name_noloader.trim(), 199 | ); 200 | max_hit = hit; 201 | } 202 | } 203 | 204 | if (max_sim <= 0.7) { 205 | return; 206 | } 207 | // Add the buttons 208 | 209 | if (is_search) { 210 | if (is_new_design) { 211 | // query = ".results-count" 212 | query = ".search-tags"; 213 | let s = document.querySelector(query); 214 | let buttonElement = htmlToElements( 215 | new_design_button 216 | .replace("ICON_SOURCE", max_hit.icon_url) 217 | .replace("MOD_NAME", max_hit.title.trim()) 218 | .replace( 219 | "REDIRECT", 220 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 221 | ) 222 | .replace("BUTTON_HTML", HTML), 223 | ); 224 | buttonElement.childNodes[0].style.marginLeft = "auto"; 225 | s.appendChild(buttonElement); 226 | } else { 227 | query = ".mt-6 > div:nth-child(3)"; 228 | let s = document.querySelector(query); 229 | let buttonElement = htmlToElements( 230 | SEARCH_PAGE_HTML.replace( 231 | "ICON_SOURCE", 232 | max_hit.icon_url, 233 | ) 234 | .replace("MOD_NAME", max_hit.title.trim()) 235 | .replace( 236 | "REDIRECT", 237 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 238 | ) 239 | .replace("BUTTON_HTML", HTML), 240 | ); 241 | s.appendChild(buttonElement); 242 | } 243 | } else { 244 | if (is_new_design) { 245 | query = ".actions"; 246 | let s = document.querySelector(query); 247 | let buttonElement = htmlToElements( 248 | new_design_button 249 | .replace("ICON_SOURCE", max_hit.icon_url) 250 | .replace("MOD_NAME", max_hit.title.trim()) 251 | .replace( 252 | "REDIRECT", 253 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 254 | ), 255 | ); 256 | s.appendChild(buttonElement); 257 | } else { 258 | query = "div.-mx-1:nth-child(1)"; 259 | let s = document.querySelector(query); 260 | let buttonElement = htmlToElements( 261 | MOD_PAGE_HTML.replace("ICON_SOURCE", max_hit.icon_url) 262 | .replace("MOD_NAME", max_hit.title.trim()) 263 | .replace( 264 | "REDIRECT", 265 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 266 | ) 267 | .replace("BUTTON_HTML", HTML), 268 | ); 269 | s.appendChild(buttonElement); 270 | } 271 | } 272 | // Add donation button if present 273 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, { 274 | method: "GET", 275 | mode: "cors", 276 | }) 277 | .then((response_p) => response_p.json()) 278 | .then((resp_p) => { 279 | if (document.querySelector("#donate-button")) { 280 | return; 281 | } 282 | if (resp_p.donation_urls.length > 0) { 283 | let redir = document.getElementById("modrinth-body"); 284 | 285 | if (is_new_design) { 286 | redir.innerHTML += new_design_donation.replace( 287 | "REDIRECT", 288 | resp_p.donation_urls[0].url, 289 | ); 290 | if (is_search) { 291 | redir.style.marginRight = "-195.5px"; 292 | } else { 293 | redir.style.marginRight = "-195.5px"; 294 | } 295 | } else { 296 | let donations = resp_p.donation_urls; 297 | let dbutton = document.createElement("div"); 298 | dbutton.innerHTML = DONATE_HTML.replace( 299 | "REDIRECT", 300 | donations[0].url, 301 | ); 302 | dbutton.style.display = "inline-block"; 303 | let redir = document.getElementById( 304 | "modrinthify-redirect", 305 | ); 306 | redir.after(dbutton); 307 | if (!is_search) { 308 | redir.parentNode.parentNode.parentNode.style.marginRight = 309 | "-150px"; 310 | } 311 | } 312 | } 313 | }); 314 | }); 315 | } 316 | 317 | main(); 318 | 319 | // document.querySelector(".classes-list").childNodes.forEach( (el) => { 320 | // el.childNodes[0].addEventListener("click", main) 321 | // }) 322 | 323 | let lastURL = document.URL; 324 | new MutationObserver(() => { 325 | let url = document.URL; 326 | if (url != lastURL) { 327 | lastURL = url; 328 | main(); 329 | } 330 | }).observe(document, { subtree: true, childList: true }); 331 | 332 | // document.querySelector(".search-input-field").addEventListener("keydown", (event) => { 333 | // if (event.key == "Enter") { 334 | // main() 335 | // } 336 | // }) 337 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Modrinthify", 4 | "version": "1.7.2", 5 | 6 | "description": "When looking at Minecraft mods on CurseForge this extension automatically searches Modrinth for the mod and shows you a redirect button if it finds it", 7 | 8 | "icons": { 9 | "48": "icons/favicon.png" 10 | }, 11 | "permissions": ["https://api.modrinth.com/v2/*", "storage", "alarms"], 12 | 13 | "browser_specific_settings": { 14 | "gecko": { 15 | "id": "{5183707f-8a46-4092-8c1f-e4515bcebbad}", 16 | "strict_min_version": "57.0" 17 | } 18 | }, 19 | 20 | "content_scripts": [ 21 | { 22 | "matches": ["*://*.curseforge.com/minecraft/*"], 23 | "js": ["main.js"] 24 | }, 25 | { 26 | "matches": ["*://*.spigotmc.org/*"], 27 | "js": ["spigot.js"] 28 | } 29 | ], 30 | "web_accessible_resources": ["icons/kofilogo.png"], 31 | "browser_action": { 32 | "default_icon": "icons/favicon.png", 33 | "default_title": "Modrinthify", 34 | "default_popup": "html/main.html" 35 | }, 36 | "background": { 37 | "scripts": ["background.js"] 38 | }, 39 | "options_ui": { 40 | "page": "html/preferences.html" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /firefox/spigot.js: -------------------------------------------------------------------------------- 1 | function similarity(s1, s2) { 2 | var longer = s1; 3 | var shorter = s2; 4 | if (s1.length < s2.length) { 5 | longer = s2; 6 | shorter = s1; 7 | } 8 | var longerLength = longer.length; 9 | if (longerLength == 0) { 10 | return 1.0; 11 | } 12 | return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); 13 | } 14 | 15 | function editDistance(s1, s2) { 16 | s1 = s1.toLowerCase(); 17 | s2 = s2.toLowerCase(); 18 | 19 | var costs = new Array(); 20 | for (var i = 0; i <= s1.length; i++) { 21 | var lastValue = i; 22 | for (var j = 0; j <= s2.length; j++) { 23 | if (i == 0) 24 | costs[j] = j; 25 | else { 26 | if (j > 0) { 27 | var newValue = costs[j - 1]; 28 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 29 | newValue = Math.min(Math.min(newValue, lastValue), 30 | costs[j]) + 1; 31 | costs[j - 1] = lastValue; 32 | lastValue = newValue; 33 | } 34 | } 35 | } 36 | if (i > 0) 37 | costs[s2.length] = lastValue; 38 | } 39 | return costs[s2.length]; 40 | } 41 | 42 | const svg = '' 43 | 44 | const HTML = ` 45 |
\ 50 |
\ 51 | ${svg} 52 |
\ 53 | Get on Modrinth\ 54 |
\ 55 | ` 56 | 57 | const DONATE_HTML = `\ 58 |
Support the Author 59 |
60 | ` 61 | 62 | const REGEX = /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gmi 63 | PLUGIN_PAGE_HTML = ` \ 80 |
MR_NAME
81 |
82 | Get on Modrinth 83 |
84 |
` 85 | 86 | const SEARCH_PAGE_HTML = `
\ 87 | \ 88 |
MOD_NAME
\ 89 |
BUTTON_HTML
` 90 | 91 | function main() { 92 | const api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]` 93 | 94 | const url = document.URL.split("/") 95 | 96 | let is_search = false 97 | let is_project_page = false 98 | if (url[3] == "search") { 99 | is_search = true 100 | } else if (url[3] == "resources") { 101 | is_project_page = true 102 | } else return 103 | 104 | let query = "" 105 | if (is_search) { 106 | query = document.querySelector(".titleBar > h1:nth-child(1)").textContent.match(/Search Results for Query: (.*)/)[1] 107 | 108 | } else if (is_project_page) { 109 | query = document.querySelector(".resourceInfo > h1:nth-child(3)").firstChild.textContent 110 | } 111 | 112 | fetch(`https://api.modrinth.com/v2/search?limit=3&query=${query}&${api_facets}`, {method: "GET", mode: "cors"}) 113 | .then(response => response.json()) 114 | .then(resp => { 115 | 116 | if (resp.hits.length == 0) { 117 | return 118 | } 119 | 120 | let max_sim = 0 121 | let max_hit = undefined 122 | 123 | for (const hit of resp.hits) { 124 | if (similarity(hit.title.trim(), query) > max_sim) { 125 | max_sim = similarity(hit.title.trim(), query) 126 | max_hit = hit 127 | } 128 | } 129 | if (max_sim <= 0.7) { 130 | return 131 | } 132 | 133 | if (is_search) { 134 | let s = document.querySelector("div.pageNavLinkGroup:nth-child(2)") 135 | let div = document.createElement("div") 136 | div.outerHTML = s.innerHTML = PLUGIN_PAGE_HTML 137 | .replace("MR_ICON", max_hit.icon_url) 138 | .replace("MR_NAME", max_hit.title.trim()) 139 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 140 | .replace("BUTTON_HTML", HTML) + s.innerHTML 141 | 142 | s.after(div) 143 | 144 | } else if (is_project_page) { 145 | let s = document.querySelector(".innerContent") 146 | 147 | s.innerHTML = PLUGIN_PAGE_HTML 148 | .replace("MR_ICON", max_hit.icon_url) 149 | .replace("MR_NAME", max_hit.title.trim()) 150 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 151 | .replace("BUTTON_HTML", HTML) + s.innerHTML 152 | } 153 | 154 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, {method: "GET", mode: "cors"}) 155 | .then(response_p => response_p.json()) 156 | .then(resp_p => { 157 | if (resp_p.donation_urls.length > 0) { 158 | document.querySelector("#modrinth-body div").innerHTML += DONATE_HTML.replace("REDIRECT", resp_p.donation_urls[0].url) 159 | // if (!is_search) { 160 | // redir.parentNode.parentNode.parentNode.style.marginRight = "-150px" 161 | // } 162 | } 163 | }) 164 | 165 | 166 | }) 167 | 168 | 169 | } 170 | 171 | main() 172 | -------------------------------------------------------------------------------- /greasyfork/spigot.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Modrinthify Spigot 3 | // @namespace Violentmonkey Scripts 4 | // @match *://*.spigotmc.org/* 5 | // @grant none 6 | // @version 1.4.1.2 7 | // @author devBoi76 8 | // @license MIT 9 | // @description Redirect spigotmc.org mod pages to modrinth.com when possible 10 | // ==/UserScript== 11 | 12 | /* jshint esversion: 6 */ 13 | 14 | function similarity(s1, s2) { 15 | var longer = s1; 16 | var shorter = s2; 17 | if (s1.length < s2.length) { 18 | longer = s2; 19 | shorter = s1; 20 | } 21 | var longerLength = longer.length; 22 | if (longerLength == 0) { 23 | return 1.0; 24 | } 25 | return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); 26 | } 27 | 28 | function editDistance(s1, s2) { 29 | s1 = s1.toLowerCase(); 30 | s2 = s2.toLowerCase(); 31 | 32 | var costs = new Array(); 33 | for (var i = 0; i <= s1.length; i++) { 34 | var lastValue = i; 35 | for (var j = 0; j <= s2.length; j++) { 36 | if (i == 0) 37 | costs[j] = j; 38 | else { 39 | if (j > 0) { 40 | var newValue = costs[j - 1]; 41 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 42 | newValue = Math.min(Math.min(newValue, lastValue), 43 | costs[j]) + 1; 44 | costs[j - 1] = lastValue; 45 | lastValue = newValue; 46 | } 47 | } 48 | } 49 | if (i > 0) 50 | costs[s2.length] = lastValue; 51 | } 52 | return costs[s2.length]; 53 | } 54 | 55 | const svg = '' 56 | 57 | const HTML = ` 58 |
\ 63 |
\ 64 | ${svg} 65 |
\ 66 | Get on Modrinth\ 67 |
\ 68 | ` 69 | 70 | const DONATE_HTML = `\ 71 |
Support the Author 72 |
73 | ` 74 | 75 | const REGEX = /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gmi 76 | PLUGIN_PAGE_HTML = ` \ 93 |
MR_NAME
94 |
95 | Get on Modrinth 96 |
97 |
` 98 | 99 | const SEARCH_PAGE_HTML = `
\ 100 | \ 101 |
MOD_NAME
\ 102 |
BUTTON_HTML
` 103 | 104 | function main() { 105 | const api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]` 106 | 107 | const url = document.URL.split("/") 108 | 109 | let is_search = false 110 | let is_project_page = false 111 | if (url[3] == "search") { 112 | is_search = true 113 | } else if (url[3] == "resources") { 114 | is_project_page = true 115 | } else return 116 | 117 | let query = "" 118 | if (is_search) { 119 | query = document.querySelector(".titleBar > h1:nth-child(1)").textContent.match(/Search Results for Query: (.*)/)[1] 120 | 121 | } else if (is_project_page) { 122 | query = document.querySelector(".resourceInfo > h1:nth-child(3)").firstChild.textContent 123 | } 124 | 125 | fetch(`https://api.modrinth.com/v2/search?limit=3&query=${query}&${api_facets}`, {method: "GET", mode: "cors"}) 126 | .then(response => response.json()) 127 | .then(resp => { 128 | 129 | if (resp.hits.length == 0) { 130 | return 131 | } 132 | 133 | let max_sim = 0 134 | let max_hit = undefined 135 | 136 | for (const hit of resp.hits) { 137 | if (similarity(hit.title.trim(), query) > max_sim) { 138 | max_sim = similarity(hit.title.trim(), query) 139 | max_hit = hit 140 | } 141 | } 142 | if (max_sim <= 0.7) { 143 | return 144 | } 145 | 146 | if (is_search) { 147 | let s = document.querySelector("div.pageNavLinkGroup:nth-child(2)") 148 | let div = document.createElement("div") 149 | div.outerHTML = s.innerHTML = PLUGIN_PAGE_HTML 150 | .replace("MR_ICON", max_hit.icon_url) 151 | .replace("MR_NAME", max_hit.title.trim()) 152 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 153 | .replace("BUTTON_HTML", HTML) + s.innerHTML 154 | 155 | s.after(div) 156 | 157 | } else if (is_project_page) { 158 | let s = document.querySelector(".innerContent") 159 | 160 | s.innerHTML = PLUGIN_PAGE_HTML 161 | .replace("MR_ICON", max_hit.icon_url) 162 | .replace("MR_NAME", max_hit.title.trim()) 163 | .replace("MR_HREF", `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`) 164 | .replace("BUTTON_HTML", HTML) + s.innerHTML 165 | } 166 | 167 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, {method: "GET", mode: "cors"}) 168 | .then(response_p => response_p.json()) 169 | .then(resp_p => { 170 | if (resp_p.donation_urls.length > 0) { 171 | document.querySelector("#modrinth-body div").innerHTML += DONATE_HTML.replace("REDIRECT", resp_p.donation_urls[0].url) 172 | // if (!is_search) { 173 | // redir.parentNode.parentNode.parentNode.style.marginRight = "-150px" 174 | // } 175 | } 176 | }) 177 | 178 | 179 | }) 180 | 181 | 182 | } 183 | 184 | main() 185 | -------------------------------------------------------------------------------- /greasyfork/userscript.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Modrinthify 3 | // @namespace Violentmonkey Scripts 4 | // @match *://*.curseforge.com/minecraft/* 5 | // @grant none 6 | // @version 1.6.2 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 ( 32 | (longerLength - editDistance(longer, shorter)) / 33 | parseFloat(longerLength) 34 | ); 35 | } 36 | 37 | function editDistance(s1, s2) { 38 | s1 = s1.toLowerCase(); 39 | s2 = s2.toLowerCase(); 40 | 41 | var costs = new Array(); 42 | for (var i = 0; i <= s1.length; i++) { 43 | var lastValue = i; 44 | for (var j = 0; j <= s2.length; j++) { 45 | if (i == 0) costs[j] = j; 46 | else { 47 | if (j > 0) { 48 | var newValue = costs[j - 1]; 49 | if (s1.charAt(i - 1) != s2.charAt(j - 1)) 50 | newValue = 51 | Math.min(Math.min(newValue, lastValue), costs[j]) + 52 | 1; 53 | costs[j - 1] = lastValue; 54 | lastValue = newValue; 55 | } 56 | } 57 | } 58 | if (i > 0) costs[s2.length] = lastValue; 59 | } 60 | return costs[s2.length]; 61 | } 62 | 63 | const new_design_button = 64 | '
MOD_NAME
Get on Modrinth
'; 65 | 66 | const new_design_donation = ``; 67 | 68 | const svg = 69 | ''; 70 | 71 | const HTML = ` \ 72 |
\ 77 |
\ 78 | ${svg} 79 |
\ 80 | Get on Modrinth\ 81 |
\ 82 | `; 83 | 84 | const DONATE_HTML = `\ 85 | \ 90 |
\ 91 | \ 92 |
\ 93 | Support the Author 94 |
\ 95 | `; 96 | 97 | const REGEX = 98 | /[\(\[](forge|fabric|forge\/fabric|fabric\/forge|unused|deprecated)[\)\]]/gim; 99 | 100 | const MOD_PAGE_HTML = `
\ 101 | \ 102 |
MOD_NAME
\ 103 |
BUTTON_HTML
`; 104 | 105 | const SEARCH_PAGE_HTML = `
\ 106 | \ 107 |
MOD_NAME
\ 108 |
BUTTON_HTML
`; 109 | 110 | let query = "head title"; 111 | const tab_title = document.querySelector(query).innerText; 112 | let mod_name = undefined; 113 | let mod_name_noloader = undefined; 114 | let page = undefined; 115 | 116 | function main() { 117 | const url = document.URL.split("/"); 118 | page = url[4]; 119 | 120 | const is_new_design = !location.hostname.startsWith("old.curseforge.com"); 121 | 122 | const is_search = is_new_design 123 | ? url[4].split("?")[0] == "search" 124 | : url[5].startsWith("search") && url[5].split("?").length >= 2; 125 | 126 | if (is_search) { 127 | if (is_new_design) { 128 | search_query = document.querySelector(".search-input-field").value; 129 | } else { 130 | search_query = document 131 | .querySelector(".mt-6 > h2:nth-child(1)") 132 | .textContent.match(/Search results for '(.*)'/)[1]; 133 | } 134 | } else { 135 | if (is_new_design) { 136 | // search_query = document.querySelector(".project-header > h1:nth-child(2)").innerText 137 | search_query = document.title.split(" - Minecraft ")[0]; 138 | } else { 139 | search_query = document 140 | .querySelector("head meta[property='og:title']") 141 | .getAttribute("content"); 142 | } 143 | } 144 | 145 | mod_name = search_query; 146 | mod_name_noloader = mod_name.replace(REGEX, ""); 147 | 148 | if (is_search && is_new_design) { 149 | page_re = /.*&class=(.*?)&.*/; 150 | page = (page.match(page_re) || ["", "all"])[1]; 151 | } 152 | 153 | api_facets = ""; 154 | switch (page) { 155 | //=Mods=============== 156 | case "mc-mods": 157 | api_facets = `facets=[["categories:'forge'","categories:'fabric'","categories:'quilt'","categories:'liteloader'","categories:'modloader'","categories:'rift'"],["project_type:mod"]]`; 158 | break; 159 | //=Server=Plugins===== 160 | case "mc-addons": 161 | return; 162 | case "customization": 163 | api_facets = `facets=[["project_type:shader"]]`; 164 | break; 165 | case "bukkit-plugins": 166 | api_facets = `facets=[["categories:'bukkit'","categories:'spigot'","categories:'paper'","categories:'purpur'","categories:'sponge'","categories:'bungeecord'","categories:'waterfall'","categories:'velocity'"],["project_type:mod"]]`; 167 | break; 168 | //=Resource=Packs===== 169 | case "texture-packs": 170 | api_facets = `facets=[["project_type:resourcepack"]]`; 171 | break; 172 | //=Modpacks=========== 173 | case "modpacks": 174 | api_facets = `facets=[["project_type:modpack"]]`; 175 | break; 176 | case "all": 177 | api_facets = ``; 178 | break; 179 | } 180 | 181 | fetch( 182 | `https://api.modrinth.com/v2/search?limit=3&query=${mod_name_noloader}&${api_facets}`, 183 | { method: "GET", mode: "cors" }, 184 | ) 185 | .then((response) => response.json()) 186 | .then((resp) => { 187 | let bd = document.querySelector("#modrinth-body"); 188 | if (bd) { 189 | bd.remove(); 190 | } 191 | 192 | if (page == undefined) { 193 | return; 194 | } 195 | 196 | if (resp.hits.length == 0) { 197 | return; 198 | } 199 | 200 | let max_sim = 0; 201 | let max_hit = undefined; 202 | 203 | for (const hit of resp.hits) { 204 | if (similarity(hit.title.trim(), mod_name) > max_sim) { 205 | max_sim = similarity(hit.title.trim(), mod_name.trim()); 206 | max_hit = hit; 207 | } 208 | if (similarity(hit.title.trim(), mod_name_noloader) > max_sim) { 209 | max_sim = similarity( 210 | hit.title.trim(), 211 | mod_name_noloader.trim(), 212 | ); 213 | max_hit = hit; 214 | } 215 | } 216 | 217 | if (max_sim <= 0.7) { 218 | return; 219 | } 220 | // Add the buttons 221 | 222 | if (is_search) { 223 | if (is_new_design) { 224 | // query = ".results-count" 225 | query = ".search-tags"; 226 | let s = document.querySelector(query); 227 | let buttonElement = htmlToElements( 228 | new_design_button 229 | .replace("ICON_SOURCE", max_hit.icon_url) 230 | .replace("MOD_NAME", max_hit.title.trim()) 231 | .replace( 232 | "REDIRECT", 233 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 234 | ) 235 | .replace("BUTTON_HTML", HTML), 236 | ); 237 | buttonElement.childNodes[0].style.marginLeft = "auto"; 238 | s.appendChild(buttonElement); 239 | } else { 240 | query = ".mt-6 > div:nth-child(3)"; 241 | let s = document.querySelector(query); 242 | let buttonElement = htmlToElements( 243 | SEARCH_PAGE_HTML.replace( 244 | "ICON_SOURCE", 245 | max_hit.icon_url, 246 | ) 247 | .replace("MOD_NAME", max_hit.title.trim()) 248 | .replace( 249 | "REDIRECT", 250 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 251 | ) 252 | .replace("BUTTON_HTML", HTML), 253 | ); 254 | s.appendChild(buttonElement); 255 | } 256 | } else { 257 | if (is_new_design) { 258 | query = ".actions"; 259 | let s = document.querySelector(query); 260 | let buttonElement = htmlToElements( 261 | new_design_button 262 | .replace("ICON_SOURCE", max_hit.icon_url) 263 | .replace("MOD_NAME", max_hit.title.trim()) 264 | .replace( 265 | "REDIRECT", 266 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 267 | ), 268 | ); 269 | s.appendChild(buttonElement); 270 | } else { 271 | query = "div.-mx-1:nth-child(1)"; 272 | let s = document.querySelector(query); 273 | let buttonElement = htmlToElements( 274 | MOD_PAGE_HTML.replace("ICON_SOURCE", max_hit.icon_url) 275 | .replace("MOD_NAME", max_hit.title.trim()) 276 | .replace( 277 | "REDIRECT", 278 | `https://modrinth.com/${max_hit.project_type}/${max_hit.slug}`, 279 | ) 280 | .replace("BUTTON_HTML", HTML), 281 | ); 282 | s.appendChild(buttonElement); 283 | } 284 | } 285 | // Add donation button if present 286 | fetch(`https://api.modrinth.com/v2/project/${max_hit.slug}`, { 287 | method: "GET", 288 | mode: "cors", 289 | }) 290 | .then((response_p) => response_p.json()) 291 | .then((resp_p) => { 292 | if (document.querySelector("#donate-button")) { 293 | return; 294 | } 295 | if (resp_p.donation_urls.length > 0) { 296 | let redir = document.getElementById("modrinth-body"); 297 | 298 | if (is_new_design) { 299 | redir.innerHTML += new_design_donation.replace( 300 | "REDIRECT", 301 | resp_p.donation_urls[0].url, 302 | ); 303 | if (is_search) { 304 | redir.style.marginRight = "-195.5px"; 305 | } else { 306 | redir.style.marginRight = "-195.5px"; 307 | } 308 | } else { 309 | let donations = resp_p.donation_urls; 310 | let dbutton = document.createElement("div"); 311 | dbutton.innerHTML = DONATE_HTML.replace( 312 | "REDIRECT", 313 | donations[0].url, 314 | ); 315 | dbutton.style.display = "inline-block"; 316 | let redir = document.getElementById( 317 | "modrinthify-redirect", 318 | ); 319 | redir.after(dbutton); 320 | if (!is_search) { 321 | redir.parentNode.parentNode.parentNode.style.marginRight = 322 | "-150px"; 323 | } 324 | } 325 | } 326 | }); 327 | }); 328 | } 329 | 330 | main(); 331 | 332 | // document.querySelector(".classes-list").childNodes.forEach( (el) => { 333 | // el.childNodes[0].addEventListener("click", main) 334 | // }) 335 | 336 | let lastURL = document.URL; 337 | new MutationObserver(() => { 338 | let url = document.URL; 339 | if (url != lastURL) { 340 | lastURL = url; 341 | main(); 342 | } 343 | }).observe(document, { subtree: true, childList: true }); 344 | --------------------------------------------------------------------------------