// ==UserScript==
// @name Enhanced 8chan UI
// @version 2.1.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 = {
customBoards: ['pol', 'a', 'v', 'co'],
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: "Shift+D", // Dice
formatCode: "Shift+C", // 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 `<a href="${baseUrl}/${board}/catalog.html">${board}</a>`;
},
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>/</span> ');
span.innerHTML = `<span>[</span> ${links} <span>]</span>`;
}
};
// 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: `
<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 ? 'Safe Mode' : '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]'],
[keybinds.formatCode]: ['[code]', '[/code]'],
[keybinds.formatLatex]: ['$$\\', '$$'],
[keybinds.formatDice]: ['/roll{', '}'],
[keybinds.formatSrzBizniz]: ['==', '=='],
[keybinds.formatEchoes]: ['(((', ')))'],
[keybinds.formatStrikethrough]: ['~~', '~~'],
[keybinds.formatSlanted]: ['///', '\\\\\\']
};
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 };
const navboardsSpan = document.getElementById('navBoardsSpan');
if (navboardsSpan) {
observer.observe(navboardsSpan, config);
}
},
appendCatalogToLinks() {
document.querySelectorAll('#navBoardsSpan a').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 document.head.appendChild(Object.assign(
document.createElement('style'), { textContent: STYLES }
));
// Initialize all modules
customBoardLinks.initialize(); // Added custom boards
boardNavigation.initialize();
scrollMemory.initialize();
imageHoverFix.initialize();
dashboard.initialize();
if (util.isThreadPage()) {
gallery.initialize();
}
}
// Load saved config and initialize
const savedConfig = util.loadConfigFromStorage();
if (savedConfig) dashboard.applyConfig(savedConfig);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();