// ==UserScript==
// @name Better Rule34
// @name:fr Meilleure règle 34
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description A script to improve the use of rule34, now with API key support!
// @description:fr Un script pour améliorer l'utilisation de rule34, maintenant avec le support de la clé API!
// @author You
// @match https://rule34.xxx/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM.xmlHttpRequest
// @license MIT
// @require https://unpkg.com/[email protected]
// ==/UserScript==
(async function() {
'use strict';
// --- NEW: API Key Management ---
const API_KEY_NAME = 'br34_api_key';
const USER_ID_NAME = 'br34_user_id';
/**
* Checks if the API key and User ID are stored. If not, it displays a prompt.
* This function will "halt" the script by returning a never-resolving promise if keys are missing.
*/
async function checkApiKey() {
const apiKey = await GM_getValue(API_KEY_NAME);
const userId = await GM_getValue(USER_ID_NAME);
// If on the options page, don't show the prompt; let the page handler do its job.
if (window.location.href.includes('page=account&s=options')) {
return true;
}
if (!apiKey || !userId) {
console.log("Better Rule34: API key or User ID not found. Displaying prompt.");
showApiKeyPrompt();
// Return a promise that never resolves to halt further script execution until the user provides keys.
return new Promise(() => {});
}
console.log("Better Rule34: API key and User ID found.");
return true;
}
/**
* Displays a modal dialog prompting the user to generate or enter API credentials.
*/
function showApiKeyPrompt() {
GM_addStyle(`
#br34-modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7); display: flex;
justify-content: center; align-items: center; z-index: 9999; font-family: sans-serif;
}
#br34-modal-content {
background-color: #1e1e1e; color: #eee; padding: 20px 30px;
border-radius: 8px; text-align: center; max-width: 400px;
border: 1px solid #555;
}
#br34-modal-content p { margin: 0 0 20px 0; line-height: 1.5; }
#br34-modal-buttons button, #br34-manual-input button {
background-color: #333; color: #fff; border: 1px solid #555;
padding: 10px 15px; border-radius: 5px; cursor: pointer; margin: 0 10px;
}
#br34-modal-buttons button:hover, #br34-manual-input button:hover { background-color: #555; }
#br34-manual-input { margin-top: 20px; }
#br34-manual-input input {
display: block; width: calc(100% - 20px); margin: 10px auto; padding: 8px;
background-color: #333; border: 1px solid #555; color: #fff; border-radius: 4px;
}
`);
const overlay = document.createElement('div');
overlay.id = 'br34-modal-overlay';
const modal = document.createElement('div');
modal.id = 'br34-modal-content';
modal.innerHTML = `
<p>Due to a recent update, this script needs an API key! Either enter one manually, or generate a new one (recommended)!</p>
<div id="br34-modal-buttons">
<button id="br34-manual-btn">Enter Manually</button>
<button id="br34-generate-btn">Generate New One</button>
</div>
<div id="br34-manual-input" style="display: none;">
<p style="font-size: 0.9em;">Go to the options page, copy the full text from the 'API Access Credentials' box, and paste it here.</p>
<input type="text" id="br34-credential-input" placeholder="&api_key=...&user_id=...">
<button id="br34-save-manual-btn">Save</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('br34-generate-btn').addEventListener('click', () => {
window.location.href = 'https://rule34.xxx/index.php?page=account&s=options';
});
document.getElementById('br34-manual-btn').addEventListener('click', () => {
document.getElementById('br34-modal-buttons').style.display = 'none';
document.getElementById('br34-manual-input').style.display = 'block';
});
document.getElementById('br34-save-manual-btn').addEventListener('click', async () => {
const credString = document.getElementById('br34-credential-input').value.trim();
if (credString) {
try {
const params = new URLSearchParams(credString.startsWith('?') ? credString : '?' + credString);
const apiKey = params.get('api_key');
const userId = params.get('user_id');
if (apiKey && userId) {
await GM_setValue(API_KEY_NAME, apiKey);
await GM_setValue(USER_ID_NAME, userId);
alert('API Key and User ID saved! The page will now reload.');
location.reload();
} else {
alert('Invalid format. Please paste the full string, e.g., &api_key=...&user_id=...');
}
} catch (e) {
alert('Could not parse the provided string. Please check the format.');
}
}
});
}
/**
* On the account options page, finds the API credentials, saves them, and shows a confirmation banner.
*/
async function handleOptionsPage() {
if (!window.location.href.includes('page=account&s=options')) {
return;
}
const textareas = document.querySelectorAll('textarea');
let credTextarea = null;
for (const textarea of textareas) {
if (textarea.value.includes('&api_key=') && textarea.value.includes('&user_id=')) {
credTextarea = textarea;
break;
}
}
if (credTextarea) {
try {
const credString = credTextarea.value.replace(/&/g, '&');
const params = new URLSearchParams(credString);
const apiKey = params.get('api_key');
const userId = params.get('user_id');
if (apiKey && userId) {
await GM_setValue(API_KEY_NAME, apiKey);
await GM_setValue(USER_ID_NAME, userId);
// Show confirmation message
const banner = document.createElement('div');
banner.id = "br34-key-saved-banner";
banner.textContent = 'Better Rule34: API Key and User ID found and saved! You can now browse other pages.';
banner.style.cssText = `
background-color: #4CAF50; color: white; padding: 15px; text-align: center;
position: fixed; top: 0; left: 0; width: 100%; z-index: 10000; font-size: 16px;
`;
document.body.prepend(banner);
setTimeout(() => banner.remove(), 5000); // Remove after 5 seconds
}
} catch (e) {
console.error("Better Rule34: Could not parse API credentials.", e);
}
} else {
console.log("Better Rule34: Could not find API credentials textarea on this page.");
}
}
const defaultConfig = {
imageResizeNotice: "resize", // Changed default to "resize"
theme: "dark",
undeletePosts: true,
clickAnywhereToStart: false, // Renamed to more descriptive "autoPlayVideo"
htmlVideoPlayer: false,
dynamicResizing: false,
scrollPostsIntoView: false,
downloadFullSizedImages: false,
fitImageToScreen: false,
hideAlerts: false,
imageHover: true
};
// Initialize settings with default values if not already set
function initializeSettings() {
for (const key in defaultConfig) {
if (GM_getValue(key) === undefined) {
GM_setValue(key, defaultConfig[key]);
}
}
}
// Theme definitions
const themes = {
"dark": {
"primary": "#121212",
"secondary": "#000011",
"contrast": "#4a4a4a",
"complementary": "#666666",
"tableBackground": "transparent",
"linkColor": "#00f"
}
};
// Apply dynamic resizing styles if enabled
function applyDynamicResizing() {
if (GM_getValue("dynamicResizing", false)) {
GM_addStyle(`
div.sidebar { max-width: 30%; }
div.sidebar li { font-size: 120%; }
div.content { width: 100%; }
.thumb { height: 20%; width: auto; }
`);
}
}
applyDynamicResizing();
const urlParams = new URLSearchParams(window.location.search);
// Settings data structure
const settingsData = {
"tabs": [
{
"name": "General",
"settings": [
{
"name": "imageResizeNotice",
"description": "Remove the image resize notice",
"type": "dropdown",
"options": ["resize", "no-resize"]
},
{
"name": "undeletePosts",
"description": "Display deleted posts",
"type": "checkbox"
},
{
"name": "hideAlerts",
"description": "Hide script warnings",
"type": "checkbox"
}
]
},
{
"name": "Theme",
"settings": [
{
"name": "theme",
"description": "Theme selection",
"type": "dropdown",
"options": Object.keys(themes), // Dynamically populate with available themes
"onChange": setTheme // Apply the theme immediately when changed
},
{
"name": "createNewTheme",
"description": "Create New Theme",
"type": "custom", // Use a custom type for more control
"render": (settingDiv, currentTheme) => {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter new theme name';
input.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 3px; margin-right: 5px;`;
settingDiv.appendChild(input);
const button = document.createElement('button');
button.classList.add('settings-button');
button.textContent = 'Create';
button.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px;`;
button.addEventListener('click', () => {
const newThemeName = input.value.trim();
if (newThemeName) {
if (themes[newThemeName]) {
alert('A theme with that name already exists!');
return;
}
themes[newThemeName] = { ...themes.dark }; // Start with a copy of the dark theme
GM_setValue("themes", themes); // Save the updated themes
// Update the theme dropdown options
const themeDropdown = document.querySelector('.settings-tab-content .settings-dropdown'); // Assuming you add a class to the dropdown
if (themeDropdown) {
const optionElement = document.createElement('option');
optionElement.value = newThemeName;
optionElement.textContent = newThemeName;
themeDropdown.appendChild(optionElement);
}
// Refresh the settings panel to show the new theme
openSettings();
} else {
alert('Please enter a theme name.');
}
});
settingDiv.appendChild(button);
}
},
{
"name": "customizeTheme",
"description": "Customize Current Theme",
"type": "custom",
"render": (settingDiv, currentTheme) => {
const currentThemeName = GM_getValue("theme", "dark");
const currentThemeData = themes[currentThemeName];
const themeSettingsContainer = document.createElement('div');
themeSettingsContainer.style.cssText = `padding: 10px; border: 1px solid ${currentTheme.contrast}; margin-top: 10px;`;
for (const key in currentThemeData) {
const settingItemDiv = document.createElement('div');
settingItemDiv.style.marginBottom = '5px';
const label = document.createElement('label');
label.textContent = key;
label.style.cssText = 'display: block; margin-bottom: 2px;';
settingItemDiv.appendChild(label);
const input = document.createElement('input');
input.type = 'color';
input.value = currentThemeData[key];
input.addEventListener('change', () => {
currentThemeData[key] = input.value;
setTheme(); // Apply changes immediately
});
settingItemDiv.appendChild(input);
themeSettingsContainer.appendChild(settingItemDiv);
}
const saveButton = document.createElement('button');
saveButton.textContent = 'Save Changes';
saveButton.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px; margin-top: 5px;`;
saveButton.addEventListener('click', () => {
GM_setValue("themes", themes);
alert(`Theme "${currentThemeName}" updated!`);
});
themeSettingsContainer.appendChild(saveButton);
settingDiv.appendChild(themeSettingsContainer);
}
}
]
},
{
"name": "Navigation",
"settings": [
{
"name": "autoPlayVideo",
"description": "Click anywhere on the page to start the video",
"type": "checkbox"
},
{
"name": "scrollPostsIntoView",
"description": "Scroll posts into view",
"type": "checkbox"
}
]
},
{
"name": "Posts",
"settings": [
{
"name": "htmlVideoPlayer",
"description": "Use HTML video player instead of the Fluid Player",
"type": "checkbox"
},
{
"name": "dynamicResizing",
"description": "Dynamically resize the page for odd aspect ratios or large screens",
"type": "checkbox"
},
{
"name": "downloadFullSizedImages",
"description": "Download the full resolution image when saving the image",
"type": "checkbox"
},
{
"name": "fitImageToScreen",
"description": "Fit image to screen (buggy)",
"type": "checkbox"
},
{
"name": "imageHover",
"description": "Displays images on the search page when hovered",
"type": "checkbox"
}
]
}
]
};
// Function to create and display the settings overlay
function openSettings() {
// Get the current theme for styling
const currentTheme = themes[GM_getValue("theme", "dark")];
// Create overlay
const overlay = document.createElement('div');
overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; z-index: 1000; /* Adjust the 0.5 for desired transparency */`;
// Create settings container
const settingsContainer = document.createElement('div');
settingsContainer.style.cssText = `width: 40vw; background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; padding: 20px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); text-align: left; display: flex; flex-direction: column; align-items: stretch; border-radius: 5px;`;
// Create tabs
const tabsContainer = document.createElement('div');
tabsContainer.style.cssText = 'display: flex; margin-bottom: 15px;';
settingsContainer.appendChild(tabsContainer);
settingsData.tabs.forEach(tab => {
const tabButton = document.createElement('button');
tabButton.classList.add('settings-tab-button');
tabButton.textContent = tab.name;
tabButton.style.cssText = `padding: 8px 15px; margin-right: 5px; background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; cursor: pointer; border-radius: 5px 5px 0 0;`;
tabsContainer.appendChild(tabButton);
const tabContent = document.createElement('div');
tabContent.classList.add('settings-tab-content');
tabContent.style.cssText = `display: none; border: 1px solid ${currentTheme.contrast}; padding: 10px; border-radius: 0 0 5px 5px;`;
settingsContainer.appendChild(tabContent);
tab.settings.forEach(setting => {
const settingDiv = document.createElement('div');
settingDiv.classList.add(`setting-${setting.name}`);
settingDiv.style.marginBottom = '10px';
const label = document.createElement('label');
label.textContent = setting.description;
label.style.cssText = 'display: block; margin-bottom: 5px; cursor: pointer;';
settingDiv.appendChild(label);
if (setting.type === "checkbox") {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = GM_getValue(setting.name, false);
checkbox.addEventListener('change', () => GM_setValue(setting.name, checkbox.checked));
label.appendChild(checkbox);
} else if (setting.type === "dropdown") {
const dropdown = document.createElement('select');
dropdown.classList.add("settings-dropdown")
dropdown.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 3px;`;
const currentValue = GM_getValue(setting.name, setting.options[0]);
setting.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option;
optionElement.textContent = option;
optionElement.selected = option === currentValue;
dropdown.appendChild(optionElement);
});
dropdown.addEventListener('change', () => {
GM_setValue(setting.name, dropdown.value);
if (setting.onChange) {
setting.onChange(); // Call the onChange function if it exists
}
});
settingDiv.appendChild(dropdown);
} else if (setting.type === "button") {
const button = document.createElement('button');
button.classList.add('settings-button');
button.textContent = setting.description;
button.style.cssText = `background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border: 1px solid ${currentTheme.contrast}; padding: 5px 10px; cursor: pointer; border-radius: 5px;`;
button.addEventListener('click', setting.action);
settingDiv.appendChild(button);
} else if (setting.type === "custom" && setting.render) {
setting.render(settingDiv, currentTheme); // Call the custom render function
}
tabContent.appendChild(settingDiv);
});
tabButton.addEventListener('click', () => {
// Show the clicked tab content and hide others
const allTabContents = document.querySelectorAll('.settings-tab-content');
allTabContents.forEach(content => content.style.display = 'none');
tabContent.style.display = 'block';
// Highlight the active tab button
const allTabButtons = document.querySelectorAll('.settings-tab-button');
allTabButtons.forEach(button => button.style.backgroundColor = currentTheme.secondary);
tabButton.style.backgroundColor = currentTheme.complementary;
});
});
// Append to overlay and body
overlay.appendChild(settingsContainer);
document.body.appendChild(overlay);
// Show the first tab by default
const firstTabButton = document.querySelector('.settings-tab-button');
if (firstTabButton) {
firstTabButton.click();
}
// Close overlay when clicking outside
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
}
// Function to set the selected theme
function setTheme() {
// Load saved themes from GM storage
const savedThemes = GM_getValue("themes");
// Check if saved themes exist and are an object, otherwise use default
if (typeof savedThemes === 'object' && savedThemes !== null) {
Object.assign(themes, savedThemes);
}
const currentTheme = themes[GM_getValue("theme", "dark")];
if (currentTheme) {
const css = `
table a:link, table a:visited { color: ${currentTheme.linkColor}; }
body { background-color: ${currentTheme.primary}; }
.flat-list, div#header ul#subnavbar, .current-page { background-color: ${currentTheme.secondary}; }
div#header ul#navbar li.current-page { background-image: url(https://imgs.search.brave.com/77L3MmxBu09NuN5WiX4HlbmWjjUe7eAsmBbakS7-DTo/rs:fit:120:120:1/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvY29tbW9ucy90/aHVtYi8wLzAyL1Ry/YW5zcGFyZW50X3Nx/dWFyZS5zdmcvMTIw/cHgtVHJhbnNwYXJl/bnRfc3F1YXJlLnN2/Zy5wbmc); }
.manual-page-chooser>input[type=text], .manual-page-chooser>input[type=submit], div.tag-search input[type=text], div.tag-search input[type=submit], button { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; }
.col2, h6, h5, .tag-count, b, li, ul, table.highlightable td, h2, table.form p, table, label { color: ${currentTheme.contrast}; }
button { box-sizing: border-box; border: 1px solid; margin-top: 3px; border-color: ${currentTheme.contrast}; }
table { background-color: ${currentTheme.tableBackground}; }
div { color: ${currentTheme.contrast}; }
.settings-tab-button { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border-color: ${currentTheme.contrast}; }
.settings-tab-button:hover, .settings-button:hover { background-color: ${currentTheme.complementary}; }
.settings-tab-content { background-color: ${currentTheme.secondary}; color: ${currentTheme.contrast}; border-color: ${currentTheme.contrast}; }
input[type="color"] { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: transparent; width: 100px; height: 40px; border: none; cursor: pointer; }
input[type="color"]::-webkit-color-swatch { border: 1px solid ${currentTheme.contrast}; border-radius: 5px; }
input[type="color"]::-moz-color-swatch { border: 1px solid ${currentTheme.contrast}; border-radius: 5px; }
`;
GM_addStyle(css);
const userIndexElement = document.getElementById("user-index");
if (userIndexElement) {
Array.from(userIndexElement.getElementsByTagName("p")).forEach(element => element.style.color = currentTheme.contrast);
}
if (GM_getValue("resizePosts", false) && window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")) {
GM_addStyle(".content{max-height: 45%; max-width: 45%; overflow: auto;}");
const imageElement = document.getElementById("image");
if (imageElement) {
imageElement.style.maxHeight = "50%";
imageElement.style.maxWidth = "fit-content";
imageElement.style.overflow = "auto";
}
}
}
}
// Function to fetch data from Rule34 API
async function getFromRule34(tags, index, limit, useBlacklist = false) {
const apiKey = await GM_getValue(API_KEY_NAME);
const userId = await GM_getValue(USER_ID_NAME);
tags = tags === "all" ? "" : tags;
let pid = index;
if (useBlacklist) {
const blacklist = decodeURIComponent(getCookie("tag_blacklist")).replaceAll("%20", " -").replaceAll("%2F", "/");
tags += blacklist ? ` -${blacklist}` : "";
}
const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=${encodeURIComponent(tags)}&limit=${limit}&pid=${pid}&json=1&api_key=${apiKey}&user_id=${userId}`;
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: url
});
if (response.status >= 200 && response.status < 300) {
return JSON.parse(response.responseText);
} else {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error(`Error fetching data: ${error}`);
return []; // Return an empty array on error
}
}
// Function to fetch data for a specific post ID
async function getFromRule34WithId(id) {
const apiKey = await GM_getValue(API_KEY_NAME);
const userId = await GM_getValue(USER_ID_NAME);
const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&id=${id}&json=1&api_key=${apiKey}&user_id=${userId}`;
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: url
});
if (response.status >= 200 && response.status < 300) {
const data = JSON.parse(response.responseText);
return data[0] || null; // Return the post or null if not found
} else {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error(`Error fetching post: ${error}`);
return null;
}
}
// Helper function to get a cookie by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// --- SCRIPT'S ORIGINAL FUNCTIONS (UNCHANGED, except where noted) ---
// (The rest of your original script functions go here, from getTagsFromUrl down to the end)
// Function to extract tags from the current URL
function getTagsFromUrl(currentUrl) {
if (currentUrl.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
return currentUrl.replace("https://rule34.xxx/index.php?page=post&s=list&tags=", "");
}
return "";
}
// Function to create modified links for post navigation
function createLinks() {
try {
if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
const imageList = document.getElementsByClassName("image-list")[0];
if (!imageList) throw new Error("Image list not found.");
const anchors = imageList.getElementsByTagName("a");
if (anchors.length === 0) throw new Error("No anchor elements found in image list.");
const urlParams = new URLSearchParams(window.location.search);
let pageNum = parseInt(urlParams.get("pid")) || 0;
for (let i = 0; i < anchors.length; i++) {
anchors[i].href = `${anchors[i].href}&srchTags=${getTagsFromUrl(window.location.href)}&index=${i + pageNum}`.replace(/[\?&]pid=\d*/g, '');
}
} else {
// This is not an error, just means we are not on the search page.
}
} catch (error) {
console.error(`Error in createLinks: ${error}`);
}
}
let preloadedData;
// Function to preload data for the next post
async function preloadNextPost(srchTags, nextIndex, limit) {
try {
preloadedData = await getFromRule34(srchTags, nextIndex, limit, true);
} catch (error) {
console.error(`Error preloading next post: ${error}`);
}
}
// Function to navigate to the next post
function navigateToNextPost(srchTags, nextIndex) {
if (!preloadedData || preloadedData.length === 0) {
console.log(preloadedData)
console.error("No preloaded data available.");
return;
}
const nextPostId = preloadedData[0].id;
const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;
window.location.href = newUrl;
}
// Function to navigate to the previous post
async function backPost() {
const urlParams = new URLSearchParams(window.location.search);
const srchTags = urlParams.get("srchTags");
const currentIndex = parseInt(urlParams.get("index"));
if (!srchTags || isNaN(currentIndex) || currentIndex <= 0) {
console.error("Invalid URL parameters or no previous post.");
return;
}
const nextIndex = currentIndex - 1;
const limit = 1;
try {
const jsonInfo = await getFromRule34(srchTags, nextIndex, limit);
if (!jsonInfo || jsonInfo.length === 0) {
console.error("No data received from API.");
return;
}
const nextPostId = jsonInfo[0].id;
const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;
window.location.href = newUrl;
} catch (error) {
console.error(`Error navigating to previous post: ${error}`);
}
}
// Function to select a random post and navigate to it
async function randomVideo() {
const urlParams = new URLSearchParams(window.location.search);
let srchTags = urlParams.get("tags");
if (!srchTags) {
const tagsInput = document.querySelector("input[name='tags']");
srchTags = tagsInput ? tagsInput.value.replace(/ /g, "+") : "";
}
try {
const posts = await getFromRule34(srchTags, 0, 1000);
if (posts.length === 0) {
console.error("No posts found for the given tags.");
return;
}
const randNum = Math.floor(Math.random() * posts.length);
const postId = posts[randNum].id;
const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${postId}&tags=${encodeURIComponent(srchTags)}&index=${randNum}`;
window.location.href = newUrl;
} catch (error) {
console.error(`Error in randomVideo: ${error}`);
}
}
// Function to download all posts for the current search
async function downloadAllPostFiles() {
const urlParams = new URLSearchParams(window.location.search);
let srchTags = urlParams.get("tags");
if (!srchTags) {
const tagsInput = document.querySelector("input[name='tags']");
srchTags = tagsInput ? tagsInput.value.replace(/ /g, "+") : "";
}
try {
const posts = await getFromRule34(srchTags, 0, 1000);
if (posts.length === 0) {
console.error("No posts found for the given tags.");
return;
}
const zipFiles = {};
const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg'];
let postsDownloaded = 0;
const totalPosts = posts.length;
for (const post of posts) {
const fileUrl = post.file_url;
const sampleUrl = post.sample_url;
const fileExtension = fileUrl.slice(fileUrl.lastIndexOf('.')).toLowerCase();
const downloadUrl = videoExtensions.includes(fileExtension) ? sampleUrl : fileUrl;
const fileName = downloadUrl.split("/").pop();
try {
const fileData = await fetchFileAsBlob(downloadUrl);
const uint8Array = new Uint8Array(await fileData.arrayBuffer());
zipFiles[fileName] = uint8Array;
console.log(`Added file ${fileName} to zip`);
postsDownloaded++;
document.title = `${postsDownloaded}/${totalPosts}`;
} catch (error) {
console.error(`Error fetching file ${downloadUrl}: ${error}`);
}
}
const zipBlob = fflate.zipSync(zipFiles, {
level: 0,
mtime: new Date()
});
console.log("Zip finished");
const a = document.createElement("a");
const zipBlobUrl = URL.createObjectURL(new Blob([zipBlob], {
type: "application/zip"
}));
a.href = zipBlobUrl;
a.download = `${srchTags.replace(/\+/g, "_")}.zip`;
a.click();
} catch (error) {
console.error(`Error in downloadAllPostFiles: ${error}`);
}
}
// Helper function to fetch a file as a Blob
async function fetchFileAsBlob(url) {
try {
const response = await GM.xmlHttpRequest({
method: "GET",
url: url,
responseType: "blob"
});
if (response.status >= 200 && response.status < 300) {
return response.response;
} else {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
} catch (error) {
throw new Error(`Error fetching ${url}: ${error}`);
}
}
// Function to create and append navigation buttons
function createNavigationButtons() {
const tagSearch = document.getElementsByClassName("tag-search")[0];
if (tagSearch) {
const randomButton = document.createElement("button");
randomButton.textContent = "Random";
randomButton.addEventListener("click", randomVideo);
tagSearch.appendChild(randomButton);
const downloadAllButton = document.createElement("button");
downloadAllButton.textContent = "↓";
downloadAllButton.addEventListener("click", downloadAllPostFiles);
tagSearch.appendChild(downloadAllButton);
}
const imageSublinks = document.getElementsByClassName("image-sublinks")[0];
if (imageSublinks) {
const backButton = document.createElement("button");
backButton.textContent = "Back";
backButton.addEventListener("click", backPost);
imageSublinks.appendChild(backButton);
const nextButton = document.createElement("button");
nextButton.id = "nextButton";
nextButton.textContent = "Next";
nextButton.addEventListener("click", () => {
const urlParams = new URLSearchParams(window.location.search);
const srchTags = urlParams.get("srchTags");
const currentIndex = parseInt(urlParams.get("index"));
navigateToNextPost(srchTags, currentIndex + 1);
});
imageSublinks.appendChild(nextButton);
}
}
// Function to enable dynamic resizing of the search input field
function enableDynamicInputResizing() {
const awesompleteElement = document.querySelector(".awesomplete > input");
if (!awesompleteElement) return;
awesompleteElement.style.position = "relative";
awesompleteElement.style.zIndex = "99";
function resizeInput() {
this.style.minWidth = "100%";
this.style.width = `${this.value.length}ch`;
}
function restoreNormalSize() {
this.style.width = "100%";
}
awesompleteElement.addEventListener('input', resizeInput);
awesompleteElement.addEventListener('click', resizeInput);
awesompleteElement.addEventListener('blur', restoreNormalSize);
}
// Add keyboard navigation for posts
const imageSublinks = document.getElementsByClassName("image-sublinks")[0];
if (imageSublinks) {
document.addEventListener("keydown", function(event) {
if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
const urlParams = new URLSearchParams(window.location.search);
const srchTags = urlParams.get("srchTags");
const currentIndex = parseInt(urlParams.get("index"));
if (event.key === "ArrowRight") {
navigateToNextPost(srchTags, currentIndex + 1);
} else if (event.key === "ArrowLeft") {
backPost();
}
});
}
// Function to handle deleted posts
async function handleDeletedPosts(id) {
const statusNotices = document.getElementsByClassName("status-notice");
if (statusNotices.length === 0) return;
let foundDeletedPost = false;
for (const statusNotice of statusNotices) {
if (statusNotice.firstChild.data.startsWith("This post was")) {
foundDeletedPost = true;
try {
const mediaJson = await getFromRule34WithId(id);
if (!mediaJson) throw new Error("Failed to retrieve post data.");
const mediaUrl = mediaJson.file_url;
const mediaType = mediaUrl.split('.').pop().toLowerCase();
const fitToScreen = document.getElementById("fit-to-screen");
const videoExtensions = ["mp4", "webm", "ogg", "mov", "avi", "wmv", "flv", "mkv", "3gp", "m4v", "mpg", "mpeg", "swf", "vob", "m2ts"];
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "svg", "webp", "heic", "heif", "ico", "raw", "psd", "ai", "eps"];
if (videoExtensions.includes(mediaType)) {
const video = document.createElement("video");
video.src = mediaUrl;
video.controls = true;
video.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";
fitToScreen.appendChild(video);
} else if (imageExtensions.includes(mediaType)) {
const image = document.createElement("img");
image.src = mediaUrl;
image.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";
fitToScreen.appendChild(image);
}
statusNotice.remove();
} catch (error) {
console.error(`Error handling deleted post: ${error}`);
}
break;
}
}
if (!foundDeletedPost) {
console.log("This post is not deleted.");
}
}
// Function to download a file
async function downloadFile(fileUrl, filename) {
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: fileUrl,
responseType: 'blob'
});
if (response.status >= 200 && response.status < 300) {
const blob = response.response;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
} else {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error(`Error downloading file: ${error}`);
}
}
// Helper function to get the last link in a parent element
function getLastLinkInParent(element) {
const elements = element.parentNode.querySelectorAll("a");
return elements[elements.length - 1];
}
// Function to handle the image resize popup
function handleImageResizePopup() {
try {
if (GM_getValue("imageResizeNotice", "resize") === "no-resize") {
const resizedNotice = document.getElementById('resized_notice');
if (resizedNotice) resizedNotice.style.display = 'none';
}
// Removed "original" option as it was causing issues and is not typically necessary
} catch (error) {
console.error(`Error handling image resize popup: ${error}`);
}
}
// Function to start the video on the first click
function autoPlayVideo() {
if (GM_getValue("autoPlayVideo", false)) {
let isFirstClick = true;
document.addEventListener("click", function() {
if (isFirstClick) {
const videoPlayer = document.getElementById("gelcomVideoPlayer");
if (videoPlayer) {
videoPlayer.autoplay = true;
const playButton = document.getElementById("gelcomVideoPlayer_fluid_initial_play");
if (playButton) playButton.click();
}
isFirstClick = false;
}
});
}
}
autoPlayVideo()
// Function to add buttons for adding tags to the search
function addTagButtons() {
const tagTypes = ["tag-type-copyright", "tag-type-general", "tag-type-character", "tag-type-artist", "tag-type-metadata"];
for (const tagType of tagTypes) {
const elements = document.getElementsByClassName(tagType);
for (const element of elements) {
const button = document.createElement("button");
button.textContent = "+";
button.addEventListener("click", function() {
const tagsInput = document.querySelector("[name='tags']");
const tagToAdd = getLastLinkInParent(this).textContent.trim().replaceAll(" ", "_");
tagsInput.value += ` ${tagToAdd}`;
});
element.insertBefore(button, element.firstChild);
}
}
}
// Function to make the video/image container resizable
function makePostResizable(isImage) {
let div = isImage ? document.getElementById("image") : document.getElementById("fluid_video_wrapper_gelcomVideoPlayer");
if (!div) return;
if (isImage) {
const newDiv = document.createElement("div");
newDiv.style.position = "relative";
div.parentNode.insertBefore(newDiv, div);
newDiv.appendChild(div);
div = newDiv;
document.getElementById("image").style.maxHeight = 'none';
}
const resizer = document.createElement("div");
resizer.style.cssText = "width: 10px; height: 10px; background-color: white; position: absolute; bottom: 0; right: 0; cursor: se-resize; z-index: 10;";
let isResizing = false;
let currentX, currentY, initialWidth, initialHeight;
resizer.addEventListener("mousedown", function(e) {
document.body.style.userSelect = 'none';
isResizing = true;
currentX = e.clientX;
currentY = e.clientY;
initialWidth = parseFloat(getComputedStyle(div).width);
initialHeight = parseFloat(getComputedStyle(div).height);
});
document.addEventListener("mouseup", () => {
document.body.style.userSelect = '';
isResizing = false;
});
document.addEventListener("mousemove", function(e) {
if (!isResizing) return;
let newWidth = initialWidth + (e.clientX - currentX);
let newHeight = initialHeight + (e.clientY - currentY);
if (!e.shiftKey) {
const ratio = initialWidth / initialHeight;
newHeight = newWidth / ratio;
}
if (isImage) {
const innerImage = div.querySelector("img");
innerImage.style.width = `${newWidth}px`;
innerImage.style.height = `${newHeight}px`;
}
div.style.width = `${newWidth}px`;
div.style.height = `${newHeight}px`;
div.style.maxHeight = "1000vh";
const videoPlayer = document.getElementById("gelcomVideoPlayer");
if (videoPlayer) {
videoPlayer.style.maxHeight = "1000vh";
videoPlayer.style.height = "100%";
}
const imageElement = document.getElementById("image");
if (imageElement) {
imageElement.style.width = `${newWidth}px`;
imageElement.style.height = `${newHeight}px`;
imageElement.style.maxHeight = "1000vh";
}
});
div.appendChild(resizer);
}
// Add an input box after the tags input field
function addInputBox() {
const tagsElement = document.querySelector("[name='tags']");
if (!tagsElement) return;
const inputBox = document.createElement("input");
inputBox.type = "text";
tagsElement.after(inputBox);
}
// Function to set the value of the tags input field
function setTags(tags) {
const tagsInput = document.querySelector("[name='tags']");
if (tagsInput) tagsInput.value = tags;
}
// Function to replace the Fluid Player with the native HTML5 video player
async function replaceWithHtmlVideoPlayer(id) {
const gelcomVideoContainer = document.getElementById("gelcomVideoContainer");
if (!gelcomVideoContainer) return;
try {
const videoUrlData = await getFromRule34WithId(id);
if (!videoUrlData) throw new Error("Failed to retrieve video URL.");
const video = document.createElement("video");
video.src = videoUrlData.file_url;
video.controls = true;
video.style.cssText = "max-height: 70%; max-width: 70%; overflow: auto;";
gelcomVideoContainer.parentNode.insertBefore(video, gelcomVideoContainer.nextSibling);
gelcomVideoContainer.remove();
const statusNotices = document.getElementById("status-notices");
if (statusNotices) statusNotices.remove();
} catch (error) {
console.error(`Error replacing video player: ${error}`);
}
}
// Function to add a close button to status notice elements
function addCloseButtonToStatusNotices() {
const statusNoticeElements = document.querySelectorAll('.status-notice');
statusNoticeElements.forEach(element => {
const closeButton = document.createElement('button');
closeButton.textContent = 'x';
closeButton.style.cssText = 'background: none; border: none; cursor: pointer;';
closeButton.addEventListener('click', () => element.remove());
element.appendChild(closeButton);
});
}
// Function to overlay the full-size image on top of the displayed image
async function overlayFullSizeImage() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
if (!id) return;
const postJson = await getFromRule34WithId(id);
if (!postJson) return;
const originalImage = document.getElementById("image");
if (!originalImage) return;
const newImage = document.createElement("img");
newImage.src = postJson.file_url;
newImage.style.cssText = `opacity: 0; width: ${originalImage.width}px; height: ${originalImage.height}px; position: absolute; top: ${originalImage.offsetTop}px; left: ${originalImage.offsetLeft}px; z-index: 1;`;
originalImage.parentNode.insertBefore(newImage, originalImage);
}
// Function to convert the search button to a link
function convertSearchToLink() {
const commitButton = document.querySelector("[name='commit']");
if (!commitButton) return;
commitButton.innerHTML = `<a href="https://rule34.xxx/index.php?page=post&s=list&tags=all">${commitButton.innerHTML}</a>`;
}
// Function to fit the post (image or video) to the screen
function fitPostToScreen() {
if (GM_getValue("downloadFullSizedImages", false) && !GM_getValue("hideAlerts", false)) {
alert(`downloadFullSizedImage and fitImageToScreen often cause bugs when used together. To disable this alert turn hide alerts on in settings.`);
}
const postElement = document.getElementById("fluid_video_wrapper_gelcomVideoPlayer") || document.getElementById("image");
if (!postElement) return;
postElement.style.maxHeight = "85vh";
postElement.style.width = "auto";
const gelcomVideoPlayer = document.getElementById("gelcomVideoPlayer");
if (gelcomVideoPlayer) {
gelcomVideoPlayer.style.maxHeight = "85vh";
gelcomVideoPlayer.style.width = "auto";
}
}
// Function to add download buttons to each post in the search results
function addDownloadButtonsToPosts() {
GM_addStyle(`
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 12px;
height: 12px;
animation: spin 1s linear infinite;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading .spinner {
display: inline-block;
}
.loading .button-text {
display: none;
}
`);
const thumbElements = document.querySelectorAll('.thumb');
thumbElements.forEach(thumb => {
const button = document.createElement('button');
button.style.cssText = 'position: absolute; top: 0; right: 0;';
const buttonText = document.createElement('span');
buttonText.classList.add('button-text');
buttonText.textContent = '↓';
button.appendChild(buttonText);
const spinner = document.createElement('div');
spinner.classList.add('spinner');
button.appendChild(spinner);
button.addEventListener('click', async () => {
button.classList.add('loading');
try {
const aElement = thumb.querySelector('a');
if (!aElement) {
alert('No <a> element found within this thumb element.');
return;
}
const id = aElement.id.substring(1);
const data = await getFromRule34WithId(id);
if (!data) return;
const fileUrl = data.file_url;
const sampleUrl = data.sample_url;
const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg'];
const fileExtension = fileUrl.split('.').pop().toLowerCase();
const downloadUrl = videoExtensions.includes(fileExtension) ? sampleUrl : fileUrl;
const filename = downloadUrl.split('/').pop();
await downloadFile(downloadUrl, filename);
} catch (error) {
console.error('Error downloading file:', error);
} finally {
button.classList.remove('loading');
}
});
thumb.style.position = 'relative';
thumb.appendChild(button);
});
}
// Function to display media information on hover
async function displayMediaData() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
if (!id) return;
const postJson = await getFromRule34WithId(id);
if (!postJson) return;
let mediaURL = postJson.file_url;
const mediaType = mediaURL.split('.').pop().toLowerCase();
const tooltipContent = document.createElement('div');
const handleImage = async () => {
const img = new Image();
img.src = mediaURL;
img.onload = async () => {
const width = img.width;
const height = img.height;
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: mediaURL,
responseType: 'blob'
});
const imageSizeBytes = response.response.size;
const imageSizeKB = imageSizeBytes / 1024;
const imageSizeMB = imageSizeKB / 1024;
const sizeInfo = imageSizeMB >= 1 ? `${imageSizeMB.toFixed(2)} MB` : `${imageSizeKB.toFixed(2)} KB`;
tooltipContent.innerHTML = `
<div>Media URL: ${mediaURL}</div>
<div>Media Type: ${mediaType}</div>
<div>Width: ${width}px</div>
<div>Height: ${height}px</div>
<div>Size: ${sizeInfo}</div>
`;
appendTooltipToPage(tooltipContent);
} catch (error) {
console.error('Failed to fetch image size:', error);
}
};
img.onerror = (error) => console.error('Failed to load image:', error);
};
const handleVideo = async () => {
const video = document.createElement('video');
video.src = mediaURL;
mediaURL = postJson.sample_url;
video.onloadedmetadata = async () => {
const width = video.videoWidth;
const height = video.videoHeight;
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: mediaURL,
responseType: 'blob'
});
const videoSizeBytes = response.response.size;
const videoSizeKB = videoSizeBytes / 1024;
const videoSizeMB = videoSizeKB / 1024;
const sizeInfo = videoSizeMB >= 1 ? `${videoSizeMB.toFixed(2)} MB` : `${videoSizeKB.toFixed(2)} KB`;
video.currentTime = 1;
video.onseeked = () => {
tooltipContent.innerHTML = `
<div>Media URL: ${mediaURL}</div>
<div>Media Type: ${mediaType}</div>
<div>Width: ${width}px</div>
<div>Height: ${height}px</div>
<div>Size: ${sizeInfo}</div>
`;
appendTooltipToPage(tooltipContent);
};
} catch (error) {
console.error('Failed to fetch video size:', error);
}
};
video.onerror = (error) => console.error('Failed to load video:', error);
};
if (['jpg', 'jpeg', 'png', 'gif'].includes(mediaType)) {
await handleImage();
} else if (['mp4', 'webm', 'ogg'].includes(mediaType)) {
await handleVideo();
} else {
console.error('Unsupported media type:', mediaType);
}
}
// Helper function to append the tooltip to the page
function appendTooltipToPage(tooltipContent) {
const infoIcon = document.createElement('span');
infoIcon.innerHTML = 'ℹ️';
infoIcon.style.cssText = 'cursor: pointer; margin-left: 10px;';
infoIcon.title = 'Media Information';
const tooltip = document.createElement('div');
tooltip.appendChild(tooltipContent);
tooltip.style.cssText = 'position: absolute; background-color: #fff; border: 1px solid #ccc; padding: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); display: none; z-index: 1000;';
infoIcon.appendChild(tooltip);
infoIcon.onmouseover = () => tooltip.style.display = 'block';
infoIcon.onmouseout = () => tooltip.style.display = 'none';
const imageSublinks = document.querySelector('.image-sublinks');
if (imageSublinks) imageSublinks.appendChild(infoIcon);
}
let activeImageContainer = null;
// Function to add a hover effect to display full-size images on the search page
async function addHoverEffect() {
const thumbImages = document.querySelectorAll('.thumb a[id^="p"]'); // More specific selector
thumbImages.forEach(thumbLink => {
const parentThumb = thumbLink.closest('.thumb');
if (!parentThumb) return;
parentThumb.addEventListener('mouseenter', async function(event) {
if (activeImageContainer) {
activeImageContainer.remove();
}
let id = thumbLink.id.replace(/\D/g, '');
const postData = await getFromRule34WithId(id);
if (!postData || !postData.file_url) {
console.error(`No file_url found for ID: ${id}`);
return;
}
const imageContainer = document.createElement('div');
imageContainer.style.cssText = 'position: absolute; z-index: 1000; border: 2px solid black; padding: 10px; background-color: white;';
const image = document.createElement('img');
image.style.maxWidth = '300px';
image.style.maxHeight = '300px';
image.src = postData.file_url;
imageContainer.appendChild(image);
document.body.appendChild(imageContainer);
activeImageContainer = imageContainer;
function moveImageAtCursor(e) {
if (parentThumb.matches(':hover')) {
imageContainer.style.left = `${e.pageX + 10}px`;
imageContainer.style.top = `${e.pageY + 10}px`;
} else {
imageContainer.remove();
document.removeEventListener('mousemove', moveImageAtCursor);
activeImageContainer = null;
}
}
document.addEventListener('mousemove', moveImageAtCursor);
parentThumb.addEventListener('mouseleave', function() {
if (activeImageContainer) {
activeImageContainer.remove();
activeImageContainer = null;
}
document.removeEventListener('mousemove', moveImageAtCursor);
});
});
});
}
// Function to scroll the post into view
function scrollPostIntoView() {
if (GM_getValue("scrollPostsIntoView", false)) {
const postElement = document.getElementById("image") || document.getElementById("gelcomVideoPlayer");
if (postElement) {
setTimeout(() => postElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
}), 250);
}
}
}
// Register the settings menu command
GM_registerMenuCommand('Settings', openSettings);
function executeScript() {
const urlParams = new URLSearchParams(window.location.search);
// Call functions based on the current page and settings
createNavigationButtons();
createLinks();
addCloseButtonToStatusNotices();
setTheme();
handleImageResizePopup();
addTagButtons();
enableDynamicInputResizing();
scrollPostIntoView();
try {
const idParam = urlParams.get("id");
if (idParam) {
if (GM_getValue("undeletePosts", false)) {
handleDeletedPosts(idParam);
}
if (GM_getValue("htmlVideoPlayer", false)) {
replaceWithHtmlVideoPlayer(idParam);
}
}
} catch (error) {
console.error(`Error in script execution: ${error}`);
}
if (GM_getValue("downloadFullSizedImages", false)) {
overlayFullSizeImage();
}
if (GM_getValue("fitImageToScreen", false)) {
fitPostToScreen();
}
if (GM_getValue("imageHover", false)) {
addHoverEffect();
}
if (window.location.href.includes('page=post&s=view')) {
displayMediaData();
}
if (window.location.href.includes('page=post&s=list')) {
addDownloadButtonsToPosts();
}
// Set tags based on URL parameters
if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")) {
setTags(urlParams.get("srchTags"));
} else if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list")) {
setTags(urlParams.get("tags"));
}
// Ensure note boxes are always on top
const noteBoxes = document.querySelectorAll(".note-box");
noteBoxes.forEach(noteBox => noteBox.style.zIndex = "999");
// Call functions to make video/image resizable after a short delay
setTimeout(() => {
makePostResizable(false); // For video
makePostResizable(true); // For image
}, 300);
if (document.readyState === "complete" && !window.betterRule34Initialized) {
const srchTags = urlParams.get("srchTags");
const currentIndex = parseInt(urlParams.get("index"));
if (!srchTags || isNaN(currentIndex)) {
// Not on a post page, which is fine.
return;
}
const nextIndex = currentIndex + 1;
const limit = 1; // Only need to preload the very next one
// Preload data for the next post
preloadNextPost(srchTags, nextIndex, limit);
// Event listener for when the user tries to navigate to the next post
const nextButton = document.getElementById("nextButton");
if (nextButton) {
nextButton.removeEventListener("click", navigateToNextPost)
nextButton.addEventListener("click", () => {
navigateToNextPost(srchTags, nextIndex);
});
}
window.betterRule34Initialized = true;
}
}
// --- Main Execution ---
// Initialize settings first, always.
initializeSettings();
// Handle the options page immediately if we're on it.
await handleOptionsPage();
// Check for API key. This will halt the script with a prompt if the key is missing.
await checkApiKey();
// If the key exists, proceed with the rest of the script.
executeScript();
// Listen for popstate event to re-run the script when navigating through history
window.addEventListener('popstate', executeScript);
})().catch(console.error);