// ==UserScript==
// @name F95Zone Developer and Game Search on Steam and RyuuGames
// @namespace http://tampermonkey.net/
// @version 2.12.0
// @description Adds buttons to search for the developer on F95Zone and the game title on Steam and RyuuGames, with robust F95Zone forum search.
// @author FunkyJustin
// @license MIT
// @match https://f95zone.to/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
// @grant GM_openInTab
// ==/UserScript==
(function() {
'use strict';
// Debounce utility to prevent rapid clicks
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Handle search if query parameter is present
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('query');
if (query) {
const tryFetchSearch = (retries = 3, delay = 1000) => {
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://f95zone.to/search/search';
form.style.display = 'none';
const inputs = {
'keywords': query,
'order': 'relevance',
'_xfToken': document.querySelector('input[name="_xfToken"]')?.value || ''
};
for (const [name, value] of Object.entries(inputs)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
fetch(form.action, {
method: 'POST',
body: new FormData(form),
redirect: 'follow'
})
.then(response => {
const finalUrl = response.url;
const match = finalUrl.match(/\/search\/(\d+)\//);
if (match && match[1]) {
const searchId = match[1];
const searchUrl = `https://f95zone.to/search/${searchId}/?q=${encodeURIComponent(query)}&o=relevance`;
window.location.href = searchUrl;
} else {
throw new Error('Could not extract search ID');
}
})
.catch(error => {
if (retries > 0) {
console.warn(`Retrying search for query "${query}" (${retries} retries left)...`);
setTimeout(() => tryFetchSearch(retries - 1, delay * 2), delay);
} else {
console.error('Error performing search for query:', query, error);
alert(`Error fetching search ID for "${query}". Redirecting to fallback URL. Details: ${error.message}`);
window.location.href = `https://f95zone.to/search/?q=${encodeURIComponent(query)}&o=relevance`;
}
})
.finally(() => {
document.body.removeChild(form);
});
};
// Wait for _xfToken using MutationObserver
const waitForToken = () => {
const tokenInput = document.querySelector('input[name="_xfToken"]');
if (tokenInput) {
tryFetchSearch();
} else {
const observer = new MutationObserver(() => {
if (document.querySelector('input[name="_xfToken"]')) {
observer.disconnect();
tryFetchSearch();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
if (!document.querySelector('input[name="_xfToken"]')) {
console.warn('No _xfToken found after timeout');
tryFetchSearch(0); // Proceed with empty token
}
}, 5000); // 5-second timeout
}
};
waitForToken();
return;
}
// Only add buttons if on a threads page
if (!window.location.pathname.startsWith('/threads/')) {
return;
}
// Inject CSS styles
const style = document.createElement('style');
style.innerHTML = `
.f95-button-container {
margin-top: 0;
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
background-color: transparent;
padding: 0 0 10px;
position: relative;
overflow-x: auto;
white-space: nowrap;
}
.f95-search-btn {
margin: 5px;
cursor: pointer;
padding: 5px 10px;
font-size: 14px;
color: #FFFFFF !important;
background: linear-gradient(45deg, #BA4545, #EC5555, #F6AAAA);
border: none;
border-radius: 3px;
display: inline-flex;
align-items: center;
text-align: center;
font-family: Arial, sans-serif;
font-weight: bold;
transition: background 0.2s, opacity 0.3s;
}
.f95-search-btn:hover {
background: #A23B3B;
opacity: 0.95;
}
`;
document.head.appendChild(style);
// Function to create a search button
function createSearchButton(buttonText, searchUrls, ariaLabel, isForumSearch = false) {
const button = document.createElement('button');
button.innerText = buttonText;
button.setAttribute('aria-label', ariaLabel);
button.className = 'f95-search-btn';
if (isForumSearch) {
const debouncedClick = debounce((event) => {
event.preventDefault();
GM_openInTab(`https://f95zone.to/?query=${searchUrls[0].split('=')[1]}`, {
active: event.button === 0,
setParent: true
});
}, 300);
button.addEventListener('mousedown', debouncedClick);
} else {
button.onclick = () => {
searchUrls.forEach(url => window.open(url, '_blank'));
};
}
return button;
}
// Find the title element
const titleElement = document.querySelector('.p-title h1.p-title-value');
if (titleElement) {
// Create a container for the buttons
const buttonContainer = document.createElement('div');
buttonContainer.className = 'f95-button-container';
// Extract the full title text
const titleText = titleElement.innerText;
// Extract the developer's name
const developerMatch = titleText.match(/\[([^\]]+)\]$/);
if (developerMatch && developerMatch[1].length > 0 && developerMatch[1].length < 100) {
const developerName = developerMatch[1];
const encodedDeveloperName = encodeURIComponent(developerName);
const developerSearchUrl = `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${developerName}`;
const developerForumSearchUrl = `https://f95zone.to/search/?q=${encodedDeveloperName}`;
const developerSteamSearchUrls = [
`https://store.steampowered.com/search/?developer=${developerName}`,
`https://store.steampowered.com/search/?term=${developerName}`
];
const developerSearchButton = createSearchButton('Search Developer on F95Zone', [developerSearchUrl], `Search for developer ${developerName} on F95Zone`);
const developerForumSearchButton = createSearchButton('Search Developer on F95Zone Forums', [developerForumSearchUrl], `Search for developer ${developerName} on F95Zone forums`, true);
const developerSteamSearchButton = createSearchButton('Search Developer on Steam', developerSteamSearchUrls, `Search for developer ${developerName} on Steam`);
buttonContainer.appendChild(developerSearchButton);
buttonContainer.appendChild(developerForumSearchButton);
buttonContainer.appendChild(developerSteamSearchButton);
} else {
const noDeveloperLabel = document.createElement('span');
noDeveloperLabel.innerText = 'Developer name not found';
noDeveloperLabel.style.color = '#888';
noDeveloperLabel.style.marginLeft = '10px';
buttonContainer.appendChild(noDeveloperLabel);
}
// Function to extract game title without modifying the DOM
function extractGameTitle(element, developer) {
let gameTitle = '';
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
gameTitle += node.textContent;
}
});
// Remove version number like [vX.Y.Z] or [vX.Y]
gameTitle = gameTitle.replace(/\[v\d+\.\d+(\.\d+)?\]/g, '').trim();
// Remove developer name if present
if (developer) {
gameTitle = gameTitle.replace(`[${developer}]`, '').trim();
}
return gameTitle.trim();
}
// Extract and clean the game title
const gameTitle = extractGameTitle(titleElement, developerMatch ? developerMatch[1] : null);
const encodedGameTitle = encodeURIComponent(gameTitle);
// Construct search URLs
const gameSearchUrl = `https://store.steampowered.com/search/?term=${encodedGameTitle}&supportedlang=english&ndl=1`;
const gameRyuuGamesSearchUrl = `https://www.ryuugames.com/?s=${encodedGameTitle}`;
const gameSearchButton = createSearchButton('Search Game on Steam', [gameSearchUrl], `Search for game ${gameTitle} on Steam`);
const gameRyuuGamesSearchButton = createSearchButton('Search Game on RyuuGames', [gameRyuuGamesSearchUrl], `Search for game ${gameTitle} on RyuuGames`);
// Append the game search buttons
buttonContainer.appendChild(gameSearchButton);
buttonContainer.appendChild(gameRyuuGamesSearchButton);
// Append the button container to the parent of the title element
titleElement.parentElement.appendChild(buttonContainer);
}
})();