// ==UserScript== // @name Enhanced 8chan UI // @version 2.1.2 // @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 // @downloadURL https://update.greasyfork.icu/scripts/533329/Enhanced%208chan%20UI.user.js // @updateURL https://update.greasyfork.icu/scripts/533329/Enhanced%208chan%20UI.meta.js // ==/UserScript== (function() { 'use strict'; // CONFIG // ============================== const CONFIG = { customBoards: ['pol', 'a', 'v', 'co'], // You can set them from dashboard 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 formatDice: "Ctrl+G", // Dice formatCode: "Ctrl+Q", // Code formatLatex: "Ctrl+L", // Format inLine Latex formatSrzBizniz: "Shift+Z", // == == formatEchoes: "Ctrl+(", // ((( ))) formatStrikethrough: "Ctrl+~", // ~~ ~~ formatSlanted: "Ctrl+/" // /// \\\ }, scrollMemory: { maxPages: 50 }, dashboard: { saveHotkey: "Ctrl+Shift+C", // Hotkey to open dashboard theme: "Tomorrow" // Only Visual still not working } }; // 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; } .config-separator { margin: 20px 0; border: 0; border-top: 1px solid rgba(255,255,255,0.1); } .formatting-heading { margin: 15px 0 10px; color: #fff; font-size: 1.1em; padding-bottom: 5px; border-bottom: 1px solid rgba(255,255,255,0.1); } .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 = { getBaseURL() { const hostname = location.hostname; if (hostname.includes('8chan.moe')) return 'https://8chan.moe'; if (hostname.includes('8chan.se')) return 'https://8chan.se'; return location.origin; }, boardLink(board, baseUrl) { return `${board}`; }, 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; } }; // CUSTOM BOARD NAVIGATION MODULE const customBoardLinks = { initialize() { this.updateNavBoardsSpan(); window.addEventListener('DOMContentLoaded', () => this.updateNavBoardsSpan()); setTimeout(() => this.updateNavBoardsSpan(), 1000); }, updateNavBoardsSpan() { const span = document.querySelector('#navBoardsSpan'); if (!span) return; const baseUrl = util.getBaseURL(); const links = CONFIG.customBoards.map(board => util.boardLink(board, baseUrl) ).join(' / '); span.innerHTML = `[ ${links} ]`; } }; // Add new DASHBOARD SYSTEM section const dashboard = { createBoardSettingsSection() { const section = util.createElement('div', { className: 'dashboard-section' }); util.createElement('h3', { text: 'Board Settings', parent: section }); const row = util.createElement('div', { className: 'config-row', parent: section }); util.createElement('span', { className: 'config-label', text: 'Custom Boards (comma separated)', parent: row }); const input = util.createElement('input', { className: 'config-input', attributes: { type: 'text', value: CONFIG.customBoards.join(', '), 'data-setting': 'customBoards' }, parent: row, events: { input: (e) => this.handleBoardInput(e.target) } }); return section; }, handleBoardInput(input) { const boards = input.value.split(',') .map(b => b.trim().replace(/\/.*$/g, '')) // Remove paths .filter(b => b.length > 0); CONFIG.customBoards = boards; customBoardLinks.updateNavBoardsSpan(); }, 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.createBoardSettingsSection(), // Added board settings 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 }); // Separate formatting and other keybinds const formattingKeys = []; const otherKeys = []; Object.entries(CONFIG.keybinds).forEach(([action, combo]) => { if (action.startsWith('format')) { formattingKeys.push({ action, combo }); } else { otherKeys.push({ action, combo }); } }); // Add non-formatting keybinds first otherKeys.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 }); }); // Add separator and formatting header util.createElement('hr', { className: 'config-separator', parent: section }); util.createElement('h4', { className: 'formatting-heading', text: 'Text Formatting Shortcuts', parent: section }); // Add formatting keybinds formattingKeys.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 = { customBoards: CONFIG.customBoards, keybinds: {}, scrollMemory: { maxPages: parseInt(document.querySelector('[data-setting="maxPages"]').value) }, dashboard: { theme: document.querySelector('#themeSelector').value } }; document.querySelectorAll('.keybind-input').forEach(input => { newConfig.keybinds[input.dataset.action] = input.value; }); util.saveConfigToStorage(newConfig); this.applyConfig(newConfig); this.close(); }, applyConfig(newConfig) { CONFIG.customBoards = newConfig.customBoards || []; Object.assign(CONFIG.keybinds, newConfig.keybinds); Object.assign(CONFIG.scrollMemory, newConfig.scrollMemory); Object.assign(CONFIG.dashboard, newConfig.dashboard); customBoardLinks.updateNavBoardsSpan(); 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: `