NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Modrinth Direct Download Pro
// @namespace https://aminahmady.vercel.app/
// @version 11.3
// @description Direct download links + cache + loading + menu + loader/version/runtime selector
// @match https://modrinth.com/*
// @grant GM_xmlhttpRequest
// @connect api.modrinth.com
// @license MIT
// @downloadURL https://update.greasyfork.org/scripts/567195/Modrinth%20Direct%20Download%20Pro.user.js
// @updateURL https://update.greasyfork.org/scripts/567195/Modrinth%20Direct%20Download%20Pro.meta.js
// ==/UserScript==
(function () {
"use strict";
/* ================= SETTINGS + CACHE ================= */
const SETTINGS_KEY = "mr-settings-full";
const CACHE_KEY = "mr-cache-full";
const HISTORY_KEY = "mr-history-full";
const CACHE_TTL = 1000 * 60 * 60 * 12;
const defaultSettings = {
enabled: true,
version: "1.21.1",
loader: "fabric",
shaderRuntime: "iris",
notifyOnDownload: true,
};
let settings = {
...defaultSettings,
...JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}"),
};
function saveSettings() {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}
function loadCache() {
return JSON.parse(localStorage.getItem(CACHE_KEY) || "{}");
}
function saveCache(cache) {
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
}
function makeCacheKey(slug, loader, version, runtime) {
return `${slug}::${loader}::${version}::${runtime}`;
}
function getCached(slug, type) {
const cache = loadCache();
const key = makeCacheKey(slug, type);
const entry = cache[key];
if (!entry) return null;
if (Date.now() - entry.timestamp > CACHE_TTL) {
delete cache[key];
saveCache(cache);
return null;
}
return entry.url;
}
function setCached(slug, type, url) {
const cache = loadCache();
cache[makeCacheKey(slug, type)] = {
url,
timestamp: Date.now(),
};
saveCache(cache);
}
function loadHistory() {
return JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]");
}
function saveHistory(history) {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
}
function addToHistory(slug, type, title) {
const history = loadHistory();
history.unshift({
slug,
type,
title,
timestamp: Date.now(),
});
if (history.length > 50) history.pop();
saveHistory(history);
if (settings.notifyOnDownload) {
const emoji = type === "mod" ? "📦" : type === "shader" ? "✨" : "🎨";
showNotification(`${emoji} Downloaded: ${title}`);
}
}
function showNotification(message) {
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #1bd96a;
color: white;
padding: 12px 16px;
border-radius: 8px;
font-family: system-ui;
font-size: 14px;
z-index: 1000000;
box-shadow: 0 4px 12px rgba(0,0,0,.3);
animation: slideIn 0.3s ease-out;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease-out";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
const style = document.createElement("style");
style.textContent = `
.mr-fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background: #1bd96a;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999999;
box-shadow: 0 5px 20px rgba(0,0,0,.5);
}
.mr-fab > svg {
width: 28px;
height: 28px;
}
.mr-panel {
position: fixed;
bottom: 90px;
right: 20px;
width: 300px;
background: #1e1e1e;
border-radius: 16px;
padding: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,.6);
color: white;
display: none;
z-index: 999999;
font-family: system-ui;
}
.mr-panel label {
font-size: 12px;
opacity: .7;
}
.mr-panel select {
width: 100%;
margin-bottom: 12px;
padding: 6px;
border-radius: 6px;
border: none;
}
.mr-save {
width: 100%;
padding: 8px;
border: none;
border-radius: 8px;
background: #1bd96a;
font-weight: 600;
cursor: pointer;
}
.mr-switch {
position: relative;
width: 46px;
height: 24px;
background: #555;
border-radius: 20px;
cursor: pointer;
margin-bottom: 14px;
transition: .2s;
}
.mr-switch::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: .2s;
}
.mr-switch.active {
background: #1bd96a;
}
.mr-switch.active::after {
left: 24px;
}
.mr-loading {
height: 100%;
width: 100%;
opacity: 0.2;
position: relative;
border-radius: 16px;
background: linear-gradient(90deg,#444,#666,#444);
background-size: 200% 100%;
z-index: 101;
animation: mr-shimmer 1.2s infinite;
}
@keyframes mr-shimmer {
0% { background-position: 200% 0 }
100% { background-position: -200% 0 }
}
.mr-cached-label {
color: #ff4d4d;
font-size: 12px;
margin-left: 6px;
font-weight: 600;
}
.mr-disabled {
opacity: 0.4 !important;
pointer-events: none !important;
}
.mr-action-badge {
display: inline-block;
font-size: 12px;
margin-left: 6px;
font-weight: 600;
cursor: pointer;
pointer-events: auto;
transition: opacity 0.2s;
}
.mr-action-badge:hover {
opacity: 0.7;
}
.mr-badge-open {
color: #4a9eff;
}
.mr-badge-copy {
color: #ff9f43;
}
.mr-clear {
width: 100%;
padding: 8px;
border: none;
border-radius: 8px;
background: #ff4d4d;
font-weight: 600;
cursor: pointer;
color: white;
margin-top: 8px;
}
.mr-clear:hover {
opacity: 0.9;
}
.mr-history-container {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #444;
max-height: 200px;
overflow-y: auto;
}
.mr-history-item {
padding: 8px 0;
font-size: 11px;
border-bottom: 1px solid #333;
line-height: 1.4;
}
.mr-history-item:last-child {
border-bottom: none;
}
.mr-history-emoji {
margin-right: 6px;
font-size: 12px;
}
.mr-history-title {
color: #1bd96a;
word-break: break-word;
}
.mr-history-time {
color: #999;
font-size: 10px;
margin-top: 2px;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
/* ================= MENU ================= */
const fab = document.createElement("div");
fab.className = "mr-fab";
fab.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 24 24"><path fill="#fff" d="M12.252.004a11.78 11.768 0 0 0-8.92 3.73a11 11 0 0 0-2.17 3.11a11.37 11.359 0 0 0-1.16 5.169c0 1.42.17 2.5.6 3.77c.24.759.77 1.899 1.17 2.529a12.3 12.298 0 0 0 8.85 5.639c.44.05 2.54.07 2.76.02c.2-.04.22.1-.26-1.7l-.36-1.37l-1.01-.06a8.5 8.489 0 0 1-5.18-1.8a5.3 5.3 0 0 1-1.3-1.26c0-.05.34-.28.74-.5a37.572 37.545 0 0 1 2.88-1.629c.03 0 .5.45 1.06.98l1 .97l2.07-.43l2.06-.43l1.47-1.47c.8-.8 1.48-1.5 1.48-1.52c0-.09-.42-1.63-.46-1.7c-.04-.06-.2-.03-1.02.18c-.53.13-1.2.3-1.45.4l-.48.15l-.53.53l-.53.53l-.93.1l-.93.07l-.52-.5a2.7 2.7 0 0 1-.96-1.7l-.13-.6l.43-.57c.68-.9.68-.9 1.46-1.1c.4-.1.65-.2.83-.33c.13-.099.65-.579 1.14-1.069l.9-.9l-.7-.7l-.7-.7l-1.95.54c-1.07.3-1.96.53-1.97.53c-.03 0-2.23 2.48-2.63 2.97l-.29.35l.28 1.03c.16.56.3 1.16.31 1.34l.03.3l-.34.23c-.37.23-2.22 1.3-2.84 1.63c-.36.2-.37.2-.44.1c-.08-.1-.23-.6-.32-1.03c-.18-.86-.17-2.75.02-3.73a8.84 8.84 0 0 1 7.9-6.93c.43-.03.77-.08.78-.1c.06-.17.5-2.999.47-3.039c-.01-.02-.1-.02-.2-.03Zm3.68.67c-.2 0-.3.1-.37.38c-.06.23-.46 2.42-.46 2.52c0 .04.1.11.22.16a8.51 8.499 0 0 1 2.99 2a8.38 8.379 0 0 1 2.16 3.449a6.9 6.9 0 0 1 .4 2.8c0 1.07 0 1.27-.1 1.73a9.4 9.4 0 0 1-1.76 3.769c-.32.4-.98 1.06-1.37 1.38c-.38.32-1.54 1.1-1.7 1.14c-.1.03-.1.06-.07.26c.03.18.64 2.56.7 2.78l.06.06a12.07 12.058 0 0 0 7.27-9.4c.13-.77.13-2.58 0-3.4a11.96 11.948 0 0 0-5.73-8.578c-.7-.42-2.05-1.06-2.25-1.06Z"/></svg>`;
document.body.appendChild(fab);
const panel = document.createElement("div");
panel.className = "mr-panel";
panel.innerHTML = `
<label>Enable Script</label>
<div id="mr-switch" class="mr-switch"></div>
<label>Minecraft Version</label>
<select id="mr-version"></select>
<label>Loader (Mods Only)</label>
<select id="mr-loader"></select>
<label>Shader Runtime</label>
<select id="mr-runtime">
<option value="iris">Iris</option>
<option value="optifine">OptiFine</option>
</select>
<label>Notifications</label>
<div id="mr-notify-switch" class="mr-switch"></div>
<button class="mr-save">Save</button>
<button id="mr-clear-cache" class="mr-clear">Clear Cache</button>
<div class="mr-history-container">
<label style="display: block; margin-bottom: 8px;">Download History</label>
<div id="mr-history" style="font-size: 11px;"></div>
</div>
`;
document.body.appendChild(panel);
const clearCacheBtn = panel.querySelector("#mr-clear-cache");
clearCacheBtn.onclick = () => {
localStorage.removeItem(CACHE_KEY);
alert("Cache cleared!");
};
fab.onclick = () => {
panel.style.display = panel.style.display === "block" ? "none" : "block";
if (panel.style.display === "block") {
renderHistory();
}
};
const switchEl = panel.querySelector("#mr-switch");
const versionSelect = panel.querySelector("#mr-version");
const loaderSelect = panel.querySelector("#mr-loader");
const runtimeSelect = panel.querySelector("#mr-runtime");
const notifySwitch = panel.querySelector("#mr-notify-switch");
const saveBtn = panel.querySelector(".mr-save");
const historyEl = panel.querySelector("#mr-history");
function updateSwitch() {
settings.enabled ?
switchEl.classList.add("active") :
switchEl.classList.remove("active");
}
function updateNotifySwitch() {
settings.notifyOnDownload ?
notifySwitch.classList.add("active") :
notifySwitch.classList.remove("active");
}
function renderHistory() {
const history = loadHistory();
historyEl.innerHTML = "";
if (history.length === 0) {
historyEl.innerHTML = '<div style="color: #999; font-size: 10px;">No downloads yet</div>';
return;
}
history.slice(0, 10).forEach((item) => {
const emoji = item.type === "mod" ? "📦" : item.type === "shader" ? "✨" : "🎨";
const date = new Date(item.timestamp);
const timeStr = date.toLocaleString();
const itemEl = document.createElement("div");
itemEl.className = "mr-history-item";
itemEl.innerHTML = `<span class="mr-history-emoji">${emoji}</span><span class="mr-history-title">${item.title}</span><div class="mr-history-time">${timeStr}</div>`;
historyEl.appendChild(itemEl);
});
}
updateSwitch();
switchEl.onclick = () => {
settings.enabled = !settings.enabled;
updateSwitch();
};
updateNotifySwitch();
notifySwitch.onclick = () => {
settings.notifyOnDownload = !settings.notifyOnDownload;
updateNotifySwitch();
saveSettings();
};
function fetchTags(endpoint, selectEl, valueField, settingKey) {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.modrinth.com/v2/tag/${endpoint}`,
onload: (r) => {
const data = JSON.parse(r.responseText);
selectEl.innerHTML = "";
data.forEach((item) => {
const opt = document.createElement("option");
opt.value = item[valueField];
opt.textContent = item.name || item.version;
selectEl.appendChild(opt);
});
if (
[...selectEl.options].some((o) => o.value === settings[settingKey])
) {
selectEl.value = settings[settingKey];
}
},
});
}
fetchTags("game_version", versionSelect, "version", "version");
fetchTags("loader", loaderSelect, "name", "loader");
runtimeSelect.value = settings.shaderRuntime;
saveBtn.onclick = () => {
settings.version = versionSelect.value;
settings.loader = loaderSelect.value;
settings.shaderRuntime = runtimeSelect.value;
saveSettings();
location.reload();
};
/* ================= PROJECT DETECTION ================= */
function getProjectInfo(card) {
const link = card.querySelector(
'a[href^="/mod/"], a[href^="/resourcepack/"], a[href^="/shader/"]',
);
if (!link) return null;
const match = link
.getAttribute("href")
.match(/^\/(mod|resourcepack|shader)\/([^/]+)/);
if (!match) return null;
return {
type: match[1],
slug: match[2],
};
}
function addCachedLabel(titleEl) {
if (!titleEl?.querySelector(".mr-cached-label")) {
const span = document.createElement("span");
span.className = "mr-cached-label";
span.textContent = "(cached)";
titleEl.appendChild(span);
}
}
function makeCacheKey(slug, type) {
if (type === "mod") {
return `${slug}::mod::${settings.loader}::${settings.version}`;
}
if (type === "shader") {
return `${slug}::shader::${settings.version}::${settings.shaderRuntime}`;
}
return `${slug}::resourcepack::${settings.version}`;
}
function addLoading(el) {
if (el.querySelector(".mr-loading")) return;
const loading = document.createElement("div");
loading.className = "mr-loading";
el.appendChild(loading);
}
function removeLoading(el) {
const loading = el.querySelector(".mr-loading");
if (loading) loading.remove();
}
function applyDownloadLink(link, url, slug, type, titleText) {
link.href = url;
link.setAttribute("download", "");
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
// Clone the element to strip ALL existing event listeners
const clone = link.cloneNode(true);
link.parentNode.replaceChild(clone, link);
clone.addEventListener(
"click",
(e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// Create a temporary link and trigger download
const a = document.createElement("a");
a.href = url;
a.download = "";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Add to history and notify only when actually clicked
addToHistory(slug, type, titleText);
},
true,
);
}
/* ================= FETCH LOGIC ================= */
function fetchDownload(slug, type, cb) {
let url;
if (type === "mod") {
url = `https://api.modrinth.com/v2/project/${slug}/version?loaders=["${settings.loader}"]&game_versions=["${settings.version}"]`;
}
else {
url = `https://api.modrinth.com/v2/project/${slug}/version?game_versions=["${settings.version}"]`;
}
GM_xmlhttpRequest({
method: "GET",
url,
onload: (r) => {
const versions = JSON.parse(r.responseText);
if (!versions.length) {
setCached(slug, null);
cb(null);
return;
}
let selected = versions[0];
if (type === "shader") {
selected =
versions.find((v) => v.loaders?.includes(settings.shaderRuntime)) ||
null;
}
if (!selected) {
setCached(slug, null);
cb(null);
return;
}
const file = selected.files[0];
setCached(slug, file.url);
cb(file.url);
},
});
}
/* ================= CARD PROCESSING ================= */
function processCard(card) {
if (!settings.enabled) return;
const project = getProjectInfo(card);
if (!project) return;
const {
slug,
type
} = project;
const link = card.querySelector(
'a[href^="/mod/"], a[href^="/resourcepack/"], a[href^="/shader/"]',
);
const cacheKey = makeCacheKey(slug, type);
if (card.dataset.mrProcessed === cacheKey) return;
card.dataset.mrProcessed = cacheKey;
removeLoading(link);
// Add action badges to title
const titleEl = card.querySelector('h3, h4, h2, [class*="title"]');
const titleText = titleEl?.textContent?.trim() || slug;
if (titleEl) {
// Check if we've already added badges to this titleEl
if (!titleEl.dataset.mrBadgesProcessed) {
titleEl.dataset.mrBadgesProcessed = true;
const openBadge = document.createElement("span");
openBadge.className = "mr-action-badge mr-badge-open";
openBadge.textContent = "(Open)";
openBadge.style.pointerEvents = "auto";
openBadge.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.open(`https://modrinth.com/${type}/${slug}`, "_blank");
}, true);
const copyBadge = document.createElement("span");
copyBadge.className = "mr-action-badge mr-badge-copy";
copyBadge.textContent = "(Copy)";
copyBadge.style.pointerEvents = "auto";
copyBadge.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
navigator.clipboard.writeText(titleText).then(() => {
copyBadge.textContent = "(Copied!)";
setTimeout(() => {
copyBadge.textContent = "(Copy)";
}, 2000);
});
}, true);
titleEl.appendChild(openBadge);
titleEl.appendChild(copyBadge);
}
}
const cached = getCached(slug, type);
if (cached !== null) {
if (cached) {
link.href = cached;
if (titleEl && !titleEl.querySelector(".mr-cached-label")) {
addCachedLabel(titleEl);
}
applyDownloadLink(link, cached, slug, type, titleText);
}
else {
card.classList.add("mr-disabled");
}
return;
}
addLoading(link);
fetchDownload(slug, type, (url) => {
removeLoading(link);
if (!url) {
card.classList.add("mr-disabled");
return;
}
setCached(slug, type, url);
applyDownloadLink(link, url, slug, type, titleText);
});
}
function scan() {
document.querySelectorAll(".smart-clickable").forEach(processCard);
}
scan();
// second pass shortly after load, handles dynamic re-render that removes badges
setTimeout(scan, 1000);
new MutationObserver(scan).observe(document.body, {
childList: true,
subtree: true,
});
})();