Moved this to a reply to save space in the main post
// ==UserScript==
// @name Auto Update Decal Privacy
// @match *://create.roblox.com/*
// @grant window.close
// @grant window.focus
// @version 1.6
// ==/UserScript==
(function () {
const BATCH_OPEN_DELAY = 300;
const QUEUE_KEY = "robloxDecalQueue";
function updateStatus(msg, color = "#fff") {
let status = document.getElementById("script-status-box");
if (status && status.dataset.msg === msg) return;
if (!status) {
status = document.createElement("div");
status.id = "script-status-box";
Object.assign(status.style, {
position: "fixed", top: "10px", left: "50%", transform: "translateX(-50%)",
zIndex: "999999", padding: "8px 15px", background: "rgba(0,0,0,0.9)",
color: color, borderRadius: "20px", fontSize: "12px", fontWeight: "bold",
pointerEvents: "none", border: "1px solid #444"
});
document.body.appendChild(status);
}
status.dataset.msg = msg;
status.innerText = "🤖 " + msg;
status.style.color = color;
status.style.display = "block";
clearTimeout(status.hideTimer);
status.hideTimer = setTimeout(() => { status.style.display = "none"; }, 3000);
}
// Instead of just closing, move on to the next queued decal page in this
// same tab. Only close (and hand focus back to the dashboard) once the
// queue is empty.
function goToNextOrClose() {
if (window.leaving) return;
window.leaving = true;
let queue = [];
try { queue = JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]"); } catch (e) { queue = []; }
if (queue.length > 0) {
const next = queue.shift();
localStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
updateStatus("Next decal...", "#00ff00");
window.location.href = next;
} else {
localStorage.setItem('roblox_decal_next_tab', Date.now());
setTimeout(() => window.close(), 500);
}
}
function runAutomationStep() {
const url = window.location.href;
if (!url.includes("/configure")) return;
const bodyText = document.body.innerText;
const buttons = Array.from(document.querySelectorAll('button'));
const allSpans = Array.from(document.querySelectorAll('span'));
// 1. Success toast
const successFound = bodyText.includes("Successfully changed") || document.querySelector('div[class*="success"]');
if (successFound) {
updateStatus("Success! Moving on...", "#00ff00");
goToNextOrClose();
return;
}
// 2. Detect Enable banner presence (used in multiple steps below)
const enableBtn = buttons.find(b => b.innerText.trim() === "Enable");
const enableText = bodyText.includes("Click here to enable Asset Privacy");
const enableBannerPresent = !!(enableBtn && enableText);
// 3. Click Enable if banner is present
if (enableBannerPresent) {
updateStatus("Enabling Asset Privacy...", "#ffaa00");
enableBtn.click();
return;
}
// 4. Confirmation modal
const confirmBtn = buttons.find(b => b.innerText.includes("Make Open Use"));
if (confirmBtn) {
updateStatus("Confirming Open Use...", "#0084ff");
confirmBtn.click();
return;
}
// 5. Detect radio controls (MUI or native)
const openUseSpan = allSpans.find(s => s.innerText.trim() === "Open Use");
const restrictedSpan = allSpans.find(s => s.innerText.trim() === "Restricted");
const openUseRadio = document.querySelector('input[type="radio"][value*="open" i], input[type="radio"][value*="public" i]');
const restrictedRadio = document.querySelector('input[type="radio"][value*="restrict" i], input[type="radio"][value*="private" i]');
const hasRadioControls = openUseRadio || restrictedRadio
|| openUseSpan?.closest('[role="radio"]')
|| restrictedSpan?.closest('[role="radio"]');
// 6. No radio controls visible
if (!hasRadioControls) {
// If Enable banner is still present in any form, page is still loading — wait
if (enableBannerPresent || enableText || enableBtn) {
updateStatus("Waiting for privacy options to load...", "#aaa");
return;
}
// Only safe to move on when Enable banner is fully gone AND plain Open Use text is present
const isPlainOpenUse = bodyText.includes("Anyone on Roblox can use this asset");
if (isPlainOpenUse) {
updateStatus("Already Open Use (no controls). Moving on...", "#00ff00");
goToNextOrClose();
return;
}
updateStatus("Waiting for privacy options...", "#aaa");
return;
}
// 7. Determine current selection via aria-checked or input.checked
const openUseAriaChecked = openUseSpan?.closest('[role="radio"]')?.getAttribute('aria-checked') === 'true'
|| openUseSpan?.parentElement?.getAttribute('aria-checked') === 'true';
const isAlreadyOpenUse = openUseRadio?.checked || openUseAriaChecked;
// 8. Already on Open Use — save if needed, else move on
if (isAlreadyOpenUse) {
const saveBtn = buttons.find(b => b.innerText.includes("Save Changes") && !b.disabled);
if (saveBtn) {
updateStatus("Saving...", "#0084ff");
saveBtn.click();
} else {
updateStatus("Already Open Use. Moving on...", "#00ff00");
goToNextOrClose();
}
return;
}
// 9. Switch to Open Use
if (openUseSpan) {
updateStatus("Switching to Open Use...", "#0084ff");
const radioParent = openUseSpan.closest('[role="radio"]') || openUseSpan.parentElement;
radioParent ? radioParent.click() : openUseSpan.click();
return;
}
// 10. Save if available
const saveBtn = buttons.find(b => b.innerText.includes("Save Changes") && !b.disabled);
if (saveBtn) {
updateStatus("Saving...", "#0084ff");
saveBtn.click();
return;
}
updateStatus("Waiting...", "#aaa");
}
// Best-effort extraction of a decal/image's display name from its link.
// Tries the most likely spots in order: image alt text, title attribute,
// aria-label, then finally falls back to whatever text is on the link.
function getDecalName(link) {
const img = link.querySelector('img');
const raw = img?.alt || link.getAttribute('title') || link.getAttribute('aria-label') || link.innerText || "";
// The link's text is "Name\nCreated <date>" — only keep the name line.
const firstLine = raw.split('\n').map(s => s.trim()).find(s => s && !s.startsWith("Created"));
return (firstLine || "").trim();
}
function handleDashboard() {
const url = window.location.href;
if (!url.includes("activeTab=Image") && !url.includes("/creations?")) return;
if (!window.hasSetupFocusListener) {
window.addEventListener('storage', (e) => {
if (e.key === 'roblox_decal_next_tab') window.focus();
});
window.hasSetupFocusListener = true;
}
if (!document.getElementById("batchControlPanel")) {
const panel = document.createElement("div");
panel.id = "batchControlPanel";
Object.assign(panel.style, {
position: "fixed", bottom: "20px", right: "20px", zIndex: "99999",
background: "#1a1a1a", padding: "15px", borderRadius: "8px", border: "1px solid #444",
display: "flex", flexDirection: "column", gap: "8px", boxShadow: "0 4px 15px rgba(0,0,0,0.5)"
});
const createInput = (id, val, labelText) => {
const container = document.createElement("div");
container.style.display = "flex";
container.style.justifyContent = "space-between";
container.style.alignItems = "center";
container.style.gap = "10px";
const l = document.createElement("label");
l.innerText = labelText;
l.style.color = "white"; l.style.fontSize = "11px";
const i = document.createElement("input");
i.type = "number"; i.id = id; i.value = val;
Object.assign(i.style, {
width: "50px", background: "#333", color: "white", border: "1px solid #555", borderRadius: "4px", padding: "3px"
});
container.appendChild(l);
container.appendChild(i);
return container;
};
const createTextInput = (id, val, labelText) => {
const container = document.createElement("div");
container.style.display = "flex";
container.style.justifyContent = "space-between";
container.style.alignItems = "center";
container.style.gap = "10px";
const l = document.createElement("label");
l.innerText = labelText;
l.style.color = "white"; l.style.fontSize = "11px";
const i = document.createElement("input");
i.type = "text"; i.id = id; i.value = val;
Object.assign(i.style, {
width: "50px", background: "#333", color: "white", border: "1px solid #555", borderRadius: "4px", padding: "3px"
});
container.appendChild(l);
container.appendChild(i);
return container;
};
const createCheckbox = (id, labelText) => {
const container = document.createElement("div");
container.style.display = "flex";
container.style.justifyContent = "space-between";
container.style.alignItems = "center";
container.style.gap = "10px";
const l = document.createElement("label");
l.innerText = labelText;
l.htmlFor = id;
l.style.color = "white"; l.style.fontSize = "11px";
const i = document.createElement("input");
i.type = "checkbox"; i.id = id;
container.appendChild(l);
container.appendChild(i);
return container;
};
const startInp = createInput("rangeStart", "1", "Start at:");
const endInp = createInput("rangeEnd", "10", "End at:");
const sepInp = createTextInput("nameSeparator", ", ", "Separator:");
const reverseInp = createCheckbox("reverseOrder", "Reverse order:");
const btn = document.createElement("button");
btn.innerText = "🚀 Set Privacy From Range";
Object.assign(btn.style, {
marginTop: "5px", padding: "10px", background: "#0084ff", color: "white",
fontWeight: "bold", borderRadius: "6px", cursor: "pointer", border: "none"
});
// Instead of opening every target as its own tab, queue them up
// and open just ONE tab. That tab will walk through the whole
// queue itself (see goToNextOrClose) instead of closing and
// dumping you back on this dashboard each time.
btn.onclick = () => {
const start = parseInt(document.getElementById("rangeStart").value) - 1;
const end = parseInt(document.getElementById("rangeEnd").value);
const links = Array.from(document.querySelectorAll('a[href*="/configure"]'));
const reverse = document.getElementById("reverseOrder").checked;
let targets = [...new Set(links.map(l => l.href))].slice(start, end);
if (reverse) targets = targets.reverse();
if (targets.length === 0) return alert("No decals found in that range!");
const [first, ...rest] = targets;
localStorage.setItem(QUEUE_KEY, JSON.stringify(rest));
window.open(first, '_blank');
};
const copyBtn = document.createElement("button");
copyBtn.innerText = "📋 Copy Names From Range";
Object.assign(copyBtn.style, {
marginTop: "5px", padding: "10px", background: "#444", color: "white",
fontWeight: "bold", borderRadius: "6px", cursor: "pointer", border: "none"
});
copyBtn.onclick = () => {
const start = parseInt(document.getElementById("rangeStart").value) - 1;
const end = parseInt(document.getElementById("rangeEnd").value);
const sep = document.getElementById("nameSeparator").value;
const links = Array.from(document.querySelectorAll('a[href*="/configure"]'));
const reverse = document.getElementById("reverseOrder").checked;
let names = [...new Map(links.map(l => [l.href, l])).values()].slice(start, end).map(getDecalName).filter(Boolean);
if (reverse) names = names.reverse();
if (names.length === 0) return alert("No decals found in that range!");
const joined = names.join(sep);
navigator.clipboard.writeText(joined)
.then(() => updateStatus(`Copied ${names.length} names!`, "#00ff00"))
.catch(() => alert(joined));
};
panel.style.cursor = "move";
panel.addEventListener("mousedown", (e) => {
if (["INPUT", "BUTTON", "LABEL"].includes(e.target.tagName)) return;
const rect = panel.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
panel.style.left = rect.left + "px";
panel.style.top = rect.top + "px";
panel.style.right = "auto";
panel.style.bottom = "auto";
const move = (ev) => {
panel.style.left = ev.clientX - offsetX + "px";
panel.style.top = ev.clientY - offsetY + "px";
};
const up = () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
e.preventDefault();
});
panel.appendChild(startInp);
panel.appendChild(endInp);
panel.appendChild(sepInp);
panel.appendChild(reverseInp);
panel.appendChild(btn);
panel.appendChild(copyBtn);
document.body.appendChild(panel);
}
}
setInterval(runAutomationStep, 1000);
setInterval(handleDashboard, 1500);
})();

