Sleazy Fork is available in English.
Full Feature Set: Sorter, Mass Downloader, Focus Editor. Works on R34, Gelbooru, Safebooru, Realbooru, Xbooru, TBIB, Yande.re, Konachan, Rule34.us, E621.
// ==UserScript==
// @name R34 Download Sorter
// @namespace http://tampermonkey.net/
// @Author Silk
// @version 6.21
// @description Full Feature Set: Sorter, Mass Downloader, Focus Editor. Works on R34, Gelbooru, Safebooru, Realbooru, Xbooru, TBIB, Yande.re, Konachan, Rule34.us, E621.
// @match https://rule34.xxx/*
// @match https://safebooru.org/*
// @match https://gelbooru.com/*
// @match https://realbooru.com/*
// @match https://xbooru.com/*
// @match https://tbib.org/*
// @match https://yande.re/*
// @match https://konachan.com/*
// @match https://konachan.net/*
// @match https://rule34.us/*
// @match https://e621.net/*
// @match https://e926.net/*
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_info
// ==/UserScript==
(function() {
'use strict';
// --- DEFAULTS ---
const DEFAULT_ROOT = "Booru_Downloads";
const DEFAULT_RULES = `invisible_woman = Marvel/Invisible Woman`;
const DEFAULT_FILENAME = "%artist% - %md5%.%ext%";
const DEFAULT_SERIES_MAP = "overwatch_2 = Overwatch";
const DEFAULT_CUSTOM_PATH = "%root%/%type%/%series%/%char%/%filename%";
const DEFAULT_IGNORED_SERIES = "blizzard_entertainment\nnintendo\noriginal\nart_stream\nvideo_game_mechanics";
const DEFAULT_IGNORED_CHARACTERS = "human\nelf\ndwarf\nunknown_character\noriginal_character\navatar_(player)";
const DEFAULT_IGNORED_ARTISTS = "unknown_artist\nanonymous\ncommentary\nvoice_actor\neditor\ntranslator";
const DEFAULT_TAGS_3D = "3d_animation\n3d_(artwork)\n3d\nsource_filmmaker\nsfm\nblender\ndaz_studio\ndaz3d\ndaz\nmikumikudance\nmmd\nxnalara\nkoikatsu\nhoney_select\nhoney_select_2\nvirt_a_mate\nvam\nunreal_engine\nunity\nmaya\ncinema_4d\n3d_scan";
// --- HELPERS ---
const normalizeTag = (str) => str.trim().toLowerCase().replace(/ /g, '_').replace(/[:\\/]/g, '');
const stripSlashes = (str) => str.replace(/\//g, '').trim();
function toTitleCase(str) {
return str.replace(/_/g, ' ').replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
function sanitizeFilename(str) {
if (!str) return "";
return str
.replace(/[\\/:*?"<>|]/g, '')
.replace(/\s+/g, ' ') // Collapse multiple spaces
.replace(/^\.+|\.+$/g, '') // Remove leading/trailing periods
.trim();
}
function getIdFromUrl(href) {
if (!href) return null;
const idMatch = href.match(/(?:id=|posts\/show\/|posts\/|r=posts\/view&id=)(\d+)/);
if (idMatch && idMatch[1]) return idMatch[1];
if (href.includes('/post/show/')) return href.split('/post/show/')[1].split('/')[0].split('?')[0];
if (href.includes('/index.php/') && href.split('/').length > 0) return href.split('/').pop();
return null;
}
function isMoebooru() {
const h = window.location.hostname;
return h.includes('yande.re') || h.includes('konachan');
}
function isRule34Us() {
return window.location.hostname.includes('rule34.us');
}
function isE621() {
return window.location.hostname.includes('e621.net') || window.location.hostname.includes('e926.net');
}
// --- DATA MANAGEMENT ---
function loadRules() {
const raw = GM_getValue('tag_rules', DEFAULT_RULES);
const groups = {};
if (!raw) return groups;
const lines = raw.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line.startsWith('#')) continue;
const parts = line.split('=');
if (parts.length === 2) {
const tag = parts[0].trim();
const folder = parts[1].trim();
if (!groups[folder]) groups[folder] = [];
if (groups[folder].indexOf(tag) === -1) groups[folder].push(tag);
}
}
return groups;
}
function saveRules(groups) {
let lines = [];
const sortedKeys = Object.keys(groups).sort();
for (const folder of sortedKeys) {
groups[folder].forEach(tag => lines.push(`${tag} = ${folder}`));
}
GM_setValue('tag_rules', lines.join('\n'));
}
function loadSeriesMap() {
const raw = GM_getValue('series_map', DEFAULT_SERIES_MAP);
const map = {};
if (!raw) return map;
raw.split('\n').forEach(line => {
const parts = line.split('=');
if (parts.length === 2) map[parts[0].trim()] = parts[1].trim();
});
return map;
}
function saveSeriesMap(mapStr) { GM_setValue('series_map', mapStr); }
function getSeriesAlias(tag) { return loadSeriesMap()[tag] || null; }
function getTagsForSeries(seriesName) {
const map = loadSeriesMap();
return Object.keys(map).filter(tag => map[tag] === seriesName);
}
function updateSeriesAliases(seriesName, newTagsArray) {
const map = loadSeriesMap();
Object.keys(map).forEach(key => { if (map[key] === seriesName) delete map[key]; });
newTagsArray.forEach(tag => { const clean = normalizeTag(tag); if (clean) map[clean] = seriesName; });
const lines = [];
Object.entries(map).forEach(([tag, series]) => lines.push(`${tag} = ${series}`));
saveSeriesMap(lines.join('\n'));
}
function loadList(key, def) {
return new Set(GM_getValue(key, def).split('\n').map(t => normalizeTag(t)).filter(t => t.length > 0));
}
function loadIgnoredSeries() { return loadList('ignored_series', DEFAULT_IGNORED_SERIES); }
function loadIgnoredCharacters() { return loadList('ignored_characters', DEFAULT_IGNORED_CHARACTERS); }
function loadIgnoredArtists() { return loadList('ignored_artists', DEFAULT_IGNORED_ARTISTS); }
function load3DTags() { return loadList('tags_3d', DEFAULT_TAGS_3D); }
// --- LOGIC: MERGE & RENAME ---
function renameUniverse(oldName, newName) {
const rules = loadRules();
const newRules = {};
let changed = false;
Object.entries(rules).forEach(([path, tags]) => {
let finalPath = path;
const parts = path.split('/');
const currentUni = parts.length > 1 ? parts[0] : "Uncategorized";
if (currentUni === oldName) {
const remainder = parts.slice(1).join('/');
finalPath = newName ? `${newName}/${remainder}` : remainder;
changed = true;
}
if (newRules[finalPath]) {
tags.forEach(t => { if (!newRules[finalPath].includes(t)) newRules[finalPath].push(t); });
} else { newRules[finalPath] = tags; }
});
if (changed && newName) {
const seriesTags = getTagsForSeries(oldName);
if(seriesTags.length > 0) updateSeriesAliases(newName, seriesTags);
saveRules(newRules);
}
return changed;
}
function updateEntryPath(oldPath, newPath) {
const rules = loadRules();
if (!rules[oldPath]) return false;
const tagsToMove = rules[oldPath];
delete rules[oldPath];
if (rules[newPath]) {
tagsToMove.forEach(t => { if (!rules[newPath].includes(t)) rules[newPath].push(t); });
} else { rules[newPath] = tagsToMove; }
saveRules(rules);
return true;
}
function updateTagsForPath(path, newTagArray) {
const rules = loadRules();
if(rules[path]) {
const cleanTags = [...new Set(newTagArray.map(t => normalizeTag(t)).filter(t => t.length > 0))];
rules[path] = cleanTags;
saveRules(rules);
}
}
function removeTagFromPath(path, tagToRemove) {
const rules = loadRules();
if(rules[path]) {
rules[path] = rules[path].filter(t => t !== tagToRemove);
if(rules[path].length === 0) delete rules[path];
saveRules(rules);
}
}
function promoteTagToSubfolder(currentPath, tagToPromote, newSubfolderName) {
const rules = loadRules();
if(!rules[currentPath]) return;
rules[currentPath] = rules[currentPath].filter(t => t !== tagToPromote);
if(rules[currentPath].length === 0) delete rules[currentPath];
const newPath = currentPath + "/" + newSubfolderName;
if(!rules[newPath]) rules[newPath] = [];
rules[newPath].push(tagToPromote);
saveRules(rules);
}
// --- SCRAPERS & INFO ---
function getPageMD5(doc = document) {
const sidebar = doc.querySelector('#tag-sidebar, #tag-list, .sidebar, .content_left, .tag-list-left, #post-information');
if (sidebar) {
const textMatch = sidebar.textContent.match(/md5:\s*([a-f0-9]{32})/i);
if (textMatch && textMatch[1]) return textMatch[1].toLowerCase();
}
if (isE621()) {
const metaMD5 = doc.querySelector('meta[property="og:image:url"], meta[property="og:video:url"]');
if (metaMD5 && metaMD5.content) {
const urlParts = metaMD5.content.split('/');
const filename = urlParts[urlParts.length - 1];
const possibleMd5 = filename.split('.')[0];
if (/^[a-f0-9]{32}$/i.test(possibleMd5)) return possibleMd5.toLowerCase();
}
}
const links = doc.querySelectorAll('a[href*="/image/"], a[href*="/file/"], a[href*="id="]');
for (let link of links) {
const filename = link.href.split('/').pop();
const possibleMd5 = filename.split('.')[0];
if (/^[a-f0-9]{32}$/i.test(possibleMd5)) return possibleMd5.toLowerCase();
}
return null;
}
function isDownloaded(md5) {
if (!GM_getValue('enable_md5_tracking', true)) return false;
return md5 && GM_getValue('md5_history', []).includes(md5);
}
function addToHistory(md5) {
if (!md5) return;
if (!GM_getValue('enable_md5_tracking', true)) return;
const history = GM_getValue('md5_history', []);
if (!history.includes(md5)) {
history.push(md5);
GM_setValue('md5_history', history);
}
}
function getTagsByType(type, doc = document) {
let selector = `.tag-type-${type} a, li.tag-type-${type} a`;
if (type === 'character') selector += `, a.model`;
if (isRule34Us()) selector += `, li.${type}-tag a`;
const allLinks = doc.querySelectorAll(selector);
const cleanTags = [];
// E621 specific data attributes with URI decoding fix
if (isE621()) {
let e621Type = type;
if(type === 'general') e621Type = 'general';
if(type === 'copyright') e621Type = 'copyright';
if(type === 'character') e621Type = 'character';
if(type === 'artist') e621Type = 'artist';
if(type === 'metadata') e621Type = 'meta';
const e621Items = doc.querySelectorAll(`li.tag-${e621Type}`);
e621Items.forEach(li => {
if (li.dataset.name) cleanTags.push(normalizeTag(decodeURIComponent(li.dataset.name)));
});
if(cleanTags.length > 0) return cleanTags;
}
allLinks.forEach(link => {
if (link.href && link.href.includes('/wiki/')) return;
const text = link.textContent.trim();
if (text === '?' || text === '+' || text === '-' || text.length === 0) return;
const tagMatch = text.match(/^(.*?)( \(\d+\))?$/);
if (tagMatch && tagMatch[1]) {
cleanTags.push(tagMatch[1]);
}
});
return cleanTags;
}
function getAllTags(doc = document) {
// E621 specific extraction via data-attributes with URI decoding
if (isE621()) {
const listItems = doc.querySelectorAll('li.tag-list-item');
if (listItems.length > 0) {
return Array.from(listItems).map(li => normalizeTag(decodeURIComponent(li.dataset.name))).filter(t => t);
}
}
const els = doc.querySelectorAll(
'#tag-sidebar li a, #tag-list li a, .sidebar li a, ' +
'.content_left li a, .tag-list-left li a, ' +
'#post-information li a, ' +
'.tag-type-general a, .tag-type-artist a, .tag-type-character a, .tag-type-copyright a, .tag-type-metadata a'
);
return [...new Set(Array.from(els).map(el => {
const text = el.textContent.trim();
const tagMatch = text.match(/^(.*?)( \(\d+\))?$/);
if (tagMatch && tagMatch[1] && tagMatch[1] !== '+' && tagMatch[1] !== '-') {
return normalizeTag(tagMatch[1]);
}
return '';
}).filter(t => t.length > 1))];
}
function getAllTagsWithTypes(doc = document) {
const results = [];
const types = ['character', 'copyright', 'artist', 'metadata', 'general'];
types.forEach(type => {
const rawTags = getTagsByType(type, doc);
rawTags.forEach(text => {
results.push({ text: text, normalized: normalizeTag(text), type: type });
});
});
return results;
}
function getAllCharacterTagsOnPage(doc = document) {
const raw = getTagsByType('character', doc).map(t => normalizeTag(t));
const ignored = loadIgnoredCharacters();
return raw.filter(t => !ignored.has(t));
}
function getAllCharacterTagsRaw(doc = document) {
const pageCharTags = getTagsByType('character', doc).map(t => normalizeTag(t));
const ignoredChars = loadIgnoredCharacters();
return pageCharTags.filter(tag => !ignoredChars.has(tag));
}
function getUnknownCharacterTags(doc = document) {
const rules = loadRules();
const pageCharTags = getAllCharacterTagsRaw(doc);
const ignoredChars = loadIgnoredCharacters();
const knownTags = new Set();
const wildcardRules = [];
Object.values(rules).forEach(list => {
list.forEach(t => {
if (t.includes('*')) wildcardRules.push(t); else knownTags.add(t);
});
});
return pageCharTags.filter(tag => {
if (ignoredChars.has(tag)) return false;
if (knownTags.has(tag)) return false;
for (const rule of wildcardRules) {
const regexStr = '^' + rule.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$';
if (new RegExp(regexStr).test(tag)) return false;
}
return true;
});
}
function detectImageType(doc) {
const allTags = getAllTagsWithTypes(doc).map(t => t.normalized);
const tags3d = load3DTags();
if(allTags.some(t => tags3d.has(t))) return "3D";
return "2D";
}
function getAllSeriesMatches(doc = document) {
const rawCopyrights = getTagsByType('copyright', doc);
const blocked = loadIgnoredSeries();
const matches = new Set();
for (let raw of rawCopyrights) {
let tag = normalizeTag(raw);
if (blocked.has(tag)) continue;
const mapped = getSeriesAlias(tag);
if (mapped) {
matches.add(mapped);
} else {
const cleanName = sanitizeFilename(stripSlashes(toTitleCase(tag)));
if (cleanName.length > 0) matches.add(cleanName);
}
}
return Array.from(matches);
}
function getBestSeriesMatch(doc = document) {
const matches = getAllSeriesMatches(doc);
return matches.length > 0 ? matches[0] : null;
}
// --- LOGIC: TARGETING ---
function getTargetFolder(doc = document) {
const rules = loadRules();
const charTags = getAllCharacterTagsRaw(doc);
const mappedChars = new Set();
let matched = new Set();
for (const [folder, tags] of Object.entries(rules)) {
let ruleMatchFound = false;
tags.forEach(t => {
if(charTags.includes(t)) { mappedChars.add(t); ruleMatchFound = true; }
else if (t.includes('*')) {
const regexStr = '^' + t.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$';
const regex = new RegExp(regexStr);
const matches = charTags.filter(charTag => regex.test(charTag));
if (matches.length > 0) { matches.forEach(m => mappedChars.add(m)); ruleMatchFound = true; }
}
});
if (ruleMatchFound) matched.add(folder);
}
const unmappedChars = charTags.filter(c => !mappedChars.has(c));
let arr = Array.from(matched);
arr = arr.filter(path => !arr.some(otherPath => otherPath !== path && otherPath.startsWith(path + "/")));
arr.sort();
if (arr.length > 0) {
let finalPath = "";
let seriesRep = "";
if (arr.length === 1) {
const parts = arr[0].split('/'); seriesRep = parts.length > 1 ? parts[0] : ""; finalPath = arr[0];
} else {
const splitPaths = arr.map(p => p.split('/'));
const uniqueUniverses = new Set(); const names = [];
splitPaths.forEach(parts => {
if (parts.length > 1) { uniqueUniverses.add(parts[0]); names.push(parts.slice(1).join(' ')); } else { names.push(parts[0]); }
});
const sortedUniverses = Array.from(uniqueUniverses).sort();
const sortedNames = names.sort().join(' & ');
if (sortedUniverses.length > 0) { finalPath = `${sortedUniverses.join(' & ')}/${sortedNames}`; } else { finalPath = sortedNames; }
seriesRep = sortedUniverses.length > 0 ? sortedUniverses.join(' & ') : "";
}
return { name: finalPath, type: "match", series: seriesRep, unmapped: unmappedChars };
}
const uncategorizedMode = GM_getValue('uncategorized_mode', 'default');
const autoSeries = getBestSeriesMatch(doc);
const bestChar = charTags.length > 0 ? charTags.map(c => toTitleCase(c)).join(' & ') : null;
if (uncategorizedMode === 'artist') {
const ignoredArtists = loadIgnoredArtists();
const artists = getTagsByType('artist', doc).map(a => normalizeTag(a)).filter(a => !ignoredArtists.has(a));
const artistName = artists.length > 0 ? toTitleCase(artists[0]) : GM_getValue('fallback_artist_name', 'Unknown Artist');
return { name: bestChar ? stripSlashes(bestChar) : stripSlashes(artistName), type: 'auto', series: stripSlashes(artistName) };
}
if (autoSeries) {
let folderName = autoSeries;
if(bestChar) folderName += "/" + stripSlashes(bestChar);
return { name: folderName, type: "auto", series: autoSeries };
}
if (bestChar) { return { name: stripSlashes(bestChar), type: "auto", series: "" }; }
return { name: "Uncategorized", type: "none", series: "" };
}
function generateFilename(doc, id, md5) {
const pattern = GM_getValue('filename_pattern', DEFAULT_FILENAME);
const ignoredArtists = loadIgnoredArtists();
const artists = getTagsByType('artist', doc).filter(a => !ignoredArtists.has(normalizeTag(a)));
const characters = getTagsByType('character', doc); const copyrights = getTagsByType('copyright', doc);
const fallbackArtist = GM_getValue('fallback_artist_name', 'Unknown Artist');
const artistStr = artists.length > 0 ? toTitleCase(artists[0]) : fallbackArtist;
const charStr = characters.length > 0 ? toTitleCase(characters[0]) : "Unknown Character";
const copyStr = copyrights.length > 0 ? toTitleCase(copyrights[0]) : "Unknown Series";
let name = pattern.replace(/%artist%/g, sanitizeFilename(artistStr))
.replace(/%character%/g, sanitizeFilename(charStr))
.replace(/%copyright%/g, sanitizeFilename(copyStr))
.replace(/%id%/g, id)
.replace(/%md5%/g, md5 || 'unknown_md5')
.replace(/%ext%/g, "%ext%");
return sanitizeFilename(name);
}
function downloadImage(characterFolder, force = false, manualType = null, forceSeries = false) {
const currentMD5 = getPageMD5();
if (!force && currentMD5 && isDownloaded(currentMD5)) { if (!confirm("File in history. Redownload?")) return; }
let fileUrl = "";
const originalLink = document.querySelector(
'a#image-download-link, a.image-download-link, ' +
'a[onclick*="javascript:show_original_image"], ' +
'a[href*="/image/"], a[href*="/file/"], ' +
'li a[href*=".png"], li a[href*=".jpg"], li a[href*=".jpeg"], li a[href*=".gif"], li a[href*=".webm"], li a[href*=".mp4"]'
);
if (originalLink && originalLink.href && !originalLink.href.includes('/wiki/')) fileUrl = originalLink.href;
if (!fileUrl && isRule34Us()) {
const r34UsOriginal = document.querySelector('a[href*="/images/"][href$=".png"], a[href*="/images/"][href$=".jpg"], a[href*="/images/"][href$=".gif"]');
if (r34UsOriginal && r34UsOriginal.textContent.trim().toLowerCase().includes('original')) fileUrl = r34UsOriginal.href;
}
if (!fileUrl && isE621()) {
const e621OriginalLink = document.querySelector('li#post-file-size a, #raw_image_container > a');
if (e621OriginalLink && e621OriginalLink.href) fileUrl = e621OriginalLink.href;
}
if (!fileUrl) {
const mainImage = document.querySelector('#image, #main_image, #img, .image-body img, #img-display');
if (mainImage && mainImage.src) fileUrl = mainImage.src;
}
if (!fileUrl) {
const mainVideo = document.querySelector('video#image, video.image-body, video#gelcomVideoPlayer');
if (mainVideo) {
const source = mainVideo.querySelector('source');
if (source && source.src) fileUrl = source.src;
else if (mainVideo.src) fileUrl = mainVideo.src;
}
}
if (!fileUrl) return alert("No image or video found on this page.");
const info = getTargetFolder();
let root = GM_getValue('root_folder', DEFAULT_ROOT);
const host = window.location.hostname;
let sepSub = null;
if (host.includes('realbooru') && GM_getValue('sep_realbooru', false)) sepSub = "Realbooru";
else if (host.includes('safebooru') && GM_getValue('sep_safebooru', false)) sepSub = "Safebooru";
else if (host.includes('gelbooru') && GM_getValue('sep_gelbooru', false)) sepSub = "Gelbooru";
else if (host.includes('rule34.xxx') && GM_getValue('sep_rule34', false)) sepSub = "Rule34";
else if (host.includes('xbooru') && GM_getValue('sep_xbooru', false)) sepSub = "Xbooru";
else if (host.includes('tbib') && GM_getValue('sep_tbib', false)) sepSub = "TBIB";
else if (host.includes('yande') && GM_getValue('sep_yande', false)) sepSub = "Yande";
else if (host.includes('konachan') && GM_getValue('sep_konachan', false)) sepSub = "Konachan";
else if (host.includes('rule34.us') && GM_getValue('sep_rule34us', false)) sepSub = "Rule34us";
else if (isE621() && GM_getValue('sep_e621', false)) sepSub = "E621";
if (sepSub) {
root = root.replace(/\/$/, '') + '/' + sepSub;
}
const urlObj = new URL(fileUrl);
const ext = urlObj.pathname.split('.').pop().split('?')[0];
const id = new URLSearchParams(window.location.search).get('id') || getIdFromUrl(window.location.href);
let filename = generateFilename(document, id, currentMD5);
filename = filename.replace('%ext%', ext.toLowerCase());
if(!filename.endsWith('.'+ext.toLowerCase())) filename += '.'+ext.toLowerCase();
let finalPath = "";
if (host.includes('realbooru')) {
const charNameOnly = info.name.split('/').pop();
finalPath = `${root}/${charNameOnly}/${filename}`;
} else {
const useCustom = GM_getValue('enable_custom_path', false);
let typeVal = manualType || detectImageType(document);
if (useCustom) {
const pattern = GM_getValue('custom_path_pattern', DEFAULT_CUSTOM_PATH);
let seriesVal = info.series || "";
if (pattern.includes('%series%') && !seriesVal) { seriesVal = GM_getValue('fallback_series_name', '_Unsorted'); }
let charPart = characterFolder;
if (seriesVal && charPart.startsWith(seriesVal + '/')) { charPart = charPart.substring(seriesVal.length + 1); }
let raw = pattern.replace(/%root%/g, root).replace(/%type%/g, typeVal).replace(/%series%/g, seriesVal).replace(/%char%/g, charPart).replace(/%filename%/g, filename);
finalPath = raw.replace(/\/+/g, '/').replace(/\/$/, '');
} else {
const subMode = GM_getValue('subfolder_mode', 'none');
const structMode = GM_getValue('folder_structure', 'full');
const orderMode = GM_getValue('order_mode', 'path_first');
let finalFolder = characterFolder;
if (!forceSeries) {
if (structMode === 'flat') { const parts = characterFolder.split('/'); finalFolder = parts[parts.length - 1]; }
else if (structMode === 'nested') { const parts = finalFolder.split('/'); if (parts.length > 1) { finalFolder = parts.slice(1).join('/'); } }
}
let typeFolder = ""; if (subMode === 'split') typeFolder = "/" + typeVal;
if (orderMode === 'type_first' && typeFolder) { finalPath = `${root}${typeFolder}/${finalFolder}/${filename}`; }
else { finalPath = `${root}/${finalFolder}${typeFolder}/${filename}`; }
}
}
const dlBtn = document.getElementById('r34-dl-btn'); if (dlBtn) dlBtn.innerText = '⏳ Downloading...';
GM_download({
url: fileUrl, name: finalPath, saveAs: false, conflictAction: 'overwrite',
onload: () => { if (currentMD5) addToHistory(currentMD5); if(dlBtn) { dlBtn.innerText = '✔ Saved'; dlBtn.style.backgroundColor = '#28a745'; setTimeout(updateMainButton, 2000); } },
onerror: (e) => {
console.error(e);
if(dlBtn) { dlBtn.innerText = '❌ Error'; dlBtn.style.backgroundColor = 'red'; }
alert(`Download Error: ${e.error || 'Unknown'}`);
}
});
}
function downloadFromDocument(doc, item, onComplete) {
const info = getTargetFolder(doc);
let root = GM_getValue('root_folder', DEFAULT_ROOT);
const host = window.location.hostname;
let sepSub = null;
if (host.includes('realbooru') && GM_getValue('sep_realbooru', false)) sepSub = "Realbooru";
else if (host.includes('safebooru') && GM_getValue('sep_safebooru', false)) sepSub = "Safebooru";
else if (host.includes('gelbooru') && GM_getValue('sep_gelbooru', false)) sepSub = "Gelbooru";
else if (host.includes('rule34.xxx') && GM_getValue('sep_rule34', false)) sepSub = "Rule34";
else if (host.includes('xbooru') && GM_getValue('sep_xbooru', false)) sepSub = "Xbooru";
else if (host.includes('tbib') && GM_getValue('sep_tbib', false)) sepSub = "TBIB";
else if (host.includes('yande') && GM_getValue('sep_yande', false)) sepSub = "Yande";
else if (host.includes('konachan') && GM_getValue('sep_konachan', false)) sepSub = "Konachan";
else if (host.includes('rule34.us') && GM_getValue('sep_rule34us', false)) sepSub = "Rule34us";
else if (isE621() && GM_getValue('sep_e621', false)) sepSub = "E621";
if (sepSub) {
root = root.replace(/\/$/, '') + '/' + sepSub;
}
const urlObj = new URL(item.fileUrl);
const ext = urlObj.pathname.split('.').pop().split('?')[0];
let filename = generateFilename(doc, item.id, item.md5);
filename = filename.replace('%ext%', ext.toLowerCase());
if(!filename.endsWith('.'+ext.toLowerCase())) filename += '.'+ext.toLowerCase();
let finalPath = "";
if (host.includes('realbooru')) {
const charNameOnly = info.name.split('/').pop();
finalPath = `${root}/${charNameOnly}/${filename}`;
} else {
const useCustom = GM_getValue('enable_custom_path', false);
let typeVal = item.manualType || item.autoType || detectImageType(doc);
if (useCustom) {
const pattern = GM_getValue('custom_path_pattern', DEFAULT_CUSTOM_PATH);
let seriesVal = info.series || "";
if (pattern.includes('%series%') && !seriesVal) { seriesVal = GM_getValue('fallback_series_name', '_Unsorted'); }
let charPart = info.name;
if (seriesVal && charPart.startsWith(seriesVal + '/')) { charPart = charPart.substring(seriesVal.length + 1); }
let raw = pattern.replace(/%root%/g, root).replace(/%type%/g, typeVal).replace(/%series%/g, seriesVal).replace(/%char%/g, charPart).replace(/%filename%/g, filename);
finalPath = raw.replace(/\/+/g, '/').replace(/\/$/, '');
} else {
const subMode = GM_getValue('subfolder_mode', 'none');
const structMode = GM_getValue('folder_structure', 'full');
const orderMode = GM_getValue('order_mode', 'path_first');
let finalFolder = info.name;
if (structMode === 'flat') { const parts = finalFolder.split('/'); finalFolder = parts[parts.length - 1]; }
else if (structMode === 'nested') { const parts = finalFolder.split('/'); if (parts.length > 1) { finalFolder = parts.slice(1).join('/'); } }
let typeFolder = ""; if (subMode === 'split') typeFolder = "/" + typeVal;
if (orderMode === 'type_first' && typeFolder) { finalPath = `${root}${typeFolder}/${finalFolder}/${filename}`; }
else { finalPath = `${root}/${finalFolder}${typeFolder}/${filename}`; }
}
}
GM_download({
url: item.fileUrl, name: finalPath, saveAs: false, conflictAction: 'overwrite',
onload: () => { addToHistory(item.md5); if(onComplete) onComplete(true); },
onerror: (e) => { if(onComplete) onComplete(false); }
});
}
// --- FOCUS EDITOR ---
function openFocusEditor(item, doc, onSave, onSingleDownload, navigation) {
const overlay = document.createElement('div');
Object.assign(overlay.style, { position:'fixed', top:'0', left:'0', width:'100%', height:'100%', backgroundColor:'rgba(0,0,0,0.95)', zIndex:'10020', display:'flex' });
const cleanupAndClose = () => { document.removeEventListener('keydown', handleKeys); document.body.removeChild(overlay); };
const handleKeys = (e) => {
const activeTag = document.activeElement.tagName; const isInput = activeTag === 'INPUT' || activeTag === 'TEXTAREA';
if (e.key === 'Escape') { cleanupAndClose(); }
else if (e.code === 'Space' && !isInput) { e.preventDefault(); if (mediaNode && mediaNode.tagName === 'VIDEO') { mediaNode.paused ? mediaNode.play() : mediaNode.pause(); } }
else if (!isInput && navigation) {
if (e.key === 'ArrowRight' && navigation.hasNext) { cleanupAndClose(); navigation.goNext(); }
else if (e.key === 'ArrowLeft' && navigation.hasPrev) { cleanupAndClose(); navigation.goPrev(); }
}
};
document.addEventListener('keydown', handleKeys);
const sidebar = document.createElement('div');
Object.assign(sidebar.style, { width:'380px', backgroundColor:'#1a1a1a', borderRight:'1px solid #444', display:'flex', flexDirection:'column', padding:'20px', gap:'15px', overflowY:'auto' });
const headerRow = document.createElement('div');
Object.assign(headerRow.style, { display:'flex', justifyContent:'space-between', alignItems:'center', backgroundColor: 'transparent' });
const titleSpan = document.createElement('h3'); titleSpan.innerText = "Focus Editor";
Object.assign(titleSpan.style, { color: '#ddd', margin: '0', backgroundColor: 'transparent' });
headerRow.appendChild(titleSpan);
const settingsBtn = document.createElement('button'); settingsBtn.innerText = "⚙";
Object.assign(settingsBtn.style, { background:'none', border:'none', color:'#888', cursor:'pointer', fontSize:'16px', marginLeft:'10px' });
settingsBtn.onclick = () => { openSettingsMenu(); };
headerRow.appendChild(settingsBtn);
if(navigation) {
const navSpan = document.createElement('span'); navSpan.innerText = `Post ${navigation.current + 1} / ${navigation.total}`; navSpan.style.color = '#777'; navSpan.style.fontSize = '12px'; navSpan.style.marginLeft = 'auto'; headerRow.appendChild(navSpan);
}
sidebar.appendChild(headerRow);
const pathPreview = document.createElement('div');
Object.assign(pathPreview.style, { fontSize:'11px', color:'#f39c12', border:'1px dashed #444', padding:'8px', borderRadius:'4px', wordBreak:'break-word', backgroundColor:'#222' });
const refreshPathPreview = () => { const info = getTargetFolder(doc); pathPreview.innerHTML = `<strong>Current Path Result:</strong><br>${info.name}`; };
refreshPathPreview(); sidebar.appendChild(pathPreview);
const bestSeries = getBestSeriesMatch(doc) || "";
const allChars = getAllCharacterTagsRaw(doc);
const unknowns = getUnknownCharacterTags(doc);
const rules = loadRules();
const mappedTags = new Set();
const wildcardRules = [];
Object.values(rules).forEach(tags => { tags.forEach(t => { if (t.includes('*')) wildcardRules.push(t); else mappedTags.add(t); }); });
const unmappedChars = allChars.filter(tag => { if (mappedTags.has(tag)) return false; for (const rule of wildcardRules) { const regexStr = '^' + rule.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$'; if (new RegExp(regexStr).test(tag)) return false; } return true; });
let defaultName = "";
let activeTags = [];
if (unmappedChars.length > 0) { defaultName = toTitleCase(unmappedChars[0]); activeTags = [...unmappedChars]; }
else if (allChars.length > 0) { defaultName = toTitleCase(allChars[0]); activeTags = [...allChars]; }
else { activeTags = [...unknowns]; }
const inputContainer = document.createElement('div');
Object.assign(inputContainer.style, { borderTop:'1px solid #444', paddingTop:'15px', backgroundColor:'transparent' });
const rulePreview = document.createElement('div');
Object.assign(rulePreview.style, { fontSize:'11px', color:'#00bc8c', marginBottom:'10px', fontStyle:'italic', backgroundColor:'transparent' });
inputContainer.appendChild(rulePreview);
const updateLiveRule = () => {
let u = stripSlashes(uniInput.value.trim()); let f = stripSlashes(nameInput.value.trim());
if(f) { const full = u ? `${u}/${f}` : f; rulePreview.innerHTML = `New Rule Target: <strong>${full}</strong>`; }
else { rulePreview.innerHTML = `New Rule Target: (Type a name)`; }
};
const uniGroup = document.createElement('div'); Object.assign(uniGroup.style, { marginBottom:'10px', backgroundColor:'transparent' });
const uniLabel = document.createElement('div'); uniLabel.innerText = "Universe / Copyright"; uniLabel.style.color = '#888'; uniLabel.style.fontSize = '12px'; uniLabel.style.backgroundColor = 'transparent';
const uniWrapper = document.createElement('div'); Object.assign(uniWrapper.style, { display:'flex', gap:'5px', backgroundColor:'transparent' });
const uniInput = document.createElement('input'); uniInput.placeholder = "Universe"; uniInput.value = bestSeries;
Object.assign(uniInput.style, { flex:'1', padding:'8px', backgroundColor:'#111', color:'#fff', border:'1px solid #555', borderRadius:'4px' });
uniInput.onkeyup = updateLiveRule;
const autoBtn = document.createElement('button'); autoBtn.innerText = "↻"; autoBtn.title = "Cycle Auto";
Object.assign(autoBtn.style, { padding:'0 12px', backgroundColor:'#444', color:'white', border:'none', cursor:'pointer', borderRadius:'4px' });
const matches = getAllSeriesMatches(doc); let autoIdx = 0;
autoBtn.onclick = () => { if(matches.length){ uniInput.value = stripSlashes(matches[autoIdx++ % matches.length]); updateLiveRule(); } };
uniWrapper.appendChild(uniInput); uniWrapper.appendChild(autoBtn); uniGroup.appendChild(uniLabel); uniGroup.appendChild(uniWrapper); inputContainer.appendChild(uniGroup);
const nameGroup = document.createElement('div'); Object.assign(nameGroup.style, { marginBottom:'10px', backgroundColor:'transparent' });
const nameLabel = document.createElement('div'); nameLabel.innerText = "Character Name"; nameLabel.style.color = '#888'; nameLabel.style.fontSize = '12px'; nameLabel.style.backgroundColor = 'transparent';
const nameInput = document.createElement('input'); nameInput.placeholder = "Character Name"; nameInput.value = defaultName;
Object.assign(nameInput.style, { width:'100%', padding:'8px', backgroundColor:'#111', color:'#fff', border:'1px solid #555', borderRadius:'4px', boxSizing:'border-box' });
nameInput.onkeyup = updateLiveRule;
nameGroup.appendChild(nameLabel); nameGroup.appendChild(nameInput);
const allPageCharacters = getAllCharacterTagsOnPage(doc);
if (allPageCharacters.length > 0) {
const charShortcutContainer = document.createElement('div');
Object.assign(charShortcutContainer.style, { display:'flex', flexWrap:'wrap', gap:'5px', marginTop:'5px', backgroundColor:'transparent' });
allPageCharacters.forEach(char => {
const btn = document.createElement('button');
btn.innerText = toTitleCase(char);
const isUnmapped = unmappedChars.includes(char);
const bgCol = isUnmapped ? '#d35400' : '#27ae60';
Object.assign(btn.style, { fontSize:'10px', padding:'2px 6px', backgroundColor:bgCol, color:'white', border:'none', borderRadius:'3px', cursor:'pointer' });
btn.title = "Set Name + Add Tag";
btn.onclick = () => { nameInput.value = toTitleCase(char); if (!activeTags.includes(char)) { activeTags.push(char); renderPills(); } updateLiveRule(); };
charShortcutContainer.appendChild(btn);
});
nameGroup.appendChild(charShortcutContainer);
}
inputContainer.appendChild(nameGroup);
const tagsGroup = document.createElement('div'); Object.assign(tagsGroup.style, { marginBottom:'15px', backgroundColor:'transparent' });
const tagsLabel = document.createElement('div'); tagsLabel.innerText = "Tags to Map"; tagsLabel.style.color = '#888'; tagsLabel.style.fontSize = '12px'; tagsLabel.style.backgroundColor = 'transparent';
const pillsArea = document.createElement('div'); Object.assign(pillsArea.style, { display:'flex', flexDirection:'column', gap:'5px', backgroundColor:'transparent' });
const activeTagsContainer = document.createElement('div');
Object.assign(activeTagsContainer.style, { minHeight:'40px', padding:'5px', backgroundColor:'#1a1a1a', border:'1px solid #28a745', borderRadius:'4px', display:'flex', flexWrap:'wrap', gap:'4px' });
const poolTagsContainer = document.createElement('div');
Object.assign(poolTagsContainer.style, { minHeight:'40px', maxHeight:'150px', overflowY:'auto', padding:'5px', backgroundColor:'#111', border:'1px solid #444', borderRadius:'4px', display:'flex', flexWrap:'wrap', gap:'4px' });
const renderPills = () => {
activeTagsContainer.innerHTML = ''; poolTagsContainer.innerHTML = '';
if(activeTags.length === 0) activeTagsContainer.innerHTML = '<span style="color:#555; font-size:10px; font-style:italic; padding:5px;">No tags selected...</span>';
activeTags.forEach((tag, idx) => {
const pill = document.createElement('span');
Object.assign(pill.style, { backgroundColor:'#1e7e34', color:'white', fontSize:'11px', padding:'2px 6px', borderRadius:'3px', cursor:'pointer', display:'flex', alignItems:'center', border:'1px solid #28a745' });
pill.innerHTML = `${tag} <span style="font-weight:bold; margin-left:5px;">×</span>`;
pill.onclick = () => { activeTags.splice(idx, 1); renderPills(); };
activeTagsContainer.appendChild(pill);
});
const pool = allPageCharacters.filter(t => !activeTags.includes(t));
pool.forEach(tag => {
const pill = document.createElement('span');
Object.assign(pill.style, { backgroundColor:'#333', color:'#ccc', fontSize:'11px', padding:'2px 6px', borderRadius:'3px', cursor:'pointer', border:'1px solid #444' });
pill.innerText = tag;
pill.onclick = () => { activeTags.push(tag); renderPills(); };
poolTagsContainer.appendChild(pill);
});
};
const manualInput = document.createElement('input');
manualInput.placeholder = 'Type tag manually & Enter...';
Object.assign(manualInput.style, { border:'1px solid #444', background:'#111', color:'#fff', fontSize:'12px', padding:'4px', width:'100%' });
manualInput.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); const val = manualInput.value.trim().replace(/,/g, ''); if (val && !activeTags.includes(val)) { activeTags.push(val); manualInput.value = ''; renderPills(); } } };
renderPills();
pillsArea.appendChild(activeTagsContainer); pillsArea.appendChild(manualInput);
const poolLabel = document.createElement('div'); poolLabel.innerText = "Available Characters (Click to Add)"; poolLabel.style.fontSize = '10px'; poolLabel.style.color = '#777'; poolLabel.style.marginTop = '5px'; poolLabel.style.backgroundColor = 'transparent';
pillsArea.appendChild(poolLabel); pillsArea.appendChild(poolTagsContainer);
tagsGroup.appendChild(tagsLabel); tagsGroup.appendChild(pillsArea); inputContainer.appendChild(tagsGroup);
const existingRulesContainer = document.createElement('div');
Object.assign(existingRulesContainer.style, { backgroundColor:'#222', border:'1px solid #444', padding:'10px', borderRadius:'4px', marginBottom: '15px' });
const existingHeader = document.createElement('div');
existingHeader.innerHTML = "<strong>Active Rules on this Image:</strong>";
existingHeader.style.marginBottom = '5px'; existingHeader.style.fontSize = '12px'; existingHeader.style.color = '#ccc'; existingHeader.style.backgroundColor = 'transparent';
existingRulesContainer.appendChild(existingHeader);
const rulesList = document.createElement('div');
Object.assign(rulesList.style, { display:'flex', flexDirection:'column', gap:'5px', backgroundColor:'transparent' });
existingRulesContainer.appendChild(rulesList);
sidebar.appendChild(existingRulesContainer);
const refreshMatchedRules = () => {
rulesList.innerHTML = ''; const rules = loadRules(); const pageTags = getAllTags(doc); const matchesByFolder = {};
Object.entries(rules).forEach(([folder, tags]) => {
const intersect = tags.filter(t => pageTags.includes(t));
if (intersect.length > 0) { if(!matchesByFolder[folder]) matchesByFolder[folder] = []; intersect.forEach(t => matchesByFolder[folder].push(t)); }
tags.forEach(ruleTag => {
if (ruleTag.includes('*')) {
const regexStr = '^' + ruleTag.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$';
const regex = new RegExp(regexStr);
const wildcardMatches = pageTags.filter(pt => regex.test(pt));
if (wildcardMatches.length > 0) { if(!matchesByFolder[folder]) matchesByFolder[folder] = []; wildcardMatches.forEach(wm => { const displayStr = `${wm} (via ${ruleTag})`; if (!matchesByFolder[folder].includes(displayStr)) matchesByFolder[folder].push(displayStr); }); }
}
});
});
const folderKeys = Object.keys(matchesByFolder);
if (folderKeys.length > 0) {
folderKeys.forEach(folder => {
const matchedTags = [...new Set(matchesByFolder[folder])].sort().join(', ');
const row = document.createElement('div');
Object.assign(row.style, { display:'flex', justifyContent:'space-between', alignItems:'center', fontSize:'11px', backgroundColor:'#333', padding:'5px', borderRadius:'3px' });
const infoSpan = document.createElement('span');
infoSpan.innerHTML = `<span style="color:#8fd3ff; margin-right:5px; word-break:break-word;">${matchedTags}</span> <span style="color:#aaa; white-space:nowrap;">→</span> <span style="color:#f39c12; margin-left:5px;">${folder}</span>`;
infoSpan.style.backgroundColor = 'transparent';
const editRuleBtn = document.createElement('span'); editRuleBtn.innerText = '✎ Edit';
Object.assign(editRuleBtn.style, { marginLeft:'8px', cursor:'pointer', color:'#aaa', border:'1px solid #555', padding:'2px 5px', borderRadius:'3px', fontSize:'10px', backgroundColor:'transparent' });
editRuleBtn.onclick = (e) => {
e.stopPropagation();
const currentRawTags = rules[folder] || [];
row.innerHTML = '';
Object.assign(row.style, { flexDirection: 'column', alignItems: 'stretch', gap: '5px', padding: '8px' });
const tagLabel = document.createElement('div'); tagLabel.innerText = "Tags (comma separated):"; tagLabel.style.color = '#888'; tagLabel.style.fontSize = '9px';
const tagInput = document.createElement('input'); tagInput.value = currentRawTags.join(', ');
Object.assign(tagInput.style, { width: '100%', backgroundColor: '#111', color: '#8fd3ff', border: '1px solid #555', fontSize: '11px', padding: '4px', boxSizing:'border-box' });
const folderLabel = document.createElement('div'); folderLabel.innerText = "Target Folder:"; folderLabel.style.color = '#888'; tagLabel.style.fontSize = '9px';
const pathInput = document.createElement('input'); pathInput.value = folder;
Object.assign(pathInput.style, { width: '100%', backgroundColor: '#111', color: '#f39c12', border: '1px solid #555', fontSize: '11px', padding: '4px', boxSizing:'border-box' });
const btnDiv = document.createElement('div'); Object.assign(btnDiv.style, { display: 'flex', gap: '5px', justifyContent: 'flex-end', marginTop:'5px' });
const saveInline = document.createElement('button'); saveInline.innerText = '💾 Save';
Object.assign(saveInline.style, { backgroundColor: '#28a745', color: 'white', border: 'none', padding: '3px 10px', borderRadius: '3px', cursor: 'pointer', fontSize: '10px' });
const cancelInline = document.createElement('button'); cancelInline.innerText = '✘ Cancel';
Object.assign(cancelInline.style, { backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '3px 10px', borderRadius: '3px', cursor: 'pointer', fontSize: '10px' });
saveInline.onclick = (evt) => {
evt.stopPropagation(); const newTagsStr = tagInput.value.trim(); const newPathStr = pathInput.value.trim();
if (newTagsStr && newPathStr) { if (folder !== newPathStr) delete rules[folder]; const newTags = newTagsStr.split(',').map(t => normalizeTag(t)).filter(t => t); rules[newPathStr] = newTags; saveRules(rules); refreshMatchedRules(); refreshPathPreview(); }
};
cancelInline.onclick = (evt) => { evt.stopPropagation(); refreshMatchedRules(); };
btnDiv.appendChild(cancelInline); btnDiv.appendChild(saveInline);
row.appendChild(tagLabel); row.appendChild(tagInput); row.appendChild(folderLabel); row.appendChild(pathInput); row.appendChild(btnDiv); tagInput.focus();
};
row.appendChild(infoSpan); row.appendChild(editRuleBtn); rulesList.appendChild(row);
});
} else { rulesList.innerHTML = '<span style="color:#666; font-style:italic; fontSize:11px; background-color:transparent;">No saved rules match this image.</span>'; }
};
refreshMatchedRules(); sidebar.appendChild(inputContainer); updateLiveRule();
const saveBtn = document.createElement('button'); saveBtn.innerText = "Save Rule";
Object.assign(saveBtn.style, { width:'100%', padding:'12px', backgroundColor:'#28a745', color:'white', border:'none', cursor:'pointer', fontWeight:'bold', borderRadius:'4px' });
saveBtn.onclick = () => {
let u = stripSlashes(uniInput.value.trim()); let f = stripSlashes(nameInput.value.trim());
const finalTags = activeTags;
if(f && finalTags.length > 0) {
if (u) f = `${u}/${f}`;
const groups = loadRules(); if(!groups[f]) groups[f] = [];
finalTags.forEach(rawTag => { const clean = normalizeTag(rawTag); if(clean && !groups[f].includes(clean)) groups[f].push(clean); });
saveRules(groups);
saveBtn.innerText = "✔ Rule Saved!"; saveBtn.style.backgroundColor = '#1e7e34';
setTimeout(() => { saveBtn.innerText = "Save Rule"; saveBtn.style.backgroundColor = '#28a745'; }, 1500);
refreshMatchedRules(); refreshPathPreview(); if(onSave) onSave();
}
};
sidebar.appendChild(saveBtn);
const typeGroup = document.createElement('div'); Object.assign(typeGroup.style, { marginTop:'15px', backgroundColor:'transparent' });
const typeLabel = document.createElement('div'); typeLabel.innerText = "Force Type (This Image)"; typeLabel.style.color = '#888'; typeLabel.style.fontSize = '12px'; typeLabel.style.backgroundColor = 'transparent';
const typeWrapper = document.createElement('div'); Object.assign(typeWrapper.style, { display:'flex', gap:'5px', backgroundColor:'transparent' });
let manualType = item.manualType || null; const autoPrediction = detectImageType(doc);
const makeTypeBtn = (txt, val, isAuto) => {
const b = document.createElement('button');
if(isAuto) b.innerText = `Auto (${autoPrediction})`; else b.innerText = txt;
const isActive = (manualType === val);
Object.assign(b.style, { flex:'1', padding:'6px', backgroundColor: isActive ? '#007bff' : '#333', color:'white', border:'1px solid #444', cursor:'pointer', borderRadius:'4px', fontSize:'11px' });
b.onclick = () => { manualType = val; item.manualType = manualType; typeWrapper.innerHTML = ''; typeWrapper.appendChild(makeTypeBtn("Auto", null, true)); typeWrapper.appendChild(makeTypeBtn("2D", "2D", false)); typeWrapper.appendChild(makeTypeBtn("3D", "3D", false)); if(onSave) onSave(); };
return b;
};
typeWrapper.appendChild(makeTypeBtn("Auto", null, true)); typeWrapper.appendChild(makeTypeBtn("2D", "2D", false)); typeWrapper.appendChild(makeTypeBtn("3D", "3D", false));
typeGroup.appendChild(typeLabel); typeGroup.appendChild(typeWrapper); sidebar.appendChild(typeGroup);
const dlBtn = document.createElement('button'); dlBtn.innerText = "⬇ Download This Image";
Object.assign(dlBtn.style, { width:'100%', padding:'10px', backgroundColor:'#17a2b8', color:'white', border:'none', cursor:'pointer', fontWeight:'bold', borderRadius:'4px', marginTop:'15px' });
dlBtn.onclick = () => { onSingleDownload(item); }; sidebar.appendChild(dlBtn);
const closeBtn = document.createElement('button'); closeBtn.innerText = "Done / Close (Esc)";
Object.assign(closeBtn.style, { width:'100%', padding:'10px', backgroundColor:'#dc3545', color:'white', border:'none', cursor:'pointer', borderRadius:'4px', marginTop:'auto' });
closeBtn.onclick = cleanupAndClose; sidebar.appendChild(closeBtn);
overlay.appendChild(sidebar);
const imgContainer = document.createElement('div');
Object.assign(imgContainer.style, { flex:'1', display:'flex', justifyContent:'center', alignItems:'center', overflow:'hidden', padding:'20px', backgroundColor:'#000' });
let mediaNode;
const mainVideo = doc.querySelector('video#image, video.image-body, video#gelcomVideoPlayer');
const mainImage = doc.querySelector('img#image, img#main_image, img#img, .image-body img, #img-display');
if (mainVideo) {
mediaNode = document.createElement('video');
mediaNode.controls = true; mediaNode.autoplay = true; mediaNode.loop = true;
const source = mainVideo.querySelector('source');
if (source && source.src) mediaNode.src = source.src;
else if (mainVideo.src) mediaNode.src = mainVideo.src;
Object.assign(mediaNode.style, { maxHeight:'95%', maxWidth:'95%', boxShadow:'0 0 20px black' });
} else if (mainImage) {
mediaNode = document.createElement('img');
mediaNode.src = mainImage.src;
Object.assign(mediaNode.style, { maxHeight:'95%', maxWidth:'95%', objectFit:'contain', boxShadow:'0 0 20px black' });
} else {
mediaNode = document.createElement('img');
mediaNode.src = item.thumbUrl;
Object.assign(mediaNode.style, { maxHeight:'95%', maxWidth:'95%', objectFit:'contain', boxShadow:'0 0 20px black' });
}
imgContainer.appendChild(mediaNode); overlay.appendChild(imgContainer);
const rightSidebar = document.createElement('div');
Object.assign(rightSidebar.style, { width:'220px', backgroundColor:'#1a1a1a', borderLeft:'1px solid #444', display:'flex', flexDirection:'column', padding:'10px', overflowY:'auto' });
rightSidebar.innerHTML = `<h4 style="color:#ddd; margin:10px 0; background-color: transparent;">Post Tags</h4>`;
const allTagsWithTypes = getAllTagsWithTypes(doc);
allTagsWithTypes.forEach(t => {
const tagSpan = document.createElement('div'); tagSpan.innerText = t.text;
let col = '#888'; if(t.type === 'character') col = '#00aa00'; else if(t.type === 'copyright') col = '#dd00dd'; else if(t.type === 'artist') col = '#cc0000';
Object.assign(tagSpan.style, { fontSize:'12px', color:col, padding:'4px', cursor:'pointer', borderBottom:'1px solid #222', backgroundColor: '#1a1a1a' });
tagSpan.onmouseenter = () => { tagSpan.style.backgroundColor = '#333'; }; tagSpan.onmouseleave = () => { tagSpan.style.backgroundColor = '#1a1a1a'; };
tagSpan.onclick = () => { if(!activeTags.includes(t.normalized)) { activeTags.push(t.normalized); renderPills(); } };
rightSidebar.appendChild(tagSpan);
});
overlay.appendChild(rightSidebar); document.body.appendChild(overlay);
}
// --- MASS DOWNLOADER ---
function openMassDownloader() {
const overlay = document.createElement('div'); overlay.id = 'r34-mass-overlay';
Object.assign(overlay.style, { position:'fixed', top:'0', left:'0', width:'100%', height:'100%', backgroundColor:'rgba(0,0,0,0.9)', zIndex:'10001', display:'flex', justifyContent:'center', alignItems:'center' });
const container = document.createElement('div');
Object.assign(container.style, { backgroundColor:'#222', color:'#fff', width:'98%', height:'95%', borderRadius:'8px', display:'flex', flexDirection:'column', border:'1px solid #444' });
const header = document.createElement('div');
Object.assign(header.style, { padding:'15px', borderBottom:'1px solid #444', display:'flex', justifyContent:'space-between', alignItems:'center' });
header.innerHTML = `<span style="font-size:18px; font-weight:bold">Page Downloader</span>`;
const controls = document.createElement('div');
Object.assign(controls.style, { display:'flex', alignItems:'center' });
const statusSpan = document.createElement('span');
Object.assign(statusSpan.style, { marginRight:'20px', color:'#f39c12', fontWeight:'bold' });
const rescanBtn = document.createElement('button'); rescanBtn.innerText = "Rescan Page";
Object.assign(rescanBtn.style, { marginRight:'10px', padding:'5px 15px', cursor:'pointer', backgroundColor:'#007bff', color:'white', border:'none', borderRadius:'3px' });
const forceContainer = document.createElement('div');
Object.assign(forceContainer.style, { display:'flex', alignItems:'center', marginRight:'15px', fontSize:'11px', cursor:'pointer', backgroundColor:'#333', padding:'4px 8px', borderRadius:'3px', border:'1px solid #444' });
const forceChk = document.createElement('input'); forceChk.type = 'checkbox'; forceChk.id = 'r34-force-dl';
Object.assign(forceChk.style, { marginRight:'5px', cursor:'pointer' });
const forceLabel = document.createElement('label'); forceLabel.htmlFor = 'r34-force-dl'; forceLabel.innerText = "Include Library"; forceLabel.style.cursor = 'pointer';
forceContainer.appendChild(forceChk); forceContainer.appendChild(forceLabel);
const selAllBtn = document.createElement('button'); selAllBtn.innerText = "Select All";
Object.assign(selAllBtn.style, { marginRight:'5px', padding:'5px 10px', cursor:'pointer', backgroundColor:'#444', color:'white', border:'none', borderRadius:'3px', fontSize:'11px' });
const deselBtn = document.createElement('button'); deselBtn.innerText = "Deselect All";
Object.assign(deselBtn.style, { marginRight:'15px', padding:'5px 10px', cursor:'pointer', backgroundColor:'#444', color:'white', border:'none', borderRadius:'3px', fontSize:'11px' });
const dlSelBtn = document.createElement('button'); dlSelBtn.innerText = "Download Selected (0)";
Object.assign(dlSelBtn.style, { marginRight:'10px', padding:'5px 15px', cursor:'pointer', backgroundColor:'#17a2b8', color:'white', border:'none', borderRadius:'3px', opacity:'0.5', pointerEvents:'none' });
const dlAllBtn = document.createElement('button'); dlAllBtn.innerText = "Download ALL (0)";
Object.assign(dlAllBtn.style, { marginRight:'10px', padding:'5px 15px', cursor:'pointer', backgroundColor:'#28a745', color:'white', border:'none', borderRadius:'3px', opacity:'0.5', pointerEvents:'none' });
const closeBtn = document.createElement('button'); closeBtn.innerText = "Close";
Object.assign(closeBtn.style, { padding:'5px 15px', cursor:'pointer', backgroundColor:'#dc3545', color:'white', border:'none', borderRadius:'3px' });
closeBtn.onclick = () => document.body.removeChild(overlay);
controls.appendChild(statusSpan); controls.appendChild(rescanBtn); controls.appendChild(forceContainer); controls.appendChild(selAllBtn); controls.appendChild(deselBtn); controls.appendChild(dlSelBtn); controls.appendChild(dlAllBtn); controls.appendChild(closeBtn);
header.appendChild(controls); container.appendChild(header);
const grid = document.createElement('div');
Object.assign(grid.style, { flex:'1', overflowY:'auto', padding:'20px', display:'grid', gridTemplateColumns:'repeat(7, 1fr)', gap:'10px', alignContent:'start' });
container.appendChild(grid); overlay.appendChild(container); document.body.appendChild(overlay);
const items = [];
const executeDownload = (item, onComplete) => { downloadFromDocument(item.doc, item, onComplete); };
const updateSelectionCount = () => {
const selCount = items.filter(i => i.selected && i.status === 'ready').length;
const readyCount = items.filter(i => i.status === 'ready').length;
dlSelBtn.innerText = `Download Selected (${selCount})`; dlAllBtn.innerText = `Download ALL (${readyCount})`;
dlSelBtn.style.opacity = selCount > 0 ? '1' : '0.5'; dlSelBtn.style.pointerEvents = selCount > 0 ? 'auto' : 'none';
dlAllBtn.style.opacity = readyCount > 0 ? '1' : '0.5'; dlAllBtn.style.pointerEvents = readyCount > 0 ? 'auto' : 'none';
};
selAllBtn.onclick = () => { items.forEach(i => { if(i.status === 'ready') { i.selected = true; i.checkbox.checked = true; }}); updateSelectionCount(); };
deselBtn.onclick = () => { items.forEach(i => { i.selected = false; i.checkbox.checked = false; }); updateSelectionCount(); };
const updateItemStatus = (item, info, isDownloaded) => {
item.info = info;
const typeBadge = item.autoType === '3D' ? ' <span style="color:#d6aaff; font-weight:bold;">[3D]</span>' : ' <span style="color:#aaa;">[2D]</span>';
const force = forceChk.checked;
if (isDownloaded && !force) { item.statusText.innerText = "✔ In Library"; item.statusText.style.color = '#50fa7b'; item.status = 'done'; item.checkbox.style.display = 'none'; item.selected = false; }
else {
item.status = 'ready'; item.checkbox.style.display = 'block';
if (isDownloaded && force) { item.statusText.innerHTML = `↻ Redownload${typeBadge}`; item.statusText.style.color = '#ffc107'; }
else if (info.type === 'auto') { item.statusText.innerHTML = `${info.name}${typeBadge}`; item.statusText.style.color = '#fd7e14'; if(!item.selected) { item.selected = true; item.checkbox.checked = true; } }
else if (info.type === 'match') { item.statusText.innerHTML = `${info.name}${typeBadge}`; item.statusText.style.color = '#007bff'; if(info.unmapped && info.unmapped.length > 0) { item.statusText.innerHTML += ` <span style="color:#d35400; font-weight:bold;">+ ${info.unmapped.map(toTitleCase).join(', ')}</span>`; } if(!item.selected) { item.selected = true; item.checkbox.checked = true; } }
else { item.statusText.innerHTML = `Uncategorized${typeBadge}`; item.statusText.style.color = '#777'; item.selected = false; item.checkbox.checked = false; }
}
item.statusText.title = info.name; updateSelectionCount();
};
forceChk.onchange = () => { items.forEach(i => updateItemStatus(i, i.info, i.isDownloaded)); };
const launchEditorForItem = (item) => {
const idx = items.indexOf(item);
openFocusEditor(item, item.doc, () => { const newInfo = getTargetFolder(item.doc); updateItemStatus(item, newInfo, item.isDownloaded); if(newInfo.type === 'match' && !item.isDownloaded) { item.selected = true; item.checkbox.checked = true; updateSelectionCount(); } }, (itm) => { item.statusText.innerText = "DL Single..."; executeDownload(itm, (success) => { if(success) { item.statusText.innerText = "✔ Saved"; item.statusText.style.color = '#50fa7b'; item.isDownloaded = true; item.status = 'done'; item.checkbox.style.display = 'none'; item.selected = false; updateSelectionCount(); } else { item.statusText.innerText = "Error"; } }); }, { current: idx, total: items.length, hasNext: (idx < items.length - 1), hasPrev: (idx > 0), goNext: () => launchEditorForItem(items[idx+1]), goPrev: () => launchEditorForItem(items[idx-1]) });
};
const runScan = async () => {
statusSpan.innerText = "Scanning..."; grid.innerHTML = ''; items.length = 0;
const thumbs = document.querySelectorAll(
'a.thumb, .thumb a, .thumbnail-preview a, ' + // General boorus
'div[style*="height: 200px;"] > a, ' + // Rule34.us specific thumbnail container
'a[href*="r=posts/view"][id], ' + // Rule34.us, check for link with ID and view param
'article.thumbnail > a' // E621 specific thumbnail classes
);
if(thumbs.length === 0) { statusSpan.innerText = "No images found."; return; }
for(let thumb of thumbs) {
let a = (thumb.tagName === 'A') ? thumb : thumb.querySelector('a');
// For rule34.us snippet, 'a' is directly inside a div, not necessarily 'thumb'
if (!a && (thumb.tagName === 'DIV' || thumb.tagName === 'ARTICLE')) {
a = thumb.querySelector('a[href*="id="], a[href*="/post/show/"], a[href*="r=posts/view"]');
}
if(!a || (!a.href.includes('id=') && !a.href.includes('/post/show/') && !a.href.includes('r=posts/view') && !a.href.match(/\/posts\/\d+/))) continue;
const id = getIdFromUrl(a.href); if (!id) continue;
const img = thumb.querySelector('img'); const thumbUrl = img ? img.src : '';
const card = document.createElement('div'); Object.assign(card.style, { display:'flex', flexDirection:'column', backgroundColor:'#333', borderRadius:'4px', overflow:'hidden', boxShadow:'0 2px 5px rgba(0,0,0,0.5)', transition:'transform 0.1s', cursor:'pointer', height:'100%', position:'relative' });
card.onmouseenter = () => card.style.transform = 'scale(1.03)'; card.onmouseleave = () => card.style.transform = 'scale(1)';
const chk = document.createElement('input'); chk.type = 'checkbox'; Object.assign(chk.style, { position:'absolute', top:'5px', left:'5px', zIndex:'10', width:'18px', height:'18px', cursor:'pointer' });
chk.onclick = (e) => { e.stopPropagation(); items.find(i=>i.id===id).selected = chk.checked; updateSelectionCount(); }; card.appendChild(chk);
const imgDiv = document.createElement('div'); Object.assign(imgDiv.style, { width:'100%', height:'220px', overflow:'hidden', backgroundColor:'#000' });
const cardImg = document.createElement('img'); cardImg.src = thumbUrl; Object.assign(cardImg.style, { width:'100%', height:'100%', objectFit:'contain' }); imgDiv.appendChild(cardImg);
const infoDiv = document.createElement('div'); Object.assign(infoDiv.style, { padding:'5px', fontSize:'10px', textAlign:'center', backgroundColor:'#222', borderTop:'1px solid #444', minHeight:'35px', display:'flex', flexDirection:'column', justifyContent:'center' });
const statusText = document.createElement('div'); statusText.innerText = "Waiting..."; statusText.style.color = '#777'; statusText.style.whiteSpace = 'nowrap'; statusText.style.overflow = 'hidden'; statusText.style.textOverflow = 'ellipsis'; infoDiv.appendChild(statusText);
card.appendChild(imgDiv); card.appendChild(infoDiv); grid.appendChild(card);
items.push({ id, card, status: 'waiting', statusText, thumbUrl, fileUrl: null, selected: false, checkbox: chk });
}
let processed = 0;
for (let item of items) {
if(!document.getElementById('r34-mass-overlay')) break;
item.status = 'fetching'; item.statusText.innerText = "Fetching...";
try {
let fetchUrl = `${window.location.origin}/index.php?page=post&s=view&id=${item.id}`;
if (isMoebooru()) { fetchUrl = `${window.location.origin}/post/show/${item.id}`; }
else if (isRule34Us()) { fetchUrl = `${window.location.origin}/index.php?r=posts/view&id=${item.id}`; }
else if (isE621()) { fetchUrl = `${window.location.origin}/posts/${item.id}`; }
const response = await fetch(fetchUrl); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html");
item.doc = doc;
item.md5 = getPageMD5(doc);
item.isDownloaded = isDownloaded(item.md5);
item.autoType = detectImageType(doc);
// --- ADDED THIS LINE TO FIX SCAN ERROR ---
const info = getTargetFolder(doc);
// ----------------------------------------
let tempFileUrl = "";
const originalLink = doc.querySelector('a#image-download-link, a.image-download-link, a[onclick*="javascript:show_original_image"], a[href*="/image/"], a[href*="/file/"]');
if (originalLink && originalLink.href && !originalLink.href.includes('/wiki/')) tempFileUrl = originalLink.href;
if (!tempFileUrl && isRule34Us()) {
const r34UsOriginal = doc.querySelector('a[href*="/images/"][href$=".png"], a[href*="/images/"][href$=".jpg"], a[href*="/images/"][href$=".gif"]');
if (r34UsOriginal && r34UsOriginal.textContent.trim().toLowerCase().includes('original')) {
tempFileUrl = r34UsOriginal.href;
}
}
if (!tempFileUrl && isE621()) {
const e621OriginalLink = doc.querySelector('li#post-file-size a, #raw_image_container > a');
if (e621OriginalLink && e621OriginalLink.href) tempFileUrl = e621OriginalLink.href;
}
if (!tempFileUrl) {
const mainImage = doc.querySelector('#image, #main_image, #img, .image-body img, #img-display');
if (mainImage && mainImage.src) tempFileUrl = mainImage.src;
}
if (!tempFileUrl) {
const mainVideo = doc.querySelector('video#image, video.image-body, video#gelcomVideoPlayer');
if (mainVideo) {
const source = mainVideo.querySelector('source');
if (source && source.src) tempFileUrl = source.src;
else if (mainVideo.src) tempFileUrl = mainVideo.src;
}
}
item.fileUrl = tempFileUrl;
item.card.onclick = (e) => { e.stopPropagation(); launchEditorForItem(item); }; updateItemStatus(item, info, item.isDownloaded);
} catch (e) { console.error(`R34 Script: Error fetching post ${item.id}:`, e); item.statusText.innerText = "Error"; item.statusText.style.color = "red"; }
processed++; statusSpan.innerText = `Scanned: ${processed}/${items.length}`;
const sDelay = parseInt(GM_getValue('scan_delay', 500));
await new Promise(r => setTimeout(r, sDelay));
}
statusSpan.innerText = `Ready: ${items.length}`;
};
rescanBtn.onclick = runScan;
dlSelBtn.onclick = () => { const toDl = items.filter(i => i.selected && i.status === 'ready' && i.fileUrl); if(toDl.length === 0) return; if(!confirm(`Download ${toDl.length} selected images?`)) return; startDownloadLoop(toDl); };
dlAllBtn.onclick = () => { const toDl = items.filter(i => i.status === 'ready' && i.fileUrl); if(toDl.length === 0) return; if(!confirm(`Download ALL ${toDl.length} ready images?`)) return; startDownloadLoop(toDl); };
const startDownloadLoop = (queue) => {
let idx = 0;
const processNext = () => {
if(idx >= queue.length) { dlSelBtn.innerText = "Finished"; dlAllBtn.innerText = "Finished"; return; }
const item = queue[idx]; item.statusText.innerText = "⏳ Downloading..."; item.statusText.style.color = "#ccc";
executeDownload(item, (success) => {
if(success) { item.statusText.innerText = "✔ Saved"; item.statusText.style.color = '#28a745'; item.checkbox.checked = false; item.selected = false; item.checkbox.style.display = 'none'; updateSelectionCount(); } else { item.statusText.innerText = "❌ Error"; item.statusText.style.color = 'red'; }
idx++; const userDelay = parseInt(GM_getValue('mass_dl_delay', 500)); const delay = userDelay + Math.random() * 250; setTimeout(processNext, delay);
});
}; processNext();
};
runScan();
}
// --- MAIN BUTTONS ---
function updateMainButton() {
const sidebar = document.querySelector('#tag-sidebar, #tag-list, .sidebar, .content_left, .tag-list-left, #post-information');
if (!sidebar) return;
const existingContainer = document.getElementById('r34-controls'); if(existingContainer) existingContainer.remove();
const existingMass = document.getElementById('r34-mass-btn'); if(existingMass) existingMass.remove();
const isView = window.location.href.includes('s=view') || window.location.href.includes('/post/show/') || window.location.href.includes('r=posts/view') || window.location.href.match(/\/posts\/\d+/);
const isList = window.location.href.includes('s=list') || document.querySelector('.thumb, .thumbnail-preview, a.thumb') || window.location.href.includes('/post') || window.location.href.includes('r=posts/index') || window.location.href.includes('/posts');
if (isView) {
const info = getTargetFolder(); const currentMD5 = getPageMD5(); const inHistory = isDownloaded(currentMD5);
const controls = document.createElement('div'); controls.id = 'r34-controls';
Object.assign(controls.style, { display: 'flex', gap: '5px', marginBottom: '10px', flexWrap: 'wrap' });
const dlBtn = document.createElement('button'); dlBtn.id = 'r34-dl-btn';
Object.assign(dlBtn.style, { width: '100%', padding: '12px', color: 'white', border: 'none', cursor: 'pointer', fontWeight: 'bold', borderRadius: '4px', marginBottom: '5px' });
dlBtn.onclick = (e) => { e.preventDefault(); downloadImage(info.name, e.shiftKey || inHistory); };
let autoType = "2D"; const allTags = getAllTags(); const tags3d = load3DTags();
if(allTags.some(t => tags3d.has(t))) autoType = "3D";
if (inHistory) { dlBtn.innerText = `✔ In Library (Redownload?)`; dlBtn.style.backgroundColor = '#50fa7b'; dlBtn.style.color = '#222'; }
else {
let displayFolder = info.name;
if (displayFolder.length > 30) displayFolder = displayFolder.substring(0, 27) + "...";
const typeLabel = (GM_getValue('subfolder_mode') === 'split' || GM_getValue('enable_custom_path', false)) ? ` [${autoType}]` : '';
if (info.type === "match") { dlBtn.innerText = `⬇ Save: ${displayFolder}` + typeLabel; dlBtn.style.backgroundColor = '#007bff'; }
else if (info.type === "auto") { dlBtn.innerText = `⬇ Auto: ${displayFolder}` + typeLabel; dlBtn.style.backgroundColor = '#fd7e14'; }
else { dlBtn.innerText = `⬇ Save (Uncategorized)` + typeLabel; dlBtn.style.backgroundColor = '#6c757d'; }
dlBtn.title = `Full Path: ${info.name}`;
}
controls.appendChild(dlBtn);
const preview = document.createElement('div'); preview.innerText = `Folder: ${info.name}`;
Object.assign(preview.style, { fontSize:'10px', color:'#aaa', width:'100%', marginBottom:'5px', wordBreak:'break-all' });
controls.appendChild(preview);
const mkBtn = (txt, col, type, forceS) => {
const b = document.createElement('button'); b.innerText = txt;
Object.assign(b.style, { flex: '1', backgroundColor: col, color: 'white', border: 'none', cursor: 'pointer', borderRadius: '4px', fontSize: '11px', fontWeight: 'bold', height: '30px' });
if(type === autoType && !forceS) b.style.border = '2px solid white';
b.onclick = (e) => { e.preventDefault(); downloadImage(info.name, e.shiftKey, type, forceS); };
return b;
};
if (GM_getValue('subfolder_mode') === 'split' || GM_getValue('enable_custom_path', false)) {
controls.appendChild(mkBtn('2D', '#17a2b8', '2D', false));
controls.appendChild(mkBtn('3D', '#6f42c1', '3D', false));
}
const unknownTags = getUnknownCharacterTags();
if (unknownTags.length > 0) {
const addBtn = document.createElement('button'); addBtn.innerText = '➕';
Object.assign(addBtn.style, { width: '30px', backgroundColor: '#28a745', color: 'white', border: 'none', cursor: 'pointer', borderRadius: '4px', fontSize: '16px', height: '30px' });
addBtn.onclick = (e) => {
e.preventDefault();
const bestSeries = getBestSeriesMatch();
openSettingsMenu(toTitleCase(unknownTags[0]), unknownTags.join(','), bestSeries || "");
};
controls.appendChild(addBtn);
}
const setBtn = document.createElement('button'); setBtn.innerText = '⚙';
Object.assign(setBtn.style, { width: '30px', backgroundColor: '#343a40', color: 'white', border: 'none', cursor: 'pointer', borderRadius: '4px', fontSize: '16px', height: '30px' });
setBtn.onclick = (e) => {
e.preventDefault();
const bestSeries = getBestSeriesMatch() || "";
openSettingsMenu("", "", bestSeries);
};
controls.appendChild(setBtn);
sidebar.insertBefore(controls, sidebar.firstChild);
}
else if (isList && !isView) {
const controls = document.createElement('div');
Object.assign(controls.style, { marginBottom:'10px' });
const massBtn = document.createElement('button');
massBtn.id = 'r34-mass-btn';
massBtn.innerText = "📥 Page Downloader";
Object.assign(massBtn.style, { width: '100%', padding: '10px', color: 'white', border: 'none', cursor: 'pointer', fontWeight: 'bold', borderRadius: '4px', backgroundColor: '#6f42c1' });
massBtn.onclick = (e) => { e.preventDefault(); openMassDownloader(); };
controls.appendChild(massBtn);
sidebar.insertBefore(controls, sidebar.firstChild);
}
}
// --- UI HELPERS & SETTINGS BUILDER ---
function createHelp(tooltipText) { const span = document.createElement('span'); span.innerText = '?'; Object.assign(span.style, { display: 'inline-block', marginLeft: '6px', backgroundColor: '#555', color: '#fff', width: '16px', height: '16px', borderRadius: '50%', textAlign: 'center', fontSize: '11px', lineHeight: '16px', cursor: 'help', fontWeight: 'bold' }); span.title = tooltipText; return span; }
function makeEditable(container, value, placeholder, onSave) { container.innerHTML = ''; const input = document.createElement('input'); input.value = value; input.placeholder = placeholder; Object.assign(input.style, { width: '100%', padding: '2px 5px', fontSize: '13px', backgroundColor: '#111', color: '#fff', border: '1px solid #007bff', borderRadius:'3px' }); const finish = () => onSave(input.value.trim()); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') finish(); if (e.key === 'Escape') onSave(null); }); input.addEventListener('blur', () => finish()); container.appendChild(input); input.focus(); }
function openSettingsMenu(prefillFolder = "", prefillTag = "", prefillCategory = "", onCloseCallback = null) {
const existing = document.getElementById('r34-settings-overlay'); if (existing) document.body.removeChild(existing);
const overlay = document.createElement('div'); overlay.id = 'r34-settings-overlay';
Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.85)', zIndex: '10100', display: 'flex', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(3px)' });
const container = document.createElement('div');
Object.assign(container.style, { backgroundColor: '#222', color: '#fff', width: '1000px', maxWidth: '95%', borderRadius: '10px', boxShadow: '0 10px 50px rgba(0,0,0,0.9)', border: '1px solid #444', display: 'flex', flexDirection: 'column', fontFamily: 'Arial, sans-serif', overflow: 'hidden', height: '90vh' });
const header = document.createElement('div'); header.innerHTML = `<span style="font-size:18px">R34 Sorter Settings</span>`;
Object.assign(header.style, { padding: '15px 20px', backgroundColor: '#007bff', fontWeight: 'bold' }); container.appendChild(header);
const tabNav = document.createElement('div'); Object.assign(tabNav.style, { display:'flex', backgroundColor:'#2c3036', borderBottom:'1px solid #444' });
const btnSettings = document.createElement('button'); btnSettings.innerText = "General Settings";
Object.assign(btnSettings.style, { flex:'1', padding:'10px', background:'#444', border:'none', color:'#fff', cursor:'pointer', fontWeight:'bold', borderBottom:'3px solid #007bff' });
const btnRules = document.createElement('button'); btnRules.innerText = "Rules & Filters";
Object.assign(btnRules.style, { flex:'1', padding:'10px', background:'transparent', border:'none', color:'#aaa', cursor:'pointer', fontWeight:'bold', borderBottom:'3px solid transparent' });
tabNav.appendChild(btnSettings); tabNav.appendChild(btnRules); container.appendChild(tabNav);
const contentArea = document.createElement('div'); Object.assign(contentArea.style, { flex:'1', overflowY:'auto', display:'flex', flexDirection:'column' });
// --- TAB 1: SETTINGS ---
const tab1 = document.createElement('div'); Object.assign(tab1.style, { padding: '20px', display:'grid', gridTemplateColumns:'1fr 1fr', gap:'20px' });
const createSetting = (label, help, configKey, defaultVal, options = null, isArea = false, previewFn = null, isCheckbox = false) => {
const div = document.createElement('div'); const top = document.createElement('div'); top.innerHTML = `<span style="font-size:12px; color:#ccc">${label}</span>`;
if(help) top.appendChild(createHelp(help)); div.appendChild(top); let input;
if(isCheckbox) { const wrapper = document.createElement('div'); Object.assign(wrapper.style, { marginTop:'5px', display:'flex', alignItems:'center', gap:'10px' }); input = document.createElement('input'); input.type = 'checkbox'; input.checked = GM_getValue(configKey, defaultVal); input.onchange = () => { GM_setValue(configKey, input.checked); }; wrapper.appendChild(input); wrapper.appendChild(document.createTextNode(input.checked?"Enabled":"Disabled")); div.appendChild(wrapper); }
else if(options) { input = document.createElement('select'); input.className = 'setting-input'; options.forEach(opt => { const o = document.createElement('option'); o.value = opt.val; o.innerText = opt.text; if(GM_getValue(configKey, defaultVal)===opt.val) o.selected=true; input.appendChild(o); }); input.onchange = () => GM_setValue(configKey, input.value); Object.assign(input.style, { width: '100%', padding: '4px', backgroundColor: '#111', color: '#fff', border:'1px solid #555' }); div.appendChild(input); }
else if(isArea) { input = document.createElement('textarea'); input.className = 'setting-input'; input.value = GM_getValue(configKey, defaultVal); input.rows = 6; input.onchange = () => GM_setValue(configKey, input.value.trim()); Object.assign(input.style, { width: '100%', fontFamily:'monospace', fontSize:'11px', resize:'vertical', backgroundColor: '#111', color: '#fff', border:'1px solid #555' }); div.appendChild(input); }
else { input = document.createElement('input'); input.className = 'setting-input'; input.value = GM_getValue(configKey, defaultVal); input.onchange = () => GM_setValue(configKey, input.value.trim()); if(previewFn) { input.onkeyup = previewFn; } Object.assign(input.style, { width: '100%', padding: '4px', backgroundColor: '#111', color: '#fff', border:'1px solid #555' }); div.appendChild(input); }
if(previewFn) { const p = document.createElement('div'); p.className = 'fn-preview-box'; Object.assign(p.style, {fontSize:'11px', color:'#8fd3ff', marginTop:'3px'}); div.appendChild(p); setTimeout(previewFn, 10); } return div;
};
const fnPreview = () => { const p = document.querySelectorAll('.fn-preview-box')[0]; if(p) p.innerText = "Ex: " + generateFilename(document, '123', 'abc'); };
tab1.appendChild(createSetting("Main Folder", "Location", "root_folder", DEFAULT_ROOT));
tab1.appendChild(createSetting("Structure", "Mode", "folder_structure", "full", [{val:'full', text:'Full'},{val:'nested', text:'Nested'},{val:'flat', text:'Flat'}]));
tab1.appendChild(createSetting("Sort Order", "Order", "order_mode", "path_first", [{val:'path_first', text:'Path > Type'},{val:'type_first', text:'Type > Path'}]));
tab1.appendChild(createSetting("Subfolders", "Split?", "subfolder_mode", "none", [{val:'none', text:'No'},{val:'split', text:'Yes (2D/3D)'}]));
tab1.appendChild(createSetting("Filename", "Pattern", "filename_pattern", DEFAULT_FILENAME, null, false, fnPreview));
tab1.appendChild(createSetting("Batch Delay (ms)", "Time to wait between downloads to prevent bans. (Default: 500)", "mass_dl_delay", 500));
tab1.appendChild(createSetting("Scan Delay (ms)", "Time to wait between fetching image details. (Default: 500)", "scan_delay", 500));
const customPathPreview = () => {
const previewBox = document.querySelector('.custom-path-preview-box'); const patternInput = document.querySelector('input[data-config-key="custom_path_pattern"]');
if (previewBox && patternInput) { const pattern = patternInput.value; let examplePath = pattern.replace(/%root%/g, "Rule34").replace(/%series%/g, "Overwatch").replace(/%type%/g, "2D").replace(/%char%/g, "Widowmaker").replace(/%filename%/g, "12345.jpg"); previewBox.innerText = "Ex: " + examplePath.replace(/\/+/g, '/'); }
};
tab1.appendChild(createSetting("Enable Custom Path", "Overrides all other path settings with the pattern below.", "enable_custom_path", false, null, false, null, true));
const customPathSetting = createSetting("Custom Path Pattern", "Define your own folder structure. Click the buttons below to build your path.", "custom_path_pattern", DEFAULT_CUSTOM_PATH, null, false, customPathPreview);
const inputElement = customPathSetting.querySelector('input.setting-input'); if (inputElement) { inputElement.setAttribute('data-config-key', 'custom_path_pattern'); }
const previewBox = customPathSetting.querySelector('.fn-preview-box'); if (previewBox) { previewBox.className = 'fn-preview-box custom-path-preview-box'; }
const pathBuilderContainer = document.createElement('div');
Object.assign(pathBuilderContainer.style, { display: 'flex', flexWrap: 'wrap', gap: '5px', marginTop: '8px', padding: '8px', backgroundColor: '#1a1a1a', border: '1px solid #444', borderRadius: '4px' });
const placeholders = ['%root%', '%series%', '%type%', '%char%', '%filename%', '/'];
const insertAtCursor = (text) => { if (!inputElement) return; const start = inputElement.selectionStart; const end = inputElement.selectionEnd; const originalValue = inputElement.value; inputElement.value = originalValue.slice(0, start) + text + originalValue.slice(end); inputElement.selectionStart = inputElement.selectionEnd = start + text.length; inputElement.focus(); customPathPreview(); };
placeholders.forEach(placeholder => { const pill = document.createElement('span'); pill.innerText = placeholder; Object.assign(pill.style, { padding: '3px 8px', backgroundColor: placeholder === '/' ? '#555' : '#007bff', color: 'white', borderRadius: '4px', cursor: 'pointer', fontSize: '11px', fontWeight: 'bold', fontFamily: 'monospace' }); pill.title = `Click to add "${placeholder}" to the path`; pill.onclick = () => insertAtCursor(placeholder); pathBuilderContainer.appendChild(pill); });
customPathSetting.appendChild(pathBuilderContainer); tab1.appendChild(customPathSetting);
const uncategorizedSettingsContainer = document.createElement('div');
Object.assign(uncategorizedSettingsContainer, { gridColumn: '1 / -1', borderTop: '1px solid #444', marginTop: '10px', paddingTop: '10px' });
const ucTitle = document.createElement('h4'); ucTitle.innerText = "Uncategorized Image Handling"; Object.assign(ucTitle.style, { marginTop: 0, marginBottom: '10px', color: '#007bff' }); uncategorizedSettingsContainer.appendChild(ucTitle);
const ucSettingsGrid = document.createElement('div'); Object.assign(ucSettingsGrid.style, { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' });
ucSettingsGrid.appendChild(createSetting("Uncategorized Mode", "What to do when an image does not match any of your rules.", "uncategorized_mode", "default", [{val:'default', text:'Save to "Uncategorized" Folder'}, {val:'artist', text:'Save to Artist Folder'}]));
ucSettingsGrid.appendChild(createSetting("Fallback Artist Name", "Folder name to use in 'Artist Mode' when no artist tag is found.", "fallback_artist_name", "Unknown Artist"));
ucSettingsGrid.appendChild(createSetting("Fallback Series Name", "Folder name to use for the %series% placeholder when an image has no series/copyright tag.", "fallback_series_name", "_Unsorted"));
uncategorizedSettingsContainer.appendChild(ucSettingsGrid); tab1.appendChild(uncategorizedSettingsContainer);
const sepContainer = document.createElement('div');
Object.assign(sepContainer.style, { gridColumn: '1 / -1', borderTop: '1px solid #444', marginTop: '10px', paddingTop: '10px' });
const sepTitle = document.createElement('h4'); sepTitle.innerText = "Site Separation (Subfolders)";
Object.assign(sepTitle.style, { marginTop: 0, marginBottom: '5px', color: '#007bff' }); sepContainer.appendChild(sepTitle);
const sepDesc = document.createElement('div'); sepDesc.innerText = "Check specific sites to isolate their downloads into a subfolder (e.g. /Root/Realbooru/...). Unchecked sites will mix into the main root.";
Object.assign(sepDesc.style, { fontSize:'11px', color:'#aaa', marginBottom:'10px' }); sepContainer.appendChild(sepDesc);
const sepGrid = document.createElement('div'); Object.assign(sepGrid.style, { display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:'10px' });
const mkSep = (label, key) => { const w = document.createElement('div'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = GM_getValue(key, false); c.onchange = () => GM_setValue(key, c.checked); const l = document.createElement('span'); l.innerText = label; l.style.marginLeft = '5px'; w.appendChild(c); w.appendChild(l); return w; };
sepGrid.appendChild(mkSep("Separate Realbooru", "sep_realbooru")); sepGrid.appendChild(mkSep("Separate Safebooru", "sep_safebooru")); sepGrid.appendChild(mkSep("Separate Gelbooru", "sep_gelbooru"));
sepGrid.appendChild(mkSep("Separate Rule34", "sep_rule34")); sepGrid.appendChild(mkSep("Separate Xbooru", "sep_xbooru")); sepGrid.appendChild(mkSep("Separate TBIB", "sep_tbib"));
sepGrid.appendChild(mkSep("Separate Yande.re", "sep_yande")); sepGrid.appendChild(mkSep("Separate Konachan", "sep_konachan")); sepGrid.appendChild(mkSep("Separate Rule34.us", "sep_rule34us"));
sepGrid.appendChild(mkSep("Separate E621/E926", "sep_e621"));
sepContainer.appendChild(sepGrid); tab1.appendChild(sepContainer);
const historyContainer = document.createElement('div');
Object.assign(historyContainer.style, { gridColumn: '1 / -1', borderTop: '1px solid #444', marginTop: '10px', paddingTop: '10px', display:'flex', flexDirection:'column', gap:'10px' });
const historyTitle = document.createElement('h4'); historyTitle.innerText = "Download History (MD5 Cache)";
Object.assign(historyTitle.style, { marginTop: 0, marginBottom: '5px', color: '#007bff' }); historyContainer.appendChild(historyTitle);
historyContainer.appendChild(createSetting("Track Download History", "Remember downloaded files to prevent duplicates. Uncheck to disable.", "enable_md5_tracking", true, null, false, null, true));
const clearRow = document.createElement('div');
Object.assign(clearRow.style, { padding: '10px', backgroundColor: '#333', border: '1px solid #444', borderRadius:'4px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' });
const clearLabel = document.createElement('span'); clearLabel.innerHTML = "<strong>Clear History Cache</strong><br><span style='font-size:10px; color:#aaa'>If you deleted files from your disk, click this to reset their 'In Library' status.</span>";
const clearBtn = document.createElement('button'); clearBtn.innerText = "🗑 Clear All History";
Object.assign(clearBtn.style, { backgroundColor: '#d9534f', color: 'white', border: 'none', padding: '8px 15px', borderRadius: '4px', cursor: 'pointer', fontWeight:'bold' });
clearBtn.onclick = () => { const count = GM_getValue('md5_history', []).length; if(confirm(`Are you sure you want to forget ${count} downloaded images?\n\nThis will allow you to download previously saved files again.`)) { GM_setValue('md5_history', []); alert("History Cleared."); clearBtn.innerText = "✔ Cleared"; } };
clearRow.appendChild(clearLabel); clearRow.appendChild(clearBtn); historyContainer.appendChild(clearRow); tab1.appendChild(historyContainer);
contentArea.appendChild(tab1);
// --- TAB 2: RULES ---
const tab2 = document.createElement('div'); Object.assign(tab2.style, { display:'none', flexDirection:'column', height:'100%' });
const profileRow = document.createElement('div');
Object.assign(profileRow.style, { padding: '10px 15px', backgroundColor: '#2a2a2a', borderBottom: '1px solid #444', display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const profileLabel = document.createElement('div'); profileLabel.innerHTML = "<strong>Profile Management</strong> <span style='font-size:11px; color:#888'>(Rules, Filters, Aliases)</span>";
const profileBtnGroup = document.createElement('div'); Object.assign(profileBtnGroup.style, { display: 'flex', gap: '10px' });
const exportBtn = document.createElement('button'); exportBtn.innerText = "⬇ Export Profile";
Object.assign(exportBtn.style, { padding: '5px 10px', backgroundColor: '#17a2b8', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' });
const importBtn = document.createElement('button'); importBtn.innerText = "⬆ Import Profile";
Object.assign(importBtn.style, { padding: '5px 10px', backgroundColor: '#f39c12', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' });
exportBtn.onclick = () => {
const data = { version: "6.2", timestamp: Date.now(), rules: loadRules(), aliases: loadSeriesMap(), filters: { ignored_series: [...loadIgnoredSeries()], ignored_characters: [...loadIgnoredCharacters()], ignored_artists: [...loadIgnoredArtists()], tags_3d: [...load3DTags()] } };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `R34_Profile_${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url);
};
const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.style.display = 'none';
fileInput.onchange = (e) => {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const imported = JSON.parse(ev.target.result);
if (!confirm("Import this profile? \n\nExisting rules for specific tags will be overwritten by the file.\nUnique local rules will be kept.")) return;
if (imported.rules) {
const currentRules = loadRules(); const reverseImportMap = new Map();
Object.entries(imported.rules).forEach(([folder, tags]) => { tags.forEach(t => reverseImportMap.set(t, folder)); });
Object.keys(currentRules).forEach(folder => { currentRules[folder] = currentRules[folder].filter(tag => !reverseImportMap.has(tag)); if (currentRules[folder].length === 0) delete currentRules[folder]; });
Object.entries(imported.rules).forEach(([folder, tags]) => { if (!currentRules[folder]) currentRules[folder] = []; tags.forEach(t => { if (!currentRules[folder].includes(t)) currentRules[folder].push(t); }); });
saveRules(currentRules);
}
if (imported.aliases) { const currentMap = loadSeriesMap(); Object.assign(currentMap, imported.aliases); const lines = []; Object.entries(currentMap).forEach(([tag, series]) => lines.push(`${tag} = ${series}`)); saveSeriesMap(lines.join('\n')); }
if (imported.filters) {
const mergeList = (key, importList) => { if (!importList) return; const current = new Set(GM_getValue(key, "").split('\n').filter(t => t.trim())); importList.forEach(t => current.add(t)); GM_setValue(key, Array.from(current).join('\n')); };
mergeList('ignored_series', imported.filters.ignored_series); mergeList('ignored_characters', imported.filters.ignored_characters); mergeList('ignored_artists', imported.filters.ignored_artists); mergeList('tags_3d', imported.filters.tags_3d);
}
alert("✔ Profile Imported Successfully!\nThe menu will now refresh."); openSettingsMenu();
} catch (err) { console.error(err); alert("❌ Error parsing profile file."); }
}; reader.readAsText(file); fileInput.value = '';
};
importBtn.onclick = () => fileInput.click();
profileBtnGroup.appendChild(exportBtn); profileBtnGroup.appendChild(importBtn); profileRow.appendChild(profileLabel); profileRow.appendChild(profileBtnGroup); tab2.appendChild(profileRow);
const blocklistRow = document.createElement('div'); Object.assign(blocklistRow.style, { display:'grid', gridTemplateColumns:'1fr 1fr 1fr 1fr', gap:'10px', padding:'15px', backgroundColor:'#222', borderBottom:'1px solid #333' });
blocklistRow.appendChild(createSetting("Ignored Series", "Prevents Bad Auto-Series Matches", "ignored_series", DEFAULT_IGNORED_SERIES, null, true));
blocklistRow.appendChild(createSetting("Ignored Characters", "Only used if no other Character exists", "ignored_characters", DEFAULT_IGNORED_CHARACTERS, null, true));
blocklistRow.appendChild(createSetting("Ignored Artists", "Excluded from Filenames", "ignored_artists", DEFAULT_IGNORED_ARTISTS, null, true));
blocklistRow.appendChild(createSetting("3D Tags", "Tags that trigger 3D Mode", "tags_3d", DEFAULT_TAGS_3D, null, true));
tab2.appendChild(blocklistRow);
const addSection = document.createElement('div');
Object.assign(addSection.style, { padding: '10px 15px', backgroundColor: '#2a2a2a', borderBottom: '1px solid #444', display:'flex', flexDirection:'column', gap:'10px' });
const inputsRow = document.createElement('div'); Object.assign(inputsRow.style, { display:'flex', gap:'8px', alignItems:'center' });
const uniInput = document.createElement('input'); uniInput.placeholder = 'Universe'; uniInput.value = prefillCategory; Object.assign(uniInput.style, { width:'100px', backgroundColor:'#111', color:'#fff', border:'1px solid #555', padding:'4px' });
const autoUniBtn = document.createElement('button'); autoUniBtn.innerText = 'Auto'; Object.assign(autoUniBtn.style, { padding: '6px 10px', backgroundColor: '#444', color: 'white', border: 'none', borderRadius: '4px', cursor:'pointer', fontSize:'11px' });
let autoUniIndex = 0; autoUniBtn.onclick = () => { const matches = getAllSeriesMatches(document); if(matches.length > 0) { uniInput.value = stripSlashes(matches[autoUniIndex % matches.length]); uniInput.style.backgroundColor = '#2c3e50'; autoUniIndex++; } else { alert("No copyright tag found."); } };
const folderInput = document.createElement('input'); folderInput.placeholder = 'Name...'; folderInput.value = prefillFolder; Object.assign(folderInput.style, { flex:'1', backgroundColor:'#111', color:'#fff', border:'1px solid #555', padding:'4px' });
const saveBtn = document.createElement('button'); saveBtn.innerText = 'ADD RULE'; Object.assign(saveBtn.style, { backgroundColor:'#28a745', color:'white', border:'none', padding:'0 15px', cursor:'pointer', fontWeight:'bold' });
inputsRow.appendChild(autoUniBtn); inputsRow.appendChild(uniInput); inputsRow.appendChild(folderInput); inputsRow.appendChild(saveBtn); addSection.appendChild(inputsRow);
let activeTags = prefillTag ? prefillTag.split(',').map(t=>t.trim()).filter(t=>t) : [];
let allPageCharacters = []; const sidebar = document.querySelector('#tag-sidebar, #tag-list, .sidebar, .tag-list-left, #post-information'); if (sidebar) { allPageCharacters = getAllCharacterTagsOnPage(document); }
if (allPageCharacters.length > 0) {
const charShortcutContainer = document.createElement('div');
Object.assign(charShortcutContainer.style, { display:'flex', flexWrap:'wrap', gap:'5px' });
allPageCharacters.forEach(char => {
const btn = document.createElement('button'); btn.innerText = toTitleCase(char);
Object.assign(btn.style, { fontSize:'10px', padding:'2px 6px', backgroundColor:'#27ae60', color:'white', border:'none', borderRadius:'3px', cursor:'pointer' });
btn.title = "Set Name + Add Tag"; btn.onclick = () => { folderInput.value = toTitleCase(char); if (!activeTags.includes(char)) { activeTags.push(char); renderAddPills(); } };
charShortcutContainer.appendChild(btn);
});
addSection.appendChild(charShortcutContainer);
}
const pillsArea = document.createElement('div'); Object.assign(pillsArea.style, { display:'flex', flexDirection:'column', gap:'5px' });
const activeTagsContainer = document.createElement('div'); Object.assign(activeTagsContainer.style, { minHeight:'30px', padding:'4px', backgroundColor:'#1a1a1a', border:'1px solid #28a745', borderRadius:'4px', display:'flex', flexWrap:'wrap', gap:'4px' });
const poolTagsContainer = document.createElement('div'); Object.assign(poolTagsContainer.style, { maxHeight:'100px', overflowY:'auto', padding:'4px', backgroundColor:'#111', border:'1px solid #444', borderRadius:'4px', display:'flex', flexWrap:'wrap', gap:'4px' });
const renderAddPills = () => {
activeTagsContainer.innerHTML = ''; poolTagsContainer.innerHTML = '';
if(activeTags.length === 0) activeTagsContainer.innerHTML = '<span style="color:#555; font-size:10px; font-style:italic; padding:2px;">No tags selected...</span>';
activeTags.forEach((tag, idx) => {
const pill = document.createElement('span'); Object.assign(pill.style, { backgroundColor:'#1e7e34', color:'white', fontSize:'11px', padding:'2px 6px', borderRadius:'3px', cursor:'pointer', display:'flex', alignItems:'center', border:'1px solid #28a745' });
pill.innerHTML = `${tag} <span style="font-weight:bold; margin-left:5px;">×</span>`; pill.onclick = () => { activeTags.splice(idx, 1); renderAddPills(); }; activeTagsContainer.appendChild(pill);
});
const pool = allPageCharacters.filter(t => !activeTags.includes(t));
if (pool.length > 0) { pool.forEach(tag => { const pill = document.createElement('span'); Object.assign(pill.style, { backgroundColor:'#333', color:'#ccc', fontSize:'11px', padding:'2px 6px', borderRadius:'3px', cursor:'pointer', border:'1px solid #444' }); pill.innerText = tag; pill.onclick = () => { activeTags.push(tag); renderAddPills(); }; poolTagsContainer.appendChild(pill); }); } else if (allPageCharacters.length > 0) { poolTagsContainer.innerHTML = '<span style="color:#555; font-size:10px; font-style:italic;">All detected characters used.</span>'; } else { poolTagsContainer.style.display = 'none'; }
};
const manualInput = document.createElement('input'); manualInput.placeholder = 'Type tag manually & Enter...';
Object.assign(manualInput.style, { border:'1px solid #444', background:'#111', color:'#fff', fontSize:'12px', padding:'4px', width:'100%' });
manualInput.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); const val = manualInput.value.trim().replace(/,/g, ''); if (val && !activeTags.includes(val)) { activeTags.push(val); manualInput.value = ''; renderAddPills(); } } };
pillsArea.appendChild(activeTagsContainer); pillsArea.appendChild(manualInput);
if(allPageCharacters.length > 0) { const poolLabel = document.createElement('div'); poolLabel.innerText = "Available Characters (Click to Add)"; poolLabel.style.fontSize = '10px'; poolLabel.style.color = '#777'; poolLabel.style.marginTop = '5px'; pillsArea.appendChild(poolLabel); pillsArea.appendChild(poolTagsContainer); }
addSection.appendChild(pillsArea);
saveBtn.onclick = () => { let u = stripSlashes(uniInput.value.trim()); let f = stripSlashes(folderInput.value.trim()); if(!f || activeTags.length === 0) return; if (u) f = `${u}/${f}`; const groups = loadRules(); if(!groups[f]) groups[f] = []; activeTags.forEach(rawTag => { const clean = normalizeTag(rawTag); if(clean && !groups[f].includes(clean)) groups[f].push(clean); }); saveRules(groups); folderInput.value = ''; activeTags = []; renderAddPills(); uniInput.value = ''; refreshList(); };
renderAddPills(); tab2.appendChild(addSection);
const searchRow = document.createElement('div'); Object.assign(searchRow.style, { padding: '10px 15px', backgroundColor: '#1f1f1f', borderBottom: '1px solid #333' });
const searchInput = document.createElement('input'); searchInput.placeholder = "🔍 Search Rules..."; Object.assign(searchInput.style, { width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #444', backgroundColor: '#111', color: '#fff' });
searchInput.onkeyup = () => refreshList(); searchRow.appendChild(searchInput); tab2.appendChild(searchRow);
const listArea = document.createElement('div'); Object.assign(listArea.style, { flex: '1', overflowY: 'auto', backgroundColor: '#181818' });
function refreshList() {
listArea.innerHTML = ''; const groups = loadRules(); const universes = {}; const uncategorized = {}; const filter = searchInput.value.trim().toLowerCase();
Object.entries(groups).forEach(([fullPath, tags]) => { if (fullPath.includes('/')) { const parts = fullPath.split('/'); const uni = parts[0]; const remainder = parts.slice(1).join('/'); if (!universes[uni]) universes[uni] = []; universes[uni].push({ name: remainder, fullPath: fullPath, tags: tags }); } else { uncategorized[fullPath] = tags; } });
const matchesFilter = (txt) => txt.toLowerCase().includes(filter);
const renderRow = (container, currentUni, charName, fullPath, tags, isSkin = false) => {
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', borderBottom: '1px solid #2a2a2a', padding: '4px 15px', alignItems: 'center', backgroundColor: '#181818' });
if (isSkin) { row.style.paddingLeft = '35px'; row.style.backgroundColor = '#1e1e1e'; }
const charDiv = document.createElement('div');
let displayName = charName;
if(isSkin && charName.includes('/')) { displayName = charName.split('/').pop(); }
charDiv.innerHTML = `${displayName} <span style="font-size:10px; color:#444; margin-left:3px;">✎</span>`;
Object.assign(charDiv.style, { flex:'1', maxWidth:'200px', fontWeight: 'bold', color: isSkin ? '#6fb3d2' : '#8fd3ff', fontSize: isSkin ? '12px' : '13px', marginRight:'10px', cursor:'pointer', transition: '0.2s', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' });
charDiv.onmouseenter = () => { charDiv.style.textDecoration = 'underline'; charDiv.style.color = '#fff'; };
charDiv.onmouseleave = () => { charDiv.style.textDecoration = 'none'; charDiv.style.color = isSkin ? '#6fb3d2' : '#8fd3ff'; };
charDiv.onclick = () => { makeEditable(charDiv, charName, "Name", (val) => { if (val === null) { charDiv.innerHTML = `${displayName} <span style="font-size:10px; color:#444; margin-left:3px;">✎</span>`; return; } const newPath = currentUni ? `${currentUni}/${val.trim()}` : val.trim(); if(newPath !== fullPath) { updateEntryPath(fullPath, newPath); refreshList(); } }); };
const leftContainer = document.createElement('div');
Object.assign(leftContainer.style, { display:'flex', alignItems:'center', width:'25%', marginRight:'10px' });
if (isSkin) {
const spacer = document.createElement('div'); spacer.innerText = "↳";
Object.assign(spacer.style, { color:'#555', marginRight:'8px', minWidth:'30px', textAlign:'right', fontWeight:'bold' });
leftContainer.appendChild(spacer);
} else if(!currentUni) {
const uniPill = document.createElement('div'); uniPill.innerText = "Root";
Object.assign(uniPill.style, { fontSize: '9px', backgroundColor: '#333', color: '#888', padding: '1px 4px', borderRadius: '3px', marginRight: '6px' });
leftContainer.appendChild(uniPill);
}
leftContainer.appendChild(charDiv); row.appendChild(leftContainer);
const tagsDiv = document.createElement('div'); Object.assign(tagsDiv.style, { flex: '1', display: 'flex', flexWrap: 'wrap', gap: '4px', alignItems:'center' });
tags.forEach(tag => {
const pill = document.createElement('span');
Object.assign(pill.style, { display:'inline-flex', alignItems:'center', backgroundColor: '#222', border: '1px solid #444', borderRadius: '4px', fontSize: '11px', overflow:'hidden' });
const promoteBtn = document.createElement('span'); promoteBtn.innerHTML = '📁';
Object.assign(promoteBtn.style, { padding: '1px 5px', color: '#f39c12', cursor:'pointer', borderRight:'1px solid #444', fontSize:'10px' });
promoteBtn.title = "Promote this tag to a Subfolder";
promoteBtn.onclick = () => { let suggestion = toTitleCase(tag.replace(/_/g, ' ')); const sub = prompt(`Promote tag "${tag}" to a subfolder?`, suggestion); if(sub) { promoteTagToSubfolder(fullPath, tag, sub); refreshList(); } };
const tagText = document.createElement('span'); tagText.innerText = tag;
Object.assign(tagText.style, { padding: '1px 6px', color: '#bbb', borderRight:'1px solid #444' });
const editBtn = document.createElement('span'); editBtn.innerText = '✎';
Object.assign(editBtn.style, { padding: '1px 5px', color: '#888', cursor:'pointer', borderRight: '1px solid #444', fontSize:'10px' });
editBtn.onclick = (e) => {
e.stopPropagation(); const input = document.createElement('input'); input.value = tag;
Object.assign(input.style, { width: Math.max(50, tag.length * 7) + 'px', backgroundColor: '#111', color: '#fff', border: '1px solid #007bff', padding: '0 2px', fontSize: '11px', borderRadius: '3px', height:'16px' });
const finishEdit = () => { const newVal = normalizeTag(input.value); if(newVal && newVal !== tag) { const r = loadRules(); if(r[fullPath]) { const idx = r[fullPath].indexOf(tag); if(idx !== -1) { r[fullPath][idx] = newVal; saveRules(r); refreshList(); } } } else { refreshList(); } };
input.addEventListener('keydown', (ev) => { if(ev.key === 'Enter') finishEdit(); if(ev.key === 'Escape') refreshList(); }); input.addEventListener('blur', finishEdit);
pill.replaceWith(input); input.focus();
};
const removeBtn = document.createElement('span'); removeBtn.innerText = '×';
Object.assign(removeBtn.style, { padding: '1px 5px', color: '#d9534f', cursor:'pointer', fontWeight:'bold' });
removeBtn.onclick = (e) => { e.stopPropagation(); removeTagFromPath(fullPath, tag); refreshList(); };
pill.appendChild(promoteBtn); pill.appendChild(tagText); pill.appendChild(editBtn); pill.appendChild(removeBtn); tagsDiv.appendChild(pill);
});
const plusTagsBtn = document.createElement('span'); plusTagsBtn.innerText = '+'; Object.assign(plusTagsBtn.style, { cursor: 'pointer', marginLeft: '5px', color: '#555', fontSize: '14px', fontWeight: 'bold' });
plusTagsBtn.onclick = () => { const currentTagString = tags.join(', ') + (tags.length>0 ? ", " : ""); tagsDiv.innerHTML = ''; const input = document.createElement('input'); input.value = currentTagString; Object.assign(input.style, { width: '100%', backgroundColor: '#111', color: '#f39c12', border: '1px solid #28a745', padding: '4px', fontSize: '12px' }); const save = () => { const newTags = input.value.split(',').map(t => t.trim()).filter(t => t); updateTagsForPath(fullPath, newTags); refreshList(); }; input.addEventListener('keydown', (e) => { if(e.key === 'Enter') save(); if(e.key === 'Escape') refreshList(); }); input.addEventListener('blur', save); tagsDiv.appendChild(input); input.focus(); }; tagsDiv.appendChild(plusTagsBtn);
const delBtn = document.createElement('button'); delBtn.innerText = '✕'; Object.assign(delBtn.style, { background: 'none', border: 'none', cursor: 'pointer', color:'#d9534f', marginLeft:'auto' }); delBtn.onclick = () => { if(confirm(`Delete "${charName}"?`)) { const g = loadRules(); delete g[fullPath]; saveRules(g); refreshList(); } };
row.appendChild(tagsDiv); row.appendChild(delBtn); container.appendChild(row);
};
Object.entries(uncategorized).forEach(([folder, tags]) => { if(matchesFilter(folder) || tags.some(t => matchesFilter(t))) renderRow(listArea, null, folder, folder, tags); });
Object.keys(universes).sort().forEach(uniName => {
const seriesTags = getTagsForSeries(uniName); const uniMatches = matchesFilter(uniName); const aliasMatches = seriesTags.some(t => matchesFilter(t)); const items = universes[uniName]; const filteredItems = (uniMatches || aliasMatches) ? items : items.filter(item => matchesFilter(item.name) || item.tags.some(t => matchesFilter(t))); if (filteredItems.length === 0) return;
const catContainer = document.createElement('div'); const catHeader = document.createElement('div'); Object.assign(catHeader.style, { padding: '8px 15px', backgroundColor: '#222', borderBottom:'1px solid #333', display:'flex', alignItems:'center' });
const title = document.createElement('span'); title.innerText = uniName; Object.assign(title.style, { fontWeight:'bold', color:'#f39c12', marginRight:'10px', cursor:'pointer' }); title.onclick = () => { makeEditable(title, uniName, "Universe Name", (val) => { if (val !== null && val !== uniName) { renameUniverse(uniName, val); refreshList(); } else { title.innerText = uniName; } }); };
const aliasSpan = document.createElement('span'); aliasSpan.innerText = seriesTags.length ? `[${seriesTags.join(', ')}]` : '[No Aliases]'; Object.assign(aliasSpan.style, { fontSize:'10px', color:'#777', cursor:'pointer' }); aliasSpan.onclick = () => { const currentStr = seriesTags.join(', '); catHeader.innerHTML = ''; const input = document.createElement('input'); input.value = currentStr; Object.assign(input.style, { width: '100%', backgroundColor: '#111', color: '#f39c12', border: '1px solid #f39c12', padding: '4px', fontSize: '12px' }); const save = () => { const newTags = input.value.split(',').map(t => t.trim()).filter(t => t); updateSeriesAliases(uniName, newTags); refreshList(); }; input.addEventListener('keydown', (e) => { if(e.key === 'Enter') save(); if(e.key === 'Escape') refreshList(); }); input.addEventListener('blur', save); catHeader.appendChild(input); input.focus(); };
const plusAliasBtn = document.createElement('span'); plusAliasBtn.innerText = '+'; Object.assign(plusAliasBtn.style, { cursor: 'pointer', marginLeft: '8px', color: '#777', fontSize: '12px', fontWeight:'bold' }); plusAliasBtn.onclick = () => { const currentStr = seriesTags.join(', ') + (seriesTags.length > 0 ? ", " : ""); catHeader.innerHTML = ''; const input = document.createElement('input'); input.value = currentStr; Object.assign(input.style, { width: '100%', backgroundColor: '#111', color: '#f39c12', border: '1px solid #28a745', padding: '4px', fontSize: '12px' }); const save = () => { const newTags = input.value.split(',').map(t => t.trim()).filter(t => t); updateSeriesAliases(uniName, newTags); refreshList(); }; input.addEventListener('keydown', (e) => { if(e.key === 'Enter') save(); if(e.key === 'Escape') refreshList(); }); input.addEventListener('blur', save); catHeader.appendChild(input); input.focus(); };
catHeader.appendChild(title); catHeader.appendChild(aliasSpan); catHeader.appendChild(plusAliasBtn); catContainer.appendChild(catHeader);
const legendRow = document.createElement('div');
Object.assign(legendRow.style, { display:'flex', padding:'2px 15px', fontSize:'9px', color:'#555', textTransform:'uppercase', letterSpacing:'1px', marginTop:'2px' });
const legendLeft = document.createElement('div'); legendLeft.innerText = "Folder / Character Name"; legendLeft.style.width = '25%';
const legendRight = document.createElement('div'); legendRight.innerText = "Trigger Tags (Aliases)"; legendRight.style.flex = '1';
legendRow.appendChild(legendLeft); legendRow.appendChild(legendRight); catContainer.appendChild(legendRow);
const lookup = {}; filteredItems.forEach(item => lookup[item.fullPath] = item); const roots = [];
filteredItems.forEach(item => { item.children = []; const lastSlash = item.fullPath.lastIndexOf('/'); if (lastSlash > -1) { const parentPath = item.fullPath.substring(0, lastSlash); if (lookup[parentPath]) { lookup[parentPath].children.push(item); return; } } roots.push(item); }); roots.sort((a,b) => a.name.localeCompare(b.name)); Object.values(lookup).forEach(node => { if(node.children) node.children.sort((a,b) => a.name.localeCompare(b.name)); });
roots.forEach(rootItem => { renderRow(catContainer, uniName, rootItem.name, rootItem.fullPath, rootItem.tags, false); if(rootItem.children) rootItem.children.forEach(child => renderRow(catContainer, uniName, child.name, child.fullPath, child.tags, true)); });
listArea.appendChild(catContainer);
});
}
tab2.appendChild(listArea); contentArea.appendChild(tab2); container.appendChild(contentArea);
const footer = document.createElement('div'); Object.assign(footer.style, { padding: '10px', textAlign: 'right', borderTop: '1px solid #444', backgroundColor:'#222' });
const closeBtn = document.createElement('button'); closeBtn.innerText = 'Done'; Object.assign(closeBtn.style, { padding: '5px 20px', fontWeight: 'bold', cursor:'pointer' });
closeBtn.onclick = () => { document.body.removeChild(overlay); updateMainButton(); if(onCloseCallback) onCloseCallback(); };
footer.appendChild(closeBtn); container.appendChild(footer);
btnSettings.onclick = () => { tab1.style.display = 'grid'; tab2.style.display = 'none'; btnSettings.style.borderBottom = '3px solid #007bff'; btnRules.style.borderBottom = '3px solid transparent'; };
btnRules.onclick = () => { tab1.style.display = 'none'; tab2.style.display = 'flex'; btnRules.style.borderBottom = '3px solid #007bff'; btnSettings.style.borderBottom = '3px solid transparent'; refreshList(); };
overlay.appendChild(container); document.body.appendChild(overlay);
if(prefillFolder) { btnRules.click(); } else { btnSettings.click(); }
}
// --- SAVED SEARCHES (Fixed for e621 Textarea) ---
function initSavedSearches() {
if (document.getElementById('r34-saved-row')) return;
// UPDATED: Added textarea selectors for e621
const searchInput = document.querySelector('input[name="tags"], input[name="q"], textarea[name="tags"], textarea#tags');
const sidebar = document.querySelector('#tag-sidebar, #tag-list, .sidebar, .content_left, .tag-list-left, #post-information');
const searchForm = searchInput ? (searchInput.closest('form') || searchInput.parentNode) : null;
if (!searchInput) return;
const savedRow = document.createElement('div');
savedRow.id = 'r34-saved-row';
Object.assign(savedRow.style, { marginBottom: '8px', display:'flex', alignItems:'center', gap:'5px', fontFamily: 'Arial, sans-serif', flexWrap: 'wrap' });
const style = document.createElement('style');
style.innerHTML = `#r34-preset-btn { display: inline-block; padding: 3px 8px; background-color: #2b2b2b; border: 1px solid #444; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 12px; color: #aae5a4; user-select: none; transition: background 0.2s; } #r34-preset-btn:hover { background-color: #383838; border-color: #666; } #r34-preset-menu { display: none; position: absolute; z-index: 9999; background: #1f1f1f; border: 1px solid #444; border-radius: 5px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); width: 300px; padding: 10px; margin-top: 2px; font-family: Verdana, sans-serif; font-size: 12px; text-align: left; color: #ddd; } .r34-preset-item { display: flex; justify-content: space-between; align-items: center; background: #2a2a2a; border: 1px solid #333; margin-bottom: 5px; padding: 5px; border-radius: 3px; cursor: pointer; } .r34-preset-item:hover { background: #333; border-color: #555; } .r34-preset-label { flex-grow: 1; font-weight: bold; color: #8fbbe6; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .r34-preset-del { color: #ff6666; font-weight: bold; padding: 0 5px; cursor: pointer; } .r34-preset-del:hover { background: #442222; border-radius: 3px; } .r34-preset-add-box { margin-top: 10px; padding-top: 10px; border-top: 1px solid #444; } .r34-inp { width: 100%; box-sizing: border-box; margin-bottom: 5px; padding: 5px; border: 1px solid #444; background: #2a2a2a; color: #eee; border-radius: 3px; } .r34-btn-small { width: 100%; padding: 5px; cursor: pointer; background: #333; border: 1px solid #555; color: #ddd; border-radius: 3px; } .r34-btn-small:hover { background: #444; } #r34-quick-save-btn { display: inline-block; padding: 3px 8px; background-color: #2b2b2b; border: 1px solid #444; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 12px; color: #ccc; user-select: none; transition: background 0.2s; } #r34-quick-save-btn:hover { background-color: #383838; border-color: #666; color: #fff; }`;
document.head.appendChild(style);
const PRESET_KEY = 'r34_saved_presets_v3'; const loadPresets = () => { try { return JSON.parse(localStorage.getItem(PRESET_KEY)) || []; } catch(e) { return []; } }; const savePresets = (data) => localStorage.setItem(PRESET_KEY, JSON.stringify(data));
if (loadPresets().length === 0) { savePresets([{ label: "3D Software Mix", query: "( 3dcg ~ 3dx ~ 3d ~ 3d_* ~ daz_studio ~ daz3d ~ daz_3d ~ blender ~ blender_(software) ~ blender_cycles ~ blender_(artwork) ~ blender_eevee* ~ blender3d ~ blender3d ~ sfm ~ source_filmmaker ~ source_filmmaker_(artwork) )" }]); }
const appendTag = (text) => { const currentVal = searchInput.value.trim(); searchInput.value = currentVal ? (currentVal + " " + text + " ") : (text + " "); searchInput.focus(); };
const renderPresets = (listContainer) => {
listContainer.innerHTML = ''; const data = loadPresets();
if (data.length === 0) { listContainer.innerHTML = '<div style="padding:5px; color:#666; text-align:center;">No presets saved.</div>'; return; }
data.forEach((item, index) => {
const div = document.createElement('div'); div.className = 'r34-preset-item'; div.title = item.query;
const label = document.createElement('div'); label.className = 'r34-preset-label'; label.innerText = item.label; label.onclick = () => appendTag(item.query);
const del = document.createElement('div'); del.className = 'r34-preset-del'; del.innerText = '×'; del.onclick = (e) => { e.stopPropagation(); if(confirm('Delete "'+item.label+'"?')) { data.splice(index, 1); savePresets(data); renderPresets(listContainer); } };
div.appendChild(label); div.appendChild(del); listContainer.appendChild(div);
});
};
const btn = document.createElement('span'); btn.id = 'r34-preset-btn'; btn.innerText = '★ Saved Searches'; savedRow.appendChild(btn);
const quickSaveBtn = document.createElement('span'); quickSaveBtn.id = 'r34-quick-save-btn'; quickSaveBtn.innerText = '+ Save Current'; quickSaveBtn.title = "Save text currently in search bar";
quickSaveBtn.onclick = (e) => { e.preventDefault(); const currentQuery = searchInput.value.trim(); if(!currentQuery) { alert("Search bar is empty!"); return; } const name = prompt("Name this search:", "My Search"); if(name) { const data = loadPresets(); data.push({ label: name, query: currentQuery }); savePresets(data); alert("Saved!"); } };
savedRow.appendChild(quickSaveBtn);
if (sidebar) { sidebar.insertBefore(savedRow, sidebar.firstChild); }
else if (searchForm && searchForm.nextSibling) { searchForm.parentNode.insertBefore(savedRow, searchForm.nextSibling); }
else if (searchForm) { searchForm.parentNode.appendChild(savedRow); }
const menu = document.createElement('div'); menu.id = 'r34-preset-menu'; menu.innerHTML = `<div id="r34-preset-list" style="max-height:200px; overflow-y:auto;"></div><div class="r34-preset-add-box"><input type="text" id="r34-new-label" class="r34-inp" placeholder="Name"><input type="text" id="r34-new-query" class="r34-inp" placeholder="Tags"><button id="r34-add-btn" class="r34-btn-small">+ Save New</button></div><div style="text-align:right; margin-top:5px;"><small style="color:#666; cursor:pointer;" onclick="document.getElementById('r34-preset-menu').style.display='none'">[Close]</small></div>`; document.body.appendChild(menu);
btn.onclick = () => { if (menu.style.display === 'block') { menu.style.display = 'none'; return; } menu.style.display = 'block'; const rect = btn.getBoundingClientRect(); menu.style.top = (rect.bottom + (window.pageYOffset||document.documentElement.scrollTop) + 2) + 'px'; menu.style.left = (rect.left + (window.pageXOffset||document.documentElement.scrollLeft)) + 'px'; renderPresets(document.getElementById('r34-preset-list')); };
document.getElementById('r34-add-btn').onclick = () => { const l = document.getElementById('r34-new-label').value; const q = document.getElementById('r34-new-query').value; if (l && q) { const data = loadPresets(); data.push({ label: l, query: q }); savePresets(data); renderPresets(document.getElementById('r34-preset-list')); document.getElementById('r34-new-label').value=''; document.getElementById('r34-new-query').value=''; } };
document.addEventListener('click', (e) => { if (!menu.contains(e.target) && !btn.contains(e.target) && !quickSaveBtn.contains(e.target)) menu.style.display = 'none'; });
}
// --- FAVORITE PILLS (Fixed for e621 Data Attributes & Textarea) ---
function initFavoritePills() {
if (document.getElementById('r34-fav-pills-wrapper')) return;
// Fix 1: Search for textarea as well as input (e621 uses textarea)
const searchInput = document.querySelector('input[name="tags"], input[name="q"], textarea[name="tags"], textarea#tags');
if (!searchInput) return;
const savedRow = document.getElementById('r34-saved-row');
const wrapper = document.createElement('div');
wrapper.id = 'r34-fav-pills-wrapper';
Object.assign(wrapper.style, { marginBottom: '10px', fontFamily: 'Arial, sans-serif', border:'1px solid #333', borderRadius:'4px', backgroundColor:'#1a1a1a' });
const header = document.createElement('div');
const isCollapsed = GM_getValue('fav_collapsed', true);
header.innerText = isCollapsed ? '▶ Show Favorite Tags' : '▼ Hide Favorite Tags';
Object.assign(header.style, { padding:'5px 10px', fontSize:'11px', color:'#888', cursor:'pointer', backgroundColor:'#222', borderBottom: isCollapsed ? 'none' : '1px solid #333' });
const container = document.createElement('div');
container.id = 'r34-fav-pills';
Object.assign(container.style, { display: isCollapsed ? 'none' : 'flex', flexWrap: 'wrap', gap: '3px', padding:'5px' });
header.onclick = () => {
const nowCollapsed = container.style.display !== 'none';
container.style.display = nowCollapsed ? 'none' : 'flex';
header.innerText = nowCollapsed ? '▶ Show Favorite Tags' : '▼ Hide Favorite Tags';
header.style.borderBottom = nowCollapsed ? 'none' : '1px solid #333';
GM_setValue('fav_collapsed', nowCollapsed);
};
const addInput = document.createElement('input');
addInput.placeholder = "Add favorite tag...";
Object.assign(addInput.style, { background: '#333', border: '1px solid #555', color: '#fff', fontSize: '10px', padding: '2px 5px', borderRadius: '3px', width: '100px' });
const getFavs = () => GM_getValue('fav_pills', []);
const saveFavs = (list) => GM_setValue('fav_pills', list);
const renderPills = () => {
Array.from(container.children).forEach(c => { if(c !== addInput) container.removeChild(c); });
const pills = getFavs();
pills.forEach(tag => {
const cleanTag = tag.replace(/_/g, ' ');
const pill = document.createElement('span');
Object.assign(pill.style, { display: 'inline-flex', alignItems: 'center', background: '#222', border: '1px solid #444', borderRadius: '4px', padding: '1px 4px', fontSize: '10px', color: '#eee' });
const link = document.createElement('a');
link.innerText = cleanTag;
link.title = "Click to Search";
if (isMoebooru()) { link.href = `/post?tags=${tag}`; }
else if (isRule34Us()) { link.href = `index.php?r=posts/index&q=${tag}`; }
else if (isE621()) { link.href = `/posts?tags=${tag}`; }
else { link.href = `index.php?page=post&s=list&tags=${tag}`; }
Object.assign(link.style, { color: '#8cbaff', textDecoration: 'none', margin: '0 4px', cursor: 'pointer', maxWidth:'100px', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' });
const plusBtn = document.createElement('span'); plusBtn.innerText = '+'; plusBtn.title = "Add to current search";
Object.assign(plusBtn.style, { cursor: 'pointer', color: '#6f6', fontWeight: 'bold', padding: '0 2px' });
plusBtn.onclick = (e) => { e.preventDefault(); searchInput.value = (searchInput.value + ' ' + tag).trim(); };
const minusBtn = document.createElement('span'); minusBtn.innerText = '-'; minusBtn.title = "Exclude from current search";
Object.assign(minusBtn.style, { cursor: 'pointer', color: '#f66', fontWeight: 'bold', padding: '0 2px' });
minusBtn.onclick = (e) => { e.preventDefault(); searchInput.value = (searchInput.value + ' -' + tag).trim(); };
const removeBtn = document.createElement('span'); removeBtn.innerText = '×'; removeBtn.title = "Remove from favorites";
Object.assign(removeBtn.style, { cursor: 'pointer', color: '#888', fontWeight: 'bold', marginLeft: '2px' });
removeBtn.onmouseenter = () => removeBtn.style.color = '#f00'; removeBtn.onmouseleave = () => removeBtn.style.color = '#888';
removeBtn.onclick = (e) => { e.preventDefault(); const newL = getFavs().filter(t => t !== tag); saveFavs(newL); renderPills(); };
pill.appendChild(plusBtn); pill.appendChild(minusBtn); pill.appendChild(link); pill.appendChild(removeBtn); container.insertBefore(pill, addInput);
});
};
const addTag = (val) => { if(!val) return; val = normalizeTag(val); const current = getFavs(); if(!current.includes(val)) { current.push(val); saveFavs(current); renderPills(); } };
window.r34AddFavPill = addTag;
addInput.addEventListener('keydown', (e) => { if(e.key === 'Enter') { e.preventDefault(); addTag(addInput.value); addInput.value = ''; } });
container.appendChild(addInput);
wrapper.appendChild(header);
wrapper.appendChild(container);
// Fix 2: Better placement logic for e621
if (isE621()) {
const tagHeader = document.querySelector('h5.tag-list-header') || document.querySelector('#tag-box h5');
const tagBox = document.querySelector('section#tag-box');
if (tagHeader && tagHeader.parentNode) { tagHeader.parentNode.insertBefore(wrapper, tagHeader); }
else if (tagBox) { tagBox.insertBefore(wrapper, tagBox.firstChild); }
else if (savedRow && savedRow.parentNode) { savedRow.parentNode.insertBefore(wrapper, savedRow.nextSibling); }
else { const form = searchInput.closest('form'); if(form && form.parentNode) form.parentNode.insertBefore(wrapper, form); }
} else {
if(savedRow && savedRow.parentNode) { savedRow.parentNode.insertBefore(wrapper, savedRow.nextSibling); }
else { let searchForm = searchInput.closest('form'); if(searchForm && searchForm.parentNode) searchForm.parentNode.appendChild(wrapper); }
}
renderPills();
// --- SIDEBAR [+] BUTTONS ---
const tagLinks = document.querySelectorAll(
'#tag-sidebar li a, #tag-list li a, .sidebar li a, .tag-list-left li a, ' +
'#post-information li a, ' +
'.tag-list-item .tag-list-search'
);
tagLinks.forEach(link => {
let tagValue = "";
// Fix 3: Use data-name on e621 for accurate, decoded tag names
if (isE621()) {
const li = link.closest('li.tag-list-item');
if (li && li.dataset.name) {
tagValue = decodeURIComponent(li.dataset.name);
} else {
const nameSpan = link.querySelector('.tag-list-name');
if (nameSpan) tagValue = nameSpan.textContent.trim();
}
} else {
tagValue = link.textContent.trim();
}
// Fallback for non-e621 or e621-fallback: remove "(123)" counts
if (!isE621() || !link.closest('li.tag-list-item')) {
const processedTextMatch = tagValue ? tagValue.match(/^(.*?)( \(\d+\))?$/) : null;
if (processedTextMatch && processedTextMatch[1]) {
tagValue = processedTextMatch[1];
}
}
if (!tagValue || tagValue === '?' || tagValue === '+' || tagValue === '-') return;
if (link.parentNode.querySelector('.r34-sidebar-add')) return;
const btn = document.createElement('span');
btn.className = 'r34-sidebar-add';
btn.innerText = '[+]';
Object.assign(btn.style, { cursor:'pointer', fontSize:'9px', color:'#6f6', marginLeft:'3px', marginRight:'3px', display:'inline-block' });
btn.title = "Add to Favorite Pills";
const finalTag = normalizeTag(tagValue);
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
window.r34AddFavPill(finalTag);
};
if(isE621()) {
const actions = link.parentNode.querySelector('.tag-list-actions');
if (actions) { actions.appendChild(btn); } else { link.after(btn); }
} else {
link.after(btn);
}
});
}
function setupQuickEdit() {
document.addEventListener('keydown', async (e) => {
if (e.key === 'Control' || e.key === 'Meta') {
if (e.repeat) return;
const allThumbs = Array.from(document.querySelectorAll(
'a.thumb, .thumb a, .thumbnail-preview a, ' + // General boorus
'div[style*="height: 200px;"] > a, ' + // Rule34.us specific thumbnail container
'a[href*="r=posts/view"][id], ' + // Rule34.us, check for link with ID and view param
'article.thumbnail > a' // E621 specific thumbnail classes
));
if (allThumbs.length === 0) return;
let hoveredLink = document.querySelector('a.thumb:hover, .thumb a:hover, .thumbnail-preview a:hover');
// For rule34.us snippet, the a tag inside the div might be hovered
if (!hoveredLink) {
hoveredLink = document.querySelector('div[style*="height: 200px;"] > a:hover, article.thumbnail > a:hover');
}
let clickedIndex = -1;
if (!hoveredLink) { const enhancerContainer = document.querySelector('#thumbPlusPreviewContainer'); if (enhancerContainer && enhancerContainer.matches(':hover')) { hoveredLink = document.querySelector('#thumbPlusPreviewLink'); } }
if (!hoveredLink) return; e.preventDefault(); e.stopPropagation();
clickedIndex = allThumbs.findIndex(a => a.href === hoveredLink.href);
if(clickedIndex === -1 && hoveredLink.id === 'thumbPlusPreviewLink') { clickedIndex = allThumbs.findIndex(a => a.href === hoveredLink.href); }
if(clickedIndex === -1) return;
launchEditorForIndex(clickedIndex, allThumbs);
}
});
}
async function launchEditorForIndex(index, allThumbs) {
if(index < 0 || index >= allThumbs.length) return;
const a = allThumbs[index];
const id = getIdFromUrl(a.href); if (!id) return;
const thumbImg = a.querySelector('img');
if(thumbImg) thumbImg.style.opacity = '0.5';
try {
let fetchUrl = `${window.location.origin}/index.php?page=post&s=view&id=${id}`;
if (isMoebooru()) { fetchUrl = `${window.location.origin}/post/show/${id}`; }
else if (isRule34Us()) { fetchUrl = `${window.location.origin}/index.php?r=posts/view&id=${id}`; }
else if (isE621()) { fetchUrl = `${window.location.origin}/posts/${id}`; }
const response = await fetch(fetchUrl); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html");
const item = { id: id, thumbUrl: thumbImg ? thumbImg.src : '', md5: getPageMD5(doc), manualType: null };
// Re-use downloadImage's robust fileUrl logic
let tempFileUrl = "";
const originalLink = doc.querySelector('a#image-download-link, a.image-download-link, a[onclick*="javascript:show_original_image"], a[href*="/image/"], a[href*="/file/"]');
if (originalLink && originalLink.href && !originalLink.href.includes('/wiki/')) tempFileUrl = originalLink.href;
if (!tempFileUrl && isRule34Us()) {
const r34UsOriginal = doc.querySelector('a[href*="/images/"][href$=".png"], a[href*="/images/"][href$=".jpg"], a[href*="/images/"][href$=".gif"]');
if (r34UsOriginal && r34UsOriginal.textContent.trim().toLowerCase().includes('original')) {
tempFileUrl = r34UsOriginal.href;
}
}
if (!tempFileUrl && isE621()) {
const e621OriginalLink = doc.querySelector('li#post-file-size a, #raw_image_container > a');
if (e621OriginalLink && e621OriginalLink.href) tempFileUrl = e621OriginalLink.href;
}
if (!tempFileUrl) {
const mainImage = doc.querySelector('#image, #main_image, #img, .image-body img, #img-display');
if (mainImage && mainImage.src) tempFileUrl = mainImage.src;
}
if (!tempFileUrl) {
const mainVideo = doc.querySelector('video#image, video.image-body, video#gelcomVideoPlayer');
if (mainVideo) {
const source = mainVideo.querySelector('source');
if (source && source.src) tempFileUrl = source.src;
else if (mainVideo.src) tempFileUrl = mainVideo.src;
}
}
item.fileUrl = tempFileUrl;
openFocusEditor(item, doc, () => { if(thumbImg) thumbImg.style.opacity = '1'; }, (itm) => { downloadFromDocument(doc, itm, (success) => { if(success && thumbImg) thumbImg.style.border = "3px solid #28a745"; }); }, { current: index, total: allThumbs.length, hasNext: (index < allThumbs.length - 1), hasPrev: (index > 0), goNext: () => launchEditorForIndex(index + 1, allThumbs), goPrev: () => launchEditorForIndex(index - 1, allThumbs) });
if(thumbImg) thumbImg.style.opacity = '1';
} catch(err) { console.error("R34 Script: Error during fetch", err); if(thumbImg) thumbImg.style.opacity = '1'; }
}
window.addEventListener('load', () => { updateMainButton(); initSavedSearches(); initFavoritePills(); });
// Run after a short delay too, in case elements load dynamically
setTimeout(() => { updateMainButton(); initSavedSearches(); initFavoritePills(); }, 1500);
setupQuickEdit();
GM_registerMenuCommand("Rule34 Sorter Settings", () => openSettingsMenu());
})();