您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Userscript to style 8chan
当前为
- // ==UserScript==
- // @name 8chanSS
- // @version 1.27
- // @namespace 8chanSS
- // @description Userscript to style 8chan
- // @author otakudude
- // @minGMVer 4.3
- // @minFFVer 121
- // @license MIT; https://github.com/otacoo/8chanSS/blob/main/LICENSE
- // @match *://8chan.moe/*
- // @match *://8chan.se/*
- // @exclude *://8chan.moe/login.html
- // @exclude *://8chan.se/login.html
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @run-at document-start
- // ==/UserScript==
- (function () {
- const userTheme = localStorage.selectedTheme;
- if (!userTheme) return;
- const swapTheme = () => {
- const themeLink = Array.from(
- document.getElementsByTagName("link")
- ).find(
- (link) =>
- link.rel === "stylesheet" &&
- /\/\.static\/css\/themes\//.test(link.href)
- );
- if (themeLink) {
- const themeBase = themeLink.href.replace(/\/[^\/]+\.css$/, "/");
- themeLink.href = themeBase + userTheme + ".css";
- }
- };
- swapTheme();
- document.addEventListener("DOMContentLoaded", swapTheme);
- document.addEventListener("DOMContentLoaded", function () {
- const themeSelector = document.getElementById("themeSelector");
- if (themeSelector) {
- for (let i = 0; i < themeSelector.options.length; i++) {
- if (
- themeSelector.options[i].value === userTheme ||
- themeSelector.options[i].text === userTheme
- ) {
- themeSelector.selectedIndex = i;
- break;
- }
- }
- }
- });
- })();
- (function () {
- try {
- localStorage.removeItem("hoveringImage");
- } catch (e) {
- }
- })();
- function onReady(fn) {
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", fn, { once: true });
- } else {
- fn();
- }
- }
- onReady(async function () {
- const scriptSettings = {
- site: {
- alwaysShowTW: { label: "Pin Thread Watcher", default: false },
- enableHeaderCatalogLinks: {
- label: "Header Catalog Links",
- default: true,
- subOptions: {
- openInNewTab: {
- label: "Always open in new tab",
- default: false,
- },
- },
- },
- enableBottomHeader: { label: "Bottom Header", default: false },
- enableScrollSave: { label: "Save Scroll Position", default: true },
- enableScrollArrows: { label: "Show Up/Down Arrows", default: false, },
- hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100, },
- },
- threads: {
- enableThreadImageHover: { label: "Thread Image Hover", default: true, },
- watchThreadOnReply: { label: "Watch Thread on Reply", default: true, },
- beepOnYou: { label: "Beep on (You)", default: false },
- notifyOnYou: { label: "Notify when (You) (!)", default: true },
- highlightOnYou: { label: "Highlight (You) posts", default: true },
- hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, },
- blurSpoilers: {
- label: "Blur Spoilers",
- default: false,
- subOptions: {
- removeSpoilers: {
- label: "Remove Spoilers",
- default: false,
- },
- },
- },
- deleteSavedName: { label: "Delete Name Checkbox", default: true },
- },
- catalog: {
- enableCatalogImageHover: { label: "Catalog Image Hover", default: true, },
- },
- styling: {
- enableStickyQR: { label: "Enable Sticky Quick Reply", default: false, },
- enableFitReplies: { label: "Fit Replies", default: false },
- enableSidebar: { label: "Enable Sidebar", default: false },
- hideAnnouncement: { label: "Hide Announcement", default: false },
- hidePanelMessage: { label: "Hide Panel Message", default: false },
- hidePostingForm: {
- label: "Hide Posting Form",
- default: false,
- subOptions: {
- showCatalogForm: {
- label: "Don't Hide in Catalog",
- default: false,
- },
- },
- },
- hideBanner: { label: "Hide Board Banners", default: false },
- },
- };
- const flatSettings = {};
- function flattenSettings() {
- Object.keys(scriptSettings).forEach((category) => {
- Object.keys(scriptSettings[category]).forEach((key) => {
- flatSettings[key] = scriptSettings[category][key];
- if (scriptSettings[category][key].subOptions) {
- Object.keys(scriptSettings[category][key].subOptions).forEach(
- (subKey) => {
- const fullKey = `${key}_${subKey}`;
- flatSettings[fullKey] =
- scriptSettings[category][key].subOptions[subKey];
- }
- );
- }
- });
- });
- }
- flattenSettings();
- async function getSetting(key) {
- if (!flatSettings[key]) {
- console.warn(`Setting key not found: ${key}`);
- return false;
- }
- let val = await GM.getValue("8chanSS_" + key, null);
- if (val === null) return flatSettings[key].default;
- if (flatSettings[key].type === "number") return Number(val);
- return val === "true";
- }
- async function setSetting(key, value) {
- await GM.setValue("8chanSS_" + key, String(value));
- }
- async function featureCssClassToggles() {
- document.documentElement.classList.add("8chanSS");
- const classToggles = {
- enableFitReplies: "fit-replies",
- enableSidebar: "ss-sidebar",
- enableStickyQR: "sticky-qr",
- enableBottomHeader: "bottom-header",
- hideHiddenPostStub: "hide-stub",
- hideBanner: "disable-banner",
- hidePostingForm: "hide-posting-form",
- hidePostingForm_showCatalogForm: "show-catalog-form",
- hideAnnouncement: "hide-announcement",
- hidePanelMessage: "hide-panelmessage",
- highlightOnYou: "highlight-you",
- };
- for (const [settingKey, className] of Object.entries(classToggles)) {
- if (await getSetting(settingKey)) {
- document.documentElement.classList.add(className);
- } else {
- document.documentElement.classList.remove(className);
- }
- }
- const urlClassMap = [
- { pattern: /\/catalog\.html$/i, className: "is-catalog" },
- { pattern: /\/res\/[^/]+\.html$/i, className: "is-thread" },
- { pattern: /^\/$/, className: "is-index" },
- ];
- const currentPath = window.location.pathname.toLowerCase();
- urlClassMap.forEach(({ pattern, className }) => {
- if (pattern.test(currentPath)) {
- document.documentElement.classList.add(className);
- } else {
- document.documentElement.classList.remove(className);
- }
- });
- }
- featureCssClassToggles();
- const themeSelector = document.getElementById("themesBefore");
- let link = null;
- let bracketSpan = null;
- if (themeSelector) {
- bracketSpan = document.createElement("span");
- bracketSpan.textContent = "] [ ";
- link = document.createElement("a");
- link.id = "8chanSS-icon";
- link.href = "#";
- link.textContent = "8chanSS";
- link.style.fontWeight = "bold";
- themeSelector.parentNode.insertBefore(
- bracketSpan,
- themeSelector.nextSibling
- );
- themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
- }
- function createShortcutsTab() {
- const container = document.createElement("div");
- const title = document.createElement("h3");
- title.textContent = "Keyboard Shortcuts";
- title.style.margin = "0 0 15px 0";
- title.style.fontSize = "16px";
- container.appendChild(title);
- const table = document.createElement("table");
- table.style.width = "100%";
- table.style.borderCollapse = "collapse";
- const tableStyles = {
- th: {
- textAlign: "left",
- padding: "8px 5px",
- borderBottom: "1px solid #444",
- fontSize: "14px",
- fontWeight: "bold",
- },
- td: {
- padding: "8px 5px",
- borderBottom: "1px solid #333",
- fontSize: "13px",
- },
- kbd: {
- background: "#333",
- border: "1px solid #555",
- borderRadius: "3px",
- padding: "2px 5px",
- fontSize: "12px",
- fontFamily: "monospace",
- },
- };
- const headerRow = document.createElement("tr");
- const shortcutHeader = document.createElement("th");
- shortcutHeader.textContent = "Shortcut";
- Object.assign(shortcutHeader.style, tableStyles.th);
- headerRow.appendChild(shortcutHeader);
- const actionHeader = document.createElement("th");
- actionHeader.textContent = "Action";
- Object.assign(actionHeader.style, tableStyles.th);
- headerRow.appendChild(actionHeader);
- table.appendChild(headerRow);
- const shortcuts = [
- { keys: ["Ctrl", "F1"], action: "Open 8chanSS settings" },
- { keys: ["Ctrl", "Q"], action: "Toggle Quick Reply" },
- { keys: ["Ctrl", "Enter"], action: "Submit post" },
- { keys: ["Ctrl", "W"], action: "Watch Thread" },
- { keys: ["Escape"], action: "Clear textarea and hide Quick Reply" },
- { keys: ["Ctrl", "B"], action: "Bold text" },
- { keys: ["Ctrl", "I"], action: "Italic text" },
- { keys: ["Ctrl", "U"], action: "Underline text" },
- { keys: ["Ctrl", "S"], action: "Spoiler text" },
- { keys: ["Ctrl", "D"], action: "Doom text" },
- { keys: ["Ctrl", "M"], action: "Moe text" },
- { keys: ["Alt", "C"], action: "Code block" },
- ];
- shortcuts.forEach((shortcut) => {
- const row = document.createElement("tr");
- const shortcutCell = document.createElement("td");
- Object.assign(shortcutCell.style, tableStyles.td);
- shortcut.keys.forEach((key, index) => {
- const kbd = document.createElement("kbd");
- kbd.textContent = key;
- Object.assign(kbd.style, tableStyles.kbd);
- shortcutCell.appendChild(kbd);
- if (index < shortcut.keys.length - 1) {
- const plus = document.createTextNode(" + ");
- shortcutCell.appendChild(plus);
- }
- });
- row.appendChild(shortcutCell);
- const actionCell = document.createElement("td");
- actionCell.textContent = shortcut.action;
- Object.assign(actionCell.style, tableStyles.td);
- row.appendChild(actionCell);
- table.appendChild(row);
- });
- container.appendChild(table);
- const note = document.createElement("p");
- note.textContent =
- "Text formatting shortcuts work when text is selected or when inserting at cursor position.";
- note.style.fontSize = "12px";
- note.style.marginTop = "15px";
- note.style.opacity = "0.7";
- note.style.fontStyle = "italic";
- container.appendChild(note);
- return container;
- }
- function addCustomCSS(css) {
- if (!css) return;
- const style = document.createElement("style");
- style.type = "text/css";
- style.id = "8chSS";
- style.appendChild(document.createTextNode(css));
- document.head.appendChild(style);
- }
- const currentPath = window.location.pathname.toLowerCase();
- const currentHost = window.location.hostname.toLowerCase();
- if (/^8chan\.(se|moe)$/.test(currentHost)) {
- const css = ":not(.is-catalog) body{margin:0}:root.ss-sidebar #mainPanel{margin-right:305px}#sideCatalogDiv{z-index:200;background:var(--background-gradient)}#navFadeEnd,#navFadeMid,#navTopBoardsSpan,:root.hide-announcement #dynamicAnnouncement,:root.hide-panelmessage #panelMessage,:root.hide-posting-form #postingForm{display:none}:root.is-catalog.show-catalog-form #postingForm{display:block!important}footer{visibility:hidden;height:0}nav.navHeader{z-index:300}:not(:root.bottom-header) .navHeader{box-shadow:0 1px 2px rgba(0,0,0,.15)}:root.bottom-header nav.navHeader{top:auto!important;bottom:0!important;box-shadow:0 -1px 2px rgba(0,0,0,.15)}.watchButton.watched-active::before{color:#dd003e!important}#watchedMenu{font-size:smaller;padding:5px!important;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#watchedMenu,#watchedMenu .floatingContainer{min-width:200px}#watchedMenu .watchedCellLabel>a:after{content:' - ' attr(href);filter:saturate(50%);font-style:italic;font-weight:700}#watchedMenu .watchedCellLabel>a::after{visibility:hidden}td.watchedCell>label.watchedCellLabel{text-overflow:ellipsis;overflow:hidden;white-space:nowrap;width:180px;display:block}td.watchedCell>label.watchedCellLabel:hover{overflow:unset;width:auto;white-space:normal}.watchedNotification::before{padding-right:2px}.scroll-arrow-btn{position:fixed;right:50px;width:36px;height:35px;background:#222;color:#fff;border:none;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.18);font-size:22px;cursor:pointer;opacity:.7;z-index:800;display:flex;align-items:center;justify-content:center;transition:opacity .2s,background .2s}:root.ss-sidebar .scroll-arrow-btn{right:330px!important}.scroll-arrow-btn:hover{opacity:1;background:#444}#scroll-arrow-up{bottom:80px}#scroll-arrow-down{bottom:32px}.innerUtility.top{margin-top:2em;background-color:transparent!important;color:var(--link-color)!important}.innerUtility.top a{color:var(--link-color)!important}.bumpLockIndicator::after{padding-right:3px}";
- addCustomCSS(css);
- }
- if (/\/res\/[^/]+\.html$/.test(currentPath)) {
- const css = ":root.sticky-qr #quick-reply{display:block;top:auto!important;bottom:0;left:auto!important;position:fixed;right:0!important}:root.sticky-qr #qrbody{resize:vertical;max-height:50vh;height:130px}#qrbody{min-width:300px}:root.bottom-header #quick-reply{bottom:28px!important}#quick-reply{padding:0;opacity:.7;transition:opacity .3s ease}#quick-reply:focus-within,#quick-reply:hover{opacity:1}.floatingMenu{padding:0!important}#qrFilesBody{max-width:300px}#unread-line{height:2px;border:none!important;pointer-events:none!important;background-image:linear-gradient(to left,rgba(185,185,185,.2),var(--text-color),rgba(185,185,185,.2));margin:-3px auto 0 auto;width:60%}:root.disable-banner #bannerImage{display:none}:root.ss-sidebar #bannerImage{width:305px;right:0;position:fixed;top:26px}:root.ss-sidebar.bottom-header #bannerImage{top:0!important}.quoteTooltip{z-index:999}.inlineQuote .replyPreview{margin-left:20px;border-left:1px solid #ccc;padding-left:10px}.nestedQuoteLink{text-decoration:underline dashed!important}:root.hide-stub .unhideButton{display:none}.quoteTooltip .innerPost{overflow:hidden;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}:root.fit-replies :not(.hidden).innerPost{margin-left:10px;display:flow-root}:root.fit-replies .quoteTooltip{display:table!important}:root.highlight-you .innerPost:has(.youName){border-left:dashed #68b723 3px}:root.highlight-you .innerPost:not(:has(.youName)):has(.quoteLink.you){border-left:solid #dd003e 3px}.originalNameLink{display:inline;overflow-wrap:anywhere;white-space:normal}.multipleUploads .uploadCell:not(.expandedCell){max-width:215px}.postCell::before{display:inline!important;height:auto!important}";
- addCustomCSS(css);
- }
- if (/\/catalog\.html$/.test(currentPath)) {
- const css = "#dynamicAnnouncement{display:none}#postingForm{margin:2em auto}";
- addCustomCSS(css);
- }
- async function createSettingsMenu() {
- let menu = document.getElementById("8chanSS-menu");
- if (menu) return menu;
- menu = document.createElement("div");
- menu.id = "8chanSS-menu";
- menu.style.position = "fixed";
- menu.style.top = "80px";
- menu.style.left = "30px";
- menu.style.zIndex = 99999;
- menu.style.background = "#222";
- menu.style.color = "#fff";
- menu.style.padding = "0";
- menu.style.borderRadius = "8px";
- menu.style.boxShadow = "0 4px 16px rgba(0,0,0,0.25)";
- menu.style.display = "none";
- menu.style.minWidth = "220px";
- menu.style.width = "100%";
- menu.style.maxWidth = "365px";
- menu.style.fontFamily = "sans-serif";
- menu.style.userSelect = "none";
- let isDragging = false,
- dragOffsetX = 0,
- dragOffsetY = 0;
- const header = document.createElement("div");
- header.style.display = "flex";
- header.style.justifyContent = "space-between";
- header.style.alignItems = "center";
- header.style.marginBottom = "0";
- header.style.cursor = "move";
- header.style.background = "#333";
- header.style.padding = "5px 18px 5px";
- header.style.borderTopLeftRadius = "8px";
- header.style.borderTopRightRadius = "8px";
- header.addEventListener("mousedown", function (e) {
- isDragging = true;
- const rect = menu.getBoundingClientRect();
- dragOffsetX = e.clientX - rect.left;
- dragOffsetY = e.clientY - rect.top;
- document.body.style.userSelect = "none";
- });
- document.addEventListener("mousemove", function (e) {
- if (!isDragging) return;
- let newLeft = e.clientX - dragOffsetX;
- let newTop = e.clientY - dragOffsetY;
- const menuRect = menu.getBoundingClientRect();
- const menuWidth = menuRect.width;
- const menuHeight = menuRect.height;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
- newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
- menu.style.left = newLeft + "px";
- menu.style.top = newTop + "px";
- menu.style.right = "auto";
- });
- document.addEventListener("mouseup", function () {
- isDragging = false;
- document.body.style.userSelect = "";
- });
- const title = document.createElement("span");
- title.textContent = "8chanSS Settings";
- title.style.fontWeight = "bold";
- header.appendChild(title);
- const closeBtn = document.createElement("button");
- closeBtn.textContent = "✕";
- closeBtn.style.background = "none";
- closeBtn.style.border = "none";
- closeBtn.style.color = "#fff";
- closeBtn.style.fontSize = "18px";
- closeBtn.style.cursor = "pointer";
- closeBtn.style.marginLeft = "10px";
- closeBtn.addEventListener("click", () => {
- menu.style.display = "none";
- });
- header.appendChild(closeBtn);
- menu.appendChild(header);
- const tabNav = document.createElement("div");
- tabNav.style.display = "flex";
- tabNav.style.borderBottom = "1px solid #444";
- tabNav.style.background = "#2a2a2a";
- const tabContent = document.createElement("div");
- tabContent.style.padding = "15px 18px";
- tabContent.style.maxHeight = "60vh";
- tabContent.style.overflowY = "auto";
- const tempSettings = {};
- await Promise.all(
- Object.keys(flatSettings).map(async (key) => {
- tempSettings[key] = await getSetting(key);
- })
- );
- const tabs = {
- site: {
- label: "Site",
- content: createTabContent("site", tempSettings),
- },
- threads: {
- label: "Threads",
- content: createTabContent("threads", tempSettings),
- },
- catalog: {
- label: "Catalog",
- content: createTabContent("catalog", tempSettings),
- },
- styling: {
- label: "Style",
- content: createTabContent("styling", tempSettings),
- },
- shortcuts: {
- label: "⌨️",
- content: createShortcutsTab(),
- },
- };
- Object.keys(tabs).forEach((tabId, index, arr) => {
- const tab = tabs[tabId];
- const tabButton = document.createElement("button");
- tabButton.textContent = tab.label;
- tabButton.dataset.tab = tabId;
- tabButton.style.background = index === 0 ? "#333" : "transparent";
- tabButton.style.border = "none";
- tabButton.style.borderRight = "1px solid #444";
- tabButton.style.color = "#fff";
- tabButton.style.padding = "8px 15px";
- tabButton.style.margin = "5px 0 0 0";
- tabButton.style.cursor = "pointer";
- tabButton.style.flex = "1";
- tabButton.style.fontSize = "14px";
- tabButton.style.transition = "background 0.2s";
- if (index === 0) {
- tabButton.style.borderTopLeftRadius = "8px";
- tabButton.style.margin = "5px 0 0 5px";
- }
- if (index === arr.length - 1) {
- tabButton.style.borderTopRightRadius = "8px";
- tabButton.style.margin = "5px 5px 0 0";
- tabButton.style.borderRight = "none";
- }
- tabButton.addEventListener("click", () => {
- Object.values(tabs).forEach((t) => {
- t.content.style.display = "none";
- });
- tab.content.style.display = "block";
- tabNav.querySelectorAll("button").forEach((btn) => {
- btn.style.background = "transparent";
- });
- tabButton.style.background = "#333";
- });
- tabNav.appendChild(tabButton);
- });
- menu.appendChild(tabNav);
- Object.values(tabs).forEach((tab, index) => {
- tab.content.style.display = index === 0 ? "block" : "none";
- tabContent.appendChild(tab.content);
- });
- menu.appendChild(tabContent);
- const buttonContainer = document.createElement("div");
- buttonContainer.style.display = "flex";
- buttonContainer.style.gap = "10px";
- buttonContainer.style.padding = "0 18px 15px";
- const saveBtn = document.createElement("button");
- saveBtn.textContent = "Save";
- saveBtn.style.background = "#4caf50";
- saveBtn.style.color = "#fff";
- saveBtn.style.border = "none";
- saveBtn.style.borderRadius = "4px";
- saveBtn.style.padding = "8px 18px";
- saveBtn.style.fontSize = "15px";
- saveBtn.style.cursor = "pointer";
- saveBtn.style.flex = "1";
- saveBtn.addEventListener("click", async function () {
- for (const key of Object.keys(tempSettings)) {
- await setSetting(key, tempSettings[key]);
- }
- saveBtn.textContent = "Saved!";
- setTimeout(() => {
- saveBtn.textContent = "Save";
- }, 900);
- setTimeout(() => {
- window.location.reload();
- }, 400);
- });
- buttonContainer.appendChild(saveBtn);
- const resetBtn = document.createElement("button");
- resetBtn.textContent = "Reset";
- resetBtn.style.background = "#dd3333";
- resetBtn.style.color = "#fff";
- resetBtn.style.border = "none";
- resetBtn.style.borderRadius = "4px";
- resetBtn.style.padding = "8px 18px";
- resetBtn.style.fontSize = "15px";
- resetBtn.style.cursor = "pointer";
- resetBtn.style.flex = "1";
- resetBtn.addEventListener("click", async function () {
- if (confirm("Reset all 8chanSS settings to defaults?")) {
- const keys = await GM.listValues();
- for (const key of keys) {
- if (key.startsWith("8chanSS_")) {
- await GM.deleteValue(key);
- }
- }
- resetBtn.textContent = "Reset!";
- setTimeout(() => {
- resetBtn.textContent = "Reset";
- }, 900);
- setTimeout(() => {
- window.location.reload();
- }, 400);
- }
- });
- buttonContainer.appendChild(resetBtn);
- menu.appendChild(buttonContainer);
- const info = document.createElement("div");
- info.style.fontSize = "11px";
- info.style.padding = "0 18px 12px";
- info.style.opacity = "0.7";
- info.style.textAlign = "center";
- info.textContent = "Press Save to apply changes. Page will reload.";
- menu.appendChild(info);
- document.body.appendChild(menu);
- return menu;
- }
- function createTabContent(category, tempSettings) {
- const container = document.createElement("div");
- const categorySettings = scriptSettings[category];
- Object.keys(categorySettings).forEach((key) => {
- const setting = categorySettings[key];
- const parentRow = document.createElement("div");
- parentRow.style.display = "flex";
- parentRow.style.alignItems = "center";
- parentRow.style.marginBottom = "0px";
- if (key === "hoverVideoVolume" && setting.type === "number") {
- const label = document.createElement("label");
- label.htmlFor = "setting_" + key;
- label.textContent = setting.label + ": ";
- label.style.flex = "1";
- const sliderContainer = document.createElement("div");
- sliderContainer.style.display = "flex";
- sliderContainer.style.alignItems = "center";
- sliderContainer.style.flex = "1";
- const slider = document.createElement("input");
- slider.type = "range";
- slider.id = "setting_" + key;
- slider.min = setting.min;
- slider.max = setting.max;
- slider.value = Number(tempSettings[key]);
- slider.style.flex = "unset";
- slider.style.width = "100px";
- slider.style.marginRight = "10px";
- const valueLabel = document.createElement("span");
- valueLabel.textContent = slider.value + "%";
- valueLabel.style.minWidth = "40px";
- valueLabel.style.textAlign = "right";
- slider.addEventListener("input", function () {
- let val = Number(slider.value);
- if (isNaN(val)) val = setting.default;
- val = Math.max(setting.min, Math.min(setting.max, val));
- slider.value = val;
- tempSettings[key] = val;
- valueLabel.textContent = val + "%";
- });
- sliderContainer.appendChild(slider);
- sliderContainer.appendChild(valueLabel);
- parentRow.appendChild(label);
- parentRow.appendChild(sliderContainer);
- const wrapper = document.createElement("div");
- wrapper.style.marginBottom = "10px";
- wrapper.appendChild(parentRow);
- container.appendChild(wrapper);
- return;
- }
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- checkbox.id = "setting_" + key;
- checkbox.checked =
- tempSettings[key] === true || tempSettings[key] === "true";
- checkbox.style.marginRight = "8px";
- const label = document.createElement("label");
- label.htmlFor = checkbox.id;
- label.textContent = setting.label;
- label.style.flex = "1";
- let chevron = null;
- let subOptionsContainer = null;
- if (setting.subOptions) {
- chevron = document.createElement("span");
- chevron.className = "ss-chevron";
- chevron.innerHTML = "▶";
- chevron.style.display = "inline-block";
- chevron.style.transition = "transform 0.2s";
- chevron.style.marginLeft = "6px";
- chevron.style.fontSize = "12px";
- chevron.style.userSelect = "none";
- chevron.style.transform = checkbox.checked
- ? "rotate(90deg)"
- : "rotate(0deg)";
- }
- checkbox.addEventListener("change", function () {
- tempSettings[key] = checkbox.checked;
- if (setting.subOptions && subOptionsContainer) {
- subOptionsContainer.style.display = checkbox.checked
- ? "block"
- : "none";
- if (chevron) {
- chevron.style.transform = checkbox.checked
- ? "rotate(90deg)"
- : "rotate(0deg)";
- }
- }
- });
- parentRow.appendChild(checkbox);
- parentRow.appendChild(label);
- if (chevron) parentRow.appendChild(chevron);
- const wrapper = document.createElement("div");
- wrapper.style.marginBottom = "10px";
- wrapper.appendChild(parentRow);
- if (setting.subOptions) {
- subOptionsContainer = document.createElement("div");
- subOptionsContainer.style.marginLeft = "25px";
- subOptionsContainer.style.marginTop = "5px";
- subOptionsContainer.style.display = checkbox.checked ? "block" : "none";
- Object.keys(setting.subOptions).forEach((subKey) => {
- const subSetting = setting.subOptions[subKey];
- const fullKey = `${key}_${subKey}`;
- const subWrapper = document.createElement("div");
- subWrapper.style.marginBottom = "5px";
- const subCheckbox = document.createElement("input");
- subCheckbox.type = "checkbox";
- subCheckbox.id = "setting_" + fullKey;
- subCheckbox.checked = tempSettings[fullKey];
- subCheckbox.style.marginRight = "8px";
- subCheckbox.addEventListener("change", function () {
- tempSettings[fullKey] = subCheckbox.checked;
- });
- const subLabel = document.createElement("label");
- subLabel.htmlFor = subCheckbox.id;
- subLabel.textContent = subSetting.label;
- subWrapper.appendChild(subCheckbox);
- subWrapper.appendChild(subLabel);
- subOptionsContainer.appendChild(subWrapper);
- });
- wrapper.appendChild(subOptionsContainer);
- }
- container.appendChild(wrapper);
- });
- if (!document.getElementById("ss-chevron-style")) {
- const style = document.createElement("style");
- style.id = "ss-chevron-style";
- style.textContent = `
- .ss-chevron {
- transition: transform 0.2s;
- margin-left: 6px;
- font-size: 12px;
- display: inline-block;
- }
- `;
- document.head.appendChild(style);
- }
- return container;
- }
- if (link) {
- let menu = await createSettingsMenu();
- link.style.cursor = "pointer";
- link.title = "Open 8chanSS settings";
- link.addEventListener("click", async function (e) {
- e.preventDefault();
- let menu = await createSettingsMenu();
- menu.style.display = menu.style.display === "none" ? "block" : "none";
- });
- }
- async function featureHeaderCatalogLinks() {
- async function appendCatalogToLinks() {
- const navboardsSpan = document.getElementById("navBoardsSpan");
- if (navboardsSpan) {
- const links = navboardsSpan.getElementsByTagName("a");
- const openInNewTab = await getSetting(
- "enableHeaderCatalogLinks_openInNewTab"
- );
- for (let link of links) {
- if (link.href && !link.href.endsWith("/catalog.html")) {
- link.href += "/catalog.html";
- if (openInNewTab) {
- link.target = "_blank";
- link.rel = "noopener noreferrer";
- } else {
- link.target = "";
- link.rel = "";
- }
- }
- }
- }
- }
- appendCatalogToLinks();
- const observer = new MutationObserver(appendCatalogToLinks);
- const config = { childList: true, subtree: true };
- const navboardsSpan = document.getElementById("navBoardsSpan");
- if (navboardsSpan) {
- observer.observe(navboardsSpan, config);
- }
- }
- async function featureImageHover() {
- function getFullMediaSrcFromMime(thumbNode, filemime) {
- if (!thumbNode || !filemime) return null;
- const thumbnailSrc = thumbNode.getAttribute("src");
- if (/\/t_/.test(thumbnailSrc)) {
- let base = thumbnailSrc.replace(/\/t_/, "/");
- base = base.replace(/\.(jpe?g|png|gif|webp|webm|mp4|webm|ogg|mp3|m4a|wav)$/i, "");
- const mimeToExt = {
- "image/jpeg": ".jpg",
- "image/jpg": ".jpg",
- "image/png": ".png",
- "image/gif": ".gif",
- "image/webp": ".webp",
- "image/bmp": ".bmp",
- "video/mp4": ".mp4",
- "video/webm": ".webm",
- "audio/ogg": ".ogg",
- "audio/mpeg": ".mp3",
- "audio/x-m4a": ".m4a",
- "audio/wav": ".wav",
- };
- const ext = mimeToExt[filemime.toLowerCase()];
- if (!ext) return null;
- return base + ext;
- }
- if (
- /\/spoiler\.png$/i.test(thumbnailSrc) ||
- /\/custom\.spoiler$/i.test(thumbnailSrc) ||
- /\/audioGenericThumb\.png$/i.test(thumbnailSrc)
- ) {
- const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
- if (parentA && parentA.getAttribute("href")) {
- return parentA.getAttribute("href");
- }
- return null;
- }
- return null;
- }
- if (!document.getElementById("audio-preview-indicator-style")) {
- const style = document.createElement("style");
- style.id = "audio-preview-indicator-style";
- style.textContent = `
- a.imgLink[data-filemime^="audio/"],
- a.originalNameLink[href$=".mp3"],
- a.originalNameLink[href$=".ogg"],
- a.originalNameLink[href$=".m4a"],
- a.originalNameLink[href$=".wav"] {
- position: relative;
- }
- .audio-preview-indicator {
- display: none;
- position: absolute;
- background: rgba(0, 0, 0, 0.7);
- color: #ffffff;
- padding: 5px;
- font-size: 12px;
- border-radius: 3px;
- z-index: 1000;
- left: 0;
- top: 0;
- white-space: nowrap;
- pointer-events: none;
- }
- a[data-filemime^="audio/"]:hover .audio-preview-indicator,
- a.originalNameLink:hover .audio-preview-indicator {
- display: block;
- }
- `;
- document.head.appendChild(style);
- }
- let floatingMedia = null;
- let removeListeners = null;
- let hoverTimeout = null;
- let lastThumb = null;
- let isStillHovering = false;
- function cleanupFloatingMedia() {
- if (hoverTimeout) {
- clearTimeout(hoverTimeout);
- hoverTimeout = null;
- }
- if (removeListeners) {
- removeListeners();
- removeListeners = null;
- }
- if (floatingMedia) {
- if (
- floatingMedia.tagName === "VIDEO" ||
- floatingMedia.tagName === "AUDIO"
- ) {
- try {
- floatingMedia.pause();
- floatingMedia.removeAttribute("src");
- floatingMedia.load();
- } catch (e) {
- }
- }
- if (floatingMedia.parentNode) {
- floatingMedia.parentNode.removeChild(floatingMedia);
- }
- }
- const indicators = document.querySelectorAll(".audio-preview-indicator");
- indicators.forEach((indicator) => {
- if (indicator.parentNode) {
- indicator.parentNode.removeChild(indicator);
- }
- });
- floatingMedia = null;
- lastThumb = null;
- isStillHovering = false;
- document.removeEventListener("mousemove", onMouseMove);
- }
- function onMouseMove(event) {
- if (!floatingMedia) return;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- let mediaWidth = 0,
- mediaHeight = 0;
- if (floatingMedia.tagName === "IMG") {
- mediaWidth =
- floatingMedia.naturalWidth ||
- floatingMedia.width ||
- floatingMedia.offsetWidth ||
- 0;
- mediaHeight =
- floatingMedia.naturalHeight ||
- floatingMedia.height ||
- floatingMedia.offsetHeight ||
- 0;
- } else if (floatingMedia.tagName === "VIDEO") {
- mediaWidth = floatingMedia.videoWidth || floatingMedia.offsetWidth || 0;
- mediaHeight =
- floatingMedia.videoHeight || floatingMedia.offsetHeight || 0;
- } else if (floatingMedia.tagName === "AUDIO") {
- return;
- }
- mediaWidth = Math.min(mediaWidth, viewportWidth * 0.9);
- mediaHeight = Math.min(mediaHeight, viewportHeight * 0.9);
- let newX = event.clientX + 10;
- let newY = event.clientY + 10;
- if (newX + mediaWidth > viewportWidth) {
- newX = viewportWidth - mediaWidth - 10;
- }
- if (newY + mediaHeight > viewportHeight) {
- newY = viewportHeight - mediaHeight - 10;
- }
- newX = Math.max(newX, 0);
- newY = Math.max(newY, 0);
- floatingMedia.style.left = `${newX}px`;
- floatingMedia.style.top = `${newY}px`;
- floatingMedia.style.maxWidth = "90vw";
- floatingMedia.style.maxHeight = "90vh";
- }
- async function onThumbEnter(e) {
- const thumb = e.currentTarget;
- if (lastThumb === thumb) return;
- lastThumb = thumb;
- cleanupFloatingMedia();
- isStillHovering = true;
- const container =
- thumb.tagName === "IMG"
- ? thumb.closest("a.linkThumb, a.imgLink")
- : thumb;
- function onLeave() {
- isStillHovering = false;
- cleanupFloatingMedia();
- }
- thumb.addEventListener("mouseleave", onLeave, { once: true });
- hoverTimeout = setTimeout(async () => {
- hoverTimeout = null;
- if (!isStillHovering) return;
- let filemime = null;
- let fullSrc = null;
- if (thumb.tagName === "IMG") {
- const parentA = thumb.closest("a.linkThumb, a.imgLink");
- if (!parentA) return;
- const href = parentA.getAttribute("href");
- if (!href) return;
- const ext = href.split(".").pop().toLowerCase();
- filemime =
- parentA.getAttribute("data-filemime") ||
- {
- jpg: "image/jpeg",
- jpeg: "image/jpeg",
- png: "image/png",
- gif: "image/gif",
- webp: "image/webp",
- bmp: "image/bmp",
- mp4: "video/mp4",
- webm: "video/webm",
- ogg: "audio/ogg",
- mp3: "audio/mpeg",
- m4a: "audio/x-m4a",
- wav: "audio/wav",
- }[ext];
- fullSrc = getFullMediaSrcFromMime(thumb, filemime);
- }
- else if (thumb.classList.contains("originalNameLink")) {
- const href = thumb.getAttribute("href");
- if (!href) return;
- const ext = href.split(".").pop().toLowerCase();
- if (["mp3", "ogg", "m4a", "wav"].includes(ext)) {
- filemime = {
- ogg: "audio/ogg",
- mp3: "audio/mpeg",
- m4a: "audio/x-m4a",
- wav: "audio/wav",
- }[ext];
- fullSrc = href;
- }
- }
- if (!fullSrc || !filemime) return;
- let loaded = false;
- function setCommonStyles(el) {
- el.style.position = "fixed";
- el.style.zIndex = 9999;
- el.style.pointerEvents = "none";
- el.style.maxWidth = "95vw";
- el.style.maxHeight = "95vh";
- el.style.transition = "opacity 0.15s";
- el.style.opacity = "0";
- el.style.left = "-9999px";
- }
- removeListeners = function () {
- window.removeEventListener("scroll", cleanupFloatingMedia, true);
- };
- window.addEventListener("scroll", cleanupFloatingMedia, true);
- if (filemime.startsWith("image/")) {
- floatingMedia = document.createElement("img");
- setCommonStyles(floatingMedia);
- floatingMedia.onload = function () {
- if (!loaded && floatingMedia && isStillHovering) {
- loaded = true;
- floatingMedia.style.opacity = "1";
- document.body.appendChild(floatingMedia);
- document.addEventListener("mousemove", onMouseMove);
- onMouseMove(e);
- }
- };
- floatingMedia.onerror = cleanupFloatingMedia;
- floatingMedia.src = fullSrc;
- } else if (filemime.startsWith("video/")) {
- floatingMedia = document.createElement("video");
- setCommonStyles(floatingMedia);
- floatingMedia.autoplay = true;
- floatingMedia.loop = true;
- floatingMedia.muted = false;
- floatingMedia.playsInline = true;
- floatingMedia.controls = false;
- let volume = 50;
- try {
- if (typeof getSetting === "function") {
- const v = await getSetting("hoverVideoVolume");
- if (typeof v === "number" && !isNaN(v)) {
- volume = v;
- }
- }
- } catch (e) {
- volume = 50;
- }
- floatingMedia.volume = Math.max(0, Math.min(1, volume / 100));
- floatingMedia.onloadeddata = function () {
- if (!loaded && floatingMedia && isStillHovering) {
- loaded = true;
- floatingMedia.style.opacity = "1";
- document.body.appendChild(floatingMedia);
- document.addEventListener("mousemove", onMouseMove);
- onMouseMove(e);
- }
- };
- floatingMedia.onerror = cleanupFloatingMedia;
- floatingMedia.src = fullSrc;
- } else if (filemime.startsWith("audio/")) {
- const oldIndicator = container.querySelector(
- ".audio-preview-indicator"
- );
- if (oldIndicator) oldIndicator.remove();
- if (container && !container.style.position) {
- container.style.position = "relative";
- }
- floatingMedia = document.createElement("audio");
- floatingMedia.src = fullSrc;
- floatingMedia.controls = false;
- floatingMedia.style.display = "none";
- let volume = 50;
- try {
- if (typeof getSetting === "function") {
- const v = await getSetting("hoverVideoVolume");
- if (typeof v === "number" && !isNaN(v)) {
- volume = v;
- }
- }
- } catch (e) {
- volume = 50;
- }
- floatingMedia.volume = Math.max(0, Math.min(1, volume / 100));
- document.body.appendChild(floatingMedia);
- const indicator = document.createElement("div");
- indicator.classList.add("audio-preview-indicator");
- indicator.textContent = "▶ Playing audio...";
- container.appendChild(indicator);
- floatingMedia.play().catch((error) => {
- console.error("Audio playback failed:", error);
- });
- function removeAudioAndIndicator() {
- if (floatingMedia) {
- floatingMedia.pause();
- floatingMedia.currentTime = 0;
- floatingMedia.remove();
- floatingMedia = null;
- }
- if (indicator) {
- indicator.remove();
- }
- }
- container.addEventListener("click", removeAudioAndIndicator, {
- once: true,
- });
- }
- }, 120);
- }
- function attachThumbListeners(root = document) {
- const thumbs = root.querySelectorAll(
- "a.linkThumb > img, a.imgLink > img"
- );
- thumbs.forEach((thumb) => {
- if (!thumb._fullImgHoverBound) {
- thumb.addEventListener("mouseenter", onThumbEnter);
- thumb._fullImgHoverBound = true;
- }
- });
- const audioLinks = root.querySelectorAll("a.originalNameLink");
- audioLinks.forEach((link) => {
- const href = link.getAttribute("href") || "";
- const ext = href.split(".").pop().toLowerCase();
- if (
- ["mp3", "wav", "ogg", "m4a"].includes(ext) &&
- !link._audioHoverBound
- ) {
- link.addEventListener("mouseenter", onThumbEnter);
- link._audioHoverBound = true;
- }
- });
- }
- attachThumbListeners();
- const observer = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- attachThumbListeners(node);
- }
- }
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- }
- function featureBlurSpoilers() {
- function revealSpoilers() {
- const spoilerLinks = document.querySelectorAll("a.imgLink");
- spoilerLinks.forEach(async (link) => {
- const img = link.querySelector("img");
- if (img) {
- const isCustomSpoiler = img.src.includes("/custom.spoiler");
- const isNotThumbnail = !img.src.includes("/.media/t_");
- if (isNotThumbnail || isCustomSpoiler) {
- let href = link.getAttribute("href");
- if (href) {
- const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
- if (match) {
- const transformedSrc = `/\.media/t_${match[1]}`;
- img.src = transformedSrc;
- if (await getSetting("blurSpoilers_removeSpoilers")) {
- img.style.filter = "";
- img.style.transition = "";
- img.onmouseover = null;
- img.onmouseout = null;
- return;
- } else {
- img.style.filter = "blur(5px)";
- img.style.transition = "filter 0.3s ease";
- img.addEventListener("mouseover", () => {
- img.style.filter = "none";
- });
- img.addEventListener("mouseout", () => {
- img.style.filter = "blur(5px)";
- });
- }
- }
- }
- }
- }
- });
- }
- revealSpoilers();
- const observer = new MutationObserver(revealSpoilers);
- observer.observe(document.body, { childList: true, subtree: true });
- }
- async function featureWatchThreadOnReply() {
- function getWatchButton() {
- return document.querySelector(".watchButton");
- }
- function isThreadWatched() {
- const btn = getWatchButton();
- return btn && btn.classList.contains("watched-active");
- }
- function watchThreadIfNotWatched() {
- const btn = getWatchButton();
- if (btn && !isThreadWatched()) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- }
- const submitButton = document.getElementById("qrbutton");
- if (submitButton) {
- submitButton.addEventListener("click", async function () {
- if (await getSetting("watchThreadOnReply")) {
- setTimeout(watchThreadIfNotWatched, 500);
- }
- });
- }
- function updateWatchButtonClass() {
- const btn = getWatchButton();
- if (!btn) return;
- if (isThreadWatched()) {
- btn.classList.add("watched-active");
- } else {
- btn.classList.remove("watched-active");
- }
- }
- updateWatchButtonClass();
- const btn = getWatchButton();
- if (btn) {
- btn.addEventListener("click", function () {
- setTimeout(updateWatchButtonClass, 100);
- });
- }
- }
- document.addEventListener("keydown", async function (event) {
- if (
- event.altKey &&
- !event.ctrlKey &&
- !event.shiftKey &&
- !event.metaKey &&
- (event.key === "w" || event.key === "W")
- ) {
- event.preventDefault();
- if (
- typeof getSetting === "function" &&
- (await getSetting("watchThreadOnReply"))
- ) {
- const btn = document.querySelector(".watchButton");
- if (btn && !btn.classList.contains("watched-active")) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- }
- }
- });
- async function featureAlwaysShowTW() {
- if (!(await getSetting("alwaysShowTW"))) return;
- function showThreadWatcher() {
- const watchedMenu = document.getElementById("watchedMenu");
- if (watchedMenu) {
- watchedMenu.style.display = "flex";
- }
- }
- function addCloseListener() {
- const watchedMenu = document.getElementById("watchedMenu");
- if (!watchedMenu) return;
- const closeBtn = watchedMenu.querySelector(".close-btn");
- if (closeBtn) {
- closeBtn.addEventListener("click", () => {
- watchedMenu.style.display = "none";
- });
- }
- }
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", () => {
- showThreadWatcher();
- addCloseListener();
- });
- } else {
- showThreadWatcher();
- addCloseListener();
- }
- }
- function processWatchedLabels() {
- document.querySelectorAll('.watchedCellLabel').forEach(label => {
- if (!label.isConnected) return;
- const notif = label.querySelector('.watchedNotification');
- const link = label.querySelector('a');
- if (!notif || !link) return;
- if (label.firstElementChild !== notif) {
- label.prepend(notif);
- }
- const match = link.getAttribute('href').match(/^\/([^\/]+)\//);
- if (!match) return;
- const board = `/${match[1]}/`;
- link.textContent = link.textContent.replace(/^\([^)]+\)\s*-\s*|^\/[^\/]+\/\s*-\s*/i, '');
- link.textContent = `${board} - ${link.textContent}`;
- });
- }
- processWatchedLabels();
- function getWatchedContainer() {
- return document.querySelector('.floatingContainer, #watchedThreads, .watchedThreads');
- }
- const container = getWatchedContainer();
- if (container) {
- const observer = new MutationObserver(() => {
- processWatchedLabels();
- });
- observer.observe(container, { childList: true, subtree: true });
- }
- window.addEventListener('DOMContentLoaded', processWatchedLabels);
- window.addEventListener('load', processWatchedLabels);
- function featureScrollArrows() {
- if (
- document.getElementById("scroll-arrow-up") ||
- document.getElementById("scroll-arrow-down")
- )
- return;
- const upBtn = document.createElement("button");
- upBtn.id = "scroll-arrow-up";
- upBtn.className = "scroll-arrow-btn";
- upBtn.title = "Scroll to top";
- upBtn.innerHTML = "▲";
- upBtn.addEventListener("click", () => {
- window.scrollTo({ top: 0, behavior: "smooth" });
- });
- const downBtn = document.createElement("button");
- downBtn.id = "scroll-arrow-down";
- downBtn.className = "scroll-arrow-btn";
- downBtn.title = "Scroll to bottom";
- downBtn.innerHTML = "▼";
- downBtn.addEventListener("click", () => {
- const footer = document.getElementById("footer");
- if (footer) {
- footer.scrollIntoView({ behavior: "smooth", block: "end" });
- } else {
- window.scrollTo({
- top: document.body.scrollHeight,
- behavior: "smooth",
- });
- }
- });
- document.body.appendChild(upBtn);
- document.body.appendChild(downBtn);
- }
- async function featureSaveScroll() {
- if (document.documentElement.classList.contains("is-index")) return;
- const MAX_PAGES = 50;
- const currentPage = window.location.href;
- const excludedPagePatterns = [
- /\/catalog\.html$/i,
- /\/.media\/$/i,
- /\/boards\.js$/i,
- /\/login\.html$/i,
- /\/overboard$/i,
- /\/sfw$/i
- ];
- function isExcludedPage(url) {
- return excludedPagePatterns.some((pattern) => pattern.test(url));
- }
- async function saveScrollPosition() {
- if (isExcludedPage(currentPage)) return;
- const scrollPosition = window.scrollY;
- const timestamp = Date.now();
- await GM.setValue(
- `8chanSS_scrollPosition_${currentPage}`,
- JSON.stringify({
- position: scrollPosition,
- timestamp: timestamp,
- })
- );
- await manageScrollStorage();
- }
- async function manageScrollStorage() {
- const allKeys = await GM.listValues();
- const scrollKeys = allKeys.filter((key) =>
- key.startsWith("8chanSS_scrollPosition_")
- );
- if (scrollKeys.length > MAX_PAGES) {
- const keyData = await Promise.all(
- scrollKeys.map(async (key) => {
- let data;
- try {
- const savedValue = await GM.getValue(key, null);
- data = savedValue ? JSON.parse(savedValue) : { position: 0, timestamp: 0 };
- } catch (e) {
- data = { position: 0, timestamp: 0 };
- }
- return {
- key: key,
- timestamp: data.timestamp || 0,
- };
- })
- );
- keyData.sort((a, b) => a.timestamp - b.timestamp);
- const keysToRemove = keyData.slice(0, keyData.length - MAX_PAGES);
- for (const item of keysToRemove) {
- await GM.deleteValue(item.key);
- }
- }
- }
- async function addUnreadLine() {
- if (window.location.hash && window.location.hash.length > 1) {
- return;
- }
- const savedData = await GM.getValue(
- `8chanSS_scrollPosition_${currentPage}`,
- null
- );
- if (savedData) {
- let position;
- try {
- const data = JSON.parse(savedData);
- position = data.position;
- await GM.setValue(
- `8chanSS_scrollPosition_${currentPage}`,
- JSON.stringify({
- position: position,
- timestamp: Date.now(),
- })
- );
- } catch (e) {
- return;
- }
- if (!isNaN(position)) {
- window.scrollTo(0, position);
- setTimeout(addUnreadLineAtViewportCenter, 100);
- }
- }
- }
- function addUnreadLineAtViewportCenter() {
- const divPosts = document.querySelector(".divPosts");
- if (!divPosts) return;
- const centerX = window.innerWidth / 2;
- const centerY = window.innerHeight / 2;
- let el = document.elementFromPoint(centerX, centerY);
- while (el && el !== divPosts && (!el.classList || !el.classList.contains("postCell"))) {
- el = el.parentElement;
- }
- if (!el || el === divPosts || !el.id) return;
- if (el.parentElement !== divPosts) return;
- const oldMarker = document.getElementById("unread-line");
- if (oldMarker && oldMarker.parentNode) {
- oldMarker.parentNode.removeChild(oldMarker);
- }
- const marker = document.createElement("hr");
- marker.id = "unread-line";
- if (el.nextSibling) {
- divPosts.insertBefore(marker, el.nextSibling);
- } else {
- divPosts.appendChild(marker);
- }
- }
- window.addEventListener("beforeunload", () => {
- saveScrollPosition();
- });
- window.addEventListener("load", async () => {
- await addUnreadLine();
- });
- await addUnreadLine();
- }
- featureSaveScroll();
- function featureDeleteNameCheckbox() {
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- checkbox.id = "saveNameCheckbox";
- checkbox.classList.add("postingCheckbox");
- const label = document.createElement("label");
- label.htmlFor = "saveNameCheckbox";
- label.textContent = "Delete Name";
- label.title = "Delete Name on refresh";
- const alwaysUseBypassCheckbox = document.getElementById("qralwaysUseBypassCheckBox");
- if (alwaysUseBypassCheckbox) {
- alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox);
- alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling);
- const savedCheckboxState = localStorage.getItem("8chanSS_deleteNameCheckbox") === "true";
- checkbox.checked = savedCheckboxState;
- const nameInput = document.getElementById("qrname");
- if (nameInput) {
- if (checkbox.checked) {
- nameInput.value = "";
- localStorage.removeItem("name");
- }
- checkbox.addEventListener("change", function () {
- localStorage.setItem("8chanSS_deleteNameCheckbox", checkbox.checked);
- });
- }
- }
- }
- function featureBeepOnYou() {
- const beep = new Audio(
- "data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA"
- );
- const originalTitle = document.title;
- let isNotifying = false;
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- mutation.addedNodes.forEach(async (node) => {
- if (
- node.nodeType === 1 &&
- node.querySelector &&
- node.querySelector("a.quoteLink.you")
- ) {
- if (await getSetting("beepOnYou")) {
- playBeep();
- }
- if (await getSetting("notifyOnYou")) {
- featureNotifyOnYou();
- }
- }
- });
- });
- });
- observer.observe(document.body, { childList: true, subtree: true });
- function playBeep() {
- if (beep.paused) {
- beep.play().catch((e) => console.warn("Beep failed:", e));
- } else {
- beep.addEventListener("ended", () => beep.play(), { once: true });
- }
- }
- function featureNotifyOnYou() {
- if (!window.originalTitle) {
- window.originalTitle = document.title;
- }
- if (!window.isNotifying && !document.hasFocus()) {
- window.isNotifying = true;
- document.title = "(!) " + window.originalTitle;
- if (!window.notifyFocusListenerAdded) {
- window.addEventListener("focus", () => {
- if (window.isNotifying) {
- document.title = window.originalTitle;
- window.isNotifying = false;
- }
- });
- window.notifyFocusListenerAdded = true;
- }
- }
- }
- function addNotificationToTitle() {
- if (!isNotifying && !document.hasFocus()) {
- isNotifying = true;
- document.title = "(!) " + originalTitle;
- }
- }
- window.addEventListener("focus", () => {
- if (isNotifying) {
- document.title = originalTitle;
- isNotifying = false;
- }
- });
- }
- if (await getSetting("enableScrollSave")) {
- featureSaveScroll();
- }
- if (await getSetting("watchThreadOnReply")) {
- featureWatchThreadOnReply();
- }
- if (await getSetting("blurSpoilers")) {
- featureBlurSpoilers();
- }
- if (await getSetting("enableHeaderCatalogLinks")) {
- featureHeaderCatalogLinks();
- }
- if (await getSetting("deleteSavedName")) {
- featureDeleteNameCheckbox();
- }
- if (await getSetting("enableScrollArrows")) {
- featureScrollArrows();
- }
- if ((await getSetting("beepOnYou")) || (await getSetting("notifyOnYou"))) {
- featureBeepOnYou();
- }
- if (await getSetting("alwaysShowTW")) {
- featureAlwaysShowTW();
- }
- const isCatalogPage = /\/catalog\.html$/.test(
- window.location.pathname.toLowerCase()
- );
- if (
- (isCatalogPage && (await getSetting("enableCatalogImageHover"))) ||
- (!isCatalogPage && (await getSetting("enableThreadImageHover")))
- ) {
- featureImageHover();
- }
- document.addEventListener("keydown", async function (event) {
- if (event.ctrlKey && event.key === "F1") {
- event.preventDefault();
- let menu =
- document.getElementById("8chanSS-menu") ||
- (await createSettingsMenu());
- menu.style.display =
- menu.style.display === "none" || menu.style.display === ""
- ? "block"
- : "none";
- }
- });
- async function submitWithCtrlEnter(event) {
- if (event.ctrlKey && event.key === "Enter") {
- event.preventDefault();
- const submitButton = document.getElementById("qrbutton");
- if (submitButton) {
- submitButton.click();
- if (await getSetting("watchThreadOnReply")) {
- setTimeout(() => {
- const btn = document.querySelector(".watchButton");
- if (btn && !btn.classList.contains("watched-active")) {
- btn.click();
- setTimeout(() => {
- btn.classList.add("watched-active");
- }, 100);
- }
- }, 500);
- }
- }
- }
- }
- const replyTextarea = document.getElementById("qrbody");
- if (replyTextarea) {
- replyTextarea.addEventListener("keydown", submitWithCtrlEnter);
- }
- function toggleQR(event) {
- if (event.ctrlKey && (event.key === "q" || event.key === "Q")) {
- const hiddenDiv = document.getElementById("quick-reply");
- if (
- hiddenDiv.style.display === "none" ||
- hiddenDiv.style.display === ""
- ) {
- hiddenDiv.style.display = "block";
- setTimeout(() => {
- const textarea = document.getElementById("qrbody");
- if (textarea) {
- textarea.focus();
- }
- }, 50);
- } else {
- hiddenDiv.style.display = "none";
- }
- }
- }
- document.addEventListener("keydown", toggleQR);
- function clearTextarea(event) {
- if (event.key === "Escape") {
- const textarea = document.getElementById("qrbody");
- if (textarea) {
- textarea.value = "";
- }
- const quickReply = document.getElementById("quick-reply");
- if (quickReply) {
- quickReply.style.display = "none";
- }
- }
- }
- document.addEventListener("keydown", clearTextarea);
- const bbCodeCombinations = new Map([
- ["s", ["[spoiler]", "[/spoiler]"]],
- ["b", ["'''", "'''"]],
- ["u", ["__", "__"]],
- ["i", ["''", "''"]],
- ["d", ["[doom]", "[/doom]"]],
- ["m", ["[moe]", "[/moe]"]],
- ["c", ["[code]", "[/code]"]],
- ]);
- function replyKeyboardShortcuts(ev) {
- const key = ev.key.toLowerCase();
- if (
- key === "c" &&
- ev.altKey &&
- !ev.ctrlKey &&
- bbCodeCombinations.has(key)
- ) {
- ev.preventDefault();
- const textBox = ev.target;
- const [openTag, closeTag] = bbCodeCombinations.get(key);
- const { selectionStart, selectionEnd, value } = textBox;
- if (selectionStart === selectionEnd) {
- const before = value.slice(0, selectionStart);
- const after = value.slice(selectionEnd);
- const newCursor = selectionStart + openTag.length;
- textBox.value = before + openTag + closeTag + after;
- textBox.selectionStart = textBox.selectionEnd = newCursor;
- } else {
- const before = value.slice(0, selectionStart);
- const selected = value.slice(selectionStart, selectionEnd);
- const after = value.slice(selectionEnd);
- textBox.value = before + openTag + selected + closeTag + after;
- textBox.selectionStart = selectionStart + openTag.length;
- textBox.selectionEnd = selectionEnd + openTag.length;
- }
- return;
- }
- if (
- ev.ctrlKey &&
- !ev.altKey &&
- bbCodeCombinations.has(key) &&
- key !== "c"
- ) {
- ev.preventDefault();
- const textBox = ev.target;
- const [openTag, closeTag] = bbCodeCombinations.get(key);
- const { selectionStart, selectionEnd, value } = textBox;
- if (selectionStart === selectionEnd) {
- const before = value.slice(0, selectionStart);
- const after = value.slice(selectionEnd);
- const newCursor = selectionStart + openTag.length;
- textBox.value = before + openTag + closeTag + after;
- textBox.selectionStart = textBox.selectionEnd = newCursor;
- } else {
- const before = value.slice(0, selectionStart);
- const selected = value.slice(selectionStart, selectionEnd);
- const after = value.slice(selectionEnd);
- textBox.value = before + openTag + selected + closeTag + after;
- textBox.selectionStart = selectionStart + openTag.length;
- textBox.selectionEnd = selectionEnd + openTag.length;
- }
- return;
- }
- }
- document
- .getElementById("qrbody")
- ?.addEventListener("keydown", replyKeyboardShortcuts);
- });