Enhanced 8chan UI

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

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

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