Enhanced 8chan UI

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

  1. // ==UserScript==
  2. // @name Enhanced 8chan UI
  3. // @version 2.1.2
  4. // @description Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements
  5. // @match https://8chan.moe/*
  6. // @match https://8chan.se/*
  7. // @grant GM_addStyle
  8. // @grant GM.addStyle
  9. // @license MIT
  10. // @namespace https://greasyfork.org/users/1459581
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // CONFIG
  17. // ==============================
  18. const CONFIG = {
  19. customBoards: ['pol', 'a', 'v', 'co'], // You can set them from dashboard
  20. keybinds: {
  21. toggleReply: "Alt+Z", // Open reply window
  22. closeModals: "Escape", // Close all modals/panels
  23. galleryPrev: "ArrowLeft", // Previous media in lightbox
  24. galleryNext: "ArrowRight", // Next media in lightbox
  25. quickReplyFocus: "Tab", // Focus quick-reply fields cycle
  26. // Text formatting keybinds
  27. formatSpoiler: "Ctrl+S", // Format text as spoiler
  28. formatBold: "Ctrl+B", // Format text as bold
  29. formatItalic: "Ctrl+I", // Format text as italic
  30. formatUnderline: "Ctrl+U", // Format text as underlined
  31. formatDoom: "Ctrl+D", // Format text as doom
  32. formatMoe: "Ctrl+M", // Format text as moe
  33. formatDice: "Ctrl+G", // Dice
  34. formatCode: "Ctrl+Q", // Code
  35. formatLatex: "Ctrl+L", // Format inLine Latex
  36. formatSrzBizniz: "Shift+Z", // == ==
  37. formatEchoes: "Ctrl+(", // ((( )))
  38. formatStrikethrough: "Ctrl+~", // ~~ ~~
  39. formatSlanted: "Ctrl+/" // /// \\\
  40. },
  41. scrollMemory: {
  42. maxPages: 50
  43. },
  44. dashboard: {
  45. saveHotkey: "Ctrl+Shift+C", // Hotkey to open dashboard
  46. theme: "Tomorrow" // Only Visual still not working
  47. }
  48. };
  49.  
  50. // STYLES
  51. // ==============================
  52. const STYLES = `
  53. /* Dashboard Styles */
  54. .dashboard-modal {
  55. position: fixed;
  56. top: 50%;
  57. left: 50%;
  58. transform: translate(-50%, -50%);
  59. background: oklch(21% 0.006 285.885);
  60. padding: 20px;
  61. border-radius: 10px;
  62. z-index: 10001;
  63. width: 80%;
  64. max-width: 600px;
  65. max-height: 90vh;
  66. overflow-y: auto;
  67. box-shadow: 0 0 20px rgba(0,0,0,0.5);
  68. display: none;
  69. }
  70.  
  71. .dashboard-section {
  72. scroll-margin-top: 20px;
  73. }
  74.  
  75. .dashboard-modal::-webkit-scrollbar {
  76. width: 8px;
  77. }
  78.  
  79. .dashboard-modal::-webkit-scrollbar-track {
  80. background: rgba(0,0,0,0.1);
  81. }
  82.  
  83. .dashboard-modal::-webkit-scrollbar-thumb {
  84. background: rgba(255,255,255,0.2);
  85. border-radius: 4px;
  86. }
  87.  
  88. .dashboard-modal::-webkit-scrollbar-thumb:hover {
  89. background: rgba(255,255,255,0.3);
  90. }
  91.  
  92. .dashboard-overlay {
  93. position: fixed;
  94. top: 0;
  95. left: 0;
  96. width: 100%;
  97. height: 100%;
  98. background: rgba(0,0,0,0.7);
  99. z-index: 10000;
  100. display: none;
  101. }
  102.  
  103. .dashboard-section {
  104. margin-bottom: 20px;
  105. padding: 15px;
  106. background: rgba(255,255,255,0.05);
  107. border-radius: 8px;
  108. }
  109.  
  110. .config-row {
  111. display: flex;
  112. justify-content: space-between;
  113. align-items: center;
  114. margin: 10px 0;
  115. }
  116.  
  117. .config-label {
  118. flex: 1;
  119. margin-right: 15px;
  120. font-weight: bold;
  121. }
  122.  
  123. .config-input {
  124. flex: 2;
  125. background: rgba(255,255,255,0.1);
  126. border: 1px solid rgba(255,255,255,0.2);
  127. color: white;
  128. padding: 8px;
  129. border-radius: 4px;
  130. }
  131.  
  132. .config-separator {
  133. margin: 20px 0;
  134. border: 0;
  135. border-top: 1px solid rgba(255,255,255,0.1);
  136. }
  137.  
  138. .formatting-heading {
  139. margin: 15px 0 10px;
  140. color: #fff;
  141. font-size: 1.1em;
  142. padding-bottom: 5px;
  143. border-bottom: 1px solid rgba(255,255,255,0.1);
  144. }
  145.  
  146. .dashboard-buttons {
  147. display: flex;
  148. gap: 10px;
  149. margin-top: 20px;
  150. }
  151.  
  152. .dashboard-btn {
  153. flex: 1;
  154. padding: 10px;
  155. border: none;
  156. border-radius: 5px;
  157. cursor: pointer;
  158. background: #444;
  159. color: white;
  160. transition: background 0.3s ease;
  161. }
  162.  
  163. .dashboard-btn:hover {
  164. background: #555;
  165. }
  166.  
  167. .keybind-input {
  168. width: 200px;
  169. text-align: center;
  170. cursor: pointer;
  171. transition: background 0.3s ease;
  172. }
  173.  
  174. .keybind-input:focus {
  175. background: rgba(255,255,255,0.2);
  176. outline: none;
  177. }
  178. /* Post styling */
  179. .postCell {
  180. margin: 0 !important;
  181. }
  182.  
  183. /* Navigation and Header */
  184. #navBoardsSpan {
  185. font-size: large;
  186. }
  187. #dynamicHeaderThread,
  188. .navHeader {
  189. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  190. }
  191.  
  192. /* Gallery and control buttons */
  193. .gallery-button {
  194. position: fixed;
  195. right: 20px;
  196. z-index: 9999;
  197. background: #333;
  198. color: white;
  199. padding: 15px;
  200. border-radius: 50%;
  201. cursor: pointer;
  202. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  203. text-align: center;
  204. line-height: 1;
  205. font-size: 20px;
  206. }
  207. .gallery-button.blur-toggle {
  208. bottom: 80px;
  209. }
  210. .gallery-button.gallery-open {
  211. bottom: 140px;
  212. }
  213. #media-count-display {
  214. position: fixed;
  215. bottom: 260px;
  216. right: 20px;
  217. background: #444;
  218. color: white;
  219. padding: 8px 12px;
  220. border-radius: 10px;
  221. font-size: 14px;
  222. z-index: 9999;
  223. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  224. white-space: nowrap;
  225. }
  226.  
  227. /* Gallery modal */
  228. .gallery-modal {
  229. display: none;
  230. position: fixed;
  231. bottom: 80px;
  232. right: 20px;
  233. width: 80%;
  234. max-width: 600px;
  235. max-height: 80vh;
  236. background: oklch(21% 0.006 285.885);
  237. border-radius: 10px;
  238. padding: 20px;
  239. overflow-y: auto;
  240. z-index: 9998;
  241. }
  242. .gallery-grid {
  243. display: grid;
  244. grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  245. gap: 10px;
  246. }
  247. .media-item {
  248. position: relative;
  249. cursor: pointer;
  250. aspect-ratio: 1;
  251. overflow: hidden;
  252. border-radius: 5px;
  253. }
  254. .media-thumbnail {
  255. width: 100%;
  256. height: 100%;
  257. object-fit: cover;
  258. }
  259. .media-type-icon {
  260. position: absolute;
  261. bottom: 5px;
  262. right: 5px;
  263. color: white;
  264. background: rgba(0,0,0,0.5);
  265. padding: 2px 5px;
  266. border-radius: 3px;
  267. font-size: 0.8em;
  268. }
  269.  
  270. /* Lightbox */
  271. .lightbox {
  272. display: none;
  273. position: fixed;
  274. top: 0;
  275. left: 0;
  276. width: 100%;
  277. height: 100%;
  278. background: rgba(0,0,0,0.9);
  279. z-index: 10000;
  280. }
  281. .lightbox-content {
  282. position: absolute;
  283. top: 45%;
  284. left: 50%;
  285. transform: translate(-50%, -50%);
  286. max-width: 90%;
  287. max-height: 90%;
  288. }
  289. .lightbox-video {
  290. max-width: 90vw;
  291. max-height: 90vh;
  292. }
  293. .close-btn {
  294. position: absolute;
  295. top: 20px;
  296. right: 20px;
  297. width: 50px;
  298. height: 50px;
  299. cursor: pointer;
  300. font-size: 24px;
  301. line-height: 50px;
  302. text-align: center;
  303. color: white;
  304. }
  305. .lightbox-nav {
  306. position: absolute;
  307. top: 50%;
  308. transform: translateY(-50%);
  309. background: rgba(255,255,255,0.2);
  310. color: white;
  311. border: none;
  312. padding: 15px;
  313. cursor: pointer;
  314. font-size: 24px;
  315. border-radius: 50%;
  316. }
  317. .lightbox-prev {
  318. left: 20px;
  319. }
  320. .lightbox-next {
  321. right: 20px;
  322. }
  323. .go-to-post-btn {
  324. position: absolute;
  325. bottom: 10px;
  326. left: 50%;
  327. transform: translateX(-50%);
  328. background: rgba(255,255,255,0.1);
  329. color: white;
  330. border: none;
  331. padding: 8px 15px;
  332. border-radius: 20px;
  333. cursor: pointer;
  334. font-size: 14px;
  335. }
  336.  
  337. /* Blur effect */
  338. .blurred-media img,
  339. .blurred-media video,
  340. .blurred-media audio {
  341. filter: blur(10px) brightness(0.8);
  342. transition: filter 0.3s ease;
  343. }
  344.  
  345. /* Quick reply styling */
  346. #quick-reply.centered {
  347. position: fixed;
  348. top: 50% !important;
  349. left: 50% !important;
  350. transform: translate(-50%, -50%);
  351. width: 80%;
  352. max-width: 800px;
  353. min-height: 550px;
  354. background: oklch(21% 0.006 285.885);
  355. padding: 10px !important;
  356. border-radius: 10px;
  357. z-index: 9999;
  358. box-shadow: 0 0 20px rgba(0,0,0,0.5);
  359. }
  360. #quick-reply.centered table,
  361. #quick-reply.centered #qrname,
  362. #quick-reply.centered #qrsubject,
  363. #quick-reply.centered #qrbody {
  364. width: 100% !important;
  365. max-width: 100% !important;
  366. box-sizing: border-box;
  367. }
  368. #quick-reply.centered #qrbody {
  369. min-height: 200px;
  370. }
  371. #quick-reply-overlay {
  372. position: fixed;
  373. top: 0;
  374. left: 0;
  375. width: 100%;
  376. height: 100%;
  377. background: rgba(0,0,0,0.7);
  378. z-index: 99;
  379. display: none;
  380. }
  381.  
  382. /* Thread watcher */
  383. #watchedMenu .floatingContainer {
  384. min-width: 330px;
  385. }
  386. #watchedMenu .watchedCellLabel > a:after {
  387. content: " - "attr(href);
  388. filter: saturate(50%);
  389. font-style: italic;
  390. font-weight: bold;
  391. }
  392. #watchedMenu {
  393. box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
  394. }
  395.  
  396. /* Quote tooltips */
  397. .quoteTooltip .innerPost {
  398. overflow: hidden;
  399. box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
  400. }
  401.  
  402. /* Hidden elements */
  403. #footer,
  404. #actionsForm,
  405. #navTopBoardsSpan,
  406. .coloredIcon.linkOverboard,
  407. .coloredIcon.linkSfwOver,
  408. .coloredIcon.multiboardButton,
  409. #navLinkSpan>span:nth-child(9),
  410. #navLinkSpan>span:nth-child(11),
  411. #navLinkSpan>span:nth-child(13),
  412. #dynamicAnnouncement {
  413. display: none;
  414. }
  415. `;
  416.  
  417. // UTILITY FUNCTIONS
  418. // ==============================
  419. const util = {
  420. getBaseURL() {
  421. const hostname = location.hostname;
  422. if (hostname.includes('8chan.moe')) return 'https://8chan.moe';
  423. if (hostname.includes('8chan.se')) return 'https://8chan.se';
  424. return location.origin;
  425. },
  426.  
  427. boardLink(board, baseUrl) {
  428. return `<a href="${baseUrl}/${board}/catalog.html">${board}</a>`;
  429. },
  430.  
  431. isThreadPage() {
  432. return window.location.href.match(/https:\/\/8chan\.(moe|se)\/.*\/res\/.*/);
  433. },
  434.  
  435. createElement(tag, options = {}) {
  436. const element = document.createElement(tag);
  437.  
  438. if (options.id) element.id = options.id;
  439. if (options.className) element.className = options.className;
  440. if (options.text) element.textContent = options.text;
  441. if (options.html) element.innerHTML = options.html;
  442. if (options.attributes) {
  443. Object.entries(options.attributes).forEach(([attr, value]) => {
  444. element.setAttribute(attr, value);
  445. });
  446. }
  447. if (options.styles) {
  448. Object.entries(options.styles).forEach(([prop, value]) => {
  449. element.style[prop] = value;
  450. });
  451. }
  452. if (options.events) {
  453. Object.entries(options.events).forEach(([event, handler]) => {
  454. element.addEventListener(event, handler);
  455. });
  456. }
  457. if (options.parent) options.parent.appendChild(element);
  458.  
  459. return element;
  460. },
  461.  
  462. saveConfigToStorage(config) {
  463. localStorage.setItem('enhanced8chan-config', JSON.stringify(config));
  464. },
  465.  
  466. loadConfigFromStorage() {
  467. const saved = localStorage.getItem('enhanced8chan-config');
  468. return saved ? JSON.parse(saved) : null;
  469. }
  470. };
  471.  
  472. // CUSTOM BOARD NAVIGATION MODULE
  473. const customBoardLinks = {
  474. initialize() {
  475. this.updateNavBoardsSpan();
  476. window.addEventListener('DOMContentLoaded', () => this.updateNavBoardsSpan());
  477. setTimeout(() => this.updateNavBoardsSpan(), 1000);
  478. },
  479.  
  480. updateNavBoardsSpan() {
  481. const span = document.querySelector('#navBoardsSpan');
  482. if (!span) return;
  483.  
  484. const baseUrl = util.getBaseURL();
  485. const links = CONFIG.customBoards.map(board =>
  486. util.boardLink(board, baseUrl)
  487. ).join(' <span>/</span> ');
  488.  
  489. span.innerHTML = `<span>[</span> ${links} <span>]</span>`;
  490. }
  491. };
  492.  
  493. // Add new DASHBOARD SYSTEM section
  494. const dashboard = {
  495.  
  496. createBoardSettingsSection() {
  497. const section = util.createElement('div', { className: 'dashboard-section' });
  498. util.createElement('h3', { text: 'Board Settings', parent: section });
  499.  
  500. const row = util.createElement('div', { className: 'config-row', parent: section });
  501. util.createElement('span', {
  502. className: 'config-label',
  503. text: 'Custom Boards (comma separated)',
  504. parent: row
  505. });
  506.  
  507. const input = util.createElement('input', {
  508. className: 'config-input',
  509. attributes: {
  510. type: 'text',
  511. value: CONFIG.customBoards.join(', '),
  512. 'data-setting': 'customBoards'
  513. },
  514. parent: row,
  515. events: {
  516. input: (e) => this.handleBoardInput(e.target)
  517. }
  518. });
  519.  
  520. return section;
  521. },
  522.  
  523. handleBoardInput(input) {
  524. const boards = input.value.split(',')
  525. .map(b => b.trim().replace(/\/.*$/g, '')) // Remove paths
  526. .filter(b => b.length > 0);
  527.  
  528. CONFIG.customBoards = boards;
  529. customBoardLinks.updateNavBoardsSpan();
  530. },
  531.  
  532.  
  533. isOpen: false,
  534. currentEditInput: null,
  535.  
  536. initialize() {
  537. this.createUI();
  538. this.setupEventListeners();
  539. this.addDashboardButton();
  540. },
  541.  
  542. createUI() {
  543. this.overlay = util.createElement('div', { className: 'dashboard-overlay', parent: document.body });
  544. this.modal = util.createElement('div', { className: 'dashboard-modal', parent: document.body });
  545.  
  546. const sections = [
  547. this.createBoardSettingsSection(), // Added board settings
  548. this.createKeybindsSection(),
  549. this.createScrollMemorySection(),
  550. this.createAppearanceSection(),
  551. this.createButtonsSection()
  552. ];
  553.  
  554. sections.forEach(section => this.modal.appendChild(section));
  555. },
  556.  
  557. createKeybindsSection() {
  558. const section = util.createElement('div', { className: 'dashboard-section' });
  559. util.createElement('h3', { text: 'Keyboard Shortcuts', parent: section });
  560.  
  561. // Separate formatting and other keybinds
  562. const formattingKeys = [];
  563. const otherKeys = [];
  564.  
  565. Object.entries(CONFIG.keybinds).forEach(([action, combo]) => {
  566. if (action.startsWith('format')) {
  567. formattingKeys.push({ action, combo });
  568. } else {
  569. otherKeys.push({ action, combo });
  570. }
  571. });
  572.  
  573. // Add non-formatting keybinds first
  574. otherKeys.forEach(({ action, combo }) => {
  575. const row = util.createElement('div', { className: 'config-row', parent: section });
  576. util.createElement('span', {
  577. className: 'config-label',
  578. text: action.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()),
  579. parent: row
  580. });
  581.  
  582. const input = util.createElement('input', {
  583. className: 'config-input keybind-input',
  584. attributes: {
  585. type: 'text',
  586. value: combo,
  587. 'data-action': action
  588. },
  589. parent: row
  590. });
  591. });
  592.  
  593. // Add separator and formatting header
  594. util.createElement('hr', {
  595. className: 'config-separator',
  596. parent: section
  597. });
  598. util.createElement('h4', {
  599. className: 'formatting-heading',
  600. text: 'Text Formatting Shortcuts',
  601. parent: section
  602. });
  603.  
  604. // Add formatting keybinds
  605. formattingKeys.forEach(({ action, combo }) => {
  606. const row = util.createElement('div', { className: 'config-row', parent: section });
  607. util.createElement('span', {
  608. className: 'config-label',
  609. text: action.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()),
  610. parent: row
  611. });
  612.  
  613. const input = util.createElement('input', {
  614. className: 'config-input keybind-input',
  615. attributes: {
  616. type: 'text',
  617. value: combo,
  618. 'data-action': action
  619. },
  620. parent: row
  621. });
  622. });
  623.  
  624. return section;
  625. },
  626.  
  627. createScrollMemorySection() {
  628. const section = util.createElement('div', { className: 'dashboard-section' });
  629. util.createElement('h3', { text: 'Scroll Memory Settings', parent: section });
  630.  
  631. // Max Pages
  632. const maxPagesRow = util.createElement('div', { className: 'config-row', parent: section });
  633. util.createElement('span', {
  634. className: 'config-label',
  635. text: 'Max Remembered Pages',
  636. parent: maxPagesRow
  637. });
  638. util.createElement('input', {
  639. className: 'config-input',
  640. attributes: {
  641. type: 'number',
  642. value: CONFIG.scrollMemory.maxPages,
  643. min: 1,
  644. max: 100,
  645. 'data-setting': 'maxPages'
  646. },
  647. parent: maxPagesRow
  648. });
  649.  
  650. return section;
  651. },
  652.  
  653. // Modified createAppearanceSection function
  654. createAppearanceSection() {
  655. const section = util.createElement('div', { className: 'dashboard-section' });
  656. util.createElement('h3', { text: 'Appearance', parent: section });
  657.  
  658. // Theme Selector
  659. const themeRow = util.createElement('div', { className: 'config-row', parent: section });
  660. util.createElement('span', { className: 'config-label', text: 'Theme', parent: themeRow });
  661. const themeSelect = util.createElement('select', {
  662. id: 'themeSelector',
  663. className: 'config-input',
  664. parent: themeRow
  665. });
  666.  
  667. const themes = [
  668. 'Default CSS', 'Board CSS', 'Yotsuba B', 'Yotsuba P', 'Yotsuba', 'Miku',
  669. 'Yukkuri', 'Hispita', 'Warosu', 'Vivian', 'Tomorrow', 'Lain', 'Royal',
  670. 'Hispaperro', 'HispaSexy', 'Avellana', 'Evita', 'Redchanit', 'MoeOS8',
  671. 'Windows 95', 'Penumbra', 'Penumbra (Clear)'
  672. ];
  673.  
  674. themes.forEach(theme => {
  675. util.createElement('option', {
  676. text: theme,
  677. value: theme.toLowerCase().replace(/\s+/g, '-'),
  678. parent: themeSelect
  679. });
  680. });
  681.  
  682. return section;
  683.  
  684. },
  685.  
  686. createButtonsSection() {
  687. const section = util.createElement('div', { className: 'dashboard-buttons' });
  688. util.createElement('button', {
  689. className: 'dashboard-btn',
  690. text: 'Save',
  691. events: { click: () => this.saveConfig() },
  692. parent: section
  693. });
  694. util.createElement('button', {
  695. className: 'dashboard-btn',
  696. text: 'Reset Defaults',
  697. events: { click: () => this.resetDefaults() },
  698. parent: section
  699. });
  700. util.createElement('button', {
  701. className: 'dashboard-btn',
  702. text: 'Close',
  703. events: { click: () => this.close() },
  704. parent: section
  705. });
  706. return section;
  707. },
  708.  
  709. addDashboardButton() {
  710. this.btn = util.createElement('div', {
  711. className: 'gallery-button',
  712. text: '⚙️',
  713. styles: { bottom: '200px' },
  714. attributes: { title: 'Settings Dashboard' },
  715. events: { click: () => this.open() },
  716. parent: document.body
  717. });
  718. },
  719.  
  720. setupEventListeners() {
  721. document.addEventListener('keydown', e => {
  722. const combo = `${e.ctrlKey ? 'Ctrl+' : ''}${e.shiftKey ? 'Shift+' : ''}${e.key}`;
  723. if (combo.replace(/\+$/, '') === CONFIG.dashboard.saveHotkey) {
  724. this.open();
  725. }
  726. });
  727.  
  728. this.modal.querySelectorAll('.keybind-input').forEach(input => {
  729. input.addEventListener('click', () => this.startRecordingKeybind(input));
  730. input.addEventListener('keydown', e => this.recordKeybind(e));
  731. });
  732. },
  733.  
  734. startRecordingKeybind(input) {
  735. this.currentEditInput = input;
  736. input.value = 'Press key combination...';
  737. input.classList.add('recording');
  738. },
  739.  
  740. recordKeybind(e) {
  741. if (!this.currentEditInput) return;
  742. e.preventDefault();
  743.  
  744. const keys = [];
  745. if (e.ctrlKey) keys.push('Ctrl');
  746. if (e.altKey) keys.push('Alt');
  747. if (e.shiftKey) keys.push('Shift');
  748. if (!['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) keys.push(e.key);
  749.  
  750. const combo = keys.join('+');
  751. this.currentEditInput.value = combo;
  752. this.currentEditInput.classList.remove('recording');
  753. this.currentEditInput = null;
  754. },
  755.  
  756. open() {
  757. this.overlay.style.display = 'block';
  758. this.modal.style.display = 'block';
  759. this.isOpen = true;
  760. },
  761.  
  762. close() {
  763. this.overlay.style.display = 'none';
  764. this.modal.style.display = 'none';
  765. this.isOpen = false;
  766. },
  767.  
  768. saveConfig() {
  769. const newConfig = {
  770. customBoards: CONFIG.customBoards,
  771. keybinds: {},
  772. scrollMemory: {
  773. maxPages: parseInt(document.querySelector('[data-setting="maxPages"]').value)
  774. },
  775. dashboard: {
  776. theme: document.querySelector('#themeSelector').value
  777. }
  778. };
  779.  
  780. document.querySelectorAll('.keybind-input').forEach(input => {
  781. newConfig.keybinds[input.dataset.action] = input.value;
  782. });
  783.  
  784. util.saveConfigToStorage(newConfig);
  785. this.applyConfig(newConfig);
  786. this.close();
  787. },
  788.  
  789. applyConfig(newConfig) {
  790. CONFIG.customBoards = newConfig.customBoards || [];
  791. Object.assign(CONFIG.keybinds, newConfig.keybinds);
  792. Object.assign(CONFIG.scrollMemory, newConfig.scrollMemory);
  793. Object.assign(CONFIG.dashboard, newConfig.dashboard);
  794.  
  795. customBoardLinks.updateNavBoardsSpan();
  796. document.documentElement.setAttribute('data-theme', newConfig.dashboard.theme);
  797. },
  798.  
  799. resetDefaults() {
  800. localStorage.removeItem('enhanced8chan-config');
  801. window.location.reload();
  802. }
  803. };
  804.  
  805. // GALLERY SYSTEM
  806. // ==============================
  807. const gallery = {
  808. mediaElements: [],
  809. currentIndex: 0,
  810. isBlurred: false,
  811.  
  812. initialize() {
  813. this.createUIElements();
  814. this.setupEventListeners();
  815. this.collectMedia();
  816. this.createGalleryItems();
  817. this.updateThreadInfoDisplay();
  818.  
  819. setInterval(() => this.updateThreadInfoDisplay(), 5000);
  820. },
  821.  
  822. createUIElements() {
  823. // Gallery button
  824. this.galleryButton = util.createElement('div', {
  825. className: 'gallery-button gallery-open',
  826. text: '🎴',
  827. attributes: { title: 'Gallery' },
  828. parent: document.body
  829. });
  830.  
  831. // Blur toggle
  832. this.blurToggle = util.createElement('div', {
  833. className: 'gallery-button blur-toggle',
  834. text: '💼',
  835. attributes: { title: 'Goon Mode' },
  836. parent: document.body
  837. });
  838.  
  839. // Reply button
  840. this.replyButton = util.createElement('div', {
  841. id: 'replyButton',
  842. className: 'gallery-button',
  843. text: '✏️',
  844. attributes: { title: 'Reply' },
  845. styles: { bottom: '20px' },
  846. parent: document.body
  847. });
  848.  
  849. // Media info display
  850. this.mediaInfoDisplay = util.createElement('div', {
  851. id: 'media-count-display',
  852. parent: document.body
  853. });
  854.  
  855. // Quick reply overlay
  856. this.overlay = util.createElement('div', {
  857. id: 'quick-reply-overlay',
  858. parent: document.body
  859. });
  860.  
  861. // Gallery modal
  862. this.galleryModal = util.createElement('div', {
  863. className: 'gallery-modal',
  864. parent: document.body
  865. });
  866.  
  867. this.galleryGrid = util.createElement('div', {
  868. className: 'gallery-grid',
  869. parent: this.galleryModal
  870. });
  871.  
  872. // Lightbox
  873. this.lightbox = util.createElement('div', {
  874. className: 'lightbox',
  875. html: `
  876. <div class="close-btn">×</div>
  877. <button class="lightbox-nav lightbox-prev">←</button>
  878. <button class="lightbox-nav lightbox-next">→</button>
  879. `,
  880. parent: document.body
  881. });
  882. },
  883.  
  884. setupEventListeners() {
  885. // Blur toggle
  886. this.blurToggle.addEventListener('click', () => {
  887. this.isBlurred = !this.isBlurred;
  888. this.blurToggle.textContent = this.isBlurred ? '🍆' : '💼';
  889. this.blurToggle.title = this.isBlurred ? 'Safe Mode' : 'Goon Mode';
  890. document.querySelectorAll('div.innerPost').forEach(post => {
  891. post.classList.toggle('blurred-media', this.isBlurred);
  892. });
  893. });
  894.  
  895. // Reply button
  896. this.replyButton.addEventListener('click', () => {
  897. const nativeReplyBtn = document.querySelector('a#replyButton[href="#postingForm"]');
  898. if (nativeReplyBtn) {
  899. nativeReplyBtn.click();
  900. } else {
  901. location.hash = '#postingForm';
  902. }
  903.  
  904. // Clear form fields and setup centered quick-reply
  905. setTimeout(() => {
  906. document.querySelectorAll('#qrname, #qrsubject, #qrbody').forEach(field => {
  907. field.value = '';
  908. });
  909. this.setupQuickReply();
  910. }, 100);
  911. });
  912.  
  913. // Gallery button
  914. this.galleryButton.addEventListener('click', () => {
  915. this.collectMedia();
  916. this.createGalleryItems();
  917. this.galleryModal.style.display = this.galleryModal.style.display === 'block' ? 'none' : 'block';
  918. });
  919.  
  920. // Lightbox navigation
  921. this.lightbox.querySelector('.lightbox-prev').addEventListener('click', () => this.navigate(-1));
  922. this.lightbox.querySelector('.lightbox-next').addEventListener('click', () => this.navigate(1));
  923. this.lightbox.querySelector('.close-btn').addEventListener('click', () => {
  924. this.lightbox.style.display = 'none';
  925. });
  926.  
  927. // Close modals when clicking outside
  928. document.addEventListener('click', (e) => {
  929. if (!this.galleryModal.contains(e.target) && !this.galleryButton.contains(e.target)) {
  930. this.galleryModal.style.display = 'none';
  931. }
  932. });
  933.  
  934. // Keyboard shortcuts
  935. document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
  936. },
  937.  
  938. handleKeyboardShortcuts(e) {
  939. const { keybinds } = CONFIG;
  940.  
  941. // Close modals/panels
  942. if (e.key === keybinds.closeModals) {
  943. if (this.lightbox.style.display === 'block') {
  944. this.lightbox.style.display = 'none';
  945. }
  946. this.galleryModal.style.display = 'none';
  947.  
  948. const qrCloseBtn = document.querySelector('.quick-reply .close-btn, th .close-btn');
  949. if (qrCloseBtn && typeof qrCloseBtn.click === 'function') {
  950. qrCloseBtn.click();
  951. }
  952.  
  953. document.getElementById('quick-reply-overlay').style.display = 'none';
  954. document.getElementById('quick-reply')?.classList.remove('centered');
  955. }
  956.  
  957. // Navigation in lightbox
  958. if (this.lightbox.style.display === 'block') {
  959. if (e.key === keybinds.galleryPrev) this.navigate(-1);
  960. if (e.key === keybinds.galleryNext) this.navigate(1);
  961. }
  962.  
  963. // Toggle reply window
  964. const [mod, key] = keybinds.toggleReply.split('+');
  965. if (e[`${mod.toLowerCase()}Key`] && e.key.toLowerCase() === key.toLowerCase()) {
  966. this.replyButton.click();
  967. }
  968.  
  969. // Quick-reply field cycling
  970. if (e.key === keybinds.quickReplyFocus) {
  971. const fields = ['#qrname', '#qrsubject', '#qrbody'];
  972. const active = document.activeElement;
  973. const currentIndex = fields.findIndex(sel => active.matches(sel));
  974.  
  975. if (currentIndex > -1) {
  976. e.preventDefault();
  977. const nextIndex = (currentIndex + 1) % fields.length;
  978. document.querySelector(fields[nextIndex])?.focus();
  979. }
  980. }
  981.  
  982. // Text formatting shortcuts
  983. if (e.target.matches('#qrbody')) {
  984. const formattingMap = {
  985. [keybinds.formatSpoiler]: ['[spoiler]', '[/spoiler]'],
  986. [keybinds.formatBold]: ["'''", "'''"],
  987. [keybinds.formatItalic]: ["''", "''"],
  988. [keybinds.formatUnderline]: ['__', '__'],
  989. [keybinds.formatDoom]: ['[doom]', '[/doom]'],
  990. [keybinds.formatMoe]: ['[moe]', '[/moe]'],
  991. [keybinds.formatCode]: ['[code]', '[/code]'],
  992. [keybinds.formatLatex]: ['$$\\', '$$'],
  993. [keybinds.formatDice]: ['/roll{', '}'],
  994. [keybinds.formatSrzBizniz]: ['==', '=='],
  995. [keybinds.formatEchoes]: ['(((', ')))'],
  996. [keybinds.formatStrikethrough]: ['~~', '~~'],
  997. [keybinds.formatSlanted]: ['///', '\\\\\\']
  998. };
  999.  
  1000. for (const [combo, [openTag, closeTag]] of Object.entries(formattingMap)) {
  1001. const [modifier, keyChar] = combo.split('+');
  1002. if (e[`${modifier.toLowerCase()}Key`] && e.key.toLowerCase() === keyChar.toLowerCase()) {
  1003. e.preventDefault();
  1004. this.wrapText(e.target, openTag, closeTag);
  1005. break;
  1006. }
  1007. }
  1008. }
  1009. },
  1010.  
  1011. // Text wrapping function for formatting
  1012. wrapText(textarea, openTag, closeTag) {
  1013. const start = textarea.selectionStart;
  1014. const end = textarea.selectionEnd;
  1015. const text = textarea.value;
  1016. const selected = text.substring(start, end);
  1017.  
  1018. let newText, newPos;
  1019. if (start === end) {
  1020. newText = text.slice(0, start) + openTag + closeTag + text.slice(end);
  1021. newPos = start + openTag.length;
  1022. } else {
  1023. newText = text.slice(0, start) + openTag + selected + closeTag + text.slice(end);
  1024. newPos = end + openTag.length + closeTag.length;
  1025. }
  1026.  
  1027. textarea.value = newText;
  1028. textarea.selectionStart = textarea.selectionEnd = newPos;
  1029. textarea.dispatchEvent(new Event('input', { bubbles: true }));
  1030. },
  1031.  
  1032. setupQuickReply() {
  1033. const quickReply = document.getElementById('quick-reply');
  1034. if (!quickReply) return;
  1035.  
  1036. // Create close button if it doesn't exist
  1037. if (!quickReply.querySelector('.qr-close-btn')) {
  1038. util.createElement('div', {
  1039. className: 'close-btn qr-close-btn',
  1040. text: '×',
  1041. styles: {
  1042. position: 'absolute',
  1043. top: '10px',
  1044. right: '10px',
  1045. cursor: 'pointer'
  1046. },
  1047. events: {
  1048. click: () => {
  1049. quickReply.classList.remove('centered');
  1050. this.overlay.style.display = 'none';
  1051. }
  1052. },
  1053. parent: quickReply
  1054. });
  1055. }
  1056.  
  1057. quickReply.classList.add('centered');
  1058. this.overlay.style.display = 'block';
  1059.  
  1060. // Focus on reply body
  1061. setTimeout(() => {
  1062. document.querySelector('#qrbody')?.focus();
  1063. }, 100);
  1064. },
  1065.  
  1066. collectMedia() {
  1067. this.mediaElements = [];
  1068. const seenUrls = new Set();
  1069.  
  1070. document.querySelectorAll('div.innerPost').forEach(post => {
  1071. // Get images
  1072. post.querySelectorAll('img[loading="lazy"]').forEach(img => {
  1073. const src = img.src;
  1074. if (!src || seenUrls.has(src)) return;
  1075.  
  1076. const parentLink = img.closest('a');
  1077. const href = parentLink?.href;
  1078.  
  1079. if (href && !seenUrls.has(href)) {
  1080. seenUrls.add(href);
  1081. this.mediaElements.push({
  1082. element: parentLink,
  1083. thumbnail: img,
  1084. url: href,
  1085. type: this.getMediaType(href),
  1086. postElement: post
  1087. });
  1088. } else {
  1089. seenUrls.add(src);
  1090. this.mediaElements.push({
  1091. element: img,
  1092. thumbnail: img,
  1093. url: src,
  1094. type: 'IMAGE',
  1095. postElement: post
  1096. });
  1097. }
  1098. });
  1099.  
  1100. // Get media links without images
  1101. post.querySelectorAll('a[href*=".media"]:not(:has(img)), a.imgLink:not(:has(img))').forEach(link => {
  1102. const href = link.href;
  1103. if (!href || seenUrls.has(href)) return;
  1104.  
  1105. if (this.isMediaFile(href)) {
  1106. seenUrls.add(href);
  1107. this.mediaElements.push({
  1108. element: link,
  1109. thumbnail: null,
  1110. url: href,
  1111. type: this.getMediaType(href),
  1112. postElement: post
  1113. });
  1114. }
  1115. });
  1116. });
  1117. },
  1118.  
  1119. getMediaType(url) {
  1120. if (/\.(mp4|webm|mov)$/i.test(url)) return 'VIDEO';
  1121. if (/\.(mp3|wav|ogg)$/i.test(url)) return 'AUDIO';
  1122. return 'IMAGE';
  1123. },
  1124.  
  1125. isMediaFile(url) {
  1126. return /\.(jpg|jpeg|png|gif|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(url);
  1127. },
  1128.  
  1129. createGalleryItems() {
  1130. this.galleryGrid.innerHTML = '';
  1131. this.mediaElements.forEach((media, index) => {
  1132. const item = util.createElement('div', {
  1133. className: 'media-item',
  1134. parent: this.galleryGrid
  1135. });
  1136.  
  1137. const thumbnailSrc = media.thumbnail?.src ||
  1138. (media.type === 'VIDEO' ? 'https://via.placeholder.com/100/333/fff?text=VID' :
  1139. media.type === 'AUDIO' ? 'https://via.placeholder.com/100/333/fff?text=AUD' :
  1140. media.url);
  1141.  
  1142. const thumbnail = util.createElement('img', {
  1143. className: 'media-thumbnail',
  1144. attributes: {
  1145. loading: 'lazy',
  1146. src: thumbnailSrc
  1147. },
  1148. parent: item
  1149. });
  1150.  
  1151. const typeIcon = util.createElement('div', {
  1152. className: 'media-type-icon',
  1153. text: media.type === 'VIDEO' ? 'VID' : media.type === 'AUDIO' ? 'AUD' : 'IMG',
  1154. parent: item
  1155. });
  1156.  
  1157. item.addEventListener('click', () => this.showLightbox(media, index));
  1158. });
  1159. },
  1160.  
  1161. showLightbox(media, index) {
  1162. this.currentIndex = typeof index === 'number' ? index : this.mediaElements.indexOf(media);
  1163. this.updateLightboxContent();
  1164. this.lightbox.style.display = 'block';
  1165. },
  1166.  
  1167. updateLightboxContent() {
  1168. const media = this.mediaElements[this.currentIndex];
  1169. let content;
  1170.  
  1171. // Create appropriate element based on media type
  1172. if (media.type === 'AUDIO') {
  1173. content = util.createElement('audio', {
  1174. className: 'lightbox-content',
  1175. attributes: {
  1176. controls: true,
  1177. src: media.url
  1178. }
  1179. });
  1180. } else if (media.type === 'VIDEO') {
  1181. content = util.createElement('video', {
  1182. className: 'lightbox-content lightbox-video',
  1183. attributes: {
  1184. controls: true,
  1185. src: media.url,
  1186. autoplay: true,
  1187. loop: true
  1188. }
  1189. });
  1190. } else {
  1191. content = util.createElement('img', {
  1192. className: 'lightbox-content',
  1193. attributes: {
  1194. src: media.url,
  1195. loading: 'eager'
  1196. }
  1197. });
  1198. }
  1199.  
  1200. // Remove existing content
  1201. this.lightbox.querySelector('.lightbox-content')?.remove();
  1202. this.lightbox.querySelector('.go-to-post-btn')?.remove();
  1203.  
  1204. // Add "Go to post" button
  1205. const goToPostBtn = util.createElement('button', {
  1206. className: 'go-to-post-btn',
  1207. text: 'Go to post',
  1208. events: {
  1209. click: () => {
  1210. this.lightbox.style.display = 'none';
  1211. media.postElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  1212. media.postElement.style.transition = 'box-shadow 0.5s ease';
  1213. media.postElement.style.boxShadow = '0 0 0 3px rgba(255, 255, 0, 0.5)';
  1214. setTimeout(() => {
  1215. media.postElement.style.boxShadow = 'none';
  1216. }, 2000);
  1217. }
  1218. }
  1219. });
  1220.  
  1221. this.lightbox.appendChild(content);
  1222. this.lightbox.appendChild(goToPostBtn);
  1223. },
  1224.  
  1225. navigate(direction) {
  1226. this.currentIndex = (this.currentIndex + direction + this.mediaElements.length) % this.mediaElements.length;
  1227. this.updateLightboxContent();
  1228. },
  1229.  
  1230. updateThreadInfoDisplay() {
  1231. const postCount = document.getElementById('postCount')?.textContent || '0';
  1232. const userCount = document.getElementById('userCountLabel')?.textContent || '0';
  1233. const fileCount = document.getElementById('fileCount')?.textContent || '0';
  1234. this.mediaInfoDisplay.textContent = `Posts: ${postCount} | Users: ${userCount} | Files: ${fileCount}`;
  1235. }
  1236. };
  1237.  
  1238. // SCROLL POSITION MEMORY
  1239. // ==============================
  1240. const scrollMemory = {
  1241. currentPage: window.location.href,
  1242.  
  1243. initialize() {
  1244. window.addEventListener('beforeunload', () => this.saveScrollPosition());
  1245. window.addEventListener('load', () => this.restoreScrollPosition());
  1246. },
  1247.  
  1248. isExcludedPage(url) {
  1249. return false; // Removed exclusion pattern check
  1250. },
  1251.  
  1252. saveScrollPosition() {
  1253. if (this.isExcludedPage(this.currentPage)) return;
  1254.  
  1255. const scrollPosition = window.scrollY;
  1256. localStorage.setItem(`scrollPosition_${this.currentPage}`, scrollPosition);
  1257. this.manageScrollStorage();
  1258. },
  1259.  
  1260. restoreScrollPosition() {
  1261. const savedPosition = localStorage.getItem(`scrollPosition_${this.currentPage}`);
  1262. if (savedPosition) {
  1263. window.scrollTo(0, parseInt(savedPosition, 10));
  1264. }
  1265. },
  1266.  
  1267. manageScrollStorage() {
  1268. const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_'));
  1269.  
  1270. if (keys.length > CONFIG.scrollMemory.maxPages) {
  1271. keys.sort((a, b) => localStorage.getItem(a) - localStorage.getItem(b));
  1272.  
  1273. while (keys.length > CONFIG.scrollMemory.maxPages) {
  1274. localStorage.removeItem(keys.shift());
  1275. }
  1276. }
  1277. }
  1278. };
  1279.  
  1280. // BOARD NAVIGATION ENHANCER (FIXED)
  1281. // ==============================
  1282. const boardNavigation = {
  1283. initialize() {
  1284. this.appendCatalogToLinks();
  1285. this.setupMutationObserver();
  1286. },
  1287.  
  1288. setupMutationObserver() {
  1289. const observer = new MutationObserver(() => this.appendCatalogToLinks());
  1290. const config = { childList: true, subtree: true };
  1291.  
  1292. const navboardsSpan = document.getElementById('navBoardsSpan');
  1293. if (navboardsSpan) {
  1294. observer.observe(navboardsSpan, config);
  1295. }
  1296. },
  1297.  
  1298. appendCatalogToLinks() {
  1299. document.querySelectorAll('#navBoardsSpan a').forEach(link => {
  1300. try {
  1301. const url = new URL(link.href);
  1302. // Only modify board links, not thread links
  1303. if (url.pathname.split('/').filter(Boolean).length === 1) {
  1304. if (!url.pathname.endsWith('/catalog.html')) {
  1305. url.pathname = url.pathname.replace(/\/?$/, '/catalog.html');
  1306. link.href = url.href;
  1307. }
  1308. }
  1309. } catch (e) {
  1310. console.error('Error processing URL:', e);
  1311. }
  1312. });
  1313. }
  1314. };
  1315.  
  1316. // IMAGE HOVER FIX
  1317. // ==============================
  1318. const imageHoverFix = {
  1319. initialize() {
  1320. const observer = new MutationObserver(mutations => {
  1321. mutations.forEach(mutation => {
  1322. mutation.addedNodes.forEach(node => {
  1323. if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
  1324. document.addEventListener('mousemove', this.handleMouseMove);
  1325. }
  1326. });
  1327.  
  1328. mutation.removedNodes.forEach(node => {
  1329. if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
  1330. document.removeEventListener('mousemove', this.handleMouseMove);
  1331. }
  1332. });
  1333. });
  1334. });
  1335.  
  1336. observer.observe(document.body, { childList: true, subtree: true });
  1337. },
  1338.  
  1339. handleMouseMove(event) {
  1340. const img = document.querySelector('img[style*="position: fixed"]');
  1341. if (!img) return;
  1342.  
  1343. const viewportWidth = window.innerWidth;
  1344. const viewportHeight = window.innerHeight;
  1345.  
  1346. let newX = event.clientX + 10;
  1347. let newY = event.clientY + 10;
  1348.  
  1349. if (newX + img.width > viewportWidth) {
  1350. newX = viewportWidth - img.width - 10;
  1351. }
  1352.  
  1353. if (newY + img.height > viewportHeight) {
  1354. newY = viewportHeight - img.height - 10;
  1355. }
  1356.  
  1357. img.style.left = `${newX}px`;
  1358. img.style.top = `${newY}px`;
  1359. }
  1360. };
  1361.  
  1362. // INITIALIZATION
  1363. function init() {
  1364. // Apply styles
  1365. if (typeof GM_addStyle === 'function') GM_addStyle(STYLES);
  1366. else if (typeof GM?.addStyle === 'function') GM.addStyle(STYLES);
  1367. else document.head.appendChild(Object.assign(
  1368. document.createElement('style'), { textContent: STYLES }
  1369. ));
  1370.  
  1371. // Initialize all modules
  1372. customBoardLinks.initialize(); // Added custom boards
  1373. boardNavigation.initialize();
  1374. scrollMemory.initialize();
  1375. imageHoverFix.initialize();
  1376. dashboard.initialize();
  1377.  
  1378. if (util.isThreadPage()) {
  1379. gallery.initialize();
  1380. }
  1381. }
  1382.  
  1383. // Load saved config and initialize
  1384. const savedConfig = util.loadConfigFromStorage();
  1385. if (savedConfig) dashboard.applyConfig(savedConfig);
  1386.  
  1387. if (document.readyState === 'loading') {
  1388. document.addEventListener('DOMContentLoaded', init);
  1389. } else {
  1390. init();
  1391. }
  1392. })();