Adds a clean dropdown menu with search buttons for F95Zone, OtomiGames, and Ryuugames on Ryuugames pages. Reliable developer detection (incl. Japanese), modular config for easy expansion, robust insertion & error handling.
// ==UserScript==
// @name RyuuGames Multi-Platform Search Buttons
// @namespace http://tampermonkey.net/
// @version 2.17.0
// @description Adds a clean dropdown menu with search buttons for F95Zone, OtomiGames, and Ryuugames on Ryuugames pages. Reliable developer detection (incl. Japanese), modular config for easy expansion, robust insertion & error handling.
// @author FunkyJustin
// @license MIT
// @match https://www.ryuugames.com/*
// @grant none
// @supportURL https://greasyfork.org/en/scripts/497408-ryuugames-f95zone-search-button/feedback
// ==/UserScript==
/*
Update History:
- v2.17.0: Fixed layout shift - now cleanly pushes content down instead of up; improved spacing and flow around the inserted UI while keeping visual design identical.
- v2.16.0: Improved game title cleaning - now properly strips version tags like (v26.06.15) (v1.02) leaving only the core title (e.g. "Altair of Strife"); enhanced robustness of title extraction.
- v2.15.0: Fixed UI duplication (stronger duplicate guard using data attribute); reordered F95Zone buttons so Product ID appears on the right; kept only icon version; improved robustness against multiple triggers.
- v2.14.0: Fixed Product ID extraction to include full "RJ" prefix (e.g. RJ01623698 instead of just digits) so PID buttons now work correctly on all platforms; improved regex to handle both title and URL slugs; kept UI unchanged.
- v2.13.0: Improved developer name extraction with better fallback & trimming; made sites fully modular/configurable for easy expansion; fixed insertion order with wrapper container; added subtle version badge; enhanced robustness, error handling & MutationObserver retries; kept UI nearly identical.
- v2.12: Added clean dropdown with groups, consistent styling, reliable developer detection including Japanese characters.
*/
(function() {
'use strict';
// ====================== CONFIGURATION (EASY TO EXTEND) ======================
const CONFIG = {
version: '2.17.0',
sites: {
f95zone: {
name: 'F95Zone',
icon: 'https://f95zone.to/favicon.ico',
searchUrl: (q) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/search=${encodeURIComponent(q)}`,
forumUrl: (q) => `https://f95zone.to/search/?q=${encodeURIComponent(q)}&t=post&c[child_nodes]=1&c[nodes][0]=2&c[title_only]=1&o=relevance`,
pidUrl: (pid) => `https://f95zone.to/search/?q=${encodeURIComponent(pid)}`,
devUrl: (dev) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${encodeURIComponent(dev)}`
},
otomi: {
name: 'OtomiGames',
icon: 'https://otomi-games.com/favicon.ico',
searchUrl: (q) => `https://otomi-games.com/?s=${encodeURIComponent(q)}`,
pidUrl: (pid) => `https://otomi-games.com/?s=${encodeURIComponent(pid)}`,
devUrl: (dev) => `https://otomi-games.com/?s=${encodeURIComponent(dev)}`
},
ryuugames: {
name: 'Ryuugames',
icon: 'https://www.ryuugames.com/wp-content/uploads/2020/05/cropped-ryuugames_logo-150x150.png',
searchUrl: (q) => `https://www.ryuugames.com/?s=${encodeURIComponent(q)}`,
pidUrl: (pid) => `https://www.ryuugames.com/?s=${encodeURIComponent(pid)}`,
devUrl: (dev) => `https://www.ryuugames.com/?s=${encodeURIComponent(dev)}`
}
}
};
// ====================== STYLES (UNCHANGED VISUALLY) ======================
const style = document.createElement('style');
style.innerHTML = `
:root {
--button-bg: linear-gradient(135deg, #1e73be, #155a8a);
--button-text: #ffffff;
--button-hover: #155a8a;
--dropdown-bg: rgba(255, 255, 255, 0.95);
--dropdown-border: #e0e0e0;
--text-color: #ffffff;
--container-bg: #1a1a1a;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.search-btn {
display: inline-flex;
align-items: center;
padding: 8px 16px;
margin: 4px;
background: var(--button-bg);
color: var(--button-text);
text-decoration: none;
border-radius: 10px;
border: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
}
.search-btn:hover {
background: var(--button-hover);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
transform: scale(1.05);
}
.search-btn img {
width: 16px;
height: 16px;
margin-right: 8px;
}
.developer-btn-wrapper .search-btn {
padding: 6px 12px;
font-size: 13px;
}
.dropdown-container {
max-width: 600px;
margin: 16px 0 20px 0;
}
.dropdown-toggle {
display: inline-flex;
align-items: center;
padding: 8px 16px;
background: var(--button-bg);
color: var(--button-text);
border: none;
border-radius: 10px;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
.dropdown-toggle:hover {
background: var(--button-hover);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
transform: scale(1.05);
}
.dropdown-toggle::after {
content: '▼';
margin-left: 8px;
transition: transform 0.2s;
}
.dropdown-toggle.collapsed::after {
transform: rotate(180deg);
}
.version-badge {
position: absolute;
top: -6px;
right: -8px;
background: #ff9800;
color: #000;
font-size: 10px;
padding: 1px 5px;
border-radius: 8px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.dropdown-content {
display: block;
background: var(--dropdown-bg);
border: 1px solid var(--dropdown-border);
border-radius: 12px;
box-shadow: var(--shadow);
padding: 16px;
margin-top: 8px;
}
.dropdown-content.hidden {
display: none;
}
.button-group {
margin-bottom: 12px;
}
.group-label {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 700;
color: #222;
margin-bottom: 8px;
}
.button-group-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.developer-btn-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 24px;
padding: 12px;
background: var(--container-bg);
border-radius: 12px;
box-shadow: var(--shadow);
}
.developer-label {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
color: var(--text-color);
margin-bottom: 8px;
}
.developer-btn-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
/* Ensure clean downward flow */
.ryuugames-search-ui {
clear: both;
width: 100%;
}
`;
document.head.appendChild(style);
// ====================== HELPER FUNCTIONS ======================
function createButton(text, url, iconUrl) {
const a = document.createElement('a');
a.className = 'search-btn';
a.href = url;
a.target = '_blank';
a.title = text;
if (iconUrl) {
const img = document.createElement('img');
img.src = iconUrl;
img.alt = '';
a.appendChild(img);
}
a.appendChild(document.createTextNode(text));
return a;
}
function extractGameInfo() {
const header = document.querySelector('header.td-post-title');
if (!header) return null;
const h1 = header.querySelector('h1.entry-title');
if (!h1) return null;
let titleText = h1.textContent.trim();
let gameTitle = titleText
.replace(/^\[ENG\]\s*/i, '')
.replace(/\s*\(v[\d\.]+\)/gi, '')
.replace(/\s*\(v[\d\.]+\)/gi, '')
.replace(/\s*\(RJ\d+\)\s*$/i, '')
.replace(/\s*\(\d+\.\d+\)/g, '')
.trim();
if (gameTitle.includes('(')) {
gameTitle = gameTitle.split('(')[0].trim();
}
let productId = '';
const titlePidMatch = titleText.match(/\(?([Rr][Jj]\d{6,})\)?/);
const urlPidMatch = window.location.href.match(/([Rr][Jj]\d{6,})/);
if (titlePidMatch) {
productId = titlePidMatch[1].toUpperCase();
} else if (urlPidMatch) {
productId = urlPidMatch[1].toUpperCase();
}
return { gameTitle, productId, h1 };
}
function extractDeveloperName() {
let developerName = 'Unknown';
const contentDiv = document.querySelector('.td-post-content');
if (!contentDiv) return developerName;
const textContent = contentDiv.textContent || '';
const paragraphs = contentDiv.getElementsByTagName('p');
for (let p of paragraphs) {
const lines = p.textContent.split(/\n|\r/);
for (let line of lines) {
line = line.trim();
const match = line.match(/^(developer|開発者)\s*:+\s*(.+)$/i);
if (match && match[2]) {
developerName = match[2].trim().split(/(?=\s*(released|language|version|18\+|censored|uncensored))/i)[0].trim();
if (developerName) return developerName;
}
}
}
const lines = textContent.split(/\n|\r/);
for (let line of lines) {
line = line.trim();
const match = line.match(/(developer|開発者)\s*:+\s*([^\n\r]+)/i);
if (match && match[2]) {
developerName = match[2].trim().split(/(?=\s*(released|language|version|18\+))/i)[0].trim();
if (developerName) return developerName;
}
}
return developerName || 'Unknown';
}
// ====================== MAIN UI CREATION ======================
function createSearchUI() {
const info = extractGameInfo();
if (!info) return false;
const { gameTitle, productId, h1 } = info;
const developerName = extractDeveloperName();
if (document.querySelector('.ryuugames-search-ui')) {
console.log('[RyuuGames Search] UI already present, skipping.');
return true;
}
const mainContainer = document.createElement('div');
mainContainer.className = 'ryuugames-search-ui';
// Dropdown container
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'dropdown-container';
const toggleButton = document.createElement('button');
toggleButton.className = 'dropdown-toggle';
toggleButton.innerHTML = `Search Options <span class="version-badge">${CONFIG.version}</span>`;
toggleButton.title = `Multi-Platform Search v${CONFIG.version}`;
const dropdownContent = document.createElement('div');
dropdownContent.className = 'dropdown-content';
toggleButton.addEventListener('click', () => {
dropdownContent.classList.toggle('hidden');
toggleButton.classList.toggle('collapsed');
});
Object.keys(CONFIG.sites).forEach(siteKey => {
const site = CONFIG.sites[siteKey];
const group = document.createElement('div');
group.className = 'button-group';
const label = document.createElement('div');
label.className = 'group-label';
label.textContent = site.name;
group.appendChild(label);
const grid = document.createElement('div');
grid.className = 'button-group-grid';
const buttonsInGroup = [];
if (site.searchUrl) {
buttonsInGroup.push(createButton(`Search ${site.name}`, site.searchUrl(gameTitle), site.icon));
}
if (siteKey === 'f95zone') {
if (site.forumUrl) {
buttonsInGroup.push(createButton('F95Zone Forum', site.forumUrl(gameTitle), site.icon));
}
if (productId && site.pidUrl) {
buttonsInGroup.push(createButton('F95Zone Product ID', site.pidUrl(productId), site.icon));
}
} else {
if (productId && site.pidUrl) {
buttonsInGroup.push(createButton(`${site.name} Product ID`, site.pidUrl(productId), site.icon));
}
}
buttonsInGroup.forEach(btn => grid.appendChild(btn));
group.appendChild(grid);
dropdownContent.appendChild(group);
});
dropdownContainer.appendChild(toggleButton);
dropdownContainer.appendChild(dropdownContent);
// Developer section
const developerWrapper = document.createElement('div');
developerWrapper.className = 'developer-btn-wrapper';
const devLabel = document.createElement('div');
devLabel.className = 'developer-label';
devLabel.textContent = `Developer: ${developerName}`;
developerWrapper.appendChild(devLabel);
const devRow = document.createElement('div');
devRow.className = 'developer-btn-row';
Object.keys(CONFIG.sites).forEach(siteKey => {
const site = CONFIG.sites[siteKey];
if (site.devUrl) {
const btn = createButton(`${site.name} Developer`, site.devUrl(developerName), site.icon);
devRow.appendChild(btn);
}
});
developerWrapper.appendChild(devRow);
mainContainer.appendChild(dropdownContainer);
mainContainer.appendChild(developerWrapper);
// Insert cleanly after the header to push content DOWN
h1.insertAdjacentElement('afterend', mainContainer);
console.log(`%c[RyuuGames Search] UI added successfully - v${CONFIG.version} | Title: "${gameTitle}" | PID: ${productId} | Developer: ${developerName}`, 'color: #1e73be; font-weight: bold');
return true;
}
// ====================== OBSERVER & ROBUST INIT ======================
let attempts = 0;
const maxAttempts = 12;
const observer = new MutationObserver(() => {
try {
const header = document.querySelector('header.td-post-title');
if (header && document.querySelector('.td-post-content')) {
if (!document.querySelector('.ryuugames-search-ui')) {
createSearchUI();
} else {
observer.disconnect();
}
}
} catch (err) {
console.error('[RyuuGames Search] Error in observer:', err);
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial + safety calls
setTimeout(() => { try { createSearchUI(); } catch (e) {} }, 600);
setTimeout(() => { try { createSearchUI(); } catch (e) {} }, 1800);
setTimeout(() => { try { if (!document.querySelector('.ryuugames-search-ui')) createSearchUI(); } catch (e) {} }, 3500);
})();