├── 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 | 
10 | 
11 |
12 | Redirects on spigotmc.org:
13 |
14 | 
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 | 
20 | 
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 |
36 |
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 = ``;
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 = ``;
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 = ``;
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 = `+${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 |
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
';
54 |
55 | const new_design_donation = `
Support the Author `;
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 |
36 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/chrome/html/preferences.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
50 |
252 |
323 |
324 |
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 |
35 |
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 = ``;
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 = ``;
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 = ``;
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 = `+${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 |
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
';
52 |
53 | const new_design_donation = `
Support the Author `;
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
';
65 |
66 | const new_design_donation = `
Support the Author `;
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 |
--------------------------------------------------------------------------------