Advanced search buttons for DLsite with settings dialog (gear icon).
// ==UserScript==
// @name Multi-Platform Search Buttons for DLsite
// @namespace http://tampermonkey.net/
// @version 4.12.0
// @description Advanced search buttons for DLsite with settings dialog (gear icon).
// @author FunkyJustin
// @match https://www.dlsite.com/maniax/work/=/product_id/*
// @match https://www.dlsite.com/pro/work/=/product_id/*
// @match https://www.dlsite.com/maniax/announce/=/product_id/*
// @match https://www.dlsite.com/pro/announce/=/product_id/*
// @match https://f95zone.to/*
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ==================== F95Zone Handler ====================
if (window.location.href.includes('f95zone.to')) {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('query');
if (query) {
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 match = response.url.match(/\/search\/(\d+)\//);
window.location.href = match
? `https://f95zone.to/search/${match[1]}/?q=${encodeURIComponent(query)}&o=relevance`
: `https://f95zone.to/search/?q=${encodeURIComponent(query)}&o=relevance`;
})
.catch(() => {
window.location.href = `https://f95zone.to/search/?q=${encodeURIComponent(query)}&o=relevance`;
})
.finally(() => {
if (form.parentNode) document.body.removeChild(form);
});
}
return;
}
// ==================== SETTINGS ====================
const SETTINGS = {
get defaultOpen() {
return GM_getValue('dropdown_default_open', true);
},
set defaultOpen(value) {
GM_setValue('dropdown_default_open', value);
}
};
// ==================== CONFIGURATION ====================
const CONFIG = {
groups: [
{
name: 'F95Zone',
buttons: [
{ text: 'Search on F95Zone', url: (d) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/search=${encodeURIComponent(d.gameTitle)}`, icon: 'f95zone' },
{ text: 'Search on F95Zone Forums', query: (d) => d.gameTitle, isF95Forum: true, icon: 'f95zone' },
{ text: 'Search Product ID on F95Zone Forums', query: (d) => d.productId, isF95Forum: true, icon: 'f95zone' },
]
},
{
name: 'Utilities',
buttons: [
{ text: 'Copy Product ID', action: 'copyPID' },
{ text: 'Search Product ID on Google', url: (d) => `https://www.google.com/search?q="${d.productId}"`, icon: 'google' },
{ text: 'Search Title on Google', url: (d) => `https://www.google.com/search?q=${encodeURIComponent(d.gameTitle)}`, icon: 'google' },
{ text: 'Copy Title', action: 'copyTitle' },
]
},
{
name: 'OtomiGames',
buttons: [
{ text: 'Search on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.gameTitle)}`, icon: 'otomi' },
{ text: 'Search Product ID on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.productId)}`, icon: 'otomi' },
]
},
{
name: 'Ryuugames',
buttons: [
{ text: 'Search on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.gameTitle)}`, icon: 'ryuugames' },
{ text: 'Search Product ID on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.productId)}`, icon: 'ryuugames' },
]
}
]
};
// ==================== CSS ====================
const style = document.createElement('style');
style.innerHTML = `
:root {
--button-bg-start: #D6BCFA;
--button-bg-end: #FBB6CE;
--button-shadow: rgba(0, 0, 0, 0.1);
--button-border: #B794F4;
--dropdown-bg: #f9f9f9;
}
.search-btn {
display: inline-flex;
align-items: center;
padding: 5px 9px;
margin: 3px;
background: linear-gradient(45deg, var(--button-bg-start), var(--button-bg-end));
color: #800080 !important;
text-decoration: none;
border-radius: 12px;
border: 1px solid var(--button-border);
box-shadow: 0 2px 4px var(--button-shadow);
font-family: Arial, sans-serif;
font-size: 14.5px;
font-weight: bold;
transition: all 0.2s;
cursor: pointer;
user-select: auto !important;
white-space: nowrap;
}
.search-btn:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
opacity: 0.95;
}
.search-btn img {
width: 18px;
height: 18px;
margin-right: 6px;
}
.dropdown-container {
max-width: 680px;
margin: 12px 0 20px 0;
position: relative;
}
.dropdown-header {
display: flex;
align-items: center;
gap: 8px;
}
.dropdown-toggle {
display: flex;
align-items: center;
padding: 6px 12px;
background-color: #E91E63;
color: #fff;
border: none;
border-radius: 12px;
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;
cursor: pointer;
flex: 1;
}
.dropdown-toggle::after {
content: ' ▼';
margin-left: 8px;
transition: transform 0.3s;
}
.dropdown-toggle.collapsed::after {
transform: rotate(180deg);
}
.gear-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 6px;
border-radius: 50%;
color: #fff;
opacity: 0.9;
}
.gear-btn:hover {
background: rgba(255,255,255,0.25);
}
.dropdown-content {
display: block;
background-color: var(--dropdown-bg);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 12px;
margin-top: 6px;
}
.dropdown-content.hidden {
display: none;
}
.version-label {
position: absolute;
bottom: 6px;
right: 12px;
font-size: 11px;
color: #888;
pointer-events: none;
}
.button-group { margin-bottom: 12px; }
.group-label {
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 6px;
}
.button-group-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 6px;
}
.circle-btn-wrapper {
display: flex;
align-items: center;
margin-top: 6px;
gap: 6px;
flex-wrap: wrap;
}
/* Settings Modal */
.settings-modal {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.65);
display: none;
align-items: center;
justify-content: center;
z-index: 99999;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 12px;
width: 340px;
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
}
.modal-content h3 { margin: 0 0 16px 0; color: #333; }
.modal-content label { display: block; margin: 14px 0; font-size: 15px; cursor: pointer; }
.modal-buttons { margin-top: 24px; text-align: right; }
.modal-buttons button {
padding: 8px 18px;
margin-left: 10px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.modal-buttons .cancel { background: #ddd; color: #333; }
.modal-buttons .save { background: #E91E63; color: white; }
`;
document.head.appendChild(style);
const icons = {
f95zone: 'https://f95zone.to/favicon.ico',
ryuugames: 'https://www.ryuugames.com/wp-content/uploads/2020/05/cropped-ryuugames_logo-1-32x32.png',
otomi: 'https://otomi-games.com/favicon.ico',
google: 'https://www.google.com/favicon.ico'
};
// ==================== HELPERS ====================
function createButton(btnConfig, data) {
let element;
if (btnConfig.isF95Forum) {
element = document.createElement('button');
element.type = 'button';
element.addEventListener('mousedown', (e) => {
if (e.button === 0 || e.button === 1) {
e.preventDefault();
GM_openInTab(`https://f95zone.to/?query=${encodeURIComponent(btnConfig.query(data))}`, {
active: e.button === 0,
setParent: true
});
}
});
} else {
element = document.createElement('a');
element.href = btnConfig.url ? btnConfig.url(data) : '#';
element.target = '_blank';
}
element.className = 'search-btn';
element.title = btnConfig.text;
if (btnConfig.icon && icons[btnConfig.icon]) {
const img = document.createElement('img');
img.src = icons[btnConfig.icon];
element.appendChild(img);
}
element.appendChild(document.createTextNode(btnConfig.text));
return element;
}
function createGroup(groupConfig, data) {
const group = document.createElement('div');
group.className = 'button-group';
const label = document.createElement('div');
label.className = 'group-label';
label.textContent = groupConfig.name;
group.appendChild(label);
const grid = document.createElement('div');
grid.className = 'button-group-grid';
groupConfig.buttons.forEach(btnConfig => {
let btn;
if (btnConfig.action === 'copyPID') {
btn = createButton({text: btnConfig.text}, data);
btn.addEventListener('click', (e) => {
e.preventDefault();
navigator.clipboard.writeText(data.productId).then(() => {
const orig = btn.textContent;
btn.textContent = '✓ Copied!';
setTimeout(() => btn.textContent = orig, 1800);
});
});
} else if (btnConfig.action === 'copyTitle') {
btn = createButton({text: btnConfig.text}, data);
btn.addEventListener('click', (e) => {
e.preventDefault();
navigator.clipboard.writeText(data.gameTitle).then(() => {
const orig = btn.textContent;
btn.textContent = '✓ Copied!';
setTimeout(() => btn.textContent = orig, 1800);
});
});
} else {
btn = createButton(btnConfig, data);
}
grid.appendChild(btn);
});
group.appendChild(grid);
return group;
}
function getProductData() {
const titleEl = document.querySelector('h1#work_name') ||
document.querySelector('h1[itemprop="name"]') ||
document.querySelector('h1');
let gameTitle = titleEl ? titleEl.innerText.trim() : '';
gameTitle = gameTitle.replace(/\s*\[.*?\]\s*$/, '').trim();
const circleSelectors = ['span.maker_name a', 'span[itemprop="brand"] a', 'a[href*="maker_id"]'];
let circleName = '';
let circleLink = null;
for (const sel of circleSelectors) {
const el = document.querySelector(sel);
if (el) {
circleName = el.innerText.trim();
circleLink = el;
break;
}
}
const pidMatch = window.location.href.match(/product_id\/(RJ\d+)/i);
const productId = pidMatch ? pidMatch[1] : '';
return { gameTitle, circleName, productId, circleLink };
}
// ==================== SETTINGS DIALOG ====================
function showSettings() {
let modal = document.getElementById('dlsite-settings-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'dlsite-settings-modal';
modal.className = 'settings-modal';
modal.innerHTML = `
<div class="modal-content">
<h3>⚙️ Script Settings</h3>
<label>
<input type="checkbox" id="default-open">
Open "Search Options" dropdown by default
</label>
<div class="modal-buttons">
<button class="cancel" id="modal-cancel">Cancel</button>
<button class="save" id="modal-save">Save & Reload</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('modal-cancel').onclick = () => modal.style.display = 'none';
document.getElementById('modal-save').onclick = () => {
SETTINGS.defaultOpen = document.getElementById('default-open').checked;
modal.style.display = 'none';
location.reload();
};
}
document.getElementById('default-open').checked = SETTINGS.defaultOpen;
modal.style.display = 'flex';
}
// ==================== MAIN FUNCTION ====================
function addSearchButtons() {
const data = getProductData();
if (!data.gameTitle || !data.productId) {
console.warn('[DLsite Search Buttons] Could not extract required data.');
return;
}
const titleContainer = document.querySelector('.base_title_br') ||
document.querySelector('h1')?.closest('div, section, header');
if (titleContainer) {
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'dropdown-container';
const header = document.createElement('div');
header.className = 'dropdown-header';
const toggle = document.createElement('button');
toggle.className = 'dropdown-toggle';
toggle.textContent = 'Search Options';
const gear = document.createElement('button');
gear.className = 'gear-btn';
gear.innerHTML = '⚙️';
gear.title = 'Script Settings';
gear.onclick = (e) => {
e.stopImmediatePropagation();
showSettings();
};
header.append(toggle, gear);
const content = document.createElement('div');
content.className = 'dropdown-content';
if (!SETTINGS.defaultOpen) {
content.classList.add('hidden');
toggle.classList.add('collapsed');
}
toggle.addEventListener('click', () => {
content.classList.toggle('hidden');
toggle.classList.toggle('collapsed');
});
const version = document.createElement('div');
version.className = 'version-label';
version.textContent = 'v4.12.0';
dropdownContainer.append(header, content, version);
CONFIG.groups.forEach(group => {
content.appendChild(createGroup(group, data));
});
titleContainer.parentNode.insertBefore(dropdownContainer, titleContainer.nextSibling);
}
// Circle Buttons
if (data.circleName && data.circleLink) {
const wrapper = document.createElement('span');
wrapper.className = 'circle-btn-wrapper';
const circleConfigs = [
{ text: 'Search Circle on F95Zone', url: (d) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${encodeURIComponent(d.circleName)}`, icon: 'f95zone' },
{ text: 'Search Circle on F95Zone Forums', query: (d) => d.circleName, isF95Forum: true, icon: 'f95zone' },
{ text: 'Search Circle on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.circleName)}`, icon: 'ryuugames' },
{ text: 'Search Circle on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.circleName)}`, icon: 'otomi' },
];
circleConfigs.forEach(cfg => wrapper.appendChild(createButton(cfg, data)));
const makerContainer = data.circleLink.closest('span.maker_name') || data.circleLink.parentNode;
makerContainer.parentNode.insertBefore(wrapper, makerContainer.nextSibling);
}
}
// ==================== RUN ====================
if (window.location.href.includes('dlsite.com')) {
const observer = new MutationObserver((_, obs) => {
if (document.querySelector('h1')) {
addSearchButtons();
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
})();