Rule34 Enhanced Dark Gallery

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

// ==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();
  }
})();