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
// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: booru-enhanced-search/index.user.js
// ==UserScript==
// @name Booru Tag Search
// @namespace codeberg.org/0xdev/booru-tag-search
// @version 2.3.2
// @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 0xdev
// @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 GELBOORU_CHEATSHEET = `
<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>`;
const GELBOORU_SORT_OPTIONS = [
{ 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' }
];
const GELBOORU_RATINGS = [
{ value: 'safe', label: 'Safe' },
{ value: 'questionable', label: 'Questionable' },
{ value: 'explicit', label: 'Explicit' }
];
const GELBOORU_METATAG_REGEX = /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/;
const SITE_CONFIG = {
"e621.net": {
site: 'e621',
sortPrefix: 'order',
inputTag: 'textarea',
selectors: { form: 'form.post-search-form', gallery: '#c-posts' },
cheatSheetUrl: 'https://e621.net/help/cheatsheet',
cheatSheetSelector: '#c-help #a-show .styled-dtext',
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: GELBOORU_RATINGS,
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": {
site: 'rule34',
sortPrefix: 'sort',
inputTag: 'input',
selectors: { form: '.sidebar .tag-search form', gallery: '#post-list' },
cheatSheetUrl: 'https://rule34.xxx/index.php?page=help&topic=cheatsheet',
cheatSheetSelector: '.content',
placeholder: "Enter tags...",
sortOptions: GELBOORU_SORT_OPTIONS,
ratings: GELBOORU_RATINGS,
metatagRegex: GELBOORU_METATAG_REGEX,
cheatSheetContent: GELBOORU_CHEATSHEET
},
// safebooru uses the same layout/API as rule34
"safebooru.org": {
site: 'rule34',
sortPrefix: 'sort',
inputTag: 'input',
selectors: { form: '.sidebar .tag-search form', gallery: '#post-list' },
placeholder: "Enter tags...",
sortOptions: GELBOORU_SORT_OPTIONS,
ratings: GELBOORU_RATINGS,
metatagRegex: GELBOORU_METATAG_REGEX,
cheatSheetContent: GELBOORU_CHEATSHEET
},
"danbooru.donmai.us": {
site: 'danbooru',
sortPrefix: 'order',
inputTag: 'input',
selectors: {
form: 'form[action*="posts"], form.search-form, #search-form, form:has(input[name="tags"])',
gallery: '#posts, .posts, #post-list, .post-list',
skipPattern: /^\/posts\/\d+/
},
cheatSheetUrl: 'https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet',
cheatSheetSelector: '#c-wiki-pages #a-show .prose, .wiki-page-body, .dtext-container',
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:)/,
},
"default": {
site: '',
sortPrefix: 'sort',
inputTag: 'input',
placeholder: "Enter tags...",
sortOptions: GELBOORU_SORT_OPTIONS,
ratings: GELBOORU_RATINGS,
metatagRegex: GELBOORU_METATAG_REGEX,
cheatSheetContent: GELBOORU_CHEATSHEET
}
};
// --- State ---
let tags = [];
const SCRIPT_VERSION = '2.3.1';
const ui = {};
const siteConfig = SITE_CONFIG[location.hostname] || SITE_CONFIG['default'];
const site = siteConfig.site;
const isWikiPage = location.pathname.includes('/wiki') || location.pathname.includes('/help') || document.title.includes('Wiki');
// --- 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();
// --- Tag Management ---
function addTag(tag) {
if (!tag || tags.includes(tag)) return;
// Cancel out contradicting include/exclude (ass + -ass = remove both)
const opposite = tag.startsWith('-') ? tag.slice(1) : `-${tag}`;
const oppIdx = tags.indexOf(opposite);
if (oppIdx !== -1) {
tags.splice(oppIdx, 1);
renderTags();
return;
}
tags.push(tag);
renderTags();
}
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 (siteConfig.sortPrefix === 'order') {
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);
}
// Filter out Gelbooru's "all" placeholder — it's not a real tag
return tags.filter(t => t && t !== 'all');
}
function syncUiOnRemove(tag) {
const { sortSelect, orderSwitch, ratingCheckboxes } = ui;
const isSortTag = siteConfig.sortPrefix === 'order' ? /^order:[a-z_]+(_asc)?$/.test(tag) : /^sort:[a-z_]+:(asc|desc)$/.test(tag);
if (isSortTag && sortSelect) {
sortSelect.value = '';
orderSwitch.style.display = 'none';
}
if (/^rating:(safe|questionable|explicit|general|sensitive)$/.test(tag) && ratingCheckboxes) {
ratingCheckboxes.forEach(cb => {
if (cb.value === tag.split(':')[1]) cb.checked = false;
});
}
}
function createTagPill(tag, className, onRemove) {
const tagEl = document.createElement('span');
tagEl.className = `bse-tag-item ${className}`;
tagEl.textContent = tag;
const removeBtn = document.createElement('span');
removeBtn.className = 'bse-remove-tag';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
if (onRemove) onRemove(tag);
const tagIdx = tags.indexOf(tag);
if (tagIdx !== -1) {
tags.splice(tagIdx, 1);
renderTags();
}
};
tagEl.appendChild(removeBtn);
return tagEl;
}
function renderTags() {
const { metatagList, metatagRowWrap, includeList, excludeList,
includeRowWrap, excludeRowWrap, allTagsRowWrap, allTagsList,
sortSelect, orderSwitch, ratingCheckboxes } = ui;
const metatagRegex = siteConfig.metatagRegex || /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/;
const sortRegex = siteConfig.sortPrefix === 'order'
? /^order:([a-z_]+)(_asc)?$/
: /^sort:([a-z_]+):(asc|desc)$/;
// Clear all lists
if (includeList) includeList.innerHTML = '';
if (excludeList) excludeList.innerHTML = '';
if (metatagList) metatagList.innerHTML = '';
if (allTagsList) allTagsList.innerHTML = '';
// Single pass: classify tags and build UI
let hasIncludeTags = false;
let hasExcludeTags = false;
let hasMetatags = false;
let sortType = '';
let sortOrder = 'desc';
let foundSort = false;
const ratingsSet = new Set();
for (const tag of tags) {
const isMeta = metatagRegex.test(tag);
if (isMeta) {
// Metatag pill
hasMetatags = true;
if (metatagList && settings.showMetatags) {
metatagList.appendChild(createTagPill(tag, 'bse-metatag-item', syncUiOnRemove));
}
// Extract sort info
const sortMatch = tag.match(sortRegex);
if (sortMatch) {
sortType = sortMatch[1];
sortOrder = siteConfig.sortPrefix === 'order'
? (sortMatch[2] ? 'asc' : 'desc')
: sortMatch[2];
foundSort = true;
}
// Extract rating info
const ratingMatch = tag.match(/^rating:(safe|questionable|explicit|general|sensitive)$/);
if (ratingMatch) {
ratingsSet.add(ratingMatch[1]);
} else {
const ratingOrMatch = tag.match(/^\(\s*([^)]+)\s*\)$/);
if (ratingOrMatch) {
for (const part of ratingOrMatch[1].split('~')) {
const m = part.trim().match(/^rating:(safe|questionable|explicit|general|sensitive)$/);
if (m) ratingsSet.add(m[1]);
}
}
}
} else {
// Regular tag — include or exclude
const isExclude = tag.startsWith('-');
if (isExclude) {
hasExcludeTags = true;
if (excludeList && settings.showIncludeExclude) {
excludeList.appendChild(createTagPill(tag, 'bse-exclude-item'));
}
} else {
hasIncludeTags = true;
if (includeList && settings.showIncludeExclude) {
includeList.appendChild(createTagPill(tag, 'bse-include-item'));
}
}
// All-tags list
if (allTagsList && settings.showAllTags) {
const cls = isExclude ? 'bse-exclude-item' : 'bse-include-item';
allTagsList.appendChild(createTagPill(tag, cls, syncUiOnRemove));
}
}
}
// Toggle row visibility
if (includeRowWrap) includeRowWrap.style.display = hasIncludeTags ? '' : 'none';
if (excludeRowWrap) excludeRowWrap.style.display = hasExcludeTags ? '' : 'none';
if (metatagRowWrap) metatagRowWrap.style.display = hasMetatags && settings.showMetatags ? '' : 'none';
if (allTagsRowWrap) allTagsRowWrap.style.display = (hasIncludeTags || hasExcludeTags) && settings.showAllTags ? '' : 'none';
// Sync sort dropdown
if (sortSelect) {
sortSelect.value = foundSort ? sortType : '';
if (foundSort) {
orderSwitch.style.display = '';
orderSwitch.dataset.state = sortOrder;
orderSwitch.textContent = sortOrder === 'asc' ? 'Ascent \u2191' : 'Descent \u2193';
} else {
orderSwitch.style.display = 'none';
}
}
// Sync rating checkboxes
if (ratingCheckboxes) {
ratingCheckboxes.forEach(cb => {
cb.checked = ratingsSet.has(cb.value);
});
}
}
// --- 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 () {
navigator.clipboard.writeText(textarea.value);
};
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();
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');
const selector = siteConfig.cheatSheetSelector || null;
const cheatsheetSection = selector && doc.querySelector(selector);
const content = cheatsheetSection ? cleanCheatSheetHTML(cheatsheetSection) : '';
return content;
} catch (error) {
console.error('Failed to fetch cheat sheet:', error);
return null;
}
}
function cleanCheatSheetHTML(element) {
const el = element.cloneNode(true);
// Remove TOC sections and navigation
el.querySelectorAll('[class*="toc"], [id*="toc"], nav').forEach(n => n.remove());
el.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => {
if (/table of contents|contents/i.test(h.textContent)) {
let next = h.nextElementSibling;
h.remove();
while (next && !/^H[1-6]$/.test(next.tagName)) {
const rm = next;
next = next.nextElementSibling;
rm.remove();
}
}
});
// Remove images and scripts
el.querySelectorAll('img, script, style, iframe').forEach(n => n.remove());
// Strip all inline styles and classes from content, keep structure
el.querySelectorAll('*').forEach(n => {
n.removeAttribute('style');
n.removeAttribute('class');
n.removeAttribute('id');
});
// Tag code elements with chip classes based on content
el.querySelectorAll('code').forEach(code => {
const text = code.textContent.trim();
if (text.startsWith('-')) code.classList.add('bse-chip-exclude');
else if (/^[a-z_]+:/.test(text) || text.startsWith('(')) code.classList.add('bse-chip-meta');
else code.classList.add('bse-chip-include');
});
return `<div class="dynamic-cheatsheet">${el.innerHTML}</div>`;
}
async function getCachedCheatSheet(site) {
const cacheKey = `booru-cheatsheet-${site}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
const now = Date.now();
if (data.timestamp && (now - data.timestamp) < CACHE_DURATION && data.version === SCRIPT_VERSION) {
return data.content;
}
} catch (e) {
console.error('Failed to parse cached cheat sheet:', e);
}
}
const url = siteConfig.cheatSheetUrl || '';
if (url) {
const content = await fetchCheatSheet(url);
if (content) {
// Cache the result
const cacheData = {
content: content,
timestamp: Date.now(),
url: url,
version: SCRIPT_VERSION
};
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();
// 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.className = 'cheatsheet-info-bar';
const refLink = document.createElement('a');
refLink.href = siteConfig.cheatSheetUrl || '#';
refLink.target = '_blank';
refLink.textContent = 'View on site →';
refLink.className = 'cheatsheet-ref-link';
const cacheInfo = document.createElement('span');
cacheInfo.className = 'cheatsheet-cache-info';
// Check cache status
const cacheKey = `booru-cheatsheet-${site}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
const age = Date.now() - 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.className = 'cheatsheet-flush-btn';
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);
}
}
function createCollapsibleRow(prefix, title, startExpanded) {
const rowWrap = document.createElement('div');
rowWrap.className = `bse-${prefix}-row-wrap`;
const rowHeader = document.createElement('div');
rowHeader.className = `bse-${prefix}-row-header`;
const rowTitle = document.createElement('span');
rowTitle.textContent = title;
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = `bse-${prefix}-toggle`;
toggle.textContent = startExpanded ? 'See less' : 'See more';
rowHeader.appendChild(rowTitle);
rowHeader.appendChild(toggle);
const list = document.createElement('div');
list.className = `bse-${prefix}-list`;
rowWrap.appendChild(rowHeader);
rowWrap.appendChild(list);
let expanded = startExpanded;
list.style.display = expanded ? '' : 'none';
toggle.onclick = () => {
expanded = !expanded;
list.style.display = expanded ? '' : 'none';
toggle.textContent = expanded ? 'See less' : 'See more';
};
return { rowWrap, list };
}
// --- Search Bar Creation ---
function createSearchSection(site) {
const centerWrap = document.createElement('div');
centerWrap.className = 'bse-center-wrap';
const searchForm = document.createElement('form');
searchForm.className = 'bse-search-form';
searchForm.method = 'GET';
searchForm.action = '';
const searchBarContainer = document.createElement('div');
searchBarContainer.className = 'bse-modern-searchbar';
const inputTag = siteConfig.inputTag || 'input';
const searchInput = document.createElement(inputTag);
searchInput.name = 'tags';
searchInput.placeholder = siteConfig.placeholder || 'Enter tags...';
searchInput.className = 'bse-search-input';
if (inputTag === 'textarea') {
searchInput.rows = 1;
searchInput.id = 'tags';
} else {
searchInput.type = 'text';
}
// --- Searchbar Buttons ---
const exportBtn = document.createElement('button');
exportBtn.type = 'button';
exportBtn.textContent = 'Export';
exportBtn.className = 'bse-export-btn';
exportBtn.title = 'Export tags';
exportBtn.onclick = showExportModal;
const cheatBtn = document.createElement('button');
cheatBtn.type = 'button';
cheatBtn.textContent = '?';
cheatBtn.className = 'bse-cheat-btn';
cheatBtn.title = 'Show cheat sheet';
cheatBtn.onclick = showCheatSheetModal;
const settingsBtn = document.createElement('button');
settingsBtn.type = 'button';
settingsBtn.textContent = '⚙';
settingsBtn.className = 'bse-settings-btn';
settingsBtn.title = 'Settings';
settingsBtn.onclick = showSettingsModal;
const searchButton = document.createElement('button');
searchButton.type = 'submit';
searchButton.textContent = 'Search';
searchButton.className = 'bse-search-button';
// --- New Sort/Order/Ratings Row ---
const sortRow = document.createElement('div');
sortRow.className = 'bse-sort-row';
// Sort dropdown
const sortSelect = document.createElement('select');
sortSelect.className = 'bse-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 = 'bse-order-switch';
orderSwitch.dataset.state = 'desc';
orderSwitch.textContent = 'Descent \u2193';
orderSwitch.style.display = 'none';
orderSwitch.onclick = function () {
if (orderSwitch.dataset.state === 'desc') {
orderSwitch.dataset.state = 'asc';
orderSwitch.textContent = 'Ascent \u2191';
} else {
orderSwitch.dataset.state = 'desc';
orderSwitch.textContent = 'Descent \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 = 'bse-rating-label';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = r.value;
checkbox.className = 'bse-rating-checkbox';
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + r.label));
sortRow.appendChild(label);
});
// Cache rating checkboxes — queried once, reused everywhere
const ratingCheckboxes = sortRow.querySelectorAll('.bse-rating-checkbox');
// --- Collapsible Tag Rows ---
const { rowWrap: includeRowWrap, list: includeList } = createCollapsibleRow('include', 'Include Tags', true);
const { rowWrap: excludeRowWrap, list: excludeList } = createCollapsibleRow('exclude', 'Exclude Tags', true);
const { rowWrap: metatagRowWrap, list: metatagList } = createCollapsibleRow('metatag', 'Metatags', false);
const { rowWrap: allTagsRowWrap, list: allTagsList } = createCollapsibleRow('all-tags', 'All Tags', true);
// 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 === 'danbooru') {
const $ = window.jQuery;
if ($ && $(searchInput).autocomplete('instance')) {
$(searchInput).autocomplete('close');
}
}
}
}
});
}
function buildAndApplyMetatags() {
const prefix = siteConfig.sortPrefix;
const prefixes = [prefix + ':', 'rating:', '( rating:'];
tags = tags.filter(tag => !prefixes.some(p => tag.startsWith(p)));
if (sortSelect.value) {
const order = orderSwitch.dataset.state || 'desc';
if (prefix === 'order') {
tags.push(order === 'asc' ? `order:${sortSelect.value}_asc` : `order:${sortSelect.value}`);
} else {
tags.push(`sort:${sortSelect.value}:${order}`);
}
}
const checkedRatings = [...ratingCheckboxes].filter(cb => cb.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(' ~ ') + ' )');
}
}
function bindFormEvents() {
searchForm.addEventListener('submit', function (e) {
e.preventDefault();
buildAndApplyMetatags();
if (tags.length > 0) {
searchInput.value = tags.join(' ');
}
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 = '';
});
}
}
// --- e621 Autocomplete integration (vanilla JS — e621 does NOT use jQuery UI) ---
const E621_TAG_COLORS = {
0: '#b4b7b4', // general
1: '#f2ac08', // artist
3: '#c797ff', // copyright
4: '#00aa00', // character
5: '#ed5d1f', // species
6: '#ff3d3d', // invalid
7: '#eeeeee', // meta
8: '#228822', // lore
};
function bindE621Autocomplete() {
if (site !== 'e621') return;
let dropdown = document.createElement('ul');
dropdown.className = 'bse-ac-dropdown';
// Styled via .bse-ac-dropdown in stylesheet
document.body.appendChild(dropdown);
let debounceTimer = null;
let selectedIdx = -1;
const positionDropdown = () => {
const rect = searchInput.getBoundingClientRect();
dropdown.style.left = rect.left + window.scrollX + 'px';
dropdown.style.top = rect.bottom + window.scrollY + 'px';
dropdown.style.width = Math.max(rect.width, 300) + 'px';
};
const hideDropdown = () => { dropdown.style.display = 'none'; selectedIdx = -1; };
const showResults = (items) => {
dropdown.replaceChildren();
if (!items.length) { hideDropdown(); return; }
items.forEach((item, i) => {
const li = document.createElement('li');
const color = E621_TAG_COLORS[item.category] || '#b4b7b4';
const tagName = item.antecedent_name || item.name;
const alias = item.antecedent_name ? item.name : null;
const count = item.post_count ? formatPostCount(item.post_count) : '';
li.innerHTML = renderAutocompleteItemHTML(tagName, alias, count, color);
li.addEventListener('mousedown', (e) => {
e.preventDefault();
addTag(item.name);
searchInput.value = '';
hideDropdown();
});
li.addEventListener('mouseenter', () => {
selectedIdx = i;
highlightItem();
});
dropdown.appendChild(li);
});
positionDropdown();
dropdown.style.display = 'block';
};
const highlightItem = () => {
[...dropdown.children].forEach((li, i) => {
li.style.background = i === selectedIdx ? 'rgba(255,255,255,.1)' : 'transparent';
});
};
let abortCtrl = null;
searchInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
const term = searchInput.value.trim();
if (term.length < 3) { hideDropdown(); return; }
debounceTimer = setTimeout(() => {
if (abortCtrl) abortCtrl.abort();
abortCtrl = new AbortController();
fetch(`/tags/autocomplete.json?search[name_matches]=${encodeURIComponent(term)}&expiry=7`, { signal: abortCtrl.signal })
.then(r => r.json())
.then(showResults)
.catch(e => { if (e.name !== 'AbortError') hideDropdown(); });
}, 225);
});
searchInput.addEventListener('keydown', (e) => {
if (dropdown.style.display === 'none') return;
const items = dropdown.children;
if (e.key === 'ArrowDown') { e.preventDefault(); selectedIdx = Math.min(selectedIdx + 1, items.length - 1); highlightItem(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIdx = Math.max(selectedIdx - 1, 0); highlightItem(); }
else if (e.key === 'Enter' && selectedIdx >= 0) {
e.preventDefault();
items[selectedIdx]?.dispatchEvent(new MouseEvent('mousedown'));
}
else if (e.key === 'Escape') { hideDropdown(); }
});
searchInput.addEventListener('blur', () => setTimeout(hideDropdown, 150));
}
// --- Danbooru jQuery UI Autocomplete integration ---
// Danbooru tag category colors — uses site's CSS custom properties
// Falls back to hardcoded values if vars aren't available
const rootStyles = getComputedStyle(document.documentElement);
const cssVar = (name, fallback) => rootStyles.getPropertyValue(name).trim() || fallback;
const DANBOORU_TAG_COLORS = {
0: cssVar('--blue-3', '#009be6'), // general
1: cssVar('--red-3', '#c00'), // artist
3: cssVar('--purple-3', '#c797ff'), // copyright
4: cssVar('--green-3', '#0a0'), // character
5: cssVar('--orange-3', '#fd9200'), // meta
};
function formatPostCount(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
return String(n);
}
function renderAutocompleteItemHTML(tagName, alias, count, color) {
const aliasPart = alias ? ` <span style="color:#888;font-size:0.85em">\u2192 ${alias.replace(/_/g, ' ')}</span>` : '';
const countPart = count ? `<span style="color:#888;font-size:0.85em;flex-shrink:0">${count}</span>` : '';
return `<span style="color:${color};flex-grow:1">${tagName.replace(/_/g, ' ')}${aliasPart}</span>${countPart}`;
}
let danbooruRetries = 0;
function bindDanbooruAutocomplete() {
if (site !== 'danbooru') return;
const $ = window.jQuery;
if (!$ || !$.fn.autocomplete) {
if (++danbooruRetries < 20) setTimeout(bindDanbooruAutocomplete, 500);
return;
}
$(searchInput).autocomplete({
delay: 100,
minLength: 1,
source: function (request, response) {
$.get('/autocomplete.json', {
'search[query]': request.term,
'search[type]': 'tag_query',
limit: 20,
version: 3
}).then(function (data) {
response(data.map(function (item) {
// post_count/category may be top-level or nested in item.tag
const tag = item.tag || {};
return {
label: item.label || item.value,
value: item.value,
category: item.category ?? tag.category ?? 0,
post_count: item.post_count ?? tag.post_count ?? 0,
antecedent: item.antecedent || null
};
}));
});
}
});
// Custom rendering with category colors and post counts
const acInstance = $(searchInput).autocomplete('instance');
acInstance._renderItem = function (ul, item) {
const color = DANBOORU_TAG_COLORS[item.category] || '#009be6';
const count = item.post_count ? formatPostCount(item.post_count) : '';
const tagName = item.antecedent || item.label || item.value;
const alias = item.antecedent ? item.label : null;
const li = $('<li>').appendTo(ul);
li.append(
$('<div>').css({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '4px 8px', gap: '12px' })
.html(renderAutocompleteItemHTML(tagName, alias, count, color))
);
return li;
};
// Style and position the dropdown menu
acInstance.menu.element.css({
background: '#1a1a2e',
border: '1px solid #333',
maxHeight: '400px',
overflowY: 'auto',
zIndex: 99999,
width: 'auto',
minWidth: $(searchInput).outerWidth() + 'px',
maxWidth: '500px'
});
// Override positioning to anchor to the search input
acInstance._resizeMenu = function () {
this.menu.element.outerWidth(Math.max(this.element.outerWidth(), 300));
};
// Intercept selection: add tag to our list instead of inserting into input
$(searchInput).on('autocompleteselect', function (e, ui) {
e.preventDefault();
if (ui.item && ui.item.value) {
addTag(ui.item.value);
searchInput.value = '';
}
});
}
function syncMetatagsFromUI() {
buildAndApplyMetatags();
renderTags();
}
sortSelect.addEventListener('change', syncMetatagsFromUI);
orderSwitch.addEventListener('click', syncMetatagsFromUI);
ratingCheckboxes.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();
bindE621Autocomplete();
bindDanbooruAutocomplete();
// Store UI references for renderTags and cross-scope access
Object.assign(ui, {
includeList, excludeList, includeRowWrap, excludeRowWrap,
metatagList, metatagRowWrap, allTagsList, allTagsRowWrap,
sortSelect, orderSwitch, ratingCheckboxes
});
return { centerWrap, searchForm, searchInput };
}
function setupSite() {
const sel = siteConfig.selectors;
if (!sel) return false;
// Skip pages that shouldn't have the enhanced search
if (sel.skipPattern && sel.skipPattern.test(location.pathname)) return false;
const originalForm = document.querySelector(sel.form);
const gallery = document.querySelector(sel.gallery);
if (!originalForm || !gallery) return false;
const { centerWrap, searchForm, searchInput } = createSearchSection(site);
searchForm.action = originalForm.action || '/posts';
searchForm.method = originalForm.method || 'GET';
gallery.parentNode.insertBefore(centerWrap, gallery);
originalForm.style.display = 'none';
tags = getTagsFromURL();
renderTags();
searchInput.value = '';
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 = 'bse-wiki-search-wrap';
const searchForm = document.createElement('form');
searchForm.action = '/posts';
searchForm.method = 'GET';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.name = 'tags';
searchInput.placeholder = 'Search posts...';
searchInput.className = 'bse-search-input';
const searchButton = document.createElement('button');
searchButton.type = 'submit';
searchButton.textContent = 'Search';
searchButton.className = 'bse-search-button';
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 = 'bse-tag-list';
searchField.insertAdjacentElement('afterend', tagList);
tags = getTagsFromURL();
renderTags();
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();
}
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 = `
.bse-center-wrap {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-bottom: 20px;
}
.bse-search-form {
width: 90vw;
max-width: 700px;
min-width: 220px;
margin: 0 auto;
}
.bse-modern-searchbar {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
width: 100%;
margin-bottom: 10px;
justify-content: center;
}
.bse-export-btn, .bse-cheat-btn, .bse-settings-btn {
border-radius: 18px;
padding: 8px 18px;
font-size: 1.1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent) !important;
background: color-mix(in srgb, currentColor 8%, transparent) !important;
color: inherit !important;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.bse-export-btn:hover, .bse-cheat-btn:hover, .bse-settings-btn:hover {
border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
background: color-mix(in srgb, currentColor 18%, transparent) !important;
}
.bse-search-input {
flex: 1 1 0%;
min-width: 0;
border-radius: 18px;
padding: 8px 16px;
font-size: 1.1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 5%, transparent);
color: inherit;
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;
}
.bse-search-input:focus {
border-color: #4a90e2;
outline: none;
}
.bse-search-button {
border-radius: 18px;
padding: 8px 28px;
font-size: 1.1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent) !important;
background: color-mix(in srgb, currentColor 8%, transparent) !important;
color: inherit !important;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.bse-search-button:hover {
border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
background: color-mix(in srgb, currentColor 18%, transparent) !important;
}
.bse-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;
}
.bse-tag-item {
border-radius: 18px;
padding: 4px 18px;
display: flex;
align-items: center;
font-size: 1.08em;
color: inherit;
font-weight: 500;
transition: background 0.2s, color 0.2s, border-color 0.2s;
/* Default neutral — overridden by type-specific classes */
background: color-mix(in srgb, currentColor 10%, transparent);
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
}
.bse-tag-item.bse-include-item {
background: #d1fae5 !important;
border-color: #6ee7b7 !important;
color: #065f46 !important;
}
.bse-tag-item.bse-exclude-item {
background: #fee2e2 !important;
border-color: #fca5a5 !important;
color: #991b1b !important;
}
.bse-tag-item.bse-metatag-item {
background: #fef9c3 !important;
border-color: #fde68a !important;
color: #854d0e !important;
}
.bse-tag-item .bse-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;
color: #222;
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 color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
}
.modal-actions button:hover {
border-color: #4a90e2;
background: color-mix(in srgb, currentColor 18%, transparent);
}
.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: inherit;
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: inherit;
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, .dynamic-cheatsheet * { color: inherit !important; line-height: 1.6; }
.dynamic-cheatsheet h1, .dynamic-cheatsheet h2,
.dynamic-cheatsheet h3, .dynamic-cheatsheet h4,
.dynamic-cheatsheet h5, .dynamic-cheatsheet h6 {
color: #4a90e2 !important;
margin: 1.2em 0 0.5em;
padding-bottom: 4px;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}
.dynamic-cheatsheet ul, .dynamic-cheatsheet ol {
list-style: none;
padding: 0;
margin: 0.5em 0;
}
.dynamic-cheatsheet li {
margin-bottom: 12px;
padding: 0;
}
.dynamic-cheatsheet p {
margin: 0.4em 0;
color: inherit;
}
/* Code chips — block-level so description wraps below */
.dynamic-cheatsheet code {
display: inline-block;
margin-bottom: 4px;
padding: 3px 10px;
border-radius: 12px;
font-family: 'Fira Mono', Consolas, Monaco, monospace;
font-size: 0.88em;
font-weight: 600;
}
.dynamic-cheatsheet code.bse-chip-include {
background: #d1fae5 !important;
border: 1px solid #6ee7b7;
color: #065f46 !important;
}
.dynamic-cheatsheet code.bse-chip-exclude {
background: #fee2e2 !important;
border: 1px solid #fca5a5;
color: #991b1b !important;
}
.dynamic-cheatsheet code.bse-chip-meta {
background: #fef9c3 !important;
border: 1px solid #fde68a;
color: #854d0e !important;
}
/* Fallback for unclassified code */
.dynamic-cheatsheet code:not([class]) {
background: #edf2f7 !important;
border: 1px solid #e2e8f0;
color: #2d3748 !important;
}
.dynamic-cheatsheet table {
width: 100%;
border-collapse: collapse;
margin: 0.5em 0;
}
.dynamic-cheatsheet td, .dynamic-cheatsheet th {
padding: 6px 10px;
border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
text-align: left;
}
.dynamic-cheatsheet a {
color: #4a90e2 !important;
}
.modal-doc ul {
margin: 0;
padding: 0;
list-style: none !important;
}
.modal-doc b {
color: inherit;
}
.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: inherit;
}
.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: color-mix(in srgb, currentColor 10%, transparent);
color: inherit;
}
.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%;
}
}
@media (max-width: 600px) {
body#body .bse-search-input {
width: 100vw;
max-width: 100vw;
}
}
.bse-sort-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 18px;
width: 100%;
margin: 8px 0 0 0;
justify-content: flex-start;
}
.bse-sort-select {
appearance: none;
border-radius: 8px;
padding: 8px 32px 8px 14px;
font-size: 0.95em;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent) !important;
background-color: color-mix(in srgb, currentColor 8%, transparent) !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23999' d='M1 1l5 5 5-5'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 10px center !important;
color: inherit !important;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s;
min-width: 8rem;
}
.bse-sort-select:hover {
background-color: color-mix(in srgb, currentColor 18%, transparent) !important;
border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
}
.bse-sort-select:focus {
border-color: #4a90e2 !important;
outline: none !important;
}
.bse-sort-select option {
background: var(--body-background-color, #1a1a2e) !important;
color: var(--body-font-color, inherit) !important;
}
.bse-order-switch {
appearance: none;
border-radius: 8px;
padding: 8px 14px;
font-size: inherit;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent) !important;
background-color: color-mix(in srgb, currentColor 8%, transparent) !important;
color: inherit !important;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s;
font-weight: 500;
white-space: nowrap;
min-width: 6rem;
text-align: center;
}
.bse-order-switch:hover {
background-color: color-mix(in srgb, currentColor 18%, transparent) !important;
border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
}
.bse-order-switch:focus {
border-color: #4a90e2 !important;
outline: none !important;
}
.bse-rating-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 1.08em;
font-weight: 500;
color: inherit;
background: color-mix(in srgb, currentColor 10%, transparent);
border-radius: 10px;
padding: 4px 12px;
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
margin-right: 4px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.bse-rating-checkbox {
accent-color: #4a90e2;
width: 1.1em;
height: 1.1em;
}
.bse-rating-label:hover {
background: #d0eaff;
border-color: #4a90e2;
}
/* Metatag Row */
.bse-metatag-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.bse-metatag-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: inherit;
margin-bottom: 2px;
}
.bse-metatag-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: inherit;
}
.bse-metatag-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.bse-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;
}
.bse-metatag-item {
background: color-mix(in srgb, currentColor 10%, transparent);
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
color: inherit;
}
@media (max-width: 700px) {
.bse-sort-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.bse-metatag-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.bse-metatag-list {
gap: 6px;
}
}
/* Include/Exclude Tag Rows */
.bse-include-row-wrap, .bse-exclude-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.bse-include-row-header, .bse-exclude-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: inherit;
margin-bottom: 2px;
}
.bse-include-toggle, .bse-exclude-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: inherit;
}
.bse-include-toggle:focus, .bse-exclude-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.bse-include-list, .bse-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;
}
.bse-include-item, .bse-exclude-item {
background: color-mix(in srgb, currentColor 10%, transparent);
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
color: inherit;
}
@media (max-width: 700px) {
.bse-include-row-header, .bse-exclude-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.bse-include-list, .bse-exclude-list {
gap: 6px;
}
.bse-all-tags-row-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.bse-all-tags-list {
gap: 6px;
}
}
/* All Tags Row */
.bse-all-tags-row-wrap {
width: 100%;
margin: 10px 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.bse-all-tags-row-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: 1.08em;
font-weight: 600;
color: inherit;
margin-bottom: 2px;
}
.bse-all-tags-toggle {
border-radius: 10px;
padding: 4px 16px;
font-size: 1em;
border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
font-weight: 500;
color: inherit;
}
.bse-all-tags-toggle:focus {
border-color: #4a90e2;
outline: none;
}
.bse-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: inherit;
cursor: pointer;
}
.settings-label input[type="checkbox"] {
accent-color: #4a90e2;
width: 1.2em;
height: 1.2em;
}
/* Cheat sheet info bar */
.cheatsheet-info-bar {
margin-top: 16px;
padding: 12px;
background: #f0f4fa;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.cheatsheet-info-bar a {
color: #4a90e2;
text-decoration: none;
font-weight: 500;
}
.cheatsheet-info-bar a:hover {
text-decoration: underline;
}
.cheatsheet-cache-info {
font-size: 0.9em;
color: #666;
}
.cheatsheet-flush-btn {
border-radius: 12px;
padding: 6px 16px;
font-size: 0.9em;
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
cursor: pointer;
transition: all 0.2s;
}
.cheatsheet-flush-btn:hover {
border-color: color-mix(in srgb, currentColor 45%, transparent);
background: color-mix(in srgb, currentColor 18%, transparent);
}
/* e621 autocomplete dropdown */
.bse-ac-dropdown {
display: none;
position: absolute;
z-index: 99999;
background: #1f3c67;
border: 1px solid #2a5a8a;
max-height: 400px;
overflow-y: auto;
list-style: none;
margin: 0;
padding: 0;
min-width: 300px;
}
.bse-ac-dropdown li {
padding: 4px 8px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
/* Wiki search */
.bse-wiki-search-wrap {
display: flex;
justify-content: center;
margin: 20px auto;
max-width: 600px;
padding: 0 20px;
}
.bse-wiki-search-wrap form {
display: flex;
gap: 10px;
width: 100%;
align-items: center;
}
`;
document.head.appendChild(style);
}
// --- Main Entrypoint ---
let initialized = false;
function init() {
if (initialized) return;
injectStyles();
let setupSuccess = false;
if (isWikiPage) setupSuccess = setupWiki();
else if (siteConfig.selectors) setupSuccess = setupSite();
else setupSuccess = setupGeneric();
if (setupSuccess !== false) initialized = true;
}
// --- 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);
})();