// ==UserScript==
// @name Booru Search Tag Enhancer (Universal)
// @namespace booru-tag-search
// @version 2.1
// @description Modernize and enhance search bar and tag input for booru sites, with modular site-specific configurations, dynamic cheat sheets with caching, e621-specific order syntax, and improved layout for rule34.xxx and e621.net
// @author Piperun
// @license LGPL-3.0-or-later
// @match *://*.booru.org/*
// @match *://*.booru.com/*
// @match *://*.booru.ca/*
// @match *://*.booru.xxx/*
// @match *://rule34.xxx/*
// @match *://safebooru.org/*
// @match *://danbooru.donmai.us/*
// @match *://e621.net/*
// @grant none
// ==/UserScript==
// --- Awesomplete hijack: always attach instance to input ---
(function hijackAwesomplete() {
const origAwesomplete = window.Awesomplete;
if (typeof origAwesomplete === "function") {
window.Awesomplete = function AwesompleteHijack(input, options) {
const instance = new origAwesomplete(input, options);
input.awesomplete = instance;
return instance;
};
// Copy prototype and static properties
Object.setPrototypeOf(window.Awesomplete, origAwesomplete);
window.Awesomplete.prototype = origAwesomplete.prototype;
}
})();
(function main() {
'use strict';
// --- Configuration ---
const SITE_CONFIG = {
"e621.net": {
placeholder: "Enter tags...",
sortOptions: [
{ value: 'score', label: 'Score' },
{ value: 'favcount', label: 'Favorites' },
{ value: 'comment_count', label: 'Comments' },
{ value: 'id', label: 'ID' },
{ value: 'mpixels', label: 'Megapixels' },
{ value: 'filesize', label: 'File Size' },
{ value: 'landscape', label: 'Landscape' },
{ value: 'portrait', label: 'Portrait' },
{ value: 'created', label: 'Created' },
{ value: 'updated', label: 'Updated' },
{ value: 'tagcount', label: 'Tag Count' },
{ value: 'duration', label: 'Duration' },
{ value: 'random', label: 'Random' }
],
ratings: [
{ value: 'safe', label: 'Safe' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
],
metatagRegex: /^(order:|rating:|user:|parent:|score:|md5:|width:|height:|source:|id:|favcount:|comment_count:|type:|date:|status:|\( rating:)/,
cheatSheetContent: `
<section><h4>Basics</h4>
<ul>
<li><code>cat dog</code> — Search for posts that are tagged with both cat and dog. Tags are separated by spaces.</li>
<li><code>red_panda african_wild_dog</code> — Words within each tag are separated by underscores.</li>
<li><code>~cat ~dog</code> — Search for posts that are tagged either cat or dog (or both). May not work well when combined with other syntaxes.</li>
<li><code>-chicken</code> — Search for posts that don't have the chicken tag.</li>
<li><code>fox -chicken</code> — Search for posts that are tagged with fox and are not tagged with chicken.</li>
<li><code>african_*</code> — Search for posts with any tag that starts with african_, such as african_wild_dog or african_golden_cat. May not work well when combined with other syntaxes. Limit one wildcard per search.</li>
<li><code>( ~cat ~tiger ~leopard ) ( ~dog ~wolf )</code> — Search for posts that are tagged with one (or more) of cat, tiger or leopard, and one (or more) of dog or wolf.</li>
</ul>
</section>
<section><h4>Sorting</h4>
<ul>
<li><code>order:id</code> — Oldest to newest</li>
<li><code>order:id_desc</code> — Newest to oldest</li>
<li><code>order:score</code> — Highest score first</li>
<li><code>order:score_asc</code> — Lowest score first</li>
<li><code>order:favcount</code> — Most favorites first</li>
<li><code>order:favcount_asc</code> — Least favorites first</li>
<li><code>order:comment_count</code> — Most comments first</li>
<li><code>order:comment_count_asc</code> — Least comments first</li>
<li><code>order:mpixels</code> — Largest resolution first</li>
<li><code>order:mpixels_asc</code> — Smallest resolution first</li>
<li><code>order:filesize</code> — Largest file size first</li>
<li><code>order:filesize_asc</code> — Smallest file size first</li>
<li><code>order:landscape</code> — Wide and short to tall and thin</li>
<li><code>order:portrait</code> — Tall and thin to wide and short</li>
<li><code>order:duration</code> — Video duration longest to shortest</li>
<li><code>order:duration_asc</code> — Video duration shortest to longest</li>
<li><code>order:random</code> — Orders posts randomly</li>
</ul>
</section>
<section><h4>User Metatags</h4>
<ul>
<li><code>user:Bob</code> — Posts uploaded by Bob</li>
<li><code>fav:Bob</code> or <code>favoritedby:Bob</code> — Posts favorited by Bob</li>
<li><code>voted:anything</code> — Posts you voted on. Only works while logged in.</li>
<li><code>votedup:anything</code> or <code>upvote:anything</code> — Posts you upvoted. Only works while logged in.</li>
<li><code>voteddown:anything</code> or <code>downvote:anything</code> — Posts you downvoted. Only works while logged in.</li>
<li><code>approver:Bob</code> — Posts approved by Bob</li>
<li><code>commenter:Bob</code> or <code>comm:Bob</code> — Posts commented on by Bob</li>
<li><code>noter:Bob</code> — Posts with notes written by Bob</li>
</ul>
</section>
<section><h4>Post Metatags - Counts</h4>
<ul>
<li><code>id:100</code> — Post with an ID of 100</li>
<li><code>score:100</code> — Posts with a score of 100</li>
<li><code>favcount:100</code> — Posts with exactly 100 favorites</li>
<li><code>comment_count:100</code> — Posts with exactly 100 comments</li>
<li><code>tagcount:2</code> — Posts with exactly 2 tags</li>
<li><code>gentags:2</code> — Posts with exactly 2 general tags</li>
<li><code>arttags:2</code> — Posts with exactly 2 artist tags</li>
<li><code>chartags:2</code> — Posts with exactly 2 character tags</li>
<li><code>copytags:2</code> — Posts with exactly 2 copyright tags</li>
<li><code>spectags:2</code> — Posts with exactly 2 species tags</li>
<li><code>invtags:2</code> — Posts with exactly 2 invalid tags</li>
<li><code>lortags:2</code> — Posts with exactly 2 lore tags</li>
<li><code>metatags:2</code> — Posts with exactly 2 meta tags</li>
</ul>
</section>
<section><h4>Rating</h4>
<ul>
<li><code>rating:safe</code> or <code>rating:s</code> — Posts rated safe</li>
<li><code>rating:questionable</code> or <code>rating:q</code> — Posts rated questionable</li>
<li><code>rating:explicit</code> or <code>rating:e</code> — Posts rated explicit</li>
</ul>
</section>
<section><h4>File Types</h4>
<ul>
<li><code>type:jpg</code> — Posts that are JPG, a type of image</li>
<li><code>type:png</code> — Posts that are PNG, a type of image</li>
<li><code>type:gif</code> — Posts that are GIF, a type of image (may be animated)</li>
<li><code>type:swf</code> — Posts that are Flash, a format used for animation</li>
<li><code>type:webm</code> — Posts that are WebM, a type of video</li>
</ul>
</section>
<section><h4>Image Size</h4>
<ul>
<li><code>width:100</code> — Posts with a width of 100 pixels</li>
<li><code>height:100</code> — Posts with a height of 100 pixels</li>
<li><code>mpixels:1</code> — Posts that are 1 megapixel (a 1000x1000 image equals 1 megapixel)</li>
<li><code>ratio:1.33</code> — Search for posts with a ratio of 4:3. All ratios are rounded to two digits, therefore 1.33 will return posts with a ratio of 4:3.</li>
<li><code>filesize:200KB</code> — Posts with a file size of 200 kilobytes. File sizes within ±5% of the value are included.</li>
<li><code>filesize:2MB</code> — Posts with a file size of 2 megabytes. File sizes within ±5% of the value are included.</li>
</ul>
</section>
<section><h4>Post Status</h4>
<ul>
<li><code>status:pending</code> — Posts that are waiting to be approved or deleted</li>
<li><code>status:active</code> — Posts that have been approved</li>
<li><code>status:deleted</code> — Posts that have been deleted</li>
<li><code>status:flagged</code> — Posts that are flagged for deletion</li>
<li><code>status:modqueue</code> — Posts that are pending or flagged</li>
<li><code>status:any</code> — All active or deleted posts</li>
</ul>
</section>
<section><h4>Dates</h4>
<ul>
<li><code>date:2012-04-27</code> or <code>date:april/27/2012</code> — Search for posts uploaded on a specific date</li>
<li><code>date:today</code> — Posts from today</li>
<li><code>date:yesterday</code> — Posts from yesterday</li>
<li><code>date:week</code> — Posts from the last 7 days</li>
<li><code>date:month</code> — Posts from the last 30 days</li>
<li><code>date:year</code> — Posts from the last 365 days</li>
<li><code>date:5_days_ago</code> — Posts from within the last 5 days</li>
<li><code>date:5_weeks_ago</code> — Posts from within the last 5 weeks</li>
<li><code>date:5_months_ago</code> — Posts from within the last 5 months</li>
<li><code>date:5_years_ago</code> — Posts from within the last 5 years</li>
<li><code>date:yesterweek</code> — Posts from last week</li>
<li><code>date:yestermonth</code> — Posts from last month</li>
<li><code>date:yesteryear</code> — Posts from last year</li>
</ul>
</section>
<section><h4>Text Searching</h4>
<ul>
<li><code>source:*example.com</code> — Posts with a source that contains "example.com", prefix matched, use wildcards as needed</li>
<li><code>source:none</code> — Posts without a source</li>
<li><code>description:whatever</code> — Posts with a description that contains the text "whatever"</li>
<li><code>description:"hello there"</code> — Posts with a description that contains the text "hello there"</li>
<li><code>note:whatever</code> — Posts with a note that contains the text "whatever"</li>
<li><code>delreason:*whatever</code> — Deleted posts that contain a reason with the text "whatever", prefix matched, use wildcards as needed</li>
</ul>
</section>
<section><h4>Parents and Children</h4>
<ul>
<li><code>ischild:true</code> — Posts that are a child</li>
<li><code>ischild:false</code> — Posts that aren't a child</li>
<li><code>isparent:true</code> — Posts that are a parent</li>
<li><code>isparent:false</code> — Posts that aren't a parent</li>
<li><code>parent:1234</code> — Posts with a parent of 1234</li>
<li><code>parent:none</code> — Posts with no parent (same as ischild:false)</li>
</ul>
</section>
<section><h4>Other</h4>
<ul>
<li><code>hassource:true</code> — Posts with a source</li>
<li><code>hassource:false</code> — Posts without a source</li>
<li><code>hasdescription:true</code> — Posts with a description</li>
<li><code>hasdescription:false</code> — Posts without a description</li>
<li><code>inpool:true</code> — Posts that are in a pool</li>
<li><code>inpool:false</code> — Posts that aren't in a pool</li>
<li><code>pool:4</code> or <code>pool:fox_and_the_grapes</code> — Posts in the pool "Fox and the Grapes"</li>
<li><code>set:17</code> or <code>set:cute_rabbits</code> — Posts in the set with the short name "cute_rabbits"</li>
<li><code>md5:02dd0...</code> — Post with the given MD5 hash. MD5 hashes will never be shared by more than one image.</li>
<li><code>duration:>120</code> — Videos with a duration of at least 120 seconds</li>
</ul>
</section>
<section><h4>Range Syntax</h4>
<ul>
<li><code>id:100</code> — Post with an ID of exactly 100</li>
<li><code>date:year..month</code> — Posts uploaded between 30 days ago and 1 year ago</li>
<li><code>filesize:200KB..300KB</code> — Posts with a file size between 200 kilobytes and 300 kilobytes</li>
<li><code>score:25..50</code> — Posts with a score between 25 and 50</li>
<li><code>score:>=100</code> — Posts with a score of 100 or greater (100+)</li>
<li><code>score:>100</code> — Posts with a score greater than 100 (101+)</li>
<li><code>favcount:<=100</code> — Posts with 100 or less favorites (0-100)</li>
<li><code>favcount:<100</code> — Posts with less than 100 favorites (0-99)</li>
</ul>
</section>
`
},
"rule34.xxx": {
placeholder: "Enter tags...",
sortOptions: [
{ value: 'score', label: 'Score' },
{ value: 'updated', label: 'Updated' },
{ value: 'id', label: 'ID' },
{ value: 'rating', label: 'Rating' },
{ value: 'user', label: 'User' },
{ value: 'height', label: 'Height' },
{ value: 'width', label: 'Width' },
{ value: 'parent', label: 'Parent' },
{ value: 'source', label: 'Source' }
],
ratings: [
{ value: 'safe', label: 'Safe' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
],
metatagRegex: /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/,
cheatSheetContent: `
<section><h4>Basic Search</h4>
<ul>
<li><code>tag1 tag2</code> — Posts with <b>tag1</b> and <b>tag2</b></li>
<li><code>( tag1 ~ tag2 )</code> — Posts with <b>tag1</b> or <b>tag2</b></li>
<li><code>-tag1</code> — Posts without <b>tag1</b></li>
<li><code>ta*1</code> — Tags starting with <b>ta</b> and ending with <b>1</b></li>
</ul>
</section>
<section><h4>Metatags</h4>
<ul>
<li><code>user:bob</code> — Posts by user <b>bob</b></li>
<li><code>rating:questionable</code> — Rated <b>questionable</b></li>
<li><code>score:>=10</code> — Score 10 or higher</li>
<li><code>width:>=1000</code> — Width 1000px or more</li>
<li><code>height:>1000</code> — Height greater than 1000px</li>
<li><code>parent:1234</code> — Has parent <b>1234</b></li>
<li><code>md5:foo</code> — Posts with MD5 hash <b>foo</b></li>
</ul>
</section>
<section><h4>Sorting</h4>
<ul>
<li><code>sort:updated:desc</code> — Sort by updated (descending)</li>
<li><code>sort:score:asc</code> — Sort by score (ascending)</li>
<li>Other types: <code>id</code>, <code>rating</code>, <code>user</code>, <code>height</code>, <code>width</code>, <code>parent</code>, <code>source</code></li>
</ul>
</section>
`
},
"danbooru.donmai.us": {
placeholder: "Enter tags...",
sortOptions: [
{ value: 'score', label: 'Score' },
{ value: 'favcount', label: 'Favorites' },
{ value: 'comment_count', label: 'Comments' },
{ value: 'id', label: 'ID' },
{ value: 'mpixels', label: 'Megapixels' },
{ value: 'filesize', label: 'File Size' },
{ value: 'landscape', label: 'Landscape' },
{ value: 'portrait', label: 'Portrait' },
{ value: 'created_at', label: 'Created' },
{ value: 'updated_at', label: 'Updated' },
{ value: 'tagcount', label: 'Tag Count' },
{ value: 'duration', label: 'Duration' },
{ value: 'random', label: 'Random' }
],
ratings: [
{ value: 'general', label: 'General' },
{ value: 'sensitive', label: 'Sensitive' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
],
metatagRegex: /^(order:|rating:|user:|parent:|score:|md5:|width:|height:|source:|id:|favcount:|comment_count:|type:|date:|status:|pool:|set:|\( rating:)/,
cheatSheetUrl: "https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet"
},
"default": {
placeholder: "Enter tags...",
sortOptions: [
{ value: 'score', label: 'Score' },
{ value: 'updated', label: 'Updated' },
{ value: 'id', label: 'ID' },
{ value: 'rating', label: 'Rating' },
{ value: 'user', label: 'User' },
{ value: 'height', label: 'Height' },
{ value: 'width', label: 'Width' },
{ value: 'parent', label: 'Parent' },
{ value: 'source', label: 'Source' }
],
ratings: [
{ value: 'safe', label: 'Safe' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
],
metatagRegex: /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/,
cheatSheetContent: `
<section><h4>Basic Search</h4>
<ul>
<li><code>tag1 tag2</code> — Posts with <b>tag1</b> and <b>tag2</b></li>
<li><code>( tag1 ~ tag2 )</code> — Posts with <b>tag1</b> or <b>tag2</b></li>
<li><code>-tag1</code> — Posts without <b>tag1</b></li>
<li><code>ta*1</code> — Tags starting with <b>ta</b> and ending with <b>1</b></li>
</ul>
</section>
<section><h4>Metatags</h4>
<ul>
<li><code>user:bob</code> — Posts by user <b>bob</b></li>
<li><code>rating:questionable</code> — Rated <b>questionable</b></li>
<li><code>score:>=10</code> — Score 10 or higher</li>
<li><code>width:>=1000</code> — Width 1000px or more</li>
<li><code>height:>1000</code> — Height greater than 1000px</li>
<li><code>parent:1234</code> — Has parent <b>1234</b></li>
<li><code>md5:foo</code> — Posts with MD5 hash <b>foo</b></li>
</ul>
</section>
<section><h4>Sorting</h4>
<ul>
<li><code>sort:updated:desc</code> — Sort by updated (descending)</li>
<li><code>sort:score:asc</code> — Sort by score (ascending)</li>
<li>Other types: <code>id</code>, <code>rating</code>, <code>user</code>, <code>height</code>, <code>width</code>, <code>parent</code>, <code>source</code></li>
</ul>
</section>
`
}
};
// --- State ---
let tags = [];
const siteConfig = SITE_CONFIG[location.hostname] || SITE_CONFIG['default'];
const isE621 = location.hostname.endsWith('e621.net');
const isRule34 = location.hostname.endsWith('rule34.xxx');
const isDanbooru = location.hostname.endsWith('danbooru.donmai.us');
const isWikiPage = location.pathname.includes('/wiki') || location.pathname.includes('/help') || document.title.includes('Wiki');
// Define global site variable
let site = '';
if (isE621) site = 'e621';
else if (isRule34) site = 'rule34';
else if (isDanbooru) site = 'danbooru';
// --- Settings State ---
let settings = {
showIncludeExclude: true,
showMetatags: true,
showAllTags: true
};
// Load settings from localStorage
function loadSettings() {
const saved = localStorage.getItem('booru-enhancer-settings');
if (saved) {
try {
settings = { ...settings, ...JSON.parse(saved) };
} catch (e) {
console.warn('Failed to load settings:', e);
}
}
}
// Save settings to localStorage
function saveSettings() {
try {
localStorage.setItem('booru-enhancer-settings', JSON.stringify(settings));
} catch (e) {
console.warn('Failed to save settings:', e);
}
}
loadSettings();
// --- Site Configuration Helper ---
function addSiteConfig(hostname, config) {
SITE_CONFIG[hostname] = config;
}
// Example usage to add a new site:
// addSiteConfig('newsite.com', {
// placeholder: "Search tags...",
// sortOptions: [{ value: 'date', label: 'Date' }],
// ratings: [{ value: 'sfw', label: 'Safe for Work' }],
// metatagRegex: /^(category:|type:)/,
// cheatSheetContent: `<section><h4>Custom Help</h4>...</section>`
// });
// --- Color/Theme Helpers ---
function getContrastYIQ(hexcolor) {
hexcolor = hexcolor.replace('#', '').trim();
// If rgb/rgba, convert to hex
if (hexcolor.startsWith('rgb')) {
const rgb = hexcolor.match(/\d+/g).map(Number);
if (rgb.length >= 3) {
hexcolor = rgb.slice(0, 3).map(x => x.toString(16).padStart(2, '0')).join('');
}
}
if (hexcolor.length === 3) {
hexcolor = hexcolor.split('').map(x => x + x).join('');
}
if (hexcolor.length !== 6) return '#222';
var r = parseInt(hexcolor.substr(0, 2), 16);
var g = parseInt(hexcolor.substr(2, 2), 16);
var b = parseInt(hexcolor.substr(4, 2), 16);
var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? '#222' : '#fff';
}
function updateTagColors() {
let bg, border;
if (isE621) {
const root = document.body;
bg = getComputedStyle(root).getPropertyValue('--color-tag-general') || '#e6f7ff';
border = getComputedStyle(root).getPropertyValue('--color-tag-general-alt') || '#7ecfff';
} else if (isRule34) {
bg = '#e6ffe6';
border = '#7edc7e';
} else if (isDanbooru) {
bg = '#ffe6e6';
border = '#ff7e7e';
} else {
bg = '#e0ffe0';
border = '#b0d0b0';
}
document.querySelectorAll('.r34-tag-item').forEach(tag => {
tag.style.background = bg.trim();
tag.style.borderColor = border.trim();
// Auto-contrast text color
let color = getContrastYIQ(bg.trim());
tag.style.color = color;
});
}
// --- Tag Management ---
function addTag(tag) {
if (!tag || tags.includes(tag)) return;
tags.push(tag);
tags = Array.from(new Set(tags));
renderTags(window.r34_tagList);
}
function getTagsFromURL() {
const params = new URLSearchParams(window.location.search);
let tagString = params.get('tags') || '';
// Replace + with space
tagString = tagString.replace(/\+/g, ' ');
// Custom split: treat parenthesized groups and tags with parentheses as single tags
const tags = [];
let buffer = '';
let parenLevel = 0;
for (let i = 0; i < tagString.length; ++i) {
const c = tagString[i];
if (c === '(') {
// Check if this looks like a grouping parenthesis (preceded by space or at start)
// vs a tag name parenthesis (preceded by underscore or alphanumeric)
const prevChar = i > 0 ? tagString[i - 1] : ' ';
const isGroupingParen = prevChar === ' ' || i === 0;
if (isGroupingParen && parenLevel === 0 && buffer.trim()) {
tags.push(buffer.trim());
buffer = '';
}
parenLevel++;
buffer += c;
} else if (c === ')' && parenLevel > 0) {
buffer += c;
parenLevel--;
// Only treat as end of group if this was a grouping parenthesis
const nextChar = i < tagString.length - 1 ? tagString[i + 1] : ' ';
const isGroupingParen = nextChar === ' ' || i === tagString.length - 1;
if (parenLevel === 0 && isGroupingParen) {
tags.push(buffer.trim());
buffer = '';
}
} else if (c === ' ' && parenLevel === 0) {
if (buffer.trim()) {
tags.push(buffer.trim());
buffer = '';
}
} else {
buffer += c;
}
}
if (buffer.trim()) tags.push(buffer.trim());
// Convert old sort syntax to new unified syntax for e621/danbooru
if (isDanbooru || isE621) {
const convertedTags = tags.map(tag => {
// Convert sort:updated_at:desc to order:updated_at
if (tag.startsWith('sort:') && tag.includes(':')) {
const parts = tag.split(':');
if (parts.length === 3) {
const [, field, direction] = parts;
const converted = direction === 'asc' ? `order:${field}_asc` : `order:${field}`;
return converted;
}
}
return tag;
});
return convertedTags.filter(Boolean);
}
return tags.filter(Boolean);
}
function renderTags(tagList) {
// Always get from window to avoid ReferenceError
const metatagList = window.r34_metatagList;
const metatagRowWrap = window.r34_metatagRowWrap;
const includeList = window.r34_includeList;
const excludeList = window.r34_excludeList;
const includeRowWrap = window.r34_includeRowWrap;
const excludeRowWrap = window.r34_excludeRowWrap;
// Defensive: ensure tags are unique before rendering
const uniqueTags = Array.from(new Set(tags));
// Use site-specific metatag regex or fallback to default
const metatagRegex = siteConfig.metatagRegex || /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/;
// --- Render include/exclude rows ---
if (includeList) includeList.innerHTML = '';
if (excludeList) excludeList.innerHTML = '';
let hasIncludeTags = false;
let hasExcludeTags = false;
uniqueTags.forEach((tag, idx) => {
if (metatagRegex.test(tag)) return; // skip metatags
if (tag.startsWith('-')) {
hasExcludeTags = true;
// Exclude tag
if (excludeList && settings.showIncludeExclude) {
const tagEl = document.createElement('span');
tagEl.className = 'r34-tag-item r34-exclude-item';
tagEl.textContent = tag;
const removeBtn = document.createElement('span');
removeBtn.className = 'r34-remove-tag';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
const tagIdx = tags.indexOf(tag);
if (tagIdx !== -1) {
tags.splice(tagIdx, 1);
renderTags(tagList);
}
};
tagEl.appendChild(removeBtn);
excludeList.appendChild(tagEl);
}
} else {
hasIncludeTags = true;
// Include tag
if (includeList && settings.showIncludeExclude) {
const tagEl = document.createElement('span');
tagEl.className = 'r34-tag-item r34-include-item';
tagEl.textContent = tag;
const removeBtn = document.createElement('span');
removeBtn.className = 'r34-remove-tag';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
const tagIdx = tags.indexOf(tag);
if (tagIdx !== -1) {
tags.splice(tagIdx, 1);
renderTags(tagList);
}
};
tagEl.appendChild(removeBtn);
includeList.appendChild(tagEl);
}
}
});
// Show/hide include/exclude rows based on content and settings
if (includeRowWrap) {
includeRowWrap.style.display = hasIncludeTags ? '' : 'none';
}
if (excludeRowWrap) {
excludeRowWrap.style.display = hasExcludeTags ? '' : 'none';
}
// --- Render metatag row ---
if (metatagList && metatagRowWrap) {
metatagList.innerHTML = '';
const metatags = uniqueTags.filter(tag => metatagRegex.test(tag));
if (metatags.length > 0 && settings.showMetatags) {
metatagRowWrap.style.display = '';
metatags.forEach((tag, idx) => {
const tagEl = document.createElement('span');
tagEl.className = 'r34-tag-item r34-metatag-item';
tagEl.textContent = tag;
const removeBtn = document.createElement('span');
removeBtn.className = 'r34-remove-tag';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
// Remove from tags and update UI controls as before
const tagIdx = tags.indexOf(tag);
if (tagIdx !== -1) {
tags.splice(tagIdx, 1);
renderTags(tagList);
}
};
tagEl.appendChild(removeBtn);
metatagList.appendChild(tagEl);
});
} else {
metatagRowWrap.style.display = 'none';
}
}
// --- Bi-directional sync: update UI controls from metatags ---
// Find sort and rating metatags
let sortType = '';
let sortOrder = 'desc';
let foundSort = false;
let ratingsSet = new Set();
uniqueTags.forEach(tag => {
// Handle unified order: format for e621/danbooru and sort: format for rule34
let sortMatch;
if (site === 'e621' || site === 'danbooru') {
// Unified e621/danbooru format: order:score or order:score_asc
sortMatch = tag.match(/^order:([a-z_]+)(_asc)?$/);
if (sortMatch) {
sortType = sortMatch[1];
sortOrder = sortMatch[2] ? 'asc' : 'desc';
foundSort = true;
}
} else {
// rule34 format: sort:score:desc or sort:score:asc
sortMatch = tag.match(/^sort:([a-z_]+):(asc|desc)$/);
if (sortMatch) {
sortType = sortMatch[1];
sortOrder = sortMatch[2];
foundSort = true;
}
}
// rating:<value>
const ratingMatch = tag.match(/^rating:(safe|questionable|explicit)$/);
if (ratingMatch) {
ratingsSet.add(ratingMatch[1]);
}
// ( rating:type ~ rating:type )
const ratingOrMatch = tag.match(/^\(\s*([^)]+)\s*\)$/);
if (ratingOrMatch) {
// Split by ~ and extract rating values
const parts = ratingOrMatch[1].split('~').map(s => s.trim());
parts.forEach(part => {
const m = part.match(/^rating:(safe|questionable|explicit)$/);
if (m) ratingsSet.add(m[1]);
});
}
});
// Update sort dropdown
if (typeof sortSelect !== 'undefined') {
sortSelect.value = foundSort ? sortType : '';
// Show/hide order switch
if (foundSort) {
orderSwitch.style.display = '';
orderSwitch.dataset.state = sortOrder;
orderSwitch.textContent = sortOrder === 'asc' ? 'Order: Ascend \u2191' : 'Order: Descend \u2193';
} else {
orderSwitch.style.display = 'none';
}
}
// Update rating checkboxes
if (typeof sortRow !== 'undefined') {
sortRow.querySelectorAll('.r34-rating-checkbox').forEach(cb => {
cb.checked = ratingsSet.has(cb.value);
});
}
// --- Render only non-metatag tag pills in the main tag list ---
const allTagsRowWrap = window.r34_allTagsRowWrap;
const allTagsList = window.r34_allTagsList;
if (allTagsList && allTagsRowWrap) {
allTagsList.innerHTML = '';
const mainTags = uniqueTags.filter(tag => !metatagRegex.test(tag));
if (mainTags.length > 0 && settings.showAllTags) {
allTagsRowWrap.style.display = '';
mainTags.forEach((tag, idx) => {
const tagEl = document.createElement('span');
tagEl.className = 'r34-tag-item';
tagEl.textContent = tag;
const removeBtn = document.createElement('span');
removeBtn.className = 'r34-remove-tag';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
// --- Bi-directional sync: update UI controls when metatag pill is removed ---
// If removing a sort or rating metatag, update UI controls
const isSortTag = (site === 'e621' || site === 'danbooru') ? /^order:[a-z_]+(_asc)?$/.test(tag) : /^sort:[a-z_]+:(asc|desc)$/.test(tag);
if (isSortTag) {
if (typeof sortSelect !== 'undefined') {
sortSelect.value = '';
orderSwitch.style.display = 'none';
}
}
if (/^rating:(safe|questionable|explicit)$/.test(tag)) {
if (typeof sortRow !== 'undefined') {
sortRow.querySelectorAll('.r34-rating-checkbox').forEach(cb => {
if (cb.value === tag.split(':')[1]) cb.checked = false;
});
}
}
const tagIdx = tags.indexOf(tag);
if (tagIdx !== -1) {
tags.splice(tagIdx, 1);
renderTags(tagList);
}
};
tagEl.appendChild(removeBtn);
allTagsList.appendChild(tagEl);
});
} else {
allTagsRowWrap.style.display = 'none';
}
}
updateTagColors();
}
// --- Modal Creation ---
function createModal(id, title, contentNode, actions) {
// Remove any existing modal with this id
const old = document.getElementById(id);
if (old) old.remove();
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = id;
modal.tabIndex = -1;
modal.style.display = 'none';
// Modal overlay
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
// Close (×) button
const closeX = document.createElement('button');
closeX.className = 'modal-close';
closeX.type = 'button';
closeX.innerHTML = '×';
closeX.title = 'Close';
closeX.onclick = () => { modal.style.display = 'none'; };
modalContent.appendChild(closeX);
// Title
const h3 = document.createElement('h3');
h3.textContent = title;
modalContent.appendChild(h3);
modalContent.appendChild(contentNode);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'modal-actions';
actions.forEach(btn => actionsDiv.appendChild(btn));
modalContent.appendChild(actionsDiv);
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Modal close on Esc/click outside
function closeModal() { modal.style.display = 'none'; }
modal.addEventListener('mousedown', e => { if (e.target === modal) closeModal(); });
modal.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
return { modal, closeModal };
}
// --- Export Modal ---
function showExportModal() {
// --- Export Data Preparation ---
const website = location.hostname;
const timestamp = new Date().toISOString();
const rawTags = tags.join(' ');
const jsonExport = JSON.stringify({ website, tags: [...tags], timestamp }, null, 2);
// --- Modal Elements ---
const tabWrap = document.createElement('div');
tabWrap.className = 'modal-tabs';
const rawTab = document.createElement('button');
rawTab.textContent = 'Raw';
rawTab.type = 'button';
rawTab.className = 'modal-tab';
const jsonTab = document.createElement('button');
jsonTab.textContent = 'JSON';
jsonTab.type = 'button';
jsonTab.className = 'modal-tab';
tabWrap.appendChild(rawTab);
tabWrap.appendChild(jsonTab);
const textarea = document.createElement('textarea');
textarea.readOnly = true;
textarea.className = 'modal-pastebin';
textarea.value = rawTags;
// --- Action Buttons ---
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy';
copyBtn.type = 'button';
copyBtn.onclick = function () {
textarea.select();
document.execCommand('copy');
};
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export to file';
exportBtn.type = 'button';
exportBtn.style.display = 'none';
exportBtn.onclick = function () {
const blob = new Blob([jsonExport], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'booru-tags.json';
document.body.appendChild(a);
a.click();
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
};
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.type = 'button';
// --- Tab Switch Logic ---
function showRaw() {
textarea.value = rawTags;
copyBtn.style.display = '';
exportBtn.style.display = 'none';
rawTab.classList.add('active');
jsonTab.classList.remove('active');
}
function showJSON() {
textarea.value = jsonExport;
copyBtn.style.display = 'none';
exportBtn.style.display = '';
rawTab.classList.remove('active');
jsonTab.classList.add('active');
}
rawTab.onclick = showRaw;
jsonTab.onclick = showJSON;
// --- Modal Content (Refactored) ---
const formWrap = document.createElement('div');
formWrap.className = 'modal-form';
formWrap.appendChild(tabWrap);
formWrap.appendChild(textarea);
// The modal-actions div is handled by createModal, so do not add buttons here
// --- Modal Actions ---
const actions = [copyBtn, exportBtn, closeBtn];
let modalObj = createModal('modal-export', 'Export Tags', formWrap, actions);
closeBtn.onclick = modalObj.closeModal;
modalObj.modal.style.display = 'flex';
modalObj.modal.focus();
// Default to raw view
showRaw();
}
// --- Settings Modal ---
function showSettingsModal() {
const formWrap = document.createElement('div');
formWrap.className = 'modal-form settings-form';
// Include/Exclude toggle
const includeExcludeRow = document.createElement('div');
includeExcludeRow.className = 'settings-row';
const includeExcludeLabel = document.createElement('label');
includeExcludeLabel.className = 'settings-label';
const includeExcludeCheckbox = document.createElement('input');
includeExcludeCheckbox.type = 'checkbox';
includeExcludeCheckbox.checked = settings.showIncludeExclude;
includeExcludeLabel.appendChild(includeExcludeCheckbox);
includeExcludeLabel.appendChild(document.createTextNode(' Show Include/Exclude Tags'));
includeExcludeRow.appendChild(includeExcludeLabel);
// Metatags toggle
const metatagsRow = document.createElement('div');
metatagsRow.className = 'settings-row';
const metatagsLabel = document.createElement('label');
metatagsLabel.className = 'settings-label';
const metatagsCheckbox = document.createElement('input');
metatagsCheckbox.type = 'checkbox';
metatagsCheckbox.checked = settings.showMetatags;
metatagsLabel.appendChild(metatagsCheckbox);
metatagsLabel.appendChild(document.createTextNode(' Show Metatags'));
metatagsRow.appendChild(metatagsLabel);
// All Tags toggle
const allTagsRow = document.createElement('div');
allTagsRow.className = 'settings-row';
const allTagsLabel = document.createElement('label');
allTagsLabel.className = 'settings-label';
const allTagsCheckbox = document.createElement('input');
allTagsCheckbox.type = 'checkbox';
allTagsCheckbox.checked = settings.showAllTags;
allTagsLabel.appendChild(allTagsCheckbox);
allTagsLabel.appendChild(document.createTextNode(' Show All Tags'));
allTagsRow.appendChild(allTagsLabel);
formWrap.appendChild(includeExcludeRow);
formWrap.appendChild(metatagsRow);
formWrap.appendChild(allTagsRow);
// Action buttons
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.type = 'button';
saveBtn.onclick = function () {
settings.showIncludeExclude = includeExcludeCheckbox.checked;
settings.showMetatags = metatagsCheckbox.checked;
settings.showAllTags = allTagsCheckbox.checked;
saveSettings();
renderTags(window.r34_tagList);
modalObj.closeModal();
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.type = 'button';
const actions = [saveBtn, cancelBtn];
let modalObj = createModal('modal-settings', 'Settings', formWrap, actions);
cancelBtn.onclick = modalObj.closeModal;
modalObj.modal.style.display = 'flex';
modalObj.modal.focus();
}
// --- Dynamic Cheat Sheet Cache ---
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
async function fetchCheatSheet(url) {
try {
const response = await fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract cheat sheet content based on site
let content = '';
if (isE621) {
const cheatsheetSection = doc.querySelector('#c-help #a-show .styled-dtext');
if (cheatsheetSection) {
// Clean up the content and convert to our format
content = extractE621CheatSheet(cheatsheetSection);
}
} else if (isRule34) {
const cheatsheetSection = doc.querySelector('.content');
if (cheatsheetSection) {
content = extractRule34CheatSheet(cheatsheetSection);
}
} else if (isDanbooru) {
const cheatsheetSection = doc.querySelector('#c-wiki-pages #a-show .prose, .wiki-page-body, .dtext-container');
if (cheatsheetSection) {
content = extractDanbooruCheatSheet(cheatsheetSection);
}
}
return content;
} catch (error) {
console.error('Failed to fetch cheat sheet:', error);
return null;
}
}
function extractE621CheatSheet(element) {
// First, convert HTML to structured JSON data
const cheatSheetData = parseCheatSheetToJSON(element, 'e621');
// Then convert JSON to standardized HTML format
const html = formatCheatSheetFromJSON(cheatSheetData);
return html;
}
function extractRule34CheatSheet(element) {
// First, convert HTML to structured JSON data
const cheatSheetData = parseCheatSheetToJSON(element, 'rule34');
// Then convert JSON to standardized HTML format
const html = formatCheatSheetFromJSON(cheatSheetData);
return html;
}
function extractDanbooruCheatSheet(element) {
// First, convert HTML to structured JSON data
const cheatSheetData = parseCheatSheetToJSON(element, 'danbooru');
// Then convert JSON to standardized HTML format
const html = formatCheatSheetFromJSON(cheatSheetData);
return html;
}
function parseCheatSheetToJSON(element, site) {
// Clone the element to avoid modifying the original
const clonedElement = element.cloneNode(true);
// Remove Table of Contents sections
const tocElements = clonedElement.querySelectorAll('h1, h2, h3, h4, h5, h6');
tocElements.forEach(heading => {
const headingText = heading.textContent.toLowerCase();
if (headingText.includes('table of contents') || headingText.includes('contents')) {
let nextElement = heading.nextElementSibling;
heading.remove();
while (nextElement && !/^H[1-6]$/.test(nextElement.tagName)) {
const toRemove = nextElement;
nextElement = nextElement.nextElementSibling;
toRemove.remove();
}
}
});
// Remove TOC lists
const tocLists = clonedElement.querySelectorAll('ul, ol');
tocLists.forEach(list => {
const listText = list.textContent.toLowerCase();
const hasMultipleSections = (listText.match(/\d+\./g) || []).length > 3;
const hasTocKeywords = listText.includes('searching') && listText.includes('metatags');
if (hasMultipleSections && hasTocKeywords) {
list.remove();
}
});
const cheatSheetData = {
site: site,
sections: []
};
const sections = clonedElement.querySelectorAll('h1, h2, h3, h4, h5, h6, p, ul, table, div');
let currentSection = null;
let skipSection = false;
let orphanedEntries = []; // For content without clear section headers
sections.forEach(el => {
if (/^H[1-6]$/.test(el.tagName)) {
const headingText = el.textContent.trim();
if (headingText.toLowerCase().includes('table of contents') || headingText.toLowerCase().includes('contents')) {
skipSection = true;
return;
} else {
skipSection = false;
}
// If we have orphaned entries, create a default section for them
if (orphanedEntries.length > 0 && !currentSection) {
currentSection = {
title: 'Search Help',
entries: orphanedEntries
};
cheatSheetData.sections.push(currentSection);
orphanedEntries = [];
}
currentSection = {
title: headingText,
entries: []
};
cheatSheetData.sections.push(currentSection);
} else if (!skipSection) {
const entries = [];
if (el.tagName === 'P') {
const text = el.textContent.trim();
if (text && !text.toLowerCase().includes('table of contents')) {
const entry = parseTextEntry(text);
if (entry) {
entries.push(entry);
}
}
} else if (el.tagName === 'UL') {
el.querySelectorAll('li').forEach(li => {
const text = li.textContent.trim();
if (text && text.length > 3 && !text.toLowerCase().includes('table of contents')) {
const entry = parseTextEntry(text);
if (entry) {
entries.push(entry);
}
}
});
} else if (el.tagName === 'TABLE') {
el.querySelectorAll('tbody tr, tr').forEach(row => {
const cells = row.querySelectorAll('td, th');
if (cells.length >= 2) {
const code = cells[0].textContent.trim();
const desc = cells[1].textContent.trim();
if (code && desc && !code.includes('Example') && !code.includes('Description')) {
const entry = {
type: 'definition',
code: code,
description: desc
};
entries.push(entry);
}
}
});
} else if (el.tagName === 'DIV' && site === 'rule34') {
// Special handling for Rule34's div-based content
const text = el.textContent.trim();
if (text && text.length > 10) {
// Split by line breaks and process each line
const lines = text.split('\n').filter(line => line.trim().length > 3);
lines.forEach(line => {
const entry = parseTextEntry(line.trim());
if (entry) {
entries.push(entry);
}
});
}
}
// Add entries to current section or orphaned list
if (currentSection) {
currentSection.entries.push(...entries);
} else {
orphanedEntries.push(...entries);
}
}
});
// Handle any remaining orphaned entries
if (orphanedEntries.length > 0) {
const defaultSection = {
title: 'Search Help',
entries: orphanedEntries
};
cheatSheetData.sections.push(defaultSection);
}
return cheatSheetData;
}
function parseTextEntry(text) {
// Try to parse different formats of text entries
if (text.includes('—')) {
const parts = text.split('—').map(s => s.trim());
if (parts.length >= 2 && parts[0] && parts[1]) {
return {
type: 'definition',
code: parts[0].replace(/^[•\s]+/, ''),
description: parts.slice(1).join(' — ')
};
}
}
// Check for colon-separated format (common in metatags)
if (text.includes(':') && !text.startsWith('http')) {
const colonIndex = text.indexOf(':');
const beforeColon = text.substring(0, colonIndex).trim();
const afterColon = text.substring(colonIndex + 1).trim();
// Only treat as code:description if the part before colon looks like a tag/command
if (beforeColon.length > 0 && beforeColon.length < 30 && !beforeColon.includes(' ')) {
return {
type: 'definition',
code: beforeColon,
description: afterColon || 'No description available'
};
}
}
// Check for parenthetical descriptions
const parenMatch = text.match(/^([^(]+)\s*\(([^)]+)\)/);
if (parenMatch) {
return {
type: 'definition',
code: parenMatch[1].trim(),
description: parenMatch[2].trim()
};
}
// Default to description-only entry
return {
type: 'description',
description: text
};
}
function formatCheatSheetFromJSON(cheatSheetData) {
let html = '<div class="dynamic-cheatsheet">';
cheatSheetData.sections.forEach(section => {
if (section.entries.length > 0) {
html += `<section><h4>${section.title}</h4><ul>`;
section.entries.forEach(entry => {
if (entry.type === 'definition' && entry.code) {
html += `<li><code>${entry.code}</code> — ${entry.description}</li>`;
} else {
html += `<li>${entry.description}</li>`;
}
});
html += '</ul></section>';
}
});
html += '</div>';
return html;
}
async function getCachedCheatSheet(site) {
const cacheKey = `booru-cheatsheet-${site}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
const now = new Date().getTime();
const age = now - data.timestamp;
// Check if cache is still valid
if (data.timestamp && (now - data.timestamp) < CACHE_DURATION) {
return data.content;
}
} catch (e) {
console.error('Failed to parse cached cheat sheet:', e);
}
}
// Fetch fresh data
let url = '';
if (site === 'e621') {
url = 'https://e621.net/help/cheatsheet';
} else if (site === 'rule34') {
url = 'https://rule34.xxx/index.php?page=help&topic=cheatsheet';
} else if (site === 'danbooru') {
url = 'https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet';
}
if (url) {
const content = await fetchCheatSheet(url);
if (content) {
// Cache the result
const cacheData = {
content: content,
timestamp: new Date().getTime(),
url: url
};
try {
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
} catch (e) {
console.error('Failed to cache cheat sheet:', e);
}
return content;
}
}
// Fallback to static content
return null;
}
function clearCheatSheetCache() {
const sites = ['e621', 'rule34', 'danbooru'];
sites.forEach(site => {
const cacheKey = `booru-cheatsheet-${site}`;
localStorage.removeItem(cacheKey);
});
}
// --- Cheat Sheet Modal ---
async function showCheatSheetModal() {
// Show loading state
const loadingDiv = document.createElement('div');
loadingDiv.className = 'modal-doc';
loadingDiv.innerHTML = '<p style="text-align: center;">Loading cheat sheet...</p>';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.type = 'button';
let modalObj = createModal('modal-cheat', 'Cheat Sheet', loadingDiv, [closeBtn]);
closeBtn.onclick = modalObj.closeModal;
modalObj.modal.style.display = 'flex';
modalObj.modal.focus();
// Determine which site we're on
let site = '';
if (isE621) site = 'e621';
else if (isRule34) site = 'rule34';
else if (isDanbooru) site = 'danbooru';
// Try to get dynamic content
let cheatSheetContent = null;
if (site) {
cheatSheetContent = await getCachedCheatSheet(site);
}
// If dynamic fetch failed, use static content
if (!cheatSheetContent) {
cheatSheetContent = siteConfig.cheatSheetContent || `
<section><h4>Basic Search</h4>
<ul>
<li><code>tag1 tag2</code> — Posts with <b>tag1</b> and <b>tag2</b></li>
<li><code>( tag1 ~ tag2 )</code> — Posts with <b>tag1</b> or <b>tag2</b></li>
<li><code>night~</code> — Fuzzy search for <b>night</b> (e.g. <b>night</b>, <b>fight</b>, <b>bright</b>)</li>
<li><code>-tag1</code> — Exclude posts with <b>tag1</b></li>
<li><code>ta*1</code> — Tags starting with <b>ta</b> and ending with <b>1</b></li>
</ul>
</section>
<section><h4>Metatags</h4>
<ul>
<li><code>user:bob</code> — Uploaded by user <b>bob</b></li>
<li><code>md5:foo</code> — Posts with MD5 <b>foo</b></li>
<li><code>md5:foo*</code> — MD5 starts with <b>foo</b></li>
<li><code>rating:questionable</code> — Rated <b>questionable</b></li>
<li><code>-rating:questionable</code> — Not rated <b>questionable</b></li>
<li><code>parent:1234</code> — Has parent <b>1234</b> (includes 1234)</li>
<li><code>width:>=1000 height:>1000</code> — Width ≥ 1000, Height > 1000</li>
<li><code>score:>=10</code> — Score ≥ 10</li>
</ul>
</section>
<section><h4>Sorting</h4>
<ul>
<li><code>sort:updated:desc</code> — Sort by <b>updated</b> (descending)</li>
<li>Other sortable types: <code>id</code>, <code>score</code>, <code>rating</code>, <code>user</code>, <code>height</code>, <code>width</code>, <code>parent</code>, <code>source</code>, <code>updated</code></li>
<li>Can be sorted by both <b>asc</b> or <b>desc</b></li>
</ul>
</section>
<section><h4>Notes</h4>
<ul>
<li>Combining the same metatags (with colons) usually does not work.</li>
<li>You can combine different metatags (e.g. <code>rating:questionable parent:100</code>).</li>
</ul>
</section>
`;
}
// Update modal content
const docWrap = document.createElement('div');
docWrap.className = 'modal-doc';
docWrap.innerHTML = cheatSheetContent;
// Add reference link and cache info
const infoBar = document.createElement('div');
infoBar.className = 'cheatsheet-info-bar';
infoBar.style.cssText = 'margin-top: 16px; padding: 12px; background: #f0f4fa; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;';
const refLink = document.createElement('a');
refLink.href = site === 'e621' ? 'https://e621.net/help/cheatsheet' :
site === 'rule34' ? 'https://rule34.xxx/index.php?page=help&topic=cheatsheet' :
site === 'danbooru' ? 'https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet' : '#';
refLink.target = '_blank';
refLink.textContent = 'View on site →';
refLink.style.cssText = 'color: #4a90e2; text-decoration: none; font-weight: 500;';
refLink.onmouseover = () => { refLink.style.textDecoration = 'underline'; };
refLink.onmouseout = () => { refLink.style.textDecoration = 'none'; };
const cacheInfo = document.createElement('span');
cacheInfo.style.cssText = 'font-size: 0.9em; color: #666;';
// Check cache status
const cacheKey = `booru-cheatsheet-${site}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
const age = new Date().getTime() - data.timestamp;
const days = Math.floor(age / (24 * 60 * 60 * 1000));
const hours = Math.floor((age % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
cacheInfo.textContent = `Cached: ${days}d ${hours}h ago`;
} catch (e) {
cacheInfo.textContent = 'Using static content';
}
} else {
cacheInfo.textContent = 'Using static content';
}
const flushBtn = document.createElement('button');
flushBtn.textContent = 'Refresh Cache';
flushBtn.type = 'button';
flushBtn.style.cssText = 'border-radius: 12px; padding: 6px 16px; font-size: 0.9em; border: 1.5px solid #b0d0b0; background: #f8fff8; cursor: pointer; transition: all 0.2s;';
flushBtn.onmouseover = () => { flushBtn.style.borderColor = '#4a90e2'; flushBtn.style.background = '#e0f7fa'; };
flushBtn.onmouseout = () => { flushBtn.style.borderColor = '#b0d0b0'; flushBtn.style.background = '#f8fff8'; };
flushBtn.onclick = async () => {
clearCheatSheetCache();
modalObj.closeModal();
await showCheatSheetModal(); // Reload the modal
};
infoBar.appendChild(refLink);
infoBar.appendChild(cacheInfo);
infoBar.appendChild(flushBtn);
docWrap.appendChild(infoBar);
// Update modal with new content
const modalContent = modalObj.modal.querySelector('.modal-content');
const oldDoc = modalContent.querySelector('.modal-doc');
if (oldDoc) {
oldDoc.replaceWith(docWrap);
}
}
// --- Search Bar Creation ---
function createSearchSection(site) {
const centerWrap = document.createElement('div');
centerWrap.className = 'r34-center-wrap';
const searchForm = document.createElement('form');
searchForm.className = 'r34-search-form';
searchForm.method = 'GET';
searchForm.action = '';
const searchBarContainer = document.createElement('div');
searchBarContainer.className = 'r34-modern-searchbar';
let searchInput;
if (site === 'e621') {
searchInput = document.createElement('textarea');
searchInput.rows = 1;
searchInput.id = 'tags';
searchInput.name = 'tags';
searchInput.setAttribute('data-autocomplete', 'tag-query');
searchInput.placeholder = siteConfig.placeholder || 'Enter tags...';
searchInput.className = 'r34-search-input';
} else {
searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.name = 'tags';
searchInput.placeholder = siteConfig.placeholder || 'Enter tags...';
searchInput.className = 'r34-search-input';
}
// --- Searchbar Buttons ---
const exportBtn = document.createElement('button');
exportBtn.type = 'button';
exportBtn.textContent = 'Export';
exportBtn.className = 'r34-export-btn';
exportBtn.title = 'Export tags';
exportBtn.onclick = showExportModal;
const cheatBtn = document.createElement('button');
cheatBtn.type = 'button';
cheatBtn.textContent = '?';
cheatBtn.className = 'r34-cheat-btn';
cheatBtn.title = 'Show cheat sheet';
cheatBtn.onclick = showCheatSheetModal;
const settingsBtn = document.createElement('button');
settingsBtn.type = 'button';
settingsBtn.textContent = '⚙';
settingsBtn.className = 'r34-settings-btn';
settingsBtn.title = 'Settings';
settingsBtn.onclick = showSettingsModal;
const searchButton = document.createElement('button');
searchButton.type = 'submit';
searchButton.textContent = 'Search';
searchButton.className = 'r34-search-button';
// --- New Sort/Order/Ratings Row ---
const sortRow = document.createElement('div');
sortRow.className = 'r34-sort-row';
// Sort dropdown
const sortSelect = document.createElement('select');
sortSelect.className = 'r34-sort-select';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = 'Sort';
sortSelect.appendChild(emptyOption);
// Use site-specific sort options or fallback to defaults
const sortOptions = siteConfig.sortOptions || [
{ value: 'score', label: 'Score' },
{ value: 'updated', label: 'Updated' },
{ value: 'id', label: 'ID' },
{ value: 'rating', label: 'Rating' },
{ value: 'user', label: 'User' },
{ value: 'height', label: 'Height' },
{ value: 'width', label: 'Width' },
{ value: 'parent', label: 'Parent' },
{ value: 'source', label: 'Source' }
];
sortOptions.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
sortSelect.appendChild(option);
});
sortRow.appendChild(sortSelect);
// Order switch
const orderSwitch = document.createElement('button');
orderSwitch.type = 'button';
orderSwitch.className = 'r34-order-switch';
orderSwitch.dataset.state = 'desc'; // default to desc
orderSwitch.textContent = 'Order: Descend \u2193';
orderSwitch.style.display = 'none'; // hidden by default
orderSwitch.onclick = function () {
// Cycle through: desc -> asc -> desc
const state = orderSwitch.dataset.state;
if (state === 'desc') {
orderSwitch.dataset.state = 'asc';
orderSwitch.textContent = 'Order: Ascend \u2191';
} else {
orderSwitch.dataset.state = 'desc';
orderSwitch.textContent = 'Order: Descend \u2193';
}
};
sortRow.appendChild(orderSwitch);
// Show/hide order switch based on sort selection
sortSelect.addEventListener('change', function () {
if (sortSelect.value) {
orderSwitch.style.display = '';
} else {
orderSwitch.style.display = 'none';
}
});
// Ratings checkboxes
const ratings = siteConfig.ratings || [
{ value: 'safe', label: 'Safe' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
];
ratings.forEach(r => {
const label = document.createElement('label');
label.className = 'r34-rating-label';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = r.value;
checkbox.className = 'r34-rating-checkbox';
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + r.label));
sortRow.appendChild(label);
});
const tagList = document.createElement('div');
tagList.className = 'r34-tag-list';
// --- Include/Exclude Tag Rows ---
// Include row
const includeRowWrap = document.createElement('div');
includeRowWrap.className = 'r34-include-row-wrap';
const includeRowHeader = document.createElement('div');
includeRowHeader.className = 'r34-include-row-header';
const includeRowTitle = document.createElement('span');
includeRowTitle.textContent = 'Include Tags';
const includeToggle = document.createElement('button');
includeToggle.type = 'button';
includeToggle.className = 'r34-include-toggle';
includeToggle.textContent = 'See less';
includeRowHeader.appendChild(includeRowTitle);
includeRowHeader.appendChild(includeToggle);
const includeList = document.createElement('div');
includeList.className = 'r34-include-list';
includeRowWrap.appendChild(includeRowHeader);
includeRowWrap.appendChild(includeList);
let includeExpanded = true;
includeList.style.display = '';
includeToggle.onclick = function () {
includeExpanded = !includeExpanded;
includeList.style.display = includeExpanded ? '' : 'none';
includeToggle.textContent = includeExpanded ? 'See less' : 'See more';
};
// Exclude row
const excludeRowWrap = document.createElement('div');
excludeRowWrap.className = 'r34-exclude-row-wrap';
const excludeRowHeader = document.createElement('div');
excludeRowHeader.className = 'r34-exclude-row-header';
const excludeRowTitle = document.createElement('span');
excludeRowTitle.textContent = 'Exclude Tags';
const excludeToggle = document.createElement('button');
excludeToggle.type = 'button';
excludeToggle.className = 'r34-exclude-toggle';
excludeToggle.textContent = 'See less';
excludeRowHeader.appendChild(excludeRowTitle);
excludeRowHeader.appendChild(excludeToggle);
const excludeList = document.createElement('div');
excludeList.className = 'r34-exclude-list';
excludeRowWrap.appendChild(excludeRowHeader);
excludeRowWrap.appendChild(excludeList);
let excludeExpanded = true;
excludeList.style.display = '';
excludeToggle.onclick = function () {
excludeExpanded = !excludeExpanded;
excludeList.style.display = excludeExpanded ? '' : 'none';
excludeToggle.textContent = excludeExpanded ? 'See less' : 'See more';
};
// --- Metatag Row ---
const metatagRowWrap = document.createElement('div');
metatagRowWrap.className = 'r34-metatag-row-wrap';
const metatagRowHeader = document.createElement('div');
metatagRowHeader.className = 'r34-metatag-row-header';
const metatagRowTitle = document.createElement('span');
metatagRowTitle.textContent = 'Metatags';
const metatagToggle = document.createElement('button');
metatagToggle.type = 'button';
metatagToggle.className = 'r34-metatag-toggle';
metatagToggle.textContent = 'See more';
metatagRowHeader.appendChild(metatagRowTitle);
metatagRowHeader.appendChild(metatagToggle);
const metatagList = document.createElement('div');
metatagList.className = 'r34-metatag-list';
metatagRowWrap.appendChild(metatagRowHeader);
metatagRowWrap.appendChild(metatagList);
let metatagExpanded = false;
metatagList.style.display = 'none';
metatagToggle.onclick = function () {
metatagExpanded = !metatagExpanded;
metatagList.style.display = metatagExpanded ? '' : 'none';
metatagToggle.textContent = metatagExpanded ? 'See less' : 'See more';
};
// --- All Tags Row ---
const allTagsRowWrap = document.createElement('div');
allTagsRowWrap.className = 'r34-all-tags-row-wrap';
const allTagsRowHeader = document.createElement('div');
allTagsRowHeader.className = 'r34-all-tags-row-header';
const allTagsRowTitle = document.createElement('span');
allTagsRowTitle.textContent = 'All Tags';
const allTagsToggle = document.createElement('button');
allTagsToggle.type = 'button';
allTagsToggle.className = 'r34-all-tags-toggle';
allTagsToggle.textContent = 'See less';
allTagsRowHeader.appendChild(allTagsRowTitle);
allTagsRowHeader.appendChild(allTagsToggle);
const allTagsList = document.createElement('div');
allTagsList.className = 'r34-all-tags-list';
allTagsRowWrap.appendChild(allTagsRowHeader);
allTagsRowWrap.appendChild(allTagsList);
let allTagsExpanded = true;
allTagsList.style.display = '';
allTagsToggle.onclick = function () {
allTagsExpanded = !allTagsExpanded;
allTagsList.style.display = allTagsExpanded ? '' : 'none';
allTagsToggle.textContent = allTagsExpanded ? 'See less' : 'See more';
};
// Track if user is navigating autocomplete with arrow keys
let autocompleteNavigating = false;
// --- Event Bindings ---
function bindInputEvents() {
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
autocompleteNavigating = true;
}
});
searchInput.addEventListener('input', function (e) {
autocompleteNavigating = false;
let value = searchInput.value;
// Only split on comma or space if not inside parentheses
const endsWithSeparator = /[ ,]$/.test(value);
if (endsWithSeparator) {
// Parse the input to check if we're inside a parenthetical group
// This handles both grouping parentheses like "( rating:safe ~ rating:questionable )"
// and tag name parentheses like "silvia_(artist)"
let parenLevel = 0;
let inGroupParens = false;
let lastChar = '';
for (let i = 0; i < value.length - 1; i++) { // -1 to exclude the trailing separator
const char = value[i];
if (char === '(' && (i === 0 || lastChar === ' ')) {
// Opening paren at start or after space - likely a grouping paren
parenLevel++;
inGroupParens = true;
} else if (char === ')' && parenLevel > 0) {
parenLevel--;
if (parenLevel === 0) {
inGroupParens = false;
}
}
if (char !== ' ') lastChar = char;
}
// Only split if we're not inside grouping parentheses
if (parenLevel === 0 && !inGroupParens) {
value = value.replace(/[ ,]+$/, '').trim();
addTag(value);
searchInput.value = '';
}
}
});
// Only submit form on Enter if input is empty and there are tags
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
if (autocompleteNavigating) {
// Let autocomplete handle Enter
return;
}
// If input is empty and there are tags, submit
if (searchInput.value.trim() === '' && tags.length > 0) {
e.preventDefault();
searchInput.value = tags.join(' ');
searchForm.submit();
} else if (searchInput.value.trim() !== '') {
// No suggestion selected, add raw input as tag
e.preventDefault();
const value = searchInput.value.trim();
addTag(value);
searchInput.value = '';
autocompleteNavigating = false;
// Close/hide autocomplete dropdown
if (site === 'rule34' && searchInput.awesomplete) {
searchInput.awesomplete.close();
}
if (site === 'e621') {
const ul = document.querySelector('ul[role="listbox"]');
if (ul) ul.setAttribute('hidden', '');
}
}
}
});
}
function bindFormEvents() {
searchForm.addEventListener('submit', function (e) {
// Prevent default form submission
e.preventDefault();
// --- Inject metatags from UI controls ---
let metatags = [];
// Sort + Order (single metatag)
if (sortSelect.value) {
let order = orderSwitch.dataset.state || 'desc';
if (site === 'e621' || site === 'danbooru') {
// Unified e621/danbooru format: order:score or order:score_asc
if (order === 'asc') {
metatags.push(`order:${sortSelect.value}_asc`);
} else {
metatags.push(`order:${sortSelect.value}`);
}
} else {
// rule34 format: sort:score:desc or sort:score:asc
metatags.push(`sort:${sortSelect.value}:${order}`);
}
}
// Ratings (OR logic)
const checkedRatings = Array.from(sortRow.querySelectorAll('.r34-rating-checkbox:checked')).map(cb => cb.value);
if (checkedRatings.length === 1) {
metatags.push(`rating:${checkedRatings[0]}`);
} else if (checkedRatings.length > 1) {
metatags.push('( ' + checkedRatings.map(r => `rating:${r}`).join(' ~ ') + ' )');
}
// Remove any existing metatags of these types from tags
const metatagPrefixes = (site === 'e621' || site === 'danbooru') ? ['order:', 'rating:', '( rating:'] : ['sort:', 'rating:', '( rating:'];
tags = tags.filter(tag => !metatagPrefixes.some(prefix => tag.startsWith(prefix)));
// Add new metatags
tags = [...tags, ...metatags];
// Update input value for submission
if (tags.length > 0) {
searchInput.value = tags.join(' ');
}
// Actually submit the form with tags
searchForm.submit();
});
}
// --- Awesomplete integration for rule34 ---
function bindAwesompleteEvents() {
if (site === 'rule34') {
searchInput.addEventListener('awesomplete-selectcomplete', function (e) {
const value = searchInput.value.trim();
addTag(value);
searchInput.value = '';
});
}
}
// --- Sync metatags with UI changes ---
function syncMetatagsFromUI() {
// Remove all order: and rating: metatags (including parenthesized OR metatags and single rating: ones)
tags = tags.filter(tag => {
// Use unified order: syntax for both e621 and danbooru
if (tag.startsWith('order:')) return false;
// Keep legacy sort: removal for rule34
if (site === 'rule34' && tag.startsWith('sort:')) return false;
if (/^rating:(safe|questionable|explicit|general|sensitive)$/.test(tag)) return false;
if (/^\(\s*rating:(safe|questionable|explicit|general|sensitive)(\s*~\s*rating:(safe|questionable|explicit|general|sensitive))*\s*\)$/.test(tag)) return false;
return true;
});
// Add sort metatag if selected
if (sortSelect.value) {
let order = orderSwitch.dataset.state || 'desc';
if (site === 'e621' || site === 'danbooru') {
// Unified e621/danbooru format: order:score or order:score_asc
const sortTag = order === 'asc' ? `order:${sortSelect.value}_asc` : `order:${sortSelect.value}`;
tags.push(sortTag);
} else {
// rule34 format: sort:score:desc or sort:score:asc
const sortTag = `sort:${sortSelect.value}:${order}`;
tags.push(sortTag);
}
}
// Add rating metatag(s) with OR logic
const checkedRatings = Array.from(sortRow.querySelectorAll('.r34-rating-checkbox:checked')).map(cb => cb.value);
if (checkedRatings.length === 1) {
tags.push(`rating:${checkedRatings[0]}`);
} else if (checkedRatings.length > 1) {
tags.push('( ' + checkedRatings.map(r => `rating:${r}`).join(' ~ ') + ' )');
}
tags = Array.from(new Set(tags));
renderTags(tagList);
}
sortSelect.addEventListener('change', syncMetatagsFromUI);
orderSwitch.addEventListener('click', syncMetatagsFromUI);
sortRow.querySelectorAll('.r34-rating-checkbox').forEach(cb => {
cb.addEventListener('change', syncMetatagsFromUI);
});
// --- Assemble ---
searchBarContainer.appendChild(exportBtn);
searchBarContainer.appendChild(searchInput);
searchBarContainer.appendChild(cheatBtn);
searchBarContainer.appendChild(settingsBtn);
searchBarContainer.appendChild(searchButton);
searchForm.appendChild(searchBarContainer);
// Insert the new row below the search bar, above the tag list
searchForm.appendChild(sortRow);
centerWrap.appendChild(searchForm);
centerWrap.appendChild(includeRowWrap);
centerWrap.appendChild(excludeRowWrap);
centerWrap.appendChild(metatagRowWrap);
centerWrap.appendChild(allTagsRowWrap);
// --- Bind Events ---
bindInputEvents();
bindFormEvents();
bindAwesompleteEvents();
// Make tagList, metatagList and metatagRowWrap globally accessible for addTag/renderTags
window.r34_tagList = tagList;
window.r34_includeList = includeList;
window.r34_excludeList = excludeList;
window.r34_includeRowWrap = includeRowWrap;
window.r34_excludeRowWrap = excludeRowWrap;
window.r34_metatagList = metatagList;
window.r34_metatagRowWrap = metatagRowWrap;
window.r34_allTagsList = allTagsList;
window.r34_allTagsRowWrap = allTagsRowWrap;
return { centerWrap, searchForm, searchInput, searchButton, tagList, includeList, excludeList, metatagList, metatagRowWrap, allTagsList, allTagsRowWrap };
}
// --- Site-Specific Setup ---
function setupE621() {
const originalForm = document.querySelector('form.post-search-form');
const gallery = document.querySelector('#c-posts');
if (!originalForm || !gallery) return false;
const formAction = originalForm.action;
const formMethod = originalForm.method;
const { centerWrap, searchForm, searchInput, searchButton, tagList, metatagList, metatagRowWrap } = createSearchSection('e621');
searchForm.action = formAction;
searchForm.method = formMethod;
gallery.parentNode.insertBefore(centerWrap, gallery);
originalForm.style.display = 'none';
tags = getTagsFromURL();
renderTags(tagList);
searchInput.value = '';
// Set up theme observer
const observer = new MutationObserver(updateTagColors);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-th-main'] });
return true;
}
function setupRule34() {
const originalForm = document.querySelector('.sidebar .tag-search form');
const gallery = document.querySelector('#post-list');
if (!originalForm || !gallery) return false;
const formAction = originalForm.action;
const formMethod = originalForm.method;
const { centerWrap, searchForm, searchInput, searchButton, tagList, metatagList, metatagRowWrap } = createSearchSection('rule34');
searchForm.action = formAction;
searchForm.method = formMethod;
gallery.parentNode.insertBefore(centerWrap, gallery);
originalForm.style.display = 'none';
tags = getTagsFromURL();
renderTags(tagList);
searchInput.value = '';
return true;
}
function setupDanbooru() {
// Try multiple possible selectors for Danbooru's search form
const originalForm = document.querySelector('form[action*="posts"]') ||
document.querySelector('form.search-form') ||
document.querySelector('#search-form') ||
document.querySelector('form:has(input[name="tags"])');
// Try multiple possible selectors for the posts container
const gallery = document.querySelector('#posts') ||
document.querySelector('.posts') ||
document.querySelector('#post-list') ||
document.querySelector('.post-list') ||
document.querySelector('#content');
if (!originalForm || !gallery) {
console.log('Danbooru setup failed: originalForm=', originalForm, 'gallery=', gallery);
return false;
}
const formAction = originalForm.action || '/posts';
const formMethod = originalForm.method || 'GET';
const { centerWrap, searchForm, searchInput, searchButton, tagList, metatagList, metatagRowWrap } = createSearchSection('danbooru');
searchForm.action = formAction;
searchForm.method = formMethod;
gallery.parentNode.insertBefore(centerWrap, gallery);
originalForm.style.display = 'none';
tags = getTagsFromURL();
renderTags(tagList);
searchInput.value = '';
console.log('Danbooru setup completed successfully');
return true;
}
function setupWiki() {
// For wiki pages, create a simple centered search input
const content = document.querySelector('#content') || document.querySelector('main') || document.body;
if (!content) return false;
// Create simple search form
const searchWrap = document.createElement('div');
searchWrap.className = 'r34-wiki-search-wrap';
searchWrap.style.cssText = `
display: flex;
justify-content: center;
margin: 20px auto;
max-width: 600px;
padding: 0 20px;
`;
const searchForm = document.createElement('form');
searchForm.action = '/posts';
searchForm.method = 'GET';
searchForm.style.cssText = `
display: flex;
gap: 10px;
width: 100%;
align-items: center;
`;
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.name = 'tags';
searchInput.placeholder = 'Search posts...';
searchInput.className = 'r34-search-input';
searchInput.style.cssText = `
flex: 1;
border-radius: 18px;
padding: 8px 16px;
font-size: 1.1em;
border: 2px solid #b0d0b0;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.06);
transition: border-color 0.2s;
`;
const searchButton = document.createElement('button');
searchButton.type = 'submit';
searchButton.textContent = 'Search';
searchButton.className = 'r34-search-button';
searchButton.style.cssText = `
border-radius: 18px;
padding: 8px 28px;
font-size: 1.1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
white-space: nowrap;
`;
// Add hover effects
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = '#4a90e2';
});
searchInput.addEventListener('blur', () => {
searchInput.style.borderColor = '#b0d0b0';
});
searchButton.addEventListener('mouseenter', () => {
searchButton.style.borderColor = '#4a90e2';
searchButton.style.background = '#e0f7fa';
});
searchButton.addEventListener('mouseleave', () => {
searchButton.style.borderColor = '#b0d0b0';
searchButton.style.background = '#f8fff8';
});
searchForm.appendChild(searchInput);
searchForm.appendChild(searchButton);
searchWrap.appendChild(searchForm);
// Insert at the top of content
content.insertBefore(searchWrap, content.firstChild);
return true;
}
function setupGeneric() {
const originalForm = findSearchForm();
if (!originalForm) return false;
const searchField = originalForm.querySelector('input[name="tags"], textarea[name="tags"]');
if (!searchField) return false;
const tagList = document.createElement('div');
tagList.className = 'r34-tag-list';
searchField.insertAdjacentElement('afterend', tagList);
tags = getTagsFromURL();
renderTags(tagList);
searchField.value = '';
searchField.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
const value = searchField.value.trim();
if (value) {
e.preventDefault();
if (!tags.includes(value)) {
tags.push(value);
renderTags(tagList);
}
searchField.value = '';
} else if (tags.length > 0) {
e.preventDefault();
searchField.value = tags.join(' ');
originalForm.submit();
}
}
});
originalForm.addEventListener('submit', function (e) {
if (tags.length > 0) {
searchField.value = tags.join(' ');
}
});
return true;
}
function findSearchForm() {
let form = document.querySelector('.sidebar form input[name="tags"], .sidebar form textarea[name="tags"]');
if (form) return form.closest('form');
form = document.querySelector('form input[name="tags"], form textarea[name="tags"]');
return form ? form.closest('form') : null;
}
// --- Style Injection ---
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
.r34-center-wrap {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-bottom: 20px;
}
.r34-search-form {
width: 90vw;
max-width: 700px;
min-width: 220px;
margin: 0 auto;
}
.r34-modern-searchbar {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
width: 100%;
margin-bottom: 10px;
justify-content: center;
}
.r34-export-btn, .r34-cheat-btn, .r34-settings-btn {
border-radius: 18px;
padding: 8px 18px;
font-size: 1.1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.r34-export-btn:hover, .r34-cheat-btn:hover, .r34-settings-btn:hover {
border-color: #4a90e2;
background: #e0f7fa;
}
.r34-search-input {
flex: 1 1 0%;
min-width: 0;
border-radius: 18px;
padding: 8px 16px;
font-size: 1.1em;
border: 2px solid #b0d0b0;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.06);
transition: border-color 0.2s;
box-sizing: border-box;
width: 90vw;
max-width: 500px;
}
.r34-search-input:focus {
border-color: #4a90e2;
outline: none;
}
.r34-search-button {
border-radius: 18px;
padding: 8px 28px;
font-size: 1.1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.r34-search-button:hover {
border-color: #4a90e2;
background: #e0f7fa;
}
.r34-tag-list {
margin: 18px 0 0 0;
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
max-width: 1000px;
justify-content: flex-start;
}
.r34-tag-item {
background: #e0ffe0;
border: 1.5px solid #b0d0b0;
border-radius: 18px;
padding: 4px 18px;
display: flex;
align-items: center;
font-size: 1.08em;
color: #222;
font-weight: 500;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.r34-tag-item .r34-remove-tag {
margin-left: 12px;
cursor: pointer;
color: #c00;
font-weight: bold;
font-size: 1.1em;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0; top: 0; right: 0; bottom: 0;
background: rgba(24, 28, 36, 0.55);
backdrop-filter: blur(2.5px);
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.modal[style*="display: flex"] {
display: flex !important;
}
.modal-content {
background: #fff;
border-radius: 18px;
padding: 32px 28px 24px 28px;
min-width: 320px;
max-width: 800px;
width: 50vw;
margin: auto;
box-shadow: 0 8px 40px 0 rgba(0,0,0,0.18);
display: flex;
flex-direction: column;
gap: 18px;
position: relative;
font-size: 1.08em;
align-items: center;
max-height: 80vh;
overflow: auto;
}
.modal-close {
position: absolute;
top: 12px;
right: 16px;
background: none;
border: none;
font-size: 1.7em;
color: #888;
cursor: pointer;
z-index: 2;
padding: 0 8px;
line-height: 1;
transition: color 0.2s;
}
.modal-close:hover {
color: #c00;
}
.modal-content h3 {
margin: 0 0 8px 0;
font-size: 1.35em;
font-weight: bold;
text-align: left;
align-self: flex-start;
color: #0066cc;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 8px;
flex-wrap: wrap;
width: 100%;
}
.modal-actions button {
border-radius: 16px;
padding: 8px 22px;
font-size: 1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
}
.modal-actions button:hover {
border-color: #4a90e2;
background: #e0f7fa;
}
.modal-pastebin {
background: #f6f8fa;
border: 1.5px solid #e0e0e0;
border-radius: 10px;
padding: 12px 14px;
font-size: 1em;
width: 100%;
min-width: 220px;
max-width: 100%;
box-sizing: border-box;
color: #222;
margin-bottom: 0;
font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', monospace;
overflow: auto;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
resize: vertical;
height: 260px;
min-height: 120px;
max-height: 50vh;
}
.modal-content pre, .modal-doc {
background: #f6f8fa;
border: 1.5px solid #e0e0e0;
border-radius: 10px;
padding: 12px 14px;
font-size: 1em;
width: 100%;
min-width: 220px;
max-width: 100%;
box-sizing: border-box;
color: #222;
margin-bottom: 0;
font-family: inherit;
overflow: auto;
}
.modal-content pre {
white-space: pre-wrap;
word-break: break-word;
max-height: 320px;
overflow: auto;
}
.modal-doc section {
margin-bottom: 18px;
}
.modal-doc h4 {
margin: 0 0 6px 0;
font-size: 1.08em;
color: #4a90e2;
font-weight: bold;
}
.dynamic-cheatsheet section {
margin-bottom: 25px;
}
.dynamic-cheatsheet h4 {
color: #0066cc;
margin-bottom: 15px;
font-size: 1.15em;
font-weight: bold;
border-bottom: 2px solid #e9ecef;
padding-bottom: 5px;
}
.dynamic-cheatsheet ul {
list-style: none;
padding: 0;
}
.dynamic-cheatsheet li {
margin-bottom: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #0066cc;
}
.dynamic-cheatsheet h5 {
margin: 0 0 8px 0;
color: #d63384;
font-size: 0.95em;
font-weight: bold;
font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', monospace;
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.dynamic-cheatsheet p {
margin: 0;
color: #333;
line-height: 1.5;
font-size: 0.95em;
}
.dynamic-cheatsheet code {
background: #e9ecef;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', monospace;
color: #d63384;
font-size: 0.9em;
}
.modal-doc ul {
margin: 0 0 0 18px;
padding: 0;
list-style: disc inside;
}
.modal-doc code {
background: #e6f7ff;
border-radius: 6px;
padding: 2px 6px;
font-size: 0.98em;
color: #00549e;
}
.modal-doc b {
color: #333;
}
.modal-tabs {
display: flex;
background: #f0f4fa;
border-radius: 999px;
padding: 4px;
gap: 8px;
margin-bottom: 16px;
width: 100%;
justify-content: flex-start;
}
.modal-tab {
border: none;
background: none;
border-radius: 999px;
padding: 8px 28px;
font-size: 1.08em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s, box-shadow 0.2s;
box-shadow: none;
outline: none;
margin-bottom: 0;
color: #00549e;
}
.modal-tab.active {
background: #4a90e2;
color: #fff;
box-shadow: 0 2px 8px 0 rgba(74,144,226,0.12);
}
.modal-tab:not(.active):hover {
background: #e6f7ff;
color: #00549e;
}
.modal-tab:focus {
outline: 2px solid #4a90e2;
}
/* Responsive for mobile */
@media (max-width: 700px) {
.modal-content {
min-width: 0;
max-width: 98vw;
width: 98vw;
padding: 18px 2vw 12px 2vw;
font-size: 1em;
max-height: 98vh;
}
.modal-content h3 {
font-size: 1.08em;
}
.modal-actions {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.modal-pastebin, .modal-content pre, .modal-doc {
font-size: 0.98em;
padding: 8px 6px;
width: 98vw;
min-width: 0;
max-width: 100vw;
height: 120px;
min-height: 60px;
max-height: 30vh;
}
.modal-tabs {
width: 98vw;
min-width: 0;
max-width: 100vw;
}
}
/* --- Add width/centering for modal-tabs and modal-pastebin --- */
.modal-tabs,
.modal-pastebin {
width: 90%;
max-width: 760px;
min-width: 220px;
margin-left: auto;
margin-right: auto;
}
.modal-form {
width: 90%;
max-width: 760px;
min-width: 220px;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
align-items: stretch;
height: auto;
max-height: 100%;
}
@media (max-width: 700px) {
.modal-tabs,
.modal-pastebin {
width: 98vw;
max-width: 100vw;
min-width: 0;
}
.modal-form {
width: 98vw;
max-width: 100vw;
min-width: 0;
height: auto;
max-height: 100%;
}
}
/* e621.net coloring */
body.c-posts.a-index.resp .r34-tag-item {
background: var(--color-tag-general, #e6f7ff);
border-color: var(--color-tag-general-alt, #7ecfff);
}
/* rule34.xxx coloring */
body#body .r34-tag-item {
background: #e6ffe6;
border-color: #7edc7e;
}
@media (max-width: 600px) {
body#body .r34-search-input {
width: 100vw;
max-width: 100vw;
}
}
.r34-sort-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 18px;
width: 100%;
margin: 8px 0 0 0;
justify-content: flex-start;
}
.r34-sort-select {
border-radius: 14px;
padding: 7px 18px;
font-size: 1.08em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
min-width: 90px;
max-width: 180px;
}
.r34-sort-select:focus {
border-color: #4a90e2;
outline: none;
}
.r34-order-switch {
border-radius: 14px;
padding: 7px 18px;
font-size: 1.08em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: #00549e;
min-width: 120px;
}
.r34-order-switch:focus {
border-color: #4a90e2;
outline: none;
}
.r34-order-switch[style*="display: none"] {
display: none !important;
}
.r34-rating-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 1.08em;
font-weight: 500;
color: #00549e;
background: #e6f7ff;
border-radius: 10px;
padding: 4px 12px;
border: 1.5px solid #b0d0b0;
margin-right: 4px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.r34-rating-checkbox {
accent-color: #4a90e2;
width: 1.1em;
height: 1.1em;
}
.r34-rating-label:hover {
background: #d0eaff;
border-color: #4a90e2;
}
/* Metatag Row */
.r34-metatag-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.r34-metatag-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: #00549e;
margin-bottom: 2px;
}
.r34-metatag-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: #00549e;
}
.r34-metatag-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.r34-metatag-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
margin-top: 2px;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
.r34-metatag-item {
background: #e6f7ff;
border: 1.5px solid #7ecfff;
color: #00549e;
}
@media (max-width: 700px) {
.r34-sort-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.r34-metatag-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.r34-metatag-list {
gap: 6px;
}
}
/* Include/Exclude Tag Rows */
.r34-include-row-wrap, .r34-exclude-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.r34-include-row-header, .r34-exclude-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: #00549e;
margin-bottom: 2px;
}
.r34-include-toggle, .r34-exclude-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: #00549e;
}
.r34-include-toggle:focus, .r34-exclude-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.r34-include-list, .r34-exclude-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
margin-top: 2px;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
.r34-include-item, .r34-exclude-item {
background: #e6f7ff;
border: 1.5px solid #7ecfff;
color: #00549e;
}
@media (max-width: 700px) {
.r34-include-row-header, .r34-exclude-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.r34-include-list, .r34-exclude-list {
gap: 6px;
}
.r34-all-tags-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.r34-all-tags-list {
gap: 6px;
}
}
/* All Tags Row */
.r34-all-tags-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.r34-all-tags-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: #00549e;
margin-bottom: 2px;
}
.r34-all-tags-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid #b0d0b0;
background: #f8fff8;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: #00549e;
}
.r34-all-tags-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.r34-all-tags-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
margin-top: 2px;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
/* Settings Form */
.settings-form {
display: flex;
flex-direction: column;
gap: 16px;
padding: 8px 0;
}
.settings-row {
display: flex;
align-items: center;
gap: 8px;
}
.settings-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.08em;
font-weight: 500;
color: #333;
cursor: pointer;
}
.settings-label input[type="checkbox"] {
accent-color: #4a90e2;
width: 1.2em;
height: 1.2em;
}
`;
document.head.appendChild(style);
}
// --- e621 Autocomplete hijack: expose jQuery UI Autocomplete instance ---
function hijackE621Autocomplete() {
if (location.hostname.endsWith('e621.net')) {
function exposeE621Autocomplete() {
var searchInput = document.querySelector('[data-autocomplete="tag-query"], [data-autocomplete="tag-edit"], [data-autocomplete="tag"]');
if (!searchInput) return;
var $input = window.jQuery && window.jQuery(searchInput);
if ($input && $input.autocomplete) {
var instance = $input.autocomplete("instance");
if (instance) {
searchInput.e621Autocomplete = instance;
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
exposeE621Autocomplete();
setTimeout(exposeE621Autocomplete, 1000);
});
} else {
exposeE621Autocomplete();
setTimeout(exposeE621Autocomplete, 1000);
}
}
}
// --- Main Entrypoint ---
let initialized = false;
function init() {
if (initialized) {
console.log('Searchbar Enhancer: Already initialized, skipping...');
return;
}
console.log('Searchbar Enhancer: Initializing...');
console.log('Site detection: isE621=', isE621, 'isRule34=', isRule34, 'isDanbooru=', isDanbooru, 'isWikiPage=', isWikiPage);
console.log('Current hostname:', location.hostname);
injectStyles();
hijackE621Autocomplete();
let setupSuccess = false;
if (isWikiPage) {
console.log('Setting up Wiki page...');
setupSuccess = setupWiki();
} else if (isE621) {
console.log('Setting up E621...');
setupSuccess = setupE621();
} else if (isRule34) {
console.log('Setting up Rule34...');
setupSuccess = setupRule34();
} else if (isDanbooru) {
console.log('Setting up Danbooru...');
setupSuccess = setupDanbooru();
} else {
console.log('Setting up Generic...');
setupSuccess = setupGeneric();
}
if (setupSuccess !== false) {
initialized = true;
console.log('Searchbar Enhancer: Initialization completed');
}
}
// --- Run ---
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Also try after a short delay in case of dynamic content
setTimeout(() => {
if (!initialized) {
init();
}
}, 1000);
})();