Sleazy Fork is available in English.

Rule34 Enhanced Dark Gallery

Modern dark theme with masonry layout, advanced search overlay, smart image loading, and collapsible sidebar

As of 08.06.2025. See the latest version.

// ==UserScript==
// @name         Rule34 Enhanced Dark Gallery
// @namespace    ko-fi.com/awesome97076
// @version      3.0
// @description  Modern dark theme with masonry layout, advanced search overlay, smart image loading, and collapsible sidebar
// @author       Awesome
// @match        https://rule34.xxx/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant        none
// @run-at       document-start
// @noframes
// ==/UserScript==

(function () {
  "use strict";

  const CONFIG = {
    debug: false,
    searchOverlay: { enabled: true, hotkey: "/" },
    imageReplacement: { enabled: true, batchDelay: 50 },
    sidebar: { collapsible: true, rememberState: true },
  };

  const Utils = {
    log: (...args) => CONFIG.debug && console.log("[Rule34 Enhanced]", ...args),
    createElement: (tag, props = {}, children = []) => {
      const element = document.createElement(tag);
      Object.assign(element, props);
      if (typeof children === "string") {
        element.innerHTML = children;
      } else {
        children.forEach((child) => element.appendChild(child));
      }
      return element;
    },
    debounce: (func, wait) => {
      let timeout;
      return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
      };
    },
    throttle: (func, limit) => {
      let inThrottle;
      return function (...args) {
        if (!inThrottle) {
          func.apply(this, args);
          inThrottle = true;
          setTimeout(() => (inThrottle = false), limit);
        }
      };
    },
  };

  const StylesManager = {
    init() {
      const style = document.createElement("style");
      style.innerHTML = this.getCSS();
      document.head.appendChild(style);
    },

    getCSS() {
      return `
:root {
  --bg-color: #121212;
  --bg-secondary: #1e1e1e;
  --bg-tertiary: #2d2d2d;
  --accent-color: #9c64a6;
  --accent-secondary: #ae81ff;
  --text-primary: #e0e0e0;
  --text-secondary: #b0b0b0;
  --text-muted: #707070;
  --border-color: rgba(255, 255, 255, 0.1);
  --tag-artist: #ff79c6;
  --tag-character: #50fa7b;
  --tag-copyright: #bd93f9;
  --tag-metadata: #f1fa8c;
  --column-count: 3;
  --column-gap: 3px;
  --border-radius: 6px;
  --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
  --font-primary: 'Inter', 'Segoe UI', Roboto, sans-serif;
  --font-secondary: 'Poppins', 'Segoe UI', Roboto, sans-serif;
  --font-size-base: clamp(14px, 2.5vw, 16px);
}

html, body {
  margin: 0;
  padding: 0;
  font-family: var(--font-primary);
  font-size: var(--font-size-base);
  background: var(--bg-color);
  color: var(--text-primary);
  min-width: 320px;
  overflow-x: hidden;
}

*, *::before, *::after { box-sizing: border-box; }

div#content {
  width: 100%;
  max-width: 100%;
  margin: 0 auto;
  padding: 8px;
}

div#header {
  background: var(--bg-secondary);
  padding: 0;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  position: sticky;
  top: 0;
  z-index: 1000;
  margin-bottom: clamp(16px, 3vw, 24px);
}

div#header #site-title {
  background-image: none;
  padding: clamp(10px, 2vw, 15px) clamp(15px, 3vw, 20px);
}

div#header #site-title a {
  color: var(--accent-color);
  font-size: clamp(20px, 4vw, 24px);
  font-weight: bold;
  font-family: var(--font-secondary);
}

div#header ul#navbar,
div#header ul#subnavbar {
  background: var(--bg-secondary);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  list-style: none;
  padding: 0 clamp(10px, 2vw, 20px);
  margin: 0;
  border-bottom: 1px solid var(--border-color);
  overflow-x: auto;
}

div#header ul#navbar li a,
div#header ul#subnavbar li a {
  display: block;
  padding: clamp(10px, 2vw, 15px);
  color: var(--text-secondary);
  transition: color 0.2s ease;
  text-decoration: none;
  white-space: nowrap;
  font-size: clamp(13px, 2.5vw, 15px);
}

div#header ul#navbar li a:hover,
div#header ul#subnavbar li a:hover {
  color: var(--accent-color);
}

a:link, a:visited {
  color: var(--accent-color);
  text-decoration: none;
  transition: color 0.2s;
}

a:hover, a:active {
  color: var(--accent-secondary);
  text-decoration: underline;
}

.tag-type-artist a, .tag-type-artist { color: var(--tag-artist) !important; }
.tag-type-character a, .tag-type-character { color: var(--tag-character) !important; }
.tag-type-copyright a, .tag-type-copyright { color: var(--tag-copyright) !important; }
.tag-type-metadata a, .tag-type-metadata { color: var(--tag-metadata) !important; }

div.image-list {
  display: block !important;
  column-count: var(--column-count);
  column-gap: var(--column-gap);
  width: 100%;
  margin: 0 auto;
  padding: 0;
}

.thumb {
  width: 100% !important;
  height: auto !important;
  display: inline-block !important;
  break-inside: avoid;
  margin-bottom: var(--column-gap);
  padding: 0;
  page-break-inside: avoid;
}

.thumb a {
  display: block !important;
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  background: var(--bg-tertiary);
  box-shadow: var(--box-shadow);
  transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.thumb a:hover {
  transform: scale(1.08);
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
  z-index: 9999;
}

.thumb img, .thumb video {
  width: 100% !important;
  height: auto !important;
  display: block !important;
  transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  object-fit: contain;
}

.webm-thumb {
  border: 2px solid #8e44ad !important;
  border-radius: 8px !important;
}

.r34-search-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(5px);
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.r34-search-overlay.show {
  opacity: 1;
  visibility: visible;
}

.r34-search-modal {
  background: var(--bg-secondary);
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  border: 1px solid var(--border-color);
  width: 90%;
  max-width: 600px;
  max-height: 80vh;
  transform: scale(0.9) translateY(20px);
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  overflow: visible;
}

.r34-search-overlay.show .r34-search-modal {
  transform: scale(1) translateY(0);
}

.r34-search-header {
  background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-secondary) 100%);
  padding: 16px 20px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-radius: 16px 16px 0 0;
}

.r34-search-title {
  color: white;
  font-family: var(--font-secondary);
  font-size: 18px;
  font-weight: 700;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

.r34-search-title::before {
  content: "⚡";
  font-size: 20px;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
}

.r34-search-controls {
  display: flex;
  gap: 8px;
}

.r34-search-minimize,
.r34-search-close {
  background: rgba(255, 255, 255, 0.15);
  color: white;
  border: none;
  border-radius: 6px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 16px;
  font-weight: bold;
  backdrop-filter: blur(10px);
}

.r34-search-minimize:hover {
  background: rgba(255, 255, 255, 0.25);
  transform: scale(1.05);
}

.r34-search-close:hover {
  background: rgba(255, 85, 85, 0.8);
  transform: scale(1.05);
}

.r34-search-body {
  padding: 25px;
}

.r34-search-form {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.r34-search-input {
  width: 100%;
  padding: 15px 20px;
  background: var(--bg-color);
  border: 2px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  color: var(--text-primary);
  font-family: var(--font-primary);
  font-size: 16px;
  transition: all 0.3s ease;
  resize: vertical;
  min-height: 60px;
  line-height: 1.4;
}

.r34-search-input:focus {
  outline: none;
  border-color: var(--accent-color);
  box-shadow: 0 0 0 3px rgba(156, 100, 166, 0.15);
  transform: translateY(-1px);
}

.r34-search-input::placeholder {
  color: var(--text-muted);
  font-style: italic;
}

.r34-search-button {
  padding: 15px 25px;
  background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-secondary) 100%);
  color: white;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 600;
  font-size: 16px;
  transition: all 0.3s ease;
}

.r34-search-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(156, 100, 166, 0.4);
}

.r34-search-hint {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  margin-top: 10px;
  font-style: italic;
}

.r34-search-shortcut {
  background: var(--bg-tertiary);
  color: var(--text-secondary);
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-family: monospace;
  margin-left: 8px;
}

.r34-autocomplete-dropdown {
  position: fixed;
  background: var(--bg-color);
  border: 2px solid var(--accent-color);
  border-top: none;
  border-radius: 0 0 12px 12px;
  max-height: 300px;
  overflow-y: auto;
  z-index: 10001;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  scrollbar-width: thin;
  scrollbar-color: var(--accent-color) var(--bg-tertiary);
}

.r34-autocomplete-item {
  padding: 12px 16px;
  cursor: pointer;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  transition: all 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.r34-autocomplete-item:hover,
.r34-autocomplete-item.selected {
  background: rgba(156, 100, 166, 0.15);
}

.r34-autocomplete-tag {
  font-size: 14px;
  font-weight: 500;
  flex: 1;
  margin-right: 8px;
}

.r34-autocomplete-count {
  font-size: 12px;
  color: var(--text-muted);
  margin-right: 8px;
}

.r34-autocomplete-type {
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 12px;
  text-transform: uppercase;
  font-weight: bold;
  min-width: 50px;
  text-align: center;
}

.r34-tag-artist {
  border-left: 3px solid var(--tag-artist);
}
.r34-tag-artist .r34-autocomplete-tag { color: var(--tag-artist); }
.r34-tag-artist .r34-autocomplete-type {
  background: rgba(255, 121, 198, 0.2);
  color: var(--tag-artist);
}

.r34-tag-character {
  border-left: 3px solid var(--tag-character);
}
.r34-tag-character .r34-autocomplete-tag { color: var(--tag-character); }
.r34-tag-character .r34-autocomplete-type {
  background: rgba(80, 250, 123, 0.2);
  color: var(--tag-character);
}

.r34-tag-copyright {
  border-left: 3px solid var(--tag-copyright);
}
.r34-tag-copyright .r34-autocomplete-tag { color: var(--tag-copyright); }
.r34-tag-copyright .r34-autocomplete-type {
  background: rgba(189, 147, 249, 0.2);
  color: var(--tag-copyright);
}

.r34-tag-metadata {
  border-left: 3px solid var(--tag-metadata);
}
.r34-tag-metadata .r34-autocomplete-tag { color: var(--tag-metadata); }
.r34-tag-metadata .r34-autocomplete-type {
  background: rgba(241, 250, 140, 0.2);
  color: var(--tag-metadata);
}

.r34-tag-general {
  border-left: 3px solid #b0b0b0;
}
.r34-tag-general .r34-autocomplete-tag { color: #b0b0b0; }
.r34-tag-general .r34-autocomplete-type {
  background: rgba(176, 176, 176, 0.2);
  color: #b0b0b0;
}

.r34-column-control {
  margin-bottom: 18px;
  background: var(--bg-secondary);
  padding: 15px;
  border-radius: 12px;
  border: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  gap: 15px;
  box-shadow: var(--box-shadow);
}

.r34-column-control label {
  color: var(--text-primary);
  font-weight: 600;
  min-width: 70px;
}

.r34-column-control input[type="range"] {
  flex: 1;
  height: 6px;
  background: var(--bg-tertiary);
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}

.r34-column-count {
  color: var(--accent-color);
  font-weight: bold;
  font-size: 18px;
  min-width: 20px;
  text-align: center;
}

div.sidebar {
  background: var(--bg-secondary);
  border-radius: 12px;
  box-shadow: var(--box-shadow);
  padding: 0;
  margin-right: clamp(10px, 2vw, 20px);
  margin-bottom: clamp(15px, 3vw, 25px);
  max-width: 280px;
  min-width: 260px;
  border: 1px solid var(--border-color);
  overflow: hidden;
}

#tag-sidebar {
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 60vh;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--accent-color) var(--bg-tertiary);
}

#tag-sidebar h6 {
  background: var(--bg-tertiary);
  color: var(--text-primary);
  margin: 0;
  padding: 12px 20px;
  font-family: var(--font-secondary);
  font-weight: 600;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 1px;
  border-bottom: 1px solid var(--border-color);
  cursor: pointer;
  user-select: none;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

#tag-sidebar h6:hover {
  background: rgba(156, 100, 166, 0.2);
  color: var(--accent-color);
}

#tag-sidebar h6::after {
  content: '';
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 8px solid currentColor;
  transition: transform 0.3s ease;
  opacity: 0.7;
}

#tag-sidebar h6.collapsed::after {
  transform: rotate(-90deg);
}

.tag-section {
  overflow: hidden;
  transition: max-height 0.4s ease, opacity 0.3s ease;
  max-height: 1000px;
  opacity: 1;
}

.tag-section.collapsed {
  max-height: 0;
  opacity: 0;
}

#tag-sidebar li {
  padding: 0;
  margin: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  transition: background 0.2s ease;
}

#tag-sidebar li:not(:has(h6)):hover {
  background: rgba(255, 255, 255, 0.03);
}

div#footer {
  margin-top: clamp(30px, 5vw, 40px);
  padding: clamp(15px, 3vw, 20px) 0;
  text-align: center;
  color: var(--text-muted);
  border-top: 1px solid var(--border-color);
  font-size: clamp(12px, 2vw, 14px);
}

div.tag-search {
  background: var(--bg-tertiary);
  padding: clamp(10px, 2vw, 15px);
  border-radius: var(--border-radius);
  margin-bottom: clamp(15px, 3vw, 20px);
  width: 100%;
  position: relative;
}

div.tag-search input[type="text"] {
  width: 100%;
  padding: 8px 12px;
  background: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  color: var(--text-primary);
  font-family: var(--font-primary);
  font-size: clamp(14px, 2.5vw, 16px);
  margin-bottom: 10px;
}

div.tag-search input[type="submit"] {
  padding: 8px 16px;
  background: var(--accent-color);
  color: white;
  border: none;
  border-radius: var(--border-radius);
  cursor: pointer;
  font-weight: bold;
  font-size: clamp(14px, 2.5vw, 16px);
  transition: background 0.2s;
  width: 100%;
  max-width: 200px;
}

div.tag-search input[type="submit"]:hover {
  background: var(--accent-secondary);
}

.r34-search-overlay-trigger {
  position: absolute;
  top: 10px;
  right: 10px;
  background: var(--accent-color);
  color: white;
  border: none;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.2s ease;
  z-index: 10;
}

.r34-search-overlay-trigger:hover {
  background: var(--accent-secondary);
  transform: scale(1.1);
}

@media (min-width: 2560px) { :root { --column-count: 6; --column-gap: 20px; } }
@media (max-width: 2559px) and (min-width: 1920px) { :root { --column-count: 4; --column-gap: 18px; } }
@media (max-width: 1919px) and (min-width: 1600px) { :root { --column-count: 4; --column-gap: 16px; } }
@media (max-width: 1599px) and (min-width: 1200px) { :root { --column-count: 3; --column-gap: 14px; } }
@media (max-width: 1199px) and (min-width: 992px) { :root { --column-count: 3; --column-gap: 12px; } }
@media (max-width: 991px) and (min-width: 768px) { :root { --column-count: 3; --column-gap: 10px; } }
@media (max-width: 767px) and (min-width: 576px) { :root { --column-count: 2; --column-gap: 8px; } }
@media (max-width: 575px) {
  :root { --column-count: 1; --column-gap: 10px; }
  .r34-search-modal { width: 95%; margin: 10px; }
  .r34-autocomplete-dropdown { max-height: 200px; }
  .r34-autocomplete-item { padding: 10px 12px; }
}
      `;
    },
  };

  const SearchOverlay = {
    overlay: null,
    isOpen: false,
    autocompleteCache: new Map(),
    currentSuggestions: [],
    selectedSuggestionIndex: -1,
    pendingRequest: null,
    lastQuery: "",

    init() {
      if (!CONFIG.searchOverlay.enabled) return;
      setTimeout(() => {
        this.createOverlay();
        this.bindEvents();
        this.addTriggerButton();
      }, 100);
    },

    createOverlay() {
      this.overlay = Utils.createElement("div", {
        className: "r34-search-overlay",
        innerHTML: `
        <div class="r34-search-modal">
          <div class="r34-search-header">
            <h3 class="r34-search-title">Enhanced Search</h3>
            <div class="r34-search-controls">
              <button class="r34-search-close" type="button" title="Close">&times;</button>
            </div>
          </div>
          <div class="r34-search-body">
            <form class="r34-search-form" action="index.php" method="get">
              <input type="hidden" name="page" value="post">
              <input type="hidden" name="s" value="list">
              <div class="r34-search-input-container" style="position: relative;">
                <textarea
                  name="tags"
                  class="r34-search-input"
                  placeholder="Enter search tags...&#10;Examples:&#10;• female solo animated&#10;• character_name -furry rating:safe&#10;• artist_name 1girl"
                  rows="6"
                ></textarea>
                <div class="r34-autocomplete-dropdown" style="display: none;"></div>
              </div>
              <button type="submit" class="r34-search-button">Search</button>
              <div class="r34-search-hint">
                Press <span class="r34-search-shortcut">/</span> to open
                <span class="r34-search-shortcut">Esc</span> to close
                <span class="r34-search-shortcut">↑↓</span> navigate
                <span class="r34-search-shortcut">Tab/Enter</span> select
              </div>
            </form>
          </div>
        </div>
      `,
      });
      document.body.appendChild(this.overlay);
    },

    bindEvents() {
      if (!this.overlay) return;

      const closeButton = this.overlay.querySelector(".r34-search-close");
      const textarea = this.overlay.querySelector(".r34-search-input");
      const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");

      closeButton?.addEventListener("click", () => this.close());
      this.overlay.addEventListener("click", (e) => {
        if (e.target === this.overlay) this.close();
      });

      document.addEventListener("keydown", (e) => {
        const isInInput = document.activeElement?.tagName === "INPUT" ||
                         document.activeElement?.tagName === "TEXTAREA";

        if (e.key === CONFIG.searchOverlay.hotkey && !this.isOpen && !isInInput) {
          e.preventDefault();
          this.open();
        } else if (e.key === "Escape" && this.isOpen) {
          e.preventDefault();
          this.close();
        }
      }, true);

      if (textarea) {
        textarea.addEventListener("input", Utils.debounce((e) => this.handleInput(e), 400));
        textarea.addEventListener("keydown", (e) => this.handleKeyDown(e));
        textarea.addEventListener("blur", () => {
          this.cancelPendingRequest();
          setTimeout(() => this.hideAutocomplete(), 150);
        });
        this.populateCurrentSearch();
      }

      dropdown?.addEventListener("click", (e) => {
        const suggestion = e.target.closest(".r34-autocomplete-item");
        if (suggestion) this.selectSuggestion(suggestion.dataset.value);
      });
    },

    async handleInput(e) {
      const textarea = e.target;
      const currentTag = this.getCurrentTag(textarea.value, textarea.selectionStart);

      if (!currentTag || currentTag.length < 2 || currentTag === this.lastQuery) {
        this.hideAutocomplete();
        return;
      }

      this.lastQuery = currentTag;
      this.cancelPendingRequest();
      await this.showAutocomplete(currentTag);
    },

    cancelPendingRequest() {
      if (this.pendingRequest) {
        this.pendingRequest.cancelled = true;
        this.pendingRequest = null;
      }
    },

    async fetchSuggestions(query) {
      if (this.autocompleteCache.has(query)) {
        return this.autocompleteCache.get(query);
      }

      this.cancelPendingRequest();

      try {
        const requestPromise = fetch(`https://ac.rule34.xxx/autocomplete.php?q=${encodeURIComponent(query)}`);
        this.pendingRequest = { promise: requestPromise, cancelled: false };

        const response = await requestPromise;
        if (this.pendingRequest?.cancelled || !response.ok) return [];

        const jsonData = await response.json();
        if (this.pendingRequest?.cancelled) return [];

        const suggestions = jsonData.slice(0, 8).map((item) => ({
          label: item.label,
          value: item.value,
          type: item.type,
          count: this.extractCount(item.label),
        }));

        this.autocompleteCache.set(query, suggestions);
        if (this.autocompleteCache.size > 50) this.cleanupCache();

        this.pendingRequest = null;
        return suggestions;
      } catch (error) {
        this.pendingRequest = null;
        return [];
      }
    },

    cleanupCache() {
      const cacheEntries = Array.from(this.autocompleteCache.entries()).slice(0, 30);
      this.autocompleteCache.clear();
      cacheEntries.forEach(([key, value]) => this.autocompleteCache.set(key, value));
    },

    async showAutocomplete(query) {
      const suggestions = await this.fetchSuggestions(query);
      const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");
      const textarea = this.overlay.querySelector(".r34-search-input");

      if (!dropdown || !textarea || suggestions.length === 0) {
        this.hideAutocomplete();
        return;
      }

      // Position dropdown with your preferred coordinates
      dropdown.style.left = "27px";
      dropdown.style.width = "545px";

      this.currentSuggestions = suggestions;
      this.selectedSuggestionIndex = -1;

      dropdown.innerHTML = suggestions
        .map((item, index) => {
          const typeClass = `r34-tag-${item.type}`;
          const countDisplay = item.count > 0 ? ` (${item.count})` : "";

          return `
        <div class="r34-autocomplete-item ${typeClass}" data-value="${item.value}" data-index="${index}">
          <span class="r34-autocomplete-tag">${item.value}</span>
          <span class="r34-autocomplete-count">${countDisplay}</span>
          <span class="r34-autocomplete-type">${item.type}</span>
        </div>`;
        })
        .join("");

      dropdown.style.display = "block";
    },

    hideAutocomplete() {
      const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");
      if (dropdown) dropdown.style.display = "none";
      this.currentSuggestions = [];
      this.selectedSuggestionIndex = -1;
    },

    handleKeyDown(e) {
      const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");
      const isDropdownVisible = dropdown && dropdown.style.display !== "none";

      if (!isDropdownVisible) return;

      switch (e.key) {
        case "ArrowDown":
          e.preventDefault();
          this.selectedSuggestionIndex = Math.min(
            this.selectedSuggestionIndex + 1,
            this.currentSuggestions.length - 1
          );
          this.updateSelectedSuggestion();
          break;
        case "ArrowUp":
          e.preventDefault();
          this.selectedSuggestionIndex = Math.max(this.selectedSuggestionIndex - 1, -1);
          this.updateSelectedSuggestion();
          break;
        case "Enter":
        case "Tab":
          if (this.selectedSuggestionIndex >= 0) {
            e.preventDefault();
            const selectedTag = this.currentSuggestions[this.selectedSuggestionIndex].value;
            this.selectSuggestion(selectedTag);
          }
          break;
        case "Escape":
          this.hideAutocomplete();
          break;
      }
    },

    updateSelectedSuggestion() {
      const items = this.overlay.querySelectorAll(".r34-autocomplete-item");
      items.forEach((item, index) => {
        item.classList.toggle("selected", index === this.selectedSuggestionIndex);
      });
    },

    selectSuggestion(selectedTag) {
      const textarea = this.overlay.querySelector(".r34-search-input");
      if (!textarea) return;

      const cursorPosition = textarea.selectionStart;
      const text = textarea.value;
      const beforeCursor = text.substring(0, cursorPosition);
      const afterCursor = text.substring(cursorPosition);
      const beforeTags = beforeCursor.split(/[\s\n]+/);
      const currentTagStart = beforeCursor.lastIndexOf(beforeTags[beforeTags.length - 1]);
      const afterTags = afterCursor.split(/[\s\n]+/);
      const currentTagEnd = cursorPosition + (afterTags[0] ? afterTags[0].length : 0);

      const newText = text.substring(0, currentTagStart) + selectedTag + " " + text.substring(currentTagEnd);
      const newCursorPosition = currentTagStart + selectedTag.length + 1;

      textarea.value = newText;
      textarea.setSelectionRange(newCursorPosition, newCursorPosition);
      this.hideAutocomplete();
      textarea.focus();
    },

    getCurrentTag(text, cursorPosition) {
      const beforeCursor = text.substring(0, cursorPosition);
      const afterCursor = text.substring(cursorPosition);
      const beforeTags = beforeCursor.split(/[\s\n]+/);
      const afterTags = afterCursor.split(/[\s\n]+/);

      let currentTag = beforeTags[beforeTags.length - 1] || "";
      if (afterTags[0] && !text[cursorPosition]?.match(/[\s\n]/)) {
        currentTag += afterTags[0];
      }
      return currentTag.trim();
    },

    extractCount(label) {
      const match = label.match(/\((\d+)\)$/);
      return match ? parseInt(match[1]) : 0;
    },

    addTriggerButton() {
      const searchForm = document.querySelector("div.tag-search");
      if (!searchForm || searchForm.querySelector(".r34-search-overlay-trigger")) return;

      const triggerButton = Utils.createElement("button", {
        className: "r34-search-overlay-trigger",
        type: "button",
        innerHTML: "⚡",
        title: "Open Advanced Search (Press /)",
      });

      triggerButton.addEventListener("click", (e) => {
        e.preventDefault();
        this.open();
      });

      searchForm.appendChild(triggerButton);
    },

    populateCurrentSearch() {
      const urlParams = new URLSearchParams(window.location.search);
      const tags = urlParams.get("tags");
      if (tags) {
        const textarea = this.overlay?.querySelector(".r34-search-input");
        if (textarea) textarea.value = decodeURIComponent(tags);
      }
    },

    open() {
      if (!this.overlay || this.isOpen) return;
      this.isOpen = true;
      this.overlay.classList.add("show");
      document.body.style.overflow = "hidden";

      setTimeout(() => {
        const textarea = this.overlay.querySelector(".r34-search-input");
        if (textarea) {
          textarea.focus();
          textarea.setSelectionRange(textarea.value.length, textarea.value.length);
        }
      }, 150);
    },

    close() {
      if (!this.overlay || !this.isOpen) return;
      this.isOpen = false;
      this.overlay.classList.remove("show");
      document.body.style.overflow = "";
      this.hideAutocomplete();
    },
  };

  const ColumnControl = {
    init() {
      this.addControlPanel();
      this.loadSavedColumns();
    },

    addControlPanel() {
      if (document.getElementById("columnSlider")) return;

      const controlPanel = Utils.createElement("div", {
        className: "r34-column-control",
        innerHTML: `
          <label for="columnSlider">Columns:</label>
          <input type="range" id="columnSlider" min="1" max="8" value="3">
          <span class="r34-column-count" id="columnCount">3</span>
        `,
      });

      const imageList = document.querySelector(".image-list");
      if (imageList?.parentNode) {
        imageList.parentNode.insertBefore(controlPanel, imageList);
      }

      this.bindEvents();
    },

    bindEvents() {
      const slider = document.getElementById("columnSlider");
      const countDisplay = document.getElementById("columnCount");

      if (!slider || !countDisplay) return;

      slider.addEventListener("input", (e) => {
        const count = e.target.value;
        countDisplay.textContent = count;
        this.setColumnCount(count);
        localStorage.setItem("galleryColumns", count);
      });
    },

    setColumnCount(count) {
      document.documentElement.style.setProperty("--column-count", count);
    },

    loadSavedColumns() {
      const savedColumns = localStorage.getItem("galleryColumns");
      if (savedColumns) {
        const slider = document.getElementById("columnSlider");
        const countDisplay = document.getElementById("columnCount");

        if (slider && countDisplay) {
          slider.value = savedColumns;
          countDisplay.textContent = savedColumns;
          this.setColumnCount(savedColumns);
        }
      }
    },
  };

  const ImageReplacement = {
    processedImages: new Set(),

    init() {
      if (!CONFIG.imageReplacement.enabled) return;
      this.bindEvents();
      setTimeout(() => this.processAllThumbnails(), 500);
    },

    bindEvents() {
      const scrollHandler = Utils.throttle(() => this.processVisibleThumbnails(), 200);
      window.addEventListener("scroll", scrollHandler);

      const observer = new MutationObserver((mutations) => {
        let hasNewThumbnails = false;
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              if (node.tagName === "IMG" && node.src.includes("/thumbnails/")) {
                hasNewThumbnails = true;
              } else if (node.querySelector?.('img[src*="/thumbnails/"]')) {
                hasNewThumbnails = true;
              }
            }
          });
        });

        if (hasNewThumbnails) {
          setTimeout(() => this.processVisibleThumbnails(), 200);
        }
      });

      observer.observe(document.body, { childList: true, subtree: true });
    },

    extractHashFromThumbnail(thumbnailUrl) {
      const match = thumbnailUrl.match(/thumbnail_([a-f0-9]+)/);
      return match ? match[1] : null;
    },

    extractDirectoryFromThumbnail(thumbnailUrl) {
      const match = thumbnailUrl.match(/thumbnails\/(\d+)\//);
      return match ? match[1] : null;
    },

    createImageUrl(thumbnailUrl, extension) {
      const hash = this.extractHashFromThumbnail(thumbnailUrl);
      const directory = this.extractDirectoryFromThumbnail(thumbnailUrl);
      if (!hash || !directory) return null;
      return `https://rule34.xxx/images/${directory}/${hash}.${extension}`;
    },

    async testImageUrl(url) {
      return new Promise((resolve) => {
        const testImg = new Image();
        testImg.onload = () => resolve(true);
        testImg.onerror = () => resolve(false);
        testImg.src = url;
      });
    },

    async replaceThumbnailWithSample(img) {
      const originalSrc = img.src;

      if (this.processedImages.has(originalSrc) || img.dataset.replaced || !originalSrc.includes("/thumbnails/")) {
        return;
      }

      this.processedImages.add(originalSrc);
      img.dataset.processing = "true";

      const extensions = ["jpg", "jpeg", "png", "gif"];
      for (const ext of extensions) {
        const sampleUrl = this.createImageUrl(originalSrc, ext);
        if (!sampleUrl) continue;

        const success = await this.testImageUrl(sampleUrl);
        if (success) {
          img.src = sampleUrl;
          img.dataset.replaced = "sample";
          img.dataset.processing = "false";
          break;
        }
      }
    },

    processAllThumbnails() {
      const thumbnails = Array.from(document.querySelectorAll('img[src*="/thumbnails/"]'));
      thumbnails.forEach((img, index) => {
        setTimeout(() => {
          this.replaceThumbnailWithSample(img);
        }, index * CONFIG.imageReplacement.batchDelay);
      });
    },

    processVisibleThumbnails() {
      const thumbnails = Array.from(document.querySelectorAll('img[src*="/thumbnails/"]')).filter((img) => {
        if (img.dataset.replaced || img.dataset.processing) return false;
        const rect = img.getBoundingClientRect();
        return rect.top < window.innerHeight + 300 && rect.bottom > -300;
      });

      thumbnails.forEach((img) => this.replaceThumbnailWithSample(img));
    },
  };

  const CollapsibleSidebar = {
    init() {
      if (!CONFIG.sidebar.collapsible) return;
      setTimeout(() => this.initializeCollapsibleSidebar(), 100);
    },

    initializeCollapsibleSidebar() {
      const tagSidebar = document.getElementById("tag-sidebar");
      if (!tagSidebar) return;

      const savedStates = CONFIG.sidebar.rememberState
        ? JSON.parse(localStorage.getItem("sidebarCollapsedStates") || "{}")
        : {};

      const headers = tagSidebar.querySelectorAll("h6");

      headers.forEach((header) => {
        const categoryName = header.textContent.toLowerCase().trim();
        const tagSection = document.createElement("div");
        tagSection.className = "tag-section";

        let currentElement = header.parentElement.nextElementSibling;
        const tagItems = [];

        while (currentElement && !currentElement.querySelector("h6")) {
          tagItems.push(currentElement);
          currentElement = currentElement.nextElementSibling;
        }

        tagItems.forEach((item) => tagSection.appendChild(item));
        header.parentElement.parentNode.insertBefore(tagSection, header.parentElement.nextElementSibling);

        if (savedStates[categoryName]) {
          header.classList.add("collapsed");
          tagSection.classList.add("collapsed");
        }

        this.bindHeaderEvents(header, tagSection, categoryName);
      });
    },

    bindHeaderEvents(header, tagSection, categoryName) {
      const toggleSection = () => {
        const isCollapsed = header.classList.contains("collapsed");
        header.classList.toggle("collapsed");

        if (isCollapsed) {
          tagSection.classList.remove("collapsed");
        } else {
          tagSection.classList.add("collapsed");
        }

        this.saveCollapsedStates();
      };

      header.addEventListener("click", (e) => {
        e.preventDefault();
        toggleSection();
      });
    },

    saveCollapsedStates() {
      if (!CONFIG.sidebar.rememberState) return;

      const states = {};
      const headers = document.querySelectorAll("#tag-sidebar h6");

      headers.forEach((header) => {
        const categoryName = header.textContent.toLowerCase().trim();
        states[categoryName] = header.classList.contains("collapsed");
      });

      localStorage.setItem("sidebarCollapsedStates", JSON.stringify(states));
    },
  };

  const MainApp = {
    init() {
      setTimeout(() => {
        StylesManager.init();
        SearchOverlay.init();
        ColumnControl.init();
        ImageReplacement.init();
        CollapsibleSidebar.init();
        this.exposeGlobalFunctions();
      }, 200);
    },

    exposeGlobalFunctions() {
      window.processAllThumbnails = () => ImageReplacement.processAllThumbnails();
      window.openSearchOverlay = () => SearchOverlay.open();
      window.closeSearchOverlay = () => SearchOverlay.close();
    },
  };

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => MainApp.init());
  } else {
    MainApp.init();
  }
})();