// ==UserScript==
// @name GreasyFork: Unified Script Downloader & Sorter
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Agrega un botón flotante para descargar todos los scripts de un usuario de Greasy Fork!
// @author YouTubeDrawaria, Konf
// @match https://greasyfork.org/es/users/*
// @match https://greasyfork.org/*/users/*
// @match https://greasyfork.org/es/scripts/by-site/drawaria.online*
// @match https://greasyfork.org/es/scripts/by-site/*
// @match https://greasyfork.org/es/scripts*
// @match https://greasyfork.org/*/scripts/by-site/drawaria.online*
// @match https://greasyfork.org/*/scripts/by-site/*
// @match https://greasyfork.org/*/scripts*
// @match https://greasyfork.org/*/scripts/*
// @match https://sleazyfork.org/*/scripts/*
// @match https://web.archive.org/web/*/https://greasyfork.org/*/scripts/*
// @match https://web.archive.org/web/*/https://sleasyfork.org/*/scripts/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect greasyfork.org
// @connect update.greasyfork.org
// @connect sleazyfork.org
// @connect cdnjs.cloudflare.com
// @license MIT
// @icon https://drawaria.online/avatar/cache/86e33830-86ea-11ec-8553-bff27824cf71.jpg
// @run-at document-end
// @noframes
// ==/UserScript==
(function() {
'use strict';
// ========== Font Awesome Inclusion ==========
function ensureFontAwesome() {
if (!document.querySelector('link[href*="fontawesome"]')) {
const fa = document.createElement('link');
fa.rel = 'stylesheet';
fa.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css';
document.head.appendChild(fa);
console.log('[Greasyfork Unified Downloader & Sorter] Font Awesome CDN injected.');
}
}
// ========== Unified Utility Functions ==========
/**
* Converts a new format Greasyfork script href to an old format URL for better compatibility with direct downloads.
* @param {string} href - The script href (often from data-code-url or install-link).
* @returns {string} The converted old format URL or the original href if conversion fails.
*/
function convertScriptHrefToAnOldFormat(href) {
// Remove web.archive.org prefix if present to get the original Greasyfork URL
const webArchiveMatch = href.match(/(https:\/\/web\.archive\.org\/web\/\d+\/)?id_?\/(https:\/\/(greas|sleaz)yfork\.org.+)/);
if (webArchiveMatch) {
href = webArchiveMatch[2];
}
// Regex to extract domain, script ID, and filename with extension
const regex = /https:\/\/(?:update\.)?(\w+\.org)\/scripts\/(\d+)(?:\/\d+)?\/?([^?#]+)/;
const match = href.match(regex);
if (!match) {
console.warn(`[Greasyfork Unified Downloader & Sorter] Could not convert href to old format, returning original: ${href}`);
return href;
}
const domain = match[1];
const scriptId = match[2];
let scriptNameWithExt = match[3];
// Ensure scriptNameWithExt includes a valid file extension
if (!scriptNameWithExt.match(/\.(user\.js|user\.css|js|css)$/i)) {
scriptNameWithExt += '.user.js'; // Default to .user.js for user scripts
}
// Construct the old format URL which typically points to the raw 'code' endpoint
return `https://${domain}/scripts/${scriptId}/code/${scriptNameWithExt}`;
}
/**
* Robustly downloads a file using GM_xmlhttpRequest, trying multiple URLs if provided.
* @param {object} options
* @param {string|string[]} options.urls - The URL(s) to download from. Can be a single string or an array of strings.
* @param {string} options.filename - The desired filename for the downloaded file.
* @returns {Promise<void>}
*/
async function downloadFileRobustly({ urls, filename }) {
if (!Array.isArray(urls)) {
urls = [urls]; // Ensure it's an array for iteration
}
const errors = [];
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
if (!url) continue;
try {
await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
responseType: "blob",
onload: function(response) {
if (response.status === 200) {
const blob = response.response;
const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objUrl;
a.download = filename;
document.body.appendChild(a); // Needed for Firefox
a.click();
a.remove();
URL.revokeObjectURL(objUrl);
resolve(); // Success, resolve the current promise
} else {
const errorMsg = `Failed to fetch ${url}: Status ${response.status} - ${response.statusText}`;
errors.push(new Error(errorMsg));
console.warn(`[Greasyfork Unified Downloader & Sorter] ${errorMsg}`);
reject(new Error('Attempt failed, try next URL')); // Reject to move to next URL
}
},
onerror: function(error) {
const errorMsg = `Network error fetching ${url}: ${error.error || 'Unknown error'}`;
errors.push(new Error(errorMsg));
console.warn(`[Greasyfork Unified Downloader & Sorter] ${errorMsg}`);
reject(new Error('Attempt failed, try next URL')); // Reject to move to next URL
}
});
});
return; // If successful, exit the function
} catch (e) {
// If the promise was rejected with 'Attempt failed, try next URL', continue loop
if (e.message !== 'Attempt failed, try next URL') {
// Catch other unexpected errors
errors.push(e);
console.error(`[Greasyfork Unified Downloader & Sorter] Unexpected error during download attempt from ${url}:`, e);
}
}
}
// If all URLs failed
errors.forEach(e => console.error(e));
throw new Error('Failed to download file after all attempts. See console for details.');
}
/**
* Sanitizes a string to be a valid and clean filename.
* @param {string} filename - The original filename string.
* @returns {string} The sanitized filename.
*/
function sanitizeFilename(filename) {
// Remove query parameters and hash fragments
let cleanedFilename = filename.split('?')[0].split('#')[0];
// Separate Greasyfork specific extensions (.user.js, .user.css)
let baseName = cleanedFilename.replace(/\.(user\.js|user\.css)$/i, '');
let extension = (cleanedFilename.match(/\.(user\.js|user\.css)$/i) || [''])[0];
// If no specific Greasyfork extension, check for general .js/.css
if (!extension) {
let generalExtMatch = cleanedFilename.match(/\.(js|css)$/i);
if (generalExtMatch) {
baseName = cleanedFilename.substring(0, cleanedFilename.lastIndexOf(generalExtMatch[0]));
extension = generalExtMatch[0];
}
}
// Remove invalid characters, keeping alphanumeric, hyphens, underscores, periods, and spaces (temporarily)
baseName = baseName.replace(/[^\p{L}\p{N}\-_. ]/gu, '');
baseName = baseName.replace(/[\s]+/g, '_'); // Replace spaces with single underscores
baseName = baseName.replace(/_+/g, '_'); // Collapse multiple underscores
baseName = baseName.replace(/^[._\-]+|[._\-]+$/g, ''); // Remove leading/trailing unwanted chars
// Ensure baseName is not empty; fallback to a generic name
if (baseName.length === 0) {
baseName = 'greasyfork_script';
}
return `${baseName}${extension}`;
}
// ========== Unified Styles ==========
function addUnifiedStyles() {
GM_addStyle(`
/* Mass Downloader Button Styles */
#downloadAllGreasyforkScriptsUnified {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99999;
padding: 12px 25px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: background-color 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease;
}
#downloadAllGreasyforkScriptsUnified:hover:not(:disabled) {
background-color: #45a049;
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
}
#downloadAllGreasyforkScriptsUnified:active:not(:disabled) {
transform: translateY(2px);
}
#downloadAllGreasyforkScriptsUnified:disabled {
background-color: #cccccc !important;
cursor: not-allowed !important;
box-shadow: none !important;
transform: translateY(0) !important;
}
/* Individual/Library Download Button Styles */
.GF-DSB__script-download-button-unified {
position: relative;
padding: 8px 22px;
cursor: pointer;
border: none;
background: #0F750F;
transition: box-shadow 0.2s;
font-size: 15px;
line-height: 1.5;
font-weight: 700;
color: #fff;
border-radius: 3px;
display: inline-block;
margin-right: 5px;
text-decoration: none;
text-align: center; /* Center text/icon within the button */
vertical-align: middle; /* Align button vertically */
}
.GF-DSB__script-download-button-unified:hover,
.GF-DSB__script-download-button-unified:focus {
box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
}
/* Base icon styling (for both download and loading states) */
.GF-DSB__script-download-icon-unified {
position: absolute;
/* Positioning for the icon within the button */
top: 4px;
left: 7px;
}
/* Download Icon Specific Styling (Font Awesome) */
.GF-DSB__script-download-icon--download-unified {
font-size: 30px; /* Size for Font Awesome icon */
color: white; /* Color of the icon */
}
/* Loading Icon Specific Styling (Spinner) */
.GF-DSB__script-download-icon--loading-unified {
font-size: 0 !important; /* Hide the Font Awesome icon */
text-indent: -9999px; /* Ensure text/icon is not visible */
overflow: hidden; /* Hide any overflow */
/* Spinner specific positioning - slightly different from download icon to center the spinner */
top: 8px;
left: 11px;
/* Spinner animation styles */
border-radius: 50%;
width: 16px;
height: 16px;
border-top: 3px solid rgba(255, 255, 255, 0.2);
border-right: 3px solid rgba(255, 255, 255, 0.2);
border-bottom: 3px solid rgba(255, 255, 255, 0.2);
border-left: 3px solid #ffffff;
transform: translateZ(0);
animation: GF-DSB__script-download-loading-icon-unified 1.1s infinite linear;
}
@keyframes GF-DSB__script-download-loading-icon-unified {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.GF-DSB__library-download-button-unified {
transition: box-shadow 0.2s;
}
.GF-DSB__library-download-button--loading-unified {
animation: GF-DSB__loading-text-unified 1s infinite linear;
}
@keyframes GF-DSB__loading-text-unified {
50% {
opacity: 0.4;
}
}
/* Estilos adicionales para perfiles de usuario */
#user-profile-sort {
font-family: inherit;
}
#user-profile-sort ul {
align-items: center;
}
#user-profile-sort a:hover {
color: #0056b3 !important;
text-decoration: underline;
}
`);
}
// ========== Block: Mass Download ==========
function initMassDownload() {
console.log('[Greasyfork Unified Downloader & Sorter] Initializing Mass Download feature.');
addDownloadAllButton();
}
// Function to add the floating download all button
function addDownloadAllButton() {
if (document.getElementById('downloadAllGreasyforkScriptsUnified')) {
console.log('[Greasyfork Unified Downloader & Sorter] Mass download button already exists, skipping.');
return;
}
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Descargar Todos los Scripts';
downloadButton.id = 'downloadAllGreasyforkScriptsUnified';
document.body.appendChild(downloadButton);
console.log('[Greasyfork Unified Downloader & Sorter] Mass download button added to DOM.');
downloadButton.addEventListener('click', downloadAllScriptsMass);
}
async function downloadAllScriptsMass() {
const scriptListItems = document.querySelectorAll('li[data-code-url]');
console.log(`[Greasyfork Unified Downloader & Sorter] Found ${scriptListItems.length} potential scripts for mass download.`);
if (scriptListItems.length === 0) {
alert('No se encontraron scripts para descarga masiva. Asegúrate de estar en una página de listado de scripts o perfil de usuario con scripts públicos.');
console.error('[Greasyfork Unified Downloader & Sorter] No <li> elements with data-code-url found for mass download.');
return;
}
const downloadQueue = [];
scriptListItems.forEach((li) => {
const url = li.getAttribute('data-code-url');
if (!url) {
console.warn(`[Greasyfork Unified Downloader & Sorter] Element <li> without data-code-url:`, li);
return;
}
let initialFilename = url.substring(url.lastIndexOf('/') + 1);
const finalFilename = sanitizeFilename(initialFilename);
downloadQueue.push({ url, filename: finalFilename });
});
const totalScripts = downloadQueue.length;
let downloadedCount = 0;
alert(`Se encontraron ${totalScripts} scripts para descargar. Iniciando descarga de cada uno...`);
const downloadButton = document.getElementById('downloadAllGreasyforkScriptsUnified');
if (downloadButton) {
downloadButton.disabled = true;
downloadButton.textContent = `Descargando 0/${totalScripts}...`;
}
const delayMs = 1200; // 1.2 seconds between each download
for (const { url, filename } of downloadQueue) {
console.log(`[Greasyfork Unified Downloader & Sorter] Attempting to download: "${filename}" from "${url}"`);
try {
const urlsToTry = [convertScriptHrefToAnOldFormat(url), url];
await downloadFileRobustly({ urls: urlsToTry, filename: filename });
downloadedCount++;
if (downloadButton) {
downloadButton.textContent = `Descargando ${downloadedCount}/${totalScripts}...`;
}
console.log(`[Greasyfork Unified Downloader & Sorter] Successfully downloaded: "${filename}"`);
} catch (error) {
console.error(`[Greasyfork Unified Downloader & Sorter] ERROR downloading "${filename}":`, error);
}
if (downloadedCount < totalScripts) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
setTimeout(() => {
alert(`Se completó el intento de descarga de ${downloadedCount} de ${totalScripts} scripts. Revise las descargas de su navegador y la consola (F12) para cualquier error.`);
if (downloadButton) {
downloadButton.disabled = false;
downloadButton.textContent = 'Descargar Todos los Scripts';
}
}, 1000);
}
// ========== Block: Individual Download Button ==========
const i18n = {
download: 'download',
downloadWithoutInstalling: 'downloadWithoutInstalling',
failedToDownload: 'failedToDownload',
};
const translate = (function() {
const userLang = location.pathname.split('/')[1];
const strings = {
'en': {
[i18n.download]: 'Download ⇩',
[i18n.downloadWithoutInstalling]: 'Download without installing',
[i18n.failedToDownload]:
'Failed to download the script. There is might be more info in the browser console',
},
'ru': {
[i18n.download]: 'Скачать ⇩',
[i18n.downloadWithoutInstalling]: 'Скачать no устанавливая',
[i18n.failedToDownload]:
'Не удалось скачать скрипт. Больше информации может быть в консоли браузера',
},
'zh-CN': {
[i18n.download]: '下载 ⇩',
[i18n.downloadWithoutInstalling]: '下载此脚本',
[i18n.failedToDownload]: '无法下载此脚本',
},
'es': {
[i18n.download]: 'Descargar ⇩',
[i18n.downloadWithoutInstalling]: 'Descargar sin instalar',
[i18n.failedToDownload]:
'No se pudo descargar el script. Puede haber más información en la consola del navegador',
}
};
return id => (strings[userLang] || strings.en)[id] || strings.en[id];
}());
function initIndividualDownloadButton() {
console.log('[Greasyfork Unified Downloader & Sorter] Initializing Individual Download Button feature.');
const installArea = document.querySelector('div#install-area');
const installBtns = installArea?.querySelectorAll(':scope > a.install-link');
const installHelpLinks = document.querySelectorAll('a.install-help-link');
const suggestion = document.querySelector('div#script-feedback-suggestion');
const libraryRequire = document.querySelector('div#script-content > p > code');
const libraryVersion = document.querySelector(
'#script-stats > dd.script-show-version > span'
);
if (
installArea &&
(installBtns.length > 0) &&
(installBtns.length === installHelpLinks.length)
) {
for (let i = 0; i < installBtns.length; i++) {
mountScriptDownloadButton(installBtns[i], installArea, installHelpLinks[i]);
}
}
else if (suggestion && libraryRequire) {
mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion);
}
}
function mountScriptDownloadButton(
installBtn,
installArea,
installHelpLink,
) {
if (!installBtn.href) {
console.error('[Greasyfork Unified Downloader & Sorter] Script href is not found for individual button.');
return;
}
if (installArea.querySelector('.GF-DSB__script-download-button-unified')) {
console.log('[Greasyfork Unified Downloader & Sorter] Individual script download button already exists, skipping.');
return;
}
const b = document.createElement('a');
const bIcon = document.createElement('i');
b.href = '#';
b.title = translate(i18n.downloadWithoutInstalling);
b.draggable = false;
b.className = 'GF-DSB__script-download-button-unified';
bIcon.className =
'fas fa-download GF-DSB__script-download-icon-unified GF-DSB__script-download-icon--download-unified';
installHelpLink.style.position = 'relative';
b.appendChild(bIcon);
installArea.insertBefore(b, installHelpLink);
let isFetchingAllowed = true;
async function clicksHandler(ev) {
ev.preventDefault();
setTimeout(() => b === document.activeElement && b.blur(), 250);
if (isFetchingAllowed === false) return;
isFetchingAllowed = false;
bIcon.className =
'GF-DSB__script-download-icon-unified GF-DSB__script-download-icon--loading-unified';
try {
let scriptName = installBtn.dataset.scriptName;
if (installBtn.dataset.scriptVersion) {
scriptName += ` ${installBtn.dataset.scriptVersion}`;
}
const fileExt = `.${installBtn.dataset.installFormat || 'user.js'}`;
const filename = sanitizeFilename(`${scriptName}${fileExt}`);
const href = installBtn.href;
const urlsToTry = [convertScriptHrefToAnOldFormat(href), href];
await downloadFileRobustly({
urls: urlsToTry,
filename: filename,
});
} catch (e) {
console.error('[Greasyfork Unified Downloader & Sorter] Error during individual script download:', e);
alert(`${translate(i18n.failedToDownload)}: \n${e.message || e}`);
} finally {
setTimeout(() => {
isFetchingAllowed = true;
bIcon.className =
'fas fa-download GF-DSB__script-download-icon-unified GF-DSB__script-download-icon--download-unified';
}, 300);
}
}
b.addEventListener('click', clicksHandler);
b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
}
function mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion) {
let match = libraryRequire.innerText.match(
/\/\/ @require (https:\/\/.+\/scripts\/\d+\/\d+\/(.*)\.js)/
);
if (!match) {
console.error('[Greasyfork Unified Downloader & Sorter] Library href regex match failed.');
return;
}
let libraryHref = match[1];
let libraryName = decodeURIComponent(match[2]);
if (!libraryHref) {
console.error('[Greasyfork Unified Downloader & Sorter] Library href is not found for library button.');
return;
}
if (suggestion.querySelector('.GF-DSB__library-download-button-unified')) {
console.log('[Greasyfork Unified Downloader & Sorter] Library download button already exists, skipping.');
return;
}
if (libraryVersion?.innerText) libraryName += ` ${libraryVersion.innerText}`;
const b = document.createElement('a');
b.href = '#';
b.draggable = false;
b.innerText = translate(i18n.download);
b.className = 'GF-DSB__library-download-button-unified';
suggestion.appendChild(b);
let isFetchingAllowed = true;
async function clicksHandler(ev) {
ev.preventDefault();
setTimeout(() => b === document.activeElement && b.blur(), 250);
if (isFetchingAllowed === false) return;
isFetchingAllowed = false;
b.className =
'GF-DSB__library-download-button-unified GF-DSB__library-download-button--loading-unified';
try {
const fileExt = '.js';
const filename = sanitizeFilename(`${libraryName}${fileExt}`);
const urlsToTry = [convertScriptHrefToAnOldFormat(libraryHref), libraryHref];
await downloadFileRobustly({
urls: urlsToTry,
filename: filename,
});
} catch (e) {
console.error('[Greasyfork Unified Downloader & Sorter] Error during library download:', e);
alert(`${translate(i18n.failedToDownload)}: \n${e.message || e}`);
} finally {
setTimeout(() => {
isFetchingAllowed = true;
b.className = 'GF-DSB__library-download-button-unified';
}, 300);
}
}
b.addEventListener('click', clicksHandler);
b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
}
// ========== Block: Order Scripts by Older ==========
function getList() {
let list = document.querySelector('#browse-script-list');
if (!list && isUserProfilePage()) {
const firstScript = document.querySelector('li[data-code-url]');
if (firstScript) {
list = firstScript.parentElement;
}
}
return list;
}
function getOptionBar() {
let bar = document.querySelector('#script-list-sort ul');
if (!bar && isUserProfilePage()) {
bar = document.querySelector('#user-profile-sort ul');
}
return bar;
}
function addOldestButton() {
const bar = getOptionBar();
if (!bar && isUserProfilePage()) {
createUserProfileSortBar();
const newBar = getOptionBar(); // Try to get the newly created bar
if (newBar) {
addOldestButtonToBar(newBar);
}
return;
}
if (!bar || bar.querySelector('.sort-oldest-added')) {
console.log('[Greasyfork Unified Downloader & Sorter] "Más antiguo" button already exists or no bar found, skipping.');
return;
}
addOldestButtonToBar(bar);
}
function addOldestButtonToBar(barElement) {
if (barElement.querySelector('.sort-oldest-added')) {
console.log('[Greasyfork Unified Downloader & Sorter] "Más antiguo" button already exists in this bar, skipping.');
return;
}
const li = document.createElement('li');
li.className = 'list-option sort-oldest-added';
const a = document.createElement('a');
a.href = "#";
a.textContent = "Más antiguo";
a.style.cssText = "font-weight: bold !important; color: #007bff; text-decoration: none;"; // Added !important
a.onclick = function(e){
e.preventDefault();
sortByOldest();
};
li.appendChild(a);
barElement.appendChild(li);
console.log('[Greasyfork Unified Downloader & Sorter] "Más antiguo" button added.');
}
function createUserProfileSortBar() {
if (document.getElementById('user-profile-sort')) {
console.log('[Greasyfork Unified Downloader & Sorter] User profile sort bar already exists, skipping creation.');
return;
}
let insertBeforeElement = document.querySelector('ul.list-options') ||
document.querySelector('div.scripts-list') ||
document.querySelector('li[data-code-url]')?.parentElement ||
document.querySelector('article.script')?.parentElement;
let targetParent = null;
if (insertBeforeElement) {
targetParent = insertBeforeElement.parentElement;
} else {
targetParent = document.querySelector('.user-content') ||
document.querySelector('main');
}
if (!targetParent) {
console.warn('[Greasyfork Unified Downloader & Sorter] Could not find a suitable target parent to insert user profile sort bar.');
return;
}
const sortBar = document.createElement('div');
sortBar.id = 'user-profile-sort';
sortBar.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
`;
const sortList = document.createElement('ul');
sortList.style.cssText = `
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 15px;
flex-wrap: wrap;
`;
sortBar.appendChild(sortList);
if (insertBeforeElement && targetParent) {
targetParent.insertBefore(sortBar, insertBeforeElement);
} else {
targetParent.appendChild(sortBar);
}
console.log('[Greasyfork Unified Downloader & Sorter] User profile sort bar created.');
}
function sortByOldest() {
const list = getList();
if (!list) {
console.warn('[Greasyfork Unified Downloader & Sorter] No script list found to sort.');
return;
}
const items = Array.from(list.querySelectorAll('li[data-script-created-date]'));
if (items.length === 0) {
alert('No se encontraron scripts con fecha de creación para ordenar.');
console.warn('[Greasyfork Unified Downloader & Sorter] No sortable items found.');
return;
}
items.sort(function (a, b) {
const da = new Date(a.getAttribute('data-script-created-date'));
const db = new Date(b.getAttribute('data-script-created-date'));
return da - db;
});
while (list.firstChild) list.removeChild(list.firstChild);
for (const it of items) list.appendChild(it);
console.log('[Greasyfork Unified Downloader & Sorter] Scripts sorted by oldest.');
}
function initOrderByOldest() {
console.log('[Greasyfork Unified Downloader & Sorter] Initializing "Order by Older" feature.');
addOldestButton();
}
// ========== Helper functions to determine the current page type ==========
function isUserProfilePage() {
const path = location.pathname;
const userProfilePattern = /\/(?:[a-z]{2}\/)?users\/\d+-/;
return userProfilePattern.test(path);
}
function isScriptListPage() {
// Check for standard list page elements
const hasGeneralScriptList = document.querySelector('#browse-script-list') &&
document.querySelector('#script-list-sort ul');
// Check for user profile page URL pattern AND presence of any script list items
const hasScriptsInUserContext = isUserProfilePage() && (
document.querySelector('li[data-code-url]') ||
document.querySelector('.script-list') ||
document.querySelector('div.scripts-list') ||
document.querySelector('ul.list-options')
);
// Fallback: check if there's *any* installable script item on the page, regardless of container
const anyDownloadableScriptItem = document.querySelectorAll('li[data-code-url]').length > 0;
const result = hasGeneralScriptList || hasScriptsInUserContext || anyDownloadableScriptItem;
console.log(`[Greasyfork Unified Downloader & Sorter] isScriptListPage returns: ${result}`);
return result;
}
function isIndividualScriptPage() {
const path = location.pathname;
const parts = path.split('/').filter(Boolean);
if (parts.length >= 2 && parts.includes('scripts')) {
const scriptsIndex = parts.indexOf('scripts');
const potentialIdPart = parts[scriptsIndex + 1];
return potentialIdPart && /^\d+/.test(potentialIdPart) &&
!path.includes('/by-site/') &&
!path.includes('/new') &&
!path.includes('/discussions') &&
!path.includes('/versions') &&
!path.includes('/users/');
}
return false;
}
// ========== Global Initialization with MutationObserver ==========
function initializeUnifiedScript() {
console.log('[Greasyfork Unified Downloader & Sorter] Initializing unified script...');
ensureFontAwesome();
addUnifiedStyles();
let listFeaturesInitialized = false;
let individualFeatureInitialized = false;
let observer = null;
let debounceTimeoutId = null;
function tryInitFeatures() {
if (debounceTimeoutId) {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = null;
}
if (!individualFeatureInitialized && isIndividualScriptPage()) {
console.log('[Greasyfork Unified Downloader & Sorter] Individual script page detected, initializing individual features.');
initIndividualDownloadButton();
individualFeatureInitialized = true;
}
if (!listFeaturesInitialized && isScriptListPage()) {
console.log('[Greasyfork Unified Downloader & Sorter] Script list page (or user profile with scripts) detected, initializing list features.');
initMassDownload();
initOrderByOldest();
listFeaturesInitialized = true;
}
if (individualFeatureInitialized || listFeaturesInitialized) {
if (observer) {
observer.disconnect();
console.log('[Greasyfork Unified Downloader & Sorter] MutationObserver disconnected as features initialized.');
}
}
}
setTimeout(tryInitFeatures, 100); // Initial quick check
const observerTarget = document.body;
if (observerTarget) {
observer = new MutationObserver((mutationsList, observerInstance) => {
if (listFeaturesInitialized && individualFeatureInitialized) {
observerInstance.disconnect();
return;
}
// Debounce the call to tryInitFeatures to avoid excessive re-runs
if (!debounceTimeoutId) {
debounceTimeoutId = setTimeout(tryInitFeatures, 200); // Wait 200ms after last mutation
}
});
observer.observe(observerTarget, { childList: true, subtree: true });
console.log('[Greasyfork Unified Downloader & Sorter] MutationObserver started for dynamic content.');
} else {
console.warn('[Greasyfork Unified Downloader & Sorter] Could not find observer target (document.body). Dynamic features might not initialize.');
}
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', initializeUnifiedScript);
} else {
initializeUnifiedScript();
}
})();