Add persistent sidebar toggles to auto-append score sorting and video exclusion tags on Rule34, Danbooru, and Gelbooru.
// ==UserScript==
// @name Booru Auto Search Tags
// @namespace https://booru.local/
// @version 1.0.7
// @description Add persistent sidebar toggles to auto-append score sorting and video exclusion tags on Rule34, Danbooru, and Gelbooru.
// @author icynic
// @match https://rule34.xxx/*
// @match https://www.rule34.xxx/*
// @match https://danbooru.donmai.us/*
// @match https://gelbooru.com/*
// @match https://www.gelbooru.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const PANEL_ID = "tm-booru-auto-tags-panel";
const SETTINGS_KEY = "tmBooruAutoSearchTagsV1";
const ALL_SORT_TERMS = ["sort:score:desc", "sort:score", "order:score", "order:favcount"];
const ALL_VIDEO_TERMS = ["-webm", "-mp4", "-video", "-filetype:webm", "-filetype:mp4"];
const EMPTY_SEARCH_TERMS = ["all"];
const SITE_CONFIGS = [
{
id: "rule34",
label: "Rule34",
hosts: ["rule34.xxx", "www.rule34.xxx"],
sortTerm: "sort:score:desc",
videoTerms: ["-video"],
sidebarSelectors: ["#tag-sidebar", "#sidebar", ".sidebar", "aside"],
},
{
id: "danbooru",
label: "Danbooru",
hosts: ["danbooru.donmai.us"],
sortLabel: "Sort by favorites",
sortTerm: "order:favcount",
videoTerms: ["-video"],
sidebarSelectors: ["#sidebar", "#tag-sidebar", "aside", ".sidebar"],
},
{
id: "gelbooru",
label: "Gelbooru",
hosts: ["gelbooru.com", "www.gelbooru.com"],
sortTerm: "sort:score:desc",
videoTerms: ["-video"],
sidebarSelectors: ["#tag-sidebar", "#sidebar", ".sidebar", "aside"],
},
];
const site = SITE_CONFIGS.find((config) => config.hosts.includes(location.hostname));
if (!site) {
return;
}
const state = {
settings: loadSettings(),
lastHref: location.href,
};
init();
function init() {
injectStyles();
renderPanelWhenReady();
bindSearchHandlers();
observeUrlChanges();
}
function loadSettings() {
const defaults = {};
for (const config of SITE_CONFIGS) {
defaults[config.id] = {
sortByScore: false,
hideVideos: false,
};
}
try {
const stored = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
return normalizeSettings({ ...defaults, ...stored });
} catch {
return defaults;
}
}
function normalizeSettings(settings) {
const normalized = {};
for (const config of SITE_CONFIGS) {
normalized[config.id] = {
sortByScore: Boolean(settings && settings[config.id] && settings[config.id].sortByScore),
hideVideos: Boolean(settings && settings[config.id] && settings[config.id].hideVideos),
};
}
return normalized;
}
function saveSettings() {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(state.settings));
} catch {
// The toggles still work for the current page if storage is unavailable.
}
}
function renderPanelWhenReady() {
const existing = document.getElementById(PANEL_ID);
if (existing) {
existing.remove();
}
if (!isPanelPageUrl(new URL(location.href))) {
return;
}
const sidebar = findSidebar();
if (!sidebar) {
window.setTimeout(renderPanelWhenReady, 300);
return;
}
const panel = document.createElement("section");
panel.id = PANEL_ID;
panel.innerHTML = [
renderToggle("sortByScore", site.sortLabel || "Sort by score", site.sortTerm),
renderToggle("hideVideos", "Hide videos", site.videoTerms.join(" ")),
].join("");
insertPanel(sidebar, panel);
panel.addEventListener("change", (event) => {
const checkbox = event.target;
if (!(checkbox instanceof HTMLInputElement) || !checkbox.dataset.setting) {
return;
}
state.settings[site.id][checkbox.dataset.setting] = checkbox.checked;
saveSettings();
});
}
function renderToggle(setting, label, hint) {
const checked = state.settings[site.id][setting] ? " checked" : "";
return [
'<label class="tm-bat-toggle">',
` <input type="checkbox" data-setting="${escapeHtml(setting)}"${checked}>`,
' <span class="tm-bat-switch" aria-hidden="true"></span>',
' <span class="tm-bat-copy">',
` <span class="tm-bat-label">${escapeHtml(label)}</span>`,
` <span class="tm-bat-hint">${escapeHtml(hint)}</span>`,
" </span>",
"</label>",
].join("");
}
function insertPanel(sidebar, panel) {
if (site.id === "danbooru") {
const searchInput = getSearchInput();
const searchContainer = searchInput && (
searchInput.closest("form") ||
searchInput.closest("section") ||
searchInput.closest("div")
);
if (searchContainer && sidebar.contains(searchContainer)) {
searchContainer.insertAdjacentElement("afterend", panel);
return;
}
}
const insertionTarget = sidebar.firstElementChild || sidebar;
if (insertionTarget === sidebar) {
sidebar.insertBefore(panel, sidebar.firstChild);
} else {
sidebar.insertBefore(panel, insertionTarget);
}
}
function findSidebar() {
for (const selector of site.sidebarSelectors) {
const element = document.querySelector(selector);
if (element) {
return element;
}
}
const likelySidebar = Array.from(document.querySelectorAll("nav, aside, div, section")).find((element) => {
const box = element.getBoundingClientRect();
return box.width >= 120 && box.width <= 360 && box.left <= 40 && box.height >= 250;
});
return likelySidebar || document.body;
}
function isPanelPageUrl(url) {
if (site.id === "danbooru") {
return url.pathname === "/" || /^\/posts(?:\/\d+)?\/?$/.test(url.pathname);
}
return url.searchParams.get("page") === "post";
}
function isTagSearchUrl(url) {
if (!site.hosts.includes(url.hostname) || !url.searchParams.has("tags")) {
return false;
}
if (site.id === "danbooru") {
return url.pathname === "/posts" || url.pathname === "/posts/";
}
return url.searchParams.get("page") === "post" && url.searchParams.get("s") !== "view";
}
function buildManagedTags(tags) {
const settings = state.settings[site.id];
const originalTerms = splitTags(tags);
const suppressSort = shouldSuppressSortTerm(originalTerms);
const terms = originalTerms.filter((term) => {
if (EMPTY_SEARCH_TERMS.includes(term.toLowerCase())) {
return false;
}
if (ALL_SORT_TERMS.includes(term)) {
return false;
}
if (ALL_VIDEO_TERMS.includes(term)) {
return false;
}
return true;
});
const managedTerms = [];
if (settings.sortByScore && !suppressSort) {
managedTerms.push(site.sortTerm);
}
if (settings.hideVideos) {
managedTerms.push(...site.videoTerms);
}
return [...managedTerms, ...terms].join(" ").trim();
}
function shouldSuppressSortTerm(terms) {
return site.id === "danbooru" && terms.some((term) => /^ordfav:/i.test(term));
}
function splitTags(tags) {
return String(tags || "")
.split(/\s+/)
.map((term) => term.trim())
.filter(Boolean);
}
function getSearchInput() {
return (
document.querySelector('input[name="tags"]') ||
document.querySelector('textarea[name="tags"]') ||
document.querySelector('input[type="search"]') ||
document.querySelector('input#tags')
);
}
function bindSearchHandlers() {
const prepareClickedTagLink = (event) => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
const link = target.closest("a[href]");
if (link instanceof HTMLAnchorElement) {
applySettingsToTagSearchLink(link);
}
};
document.addEventListener("pointerdown", prepareClickedTagLink, true);
document.addEventListener("auxclick", prepareClickedTagLink, true);
document.addEventListener(
"submit",
(event) => {
const form = event.target;
if (form instanceof HTMLFormElement) {
applySettingsToSearchInput(findSearchInputIn(form));
}
},
true
);
document.addEventListener(
"click",
(event) => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
prepareClickedTagLink(event);
const submitter = target.closest('button, input[type="submit"], input[type="image"]');
if (submitter) {
const form = submitter.form instanceof HTMLFormElement ? submitter.form : null;
applySettingsToSearchInput(form ? findSearchInputIn(form) : getSearchInput());
}
},
true
);
document.addEventListener(
"keydown",
(event) => {
if (event.key !== "Enter") {
return;
}
const target = event.target;
if (isSearchInput(target)) {
applySettingsToSearchInput(target);
}
},
true
);
}
function findSearchInputIn(root) {
return (
root.querySelector('input[name="tags"]') ||
root.querySelector('textarea[name="tags"]') ||
root.querySelector('input[type="search"]') ||
root.querySelector('input#tags')
);
}
function isSearchInput(target) {
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement
? target.matches('[name="tags"], input[type="search"], input#tags, textarea[name="tags"]')
: false;
}
function applySettingsToSearchInput(input) {
const targetInput = input || getSearchInput();
if (targetInput && typeof targetInput.value === "string") {
targetInput.value = buildManagedTags(targetInput.value);
}
}
function applySettingsToTagSearchLink(link) {
const url = new URL(link.href, location.href);
if (!isTagSearchUrl(url)) {
return;
}
url.searchParams.set("tags", buildManagedTags(url.searchParams.get("tags") || ""));
link.href = url.toString();
}
function observeUrlChanges() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
const result = originalPushState.apply(this, args);
onPossibleUrlChange();
return result;
};
history.replaceState = function (...args) {
const result = originalReplaceState.apply(this, args);
onPossibleUrlChange();
return result;
};
window.addEventListener("popstate", onPossibleUrlChange);
window.setInterval(onPossibleUrlChange, 500);
}
function onPossibleUrlChange() {
if (state.lastHref === location.href) {
return;
}
state.lastHref = location.href;
window.setTimeout(() => {
renderPanelWhenReady();
}, 50);
}
function injectStyles() {
if (document.getElementById(`${PANEL_ID}-style`)) {
return;
}
const style = document.createElement("style");
style.id = `${PANEL_ID}-style`;
style.textContent = `
#${PANEL_ID} {
box-sizing: border-box;
width: min(100%, 260px);
margin: 0 0 12px;
padding: 10px;
border: 1px solid rgba(128, 128, 128, 0.35);
border-radius: 6px;
background: transparent;
color: inherit;
font: 13px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#${PANEL_ID} * {
box-sizing: border-box;
}
#${PANEL_ID} .tm-bat-hint {
color: inherit;
font-size: 12px;
opacity: 0.72;
}
#${PANEL_ID} .tm-bat-toggle {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 8px;
align-items: center;
margin-top: 10px;
cursor: pointer;
}
#${PANEL_ID} .tm-bat-toggle input {
position: absolute;
opacity: 0;
pointer-events: none;
}
#${PANEL_ID} .tm-bat-switch {
position: relative;
width: 34px;
height: 20px;
border-radius: 999px;
background: rgba(128, 128, 128, 0.45);
transition: background 0.15s ease;
}
#${PANEL_ID} .tm-bat-switch::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: Canvas;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
transition: transform 0.15s ease;
}
#${PANEL_ID} input:checked + .tm-bat-switch {
background: #1f7a4d;
}
#${PANEL_ID} input:checked + .tm-bat-switch::after {
transform: translateX(14px);
}
#${PANEL_ID} .tm-bat-copy {
min-width: 0;
}
#${PANEL_ID} .tm-bat-label,
#${PANEL_ID} .tm-bat-hint {
display: block;
overflow-wrap: anywhere;
}
`;
document.head.appendChild(style);
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
})();