Enhanced 8chan UI

Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements

当前为 2025-04-20 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Enhanced 8chan UI
// @version      2.0.1
// @description  Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM_addStyle
// @grant        GM.addStyle
// @license MIT
// @namespace https://greasyfork.org/users/1459581
// ==/UserScript==



(function() {
  'use strict';

  // CONFIG
  // ==============================
  const CONFIG = {
    keybinds: {
      toggleReply: "Alt+Z",      // Open reply window
      closeModals: "Escape",     // Close all modals/panels
      galleryPrev: "ArrowLeft",  // Previous media in lightbox
      galleryNext: "ArrowRight", // Next media in lightbox
      quickReplyFocus: "Tab",    // Focus quick-reply fields cycle
      // Text formatting keybinds
      formatSpoiler: "Ctrl+S",   // Format text as spoiler
      formatBold: "Ctrl+B",      // Format text as bold
      formatItalic: "Ctrl+I",    // Format text as italic
      formatUnderline: "Ctrl+U", // Format text as underlined
      formatDoom: "Ctrl+D",      // Format text as doom
      formatMoe: "Ctrl+M"        // Format text as moe
    },
    scrollMemory: {
      maxPages: 50
    },
    dashboard: {
      saveHotkey: "Ctrl+Shift+C", // Hotkey to open dashboard
      theme: "dark"             // dark/light
    }
  };

  // STYLES
  // ==============================
  const STYLES = `
    /* Dashboard Styles */
.dashboard-modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: oklch(21% 0.006 285.885);
    padding: 20px;
    border-radius: 10px;
    z-index: 10001;
    width: 80%;
    max-width: 600px;
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: 0 0 20px rgba(0,0,0,0.5);
    display: none;
}

.dashboard-section {
    scroll-margin-top: 20px;
}

.dashboard-modal::-webkit-scrollbar {
    width: 8px;
}

.dashboard-modal::-webkit-scrollbar-track {
    background: rgba(0,0,0,0.1);
}

.dashboard-modal::-webkit-scrollbar-thumb {
    background: rgba(255,255,255,0.2);
    border-radius: 4px;
}

.dashboard-modal::-webkit-scrollbar-thumb:hover {
    background: rgba(255,255,255,0.3);
}

  .dashboard-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    z-index: 10000;
    display: none;
  }

  .dashboard-section {
    margin-bottom: 20px;
    padding: 15px;
    background: rgba(255,255,255,0.05);
    border-radius: 8px;
  }

  .config-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 10px 0;
  }

  .config-label {
    flex: 1;
    margin-right: 15px;
    font-weight: bold;
  }

  .config-input {
    flex: 2;
    background: rgba(255,255,255,0.1);
    border: 1px solid rgba(255,255,255,0.2);
    color: white;
    padding: 8px;
    border-radius: 4px;
  }

  .dashboard-buttons {
    display: flex;
    gap: 10px;
    margin-top: 20px;
  }

  .dashboard-btn {
    flex: 1;
    padding: 10px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background: #444;
    color: white;
    transition: background 0.3s ease;
  }

  .dashboard-btn:hover {
    background: #555;
  }

  .keybind-input {
    width: 200px;
    text-align: center;
    cursor: pointer;
    transition: background 0.3s ease;
  }

  .keybind-input:focus {
    background: rgba(255,255,255,0.2);
    outline: none;
  }
    /* Post styling */
    .postCell {
      margin: 0 !important;
    }

    /* Navigation and Header */
    #navBoardsSpan {
      font-size: large;
    }
    #dynamicHeaderThread,
    .navHeader {
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
    }

    /* Gallery and control buttons */
    .gallery-button {
      position: fixed;
      right: 20px;
      z-index: 9999;
      background: #333;
      color: white;
      padding: 15px;
      border-radius: 50%;
      cursor: pointer;
      box-shadow: 0 2px 5px rgba(0,0,0,0.3);
      text-align: center;
      line-height: 1;
      font-size: 20px;
    }
    .gallery-button.blur-toggle {
      bottom: 80px;
    }
    .gallery-button.gallery-open {
      bottom: 140px;
    }
    #media-count-display {
      position: fixed;
      bottom: 260px;
      right: 20px;
      background: #444;
      color: white;
      padding: 8px 12px;
      border-radius: 10px;
      font-size: 14px;
      z-index: 9999;
      box-shadow: 0 2px 5px rgba(0,0,0,0.3);
      white-space: nowrap;
    }

    /* Gallery modal */
    .gallery-modal {
      display: none;
      position: fixed;
      bottom: 80px;
      right: 20px;
      width: 80%;
      max-width: 600px;
      max-height: 80vh;
      background: oklch(21% 0.006 285.885);
      border-radius: 10px;
      padding: 20px;
      overflow-y: auto;
      z-index: 9998;
    }
    .gallery-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 10px;
    }
    .media-item {
      position: relative;
      cursor: pointer;
      aspect-ratio: 1;
      overflow: hidden;
      border-radius: 5px;
    }
    .media-thumbnail {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    .media-type-icon {
      position: absolute;
      bottom: 5px;
      right: 5px;
      color: white;
      background: rgba(0,0,0,0.5);
      padding: 2px 5px;
      border-radius: 3px;
      font-size: 0.8em;
    }

    /* Lightbox */
    .lightbox {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.9);
      z-index: 10000;
    }
    .lightbox-content {
      position: absolute;
      top: 45%;
      left: 50%;
      transform: translate(-50%, -50%);
      max-width: 90%;
      max-height: 90%;
    }
    .lightbox-video {
      max-width: 90vw;
      max-height: 90vh;
    }
    .close-btn {
      position: absolute;
      top: 20px;
      right: 20px;
      width: 50px;
      height: 50px;
      cursor: pointer;
      font-size: 24px;
      line-height: 50px;
      text-align: center;
      color: white;
    }
    .lightbox-nav {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      background: rgba(255,255,255,0.2);
      color: white;
      border: none;
      padding: 15px;
      cursor: pointer;
      font-size: 24px;
      border-radius: 50%;
    }
    .lightbox-prev {
      left: 20px;
    }
    .lightbox-next {
      right: 20px;
    }
    .go-to-post-btn {
      position: absolute;
      bottom: 10px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(255,255,255,0.1);
      color: white;
      border: none;
      padding: 8px 15px;
      border-radius: 20px;
      cursor: pointer;
      font-size: 14px;
    }

    /* Blur effect */
    .blurred-media img,
    .blurred-media video,
    .blurred-media audio {
      filter: blur(10px) brightness(0.8);
      transition: filter 0.3s ease;
    }

    /* Quick reply styling */
    #quick-reply.centered {
      position: fixed;
      top: 50% !important;
      left: 50% !important;
      transform: translate(-50%, -50%);
      width: 80%;
      max-width: 800px;
      min-height: 550px;
      background: oklch(21% 0.006 285.885);
      padding: 10px !important;
      border-radius: 10px;
      z-index: 9999;
      box-shadow: 0 0 20px rgba(0,0,0,0.5);
    }
    #quick-reply.centered table,
    #quick-reply.centered #qrname,
    #quick-reply.centered #qrsubject,
    #quick-reply.centered #qrbody {
      width: 100% !important;
      max-width: 100% !important;
      box-sizing: border-box;
    }
    #quick-reply.centered #qrbody {
      min-height: 200px;
    }
    #quick-reply-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.7);
      z-index: 99;
      display: none;
    }

    /* Thread watcher */
    #watchedMenu .floatingContainer {
      min-width: 330px;
    }
    #watchedMenu .watchedCellLabel > a:after {
      content: " - "attr(href);
      filter: saturate(50%);
      font-style: italic;
      font-weight: bold;
    }
    #watchedMenu {
      box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
    }

    /* Quote tooltips */
    .quoteTooltip .innerPost {
      overflow: hidden;
      box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
    }

    /* Hidden elements */
    #footer,
    #actionsForm,
    #navTopBoardsSpan,
    .coloredIcon.linkOverboard,
    .coloredIcon.linkSfwOver,
    .coloredIcon.multiboardButton,
    #navLinkSpan>span:nth-child(9),
    #navLinkSpan>span:nth-child(11),
    #navLinkSpan>span:nth-child(13),
    #dynamicAnnouncement {
      display: none;
    }
  `;

  // UTILITY FUNCTIONS
  // ==============================
  const util = {
    isThreadPage() {
      return window.location.href.match(/https:\/\/8chan\.(moe|se)\/.*\/res\/.*/);
    },

    createElement(tag, options = {}) {
      const element = document.createElement(tag);

      if (options.id) element.id = options.id;
      if (options.className) element.className = options.className;
      if (options.text) element.textContent = options.text;
      if (options.html) element.innerHTML = options.html;
      if (options.attributes) {
        Object.entries(options.attributes).forEach(([attr, value]) => {
          element.setAttribute(attr, value);
        });
      }
      if (options.styles) {
        Object.entries(options.styles).forEach(([prop, value]) => {
          element.style[prop] = value;
        });
      }
      if (options.events) {
        Object.entries(options.events).forEach(([event, handler]) => {
          element.addEventListener(event, handler);
        });
      }
      if (options.parent) options.parent.appendChild(element);

      return element;
    },

    saveConfigToStorage(config) {
      localStorage.setItem('enhanced8chan-config', JSON.stringify(config));
    },

    loadConfigFromStorage() {
      const saved = localStorage.getItem('enhanced8chan-config');
      return saved ? JSON.parse(saved) : null;
    }
  };

// Add new DASHBOARD SYSTEM section
const dashboard = {
  isOpen: false,
  currentEditInput: null,

  initialize() {
    this.createUI();
    this.setupEventListeners();
    this.addDashboardButton();
  },

  createUI() {
    this.overlay = util.createElement('div', {
      className: 'dashboard-overlay',
      parent: document.body
    });

    this.modal = util.createElement('div', {
      className: 'dashboard-modal',
      parent: document.body
    });

    const sections = [
      this.createKeybindsSection(),
      this.createScrollMemorySection(),
      this.createAppearanceSection(),
      this.createButtonsSection()
    ];

    sections.forEach(section => this.modal.appendChild(section));
  },

  createKeybindsSection() {
    const section = util.createElement('div', { className: 'dashboard-section' });
    util.createElement('h3', { text: 'Keyboard Shortcuts', parent: section });

    Object.entries(CONFIG.keybinds).forEach(([action, combo]) => {
      const row = util.createElement('div', { className: 'config-row', parent: section });
      util.createElement('span', {
        className: 'config-label',
        text: action.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()),
        parent: row
      });

      const input = util.createElement('input', {
        className: 'config-input keybind-input',
        attributes: {
          type: 'text',
          value: combo,
          'data-action': action
        },
        parent: row
      });
    });

    return section;
  },

  createScrollMemorySection() {
    const section = util.createElement('div', { className: 'dashboard-section' });
    util.createElement('h3', { text: 'Scroll Memory Settings', parent: section });

    // Max Pages
    const maxPagesRow = util.createElement('div', { className: 'config-row', parent: section });
    util.createElement('span', {
      className: 'config-label',
      text: 'Max Remembered Pages',
      parent: maxPagesRow
    });
    util.createElement('input', {
      className: 'config-input',
      attributes: {
        type: 'number',
        value: CONFIG.scrollMemory.maxPages,
        min: 1,
        max: 100,
        'data-setting': 'maxPages'
      },
      parent: maxPagesRow
    });

    return section;
  },

  // Modified createAppearanceSection function
  createAppearanceSection() {
    const section = util.createElement('div', { className: 'dashboard-section' });
    util.createElement('h3', { text: 'Appearance', parent: section });

    // Theme Selector
    const themeRow = util.createElement('div', { className: 'config-row', parent: section });
    util.createElement('span', { className: 'config-label', text: 'Theme', parent: themeRow });
    const themeSelect = util.createElement('select', {
      id: 'themeSelector',
      className: 'config-input',
      parent: themeRow
    });

    const themes = [
      'Default CSS', 'Board CSS', 'Yotsuba B', 'Yotsuba P', 'Yotsuba', 'Miku',
      'Yukkuri', 'Hispita', 'Warosu', 'Vivian', 'Tomorrow', 'Lain', 'Royal',
      'Hispaperro', 'HispaSexy', 'Avellana', 'Evita', 'Redchanit', 'MoeOS8',
      'Windows 95', 'Penumbra', 'Penumbra (Clear)'
    ];

    themes.forEach(theme => {
      util.createElement('option', {
        text: theme,
        value: theme.toLowerCase().replace(/\s+/g, '-'),
        parent: themeSelect
      });
    });

    return section;

  },

  createButtonsSection() {
    const section = util.createElement('div', { className: 'dashboard-buttons' });
    util.createElement('button', {
      className: 'dashboard-btn',
      text: 'Save',
      events: { click: () => this.saveConfig() },
      parent: section
    });
    util.createElement('button', {
      className: 'dashboard-btn',
      text: 'Reset Defaults',
      events: { click: () => this.resetDefaults() },
      parent: section
    });
    util.createElement('button', {
      className: 'dashboard-btn',
      text: 'Close',
      events: { click: () => this.close() },
      parent: section
    });
    return section;
  },

  addDashboardButton() {
    this.btn = util.createElement('div', {
      className: 'gallery-button',
      text: '⚙️',
      styles: { bottom: '200px' },
      attributes: { title: 'Settings Dashboard' },
      events: { click: () => this.open() },
      parent: document.body
    });
  },

  setupEventListeners() {
    document.addEventListener('keydown', e => {
      const combo = `${e.ctrlKey ? 'Ctrl+' : ''}${e.shiftKey ? 'Shift+' : ''}${e.key}`;
      if (combo.replace(/\+$/, '') === CONFIG.dashboard.saveHotkey) {
        this.open();
      }
    });

    this.modal.querySelectorAll('.keybind-input').forEach(input => {
      input.addEventListener('click', () => this.startRecordingKeybind(input));
      input.addEventListener('keydown', e => this.recordKeybind(e));
    });
  },

  startRecordingKeybind(input) {
    this.currentEditInput = input;
    input.value = 'Press key combination...';
    input.classList.add('recording');
  },

  recordKeybind(e) {
    if (!this.currentEditInput) return;
    e.preventDefault();

    const keys = [];
    if (e.ctrlKey) keys.push('Ctrl');
    if (e.altKey) keys.push('Alt');
    if (e.shiftKey) keys.push('Shift');
    if (!['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) keys.push(e.key);

    const combo = keys.join('+');
    this.currentEditInput.value = combo;
    this.currentEditInput.classList.remove('recording');
    this.currentEditInput = null;
  },

  open() {
    this.overlay.style.display = 'block';
    this.modal.style.display = 'block';
    this.isOpen = true;
  },

  close() {
    this.overlay.style.display = 'none';
    this.modal.style.display = 'none';
    this.isOpen = false;
  },

  saveConfig() {
    const newConfig = {
      keybinds: {},
      scrollMemory: {
        maxPages: parseInt(document.querySelector('[data-setting="maxPages"]').value)
      },
      dashboard: {
        theme: document.querySelector('[data-setting="theme"]').value
      }
    };

    document.querySelectorAll('.keybind-input').forEach(input => {
      newConfig.keybinds[input.dataset.action] = input.value;
    });

    util.saveConfigToStorage(newConfig);
    this.applyConfig(newConfig);
    this.close();
  },

  applyConfig(newConfig) {
    // Update live config
    Object.assign(CONFIG.keybinds, newConfig.keybinds);
    Object.assign(CONFIG.scrollMemory, newConfig.scrollMemory);
    Object.assign(CONFIG.dashboard, newConfig.dashboard);

    // Apply visual changes
    document.documentElement.setAttribute('data-theme', newConfig.dashboard.theme);
  },

  resetDefaults() {
    localStorage.removeItem('enhanced8chan-config');
    window.location.reload();
  }
};

  // GALLERY SYSTEM
  // ==============================
  const gallery = {
    mediaElements: [],
    currentIndex: 0,
    isBlurred: false,

    initialize() {
      this.createUIElements();
      this.setupEventListeners();
      this.collectMedia();
      this.createGalleryItems();
      this.updateThreadInfoDisplay();

      setInterval(() => this.updateThreadInfoDisplay(), 5000);
    },

    createUIElements() {
      // Gallery button
      this.galleryButton = util.createElement('div', {
        className: 'gallery-button gallery-open',
        text: '🎴',
        attributes: { title: 'Gallery' },
        parent: document.body
      });

      // Blur toggle
      this.blurToggle = util.createElement('div', {
        className: 'gallery-button blur-toggle',
        text: '💼',
        attributes: { title: 'Goon Mode' },
        parent: document.body
      });

      // Reply button
      this.replyButton = util.createElement('div', {
        id: 'replyButton',
        className: 'gallery-button',
        text: '✏️',
        attributes: { title: 'Reply' },
        styles: { bottom: '20px' },
        parent: document.body
      });

      // Media info display
      this.mediaInfoDisplay = util.createElement('div', {
        id: 'media-count-display',
        parent: document.body
      });

      // Quick reply overlay
      this.overlay = util.createElement('div', {
        id: 'quick-reply-overlay',
        parent: document.body
      });

      // Gallery modal
      this.galleryModal = util.createElement('div', {
        className: 'gallery-modal',
        parent: document.body
      });

      this.galleryGrid = util.createElement('div', {
        className: 'gallery-grid',
        parent: this.galleryModal
      });

      // Lightbox
      this.lightbox = util.createElement('div', {
        className: 'lightbox',
        html: `
          <div class="close-btn">×</div>
          <button class="lightbox-nav lightbox-prev">←</button>
          <button class="lightbox-nav lightbox-next">→</button>
        `,
        parent: document.body
      });
    },

    setupEventListeners() {
      // Blur toggle
      this.blurToggle.addEventListener('click', () => {
        this.isBlurred = !this.isBlurred;
        this.blurToggle.textContent = this.isBlurred ? '🍆' : '💼';
        this.blurToggle.title = this.isBlurred ? 'SafeMode' : 'Goon Mode';
        document.querySelectorAll('div.innerPost').forEach(post => {
          post.classList.toggle('blurred-media', this.isBlurred);
        });
      });

      // Reply button
      this.replyButton.addEventListener('click', () => {
        const nativeReplyBtn = document.querySelector('a#replyButton[href="#postingForm"]');
        if (nativeReplyBtn) {
          nativeReplyBtn.click();
        } else {
          location.hash = '#postingForm';
        }

        // Clear form fields and setup centered quick-reply
        setTimeout(() => {
          document.querySelectorAll('#qrname, #qrsubject, #qrbody').forEach(field => {
            field.value = '';
          });
          this.setupQuickReply();
        }, 100);
      });

      // Gallery button
      this.galleryButton.addEventListener('click', () => {
        this.collectMedia();
        this.createGalleryItems();
        this.galleryModal.style.display = this.galleryModal.style.display === 'block' ? 'none' : 'block';
      });

      // Lightbox navigation
      this.lightbox.querySelector('.lightbox-prev').addEventListener('click', () => this.navigate(-1));
      this.lightbox.querySelector('.lightbox-next').addEventListener('click', () => this.navigate(1));
      this.lightbox.querySelector('.close-btn').addEventListener('click', () => {
        this.lightbox.style.display = 'none';
      });

      // Close modals when clicking outside
      document.addEventListener('click', (e) => {
        if (!this.galleryModal.contains(e.target) && !this.galleryButton.contains(e.target)) {
          this.galleryModal.style.display = 'none';
        }
      });

      // Keyboard shortcuts
      document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
    },

    handleKeyboardShortcuts(e) {
      const { keybinds } = CONFIG;

      // Close modals/panels
      if (e.key === keybinds.closeModals) {
        if (this.lightbox.style.display === 'block') {
          this.lightbox.style.display = 'none';
        }
        this.galleryModal.style.display = 'none';

        const qrCloseBtn = document.querySelector('.quick-reply .close-btn, th .close-btn');
        if (qrCloseBtn && typeof qrCloseBtn.click === 'function') {
          qrCloseBtn.click();
        }

        document.getElementById('quick-reply-overlay').style.display = 'none';
        document.getElementById('quick-reply')?.classList.remove('centered');
      }

      // Navigation in lightbox
      if (this.lightbox.style.display === 'block') {
        if (e.key === keybinds.galleryPrev) this.navigate(-1);
        if (e.key === keybinds.galleryNext) this.navigate(1);
      }

      // Toggle reply window
      const [mod, key] = keybinds.toggleReply.split('+');
      if (e[`${mod.toLowerCase()}Key`] && e.key.toLowerCase() === key.toLowerCase()) {
        this.replyButton.click();
      }

      // Quick-reply field cycling
      if (e.key === keybinds.quickReplyFocus) {
        const fields = ['#qrname', '#qrsubject', '#qrbody'];
        const active = document.activeElement;
        const currentIndex = fields.findIndex(sel => active.matches(sel));

        if (currentIndex > -1) {
          e.preventDefault();
          const nextIndex = (currentIndex + 1) % fields.length;
          document.querySelector(fields[nextIndex])?.focus();
        }
      }

      // Text formatting shortcuts
      if (e.target.matches('#qrbody')) {
        const formattingMap = {
          [keybinds.formatSpoiler]: ['[spoiler]', '[/spoiler]'],
          [keybinds.formatBold]: ["'''", "'''"],
          [keybinds.formatItalic]: ["''", "''"],
          [keybinds.formatUnderline]: ['__', '__'],
          [keybinds.formatDoom]: ['[doom]', '[/doom]'],
          [keybinds.formatMoe]: ['[moe]', '[/moe]']
        };

        for (const [combo, [openTag, closeTag]] of Object.entries(formattingMap)) {
          const [modifier, keyChar] = combo.split('+');
          if (e[`${modifier.toLowerCase()}Key`] && e.key.toLowerCase() === keyChar.toLowerCase()) {
            e.preventDefault();
            this.wrapText(e.target, openTag, closeTag);
            break;
          }
        }
      }
    },

    // Text wrapping function for formatting
    wrapText(textarea, openTag, closeTag) {
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      const text = textarea.value;
      const selected = text.substring(start, end);

      let newText, newPos;
      if (start === end) {
        newText = text.slice(0, start) + openTag + closeTag + text.slice(end);
        newPos = start + openTag.length;
      } else {
        newText = text.slice(0, start) + openTag + selected + closeTag + text.slice(end);
        newPos = end + openTag.length + closeTag.length;
      }

      textarea.value = newText;
      textarea.selectionStart = textarea.selectionEnd = newPos;
      textarea.dispatchEvent(new Event('input', { bubbles: true }));
    },

    setupQuickReply() {
      const quickReply = document.getElementById('quick-reply');
      if (!quickReply) return;

      // Create close button if it doesn't exist
      if (!quickReply.querySelector('.qr-close-btn')) {
        util.createElement('div', {
          className: 'close-btn qr-close-btn',
          text: '×',
          styles: {
            position: 'absolute',
            top: '10px',
            right: '10px',
            cursor: 'pointer'
          },
          events: {
            click: () => {
              quickReply.classList.remove('centered');
              this.overlay.style.display = 'none';
            }
          },
          parent: quickReply
        });
      }

      quickReply.classList.add('centered');
      this.overlay.style.display = 'block';

      // Focus on reply body
      setTimeout(() => {
        document.querySelector('#qrbody')?.focus();
      }, 100);
    },

    collectMedia() {
      this.mediaElements = [];
      const seenUrls = new Set();

      document.querySelectorAll('div.innerPost').forEach(post => {
        // Get images
        post.querySelectorAll('img[loading="lazy"]').forEach(img => {
          const src = img.src;
          if (!src || seenUrls.has(src)) return;

          const parentLink = img.closest('a');
          const href = parentLink?.href;

          if (href && !seenUrls.has(href)) {
            seenUrls.add(href);
            this.mediaElements.push({
              element: parentLink,
              thumbnail: img,
              url: href,
              type: this.getMediaType(href),
              postElement: post
            });
          } else {
            seenUrls.add(src);
            this.mediaElements.push({
              element: img,
              thumbnail: img,
              url: src,
              type: 'IMAGE',
              postElement: post
            });
          }
        });

        // Get media links without images
        post.querySelectorAll('a[href*=".media"]:not(:has(img)), a.imgLink:not(:has(img))').forEach(link => {
          const href = link.href;
          if (!href || seenUrls.has(href)) return;

          if (this.isMediaFile(href)) {
            seenUrls.add(href);
            this.mediaElements.push({
              element: link,
              thumbnail: null,
              url: href,
              type: this.getMediaType(href),
              postElement: post
            });
          }
        });
      });
    },

    getMediaType(url) {
      if (/\.(mp4|webm|mov)$/i.test(url)) return 'VIDEO';
      if (/\.(mp3|wav|ogg)$/i.test(url)) return 'AUDIO';
      return 'IMAGE';
    },

    isMediaFile(url) {
      return /\.(jpg|jpeg|png|gif|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(url);
    },

    createGalleryItems() {
      this.galleryGrid.innerHTML = '';
      this.mediaElements.forEach((media, index) => {
        const item = util.createElement('div', {
          className: 'media-item',
          parent: this.galleryGrid
        });

        const thumbnailSrc = media.thumbnail?.src ||
          (media.type === 'VIDEO' ? 'https://via.placeholder.com/100/333/fff?text=VID' :
          media.type === 'AUDIO' ? 'https://via.placeholder.com/100/333/fff?text=AUD' :
          media.url);

        const thumbnail = util.createElement('img', {
          className: 'media-thumbnail',
          attributes: {
            loading: 'lazy',
            src: thumbnailSrc
          },
          parent: item
        });

        const typeIcon = util.createElement('div', {
          className: 'media-type-icon',
          text: media.type === 'VIDEO' ? 'VID' : media.type === 'AUDIO' ? 'AUD' : 'IMG',
          parent: item
        });

        item.addEventListener('click', () => this.showLightbox(media, index));
      });
    },

    showLightbox(media, index) {
      this.currentIndex = typeof index === 'number' ? index : this.mediaElements.indexOf(media);
      this.updateLightboxContent();
      this.lightbox.style.display = 'block';
    },

    updateLightboxContent() {
      const media = this.mediaElements[this.currentIndex];
      let content;

      // Create appropriate element based on media type
      if (media.type === 'AUDIO') {
        content = util.createElement('audio', {
          className: 'lightbox-content',
          attributes: {
            controls: true,
            src: media.url
          }
        });
      } else if (media.type === 'VIDEO') {
        content = util.createElement('video', {
          className: 'lightbox-content lightbox-video',
          attributes: {
            controls: true,
            src: media.url,
            autoplay: true,
            loop: true
          }
        });
      } else {
        content = util.createElement('img', {
          className: 'lightbox-content',
          attributes: {
            src: media.url,
            loading: 'eager'
          }
        });
      }

      // Remove existing content
      this.lightbox.querySelector('.lightbox-content')?.remove();
      this.lightbox.querySelector('.go-to-post-btn')?.remove();

      // Add "Go to post" button
      const goToPostBtn = util.createElement('button', {
        className: 'go-to-post-btn',
        text: 'Go to post',
        events: {
          click: () => {
            this.lightbox.style.display = 'none';
            media.postElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
            media.postElement.style.transition = 'box-shadow 0.5s ease';
            media.postElement.style.boxShadow = '0 0 0 3px rgba(255, 255, 0, 0.5)';
            setTimeout(() => {
              media.postElement.style.boxShadow = 'none';
            }, 2000);
          }
        }
      });

      this.lightbox.appendChild(content);
      this.lightbox.appendChild(goToPostBtn);
    },

    navigate(direction) {
      this.currentIndex = (this.currentIndex + direction + this.mediaElements.length) % this.mediaElements.length;
      this.updateLightboxContent();
    },

    updateThreadInfoDisplay() {
      const postCount = document.getElementById('postCount')?.textContent || '0';
      const userCount = document.getElementById('userCountLabel')?.textContent || '0';
      const fileCount = document.getElementById('fileCount')?.textContent || '0';
      this.mediaInfoDisplay.textContent = `Posts: ${postCount} | Users: ${userCount} | Files: ${fileCount}`;
    }
  };

  // SCROLL POSITION MEMORY
  // ==============================
  const scrollMemory = {
    currentPage: window.location.href,

    initialize() {
      window.addEventListener('beforeunload', () => this.saveScrollPosition());
      window.addEventListener('load', () => this.restoreScrollPosition());
    },

    isExcludedPage(url) {
      return false; // Removed exclusion pattern check
    },

    saveScrollPosition() {
      if (this.isExcludedPage(this.currentPage)) return;

      const scrollPosition = window.scrollY;
      localStorage.setItem(`scrollPosition_${this.currentPage}`, scrollPosition);
      this.manageScrollStorage();
    },

    restoreScrollPosition() {
      const savedPosition = localStorage.getItem(`scrollPosition_${this.currentPage}`);
      if (savedPosition) {
        window.scrollTo(0, parseInt(savedPosition, 10));
      }
    },

    manageScrollStorage() {
      const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_'));

      if (keys.length > CONFIG.scrollMemory.maxPages) {
        keys.sort((a, b) => localStorage.getItem(a) - localStorage.getItem(b));

        while (keys.length > CONFIG.scrollMemory.maxPages) {
          localStorage.removeItem(keys.shift());
        }
      }
    }
  };

  // BOARD NAVIGATION ENHANCER (FIXED)
  // ==============================
  const boardNavigation = {
    initialize() {
      this.appendCatalogToLinks();
      this.setupMutationObserver();
    },

    setupMutationObserver() {
      const observer = new MutationObserver(() => this.appendCatalogToLinks());
      const config = { childList: true, subtree: true };

      // Watch both the boards span and the entire document for new links
      const navboardsSpan = document.getElementById('navBoardsSpan');
      if (navboardsSpan) observer.observe(navboardsSpan, config);
      observer.observe(document.body, config);
    },

    appendCatalogToLinks() {
      document.querySelectorAll('#navBoardsSpan a, a[href*="/"]').forEach(link => {
        try {
          const url = new URL(link.href);
          // Only modify board links, not thread links
          if (url.pathname.split('/').filter(Boolean).length === 1) {
            if (!url.pathname.endsWith('/catalog.html')) {
              url.pathname = url.pathname.replace(/\/?$/, '/catalog.html');
              link.href = url.href;
            }
          }
        } catch (e) {
          console.error('Error processing URL:', e);
        }
      });
    }
  };

  // IMAGE HOVER FIX
  // ==============================
  const imageHoverFix = {
    initialize() {
      const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
              document.addEventListener('mousemove', this.handleMouseMove);
            }
          });

          mutation.removedNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
              document.removeEventListener('mousemove', this.handleMouseMove);
            }
          });
        });
      });

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

    handleMouseMove(event) {
      const img = document.querySelector('img[style*="position: fixed"]');
      if (!img) return;

      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;

      let newX = event.clientX + 10;
      let newY = event.clientY + 10;

      if (newX + img.width > viewportWidth) {
        newX = viewportWidth - img.width - 10;
      }

      if (newY + img.height > viewportHeight) {
        newY = viewportHeight - img.height - 10;
      }

      img.style.left = `${newX}px`;
      img.style.top = `${newY}px`;
    }
  };

  // INITIALIZATION
  // ==============================
  function init() {
    // Apply styles
    if (typeof GM_addStyle === 'function') {
      GM_addStyle(STYLES);
    } else if (typeof GM?.addStyle === 'function') {
      GM.addStyle(STYLES);
    } else {
      const style = document.createElement('style');
      style.textContent = STYLES;
      document.head.appendChild(style);
    }

    // Initialize features
    if (util.isThreadPage()) {
      gallery.initialize();
    }

    boardNavigation.initialize();
    scrollMemory.initialize();
    imageHoverFix.initialize();
    dashboard.initialize();
  }
// Load saved config on startup
const savedConfig = util.loadConfigFromStorage();
if (savedConfig) {
  dashboard.applyConfig(savedConfig);
}
  // Run initialization when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();