// ==UserScript==
// @name 8chanSS
// @version 1.60.6
// @namespace 8chanss
// @description A userscript to add functionality to 8chan.
// @author otakudude
// @license MIT; https://github.com/otacoo/8chanSS/blob/main/LICENSE
// @match *://*.8chan.moe/*
// @match *://*.8chan.st/*
// @match *://*.8chan.cc/*
// @match *://alephchvkipd2houttjirmgivro5pxullvcgm4c47ptm7mhubbja6kad.onion/*
// @exclude *://*.8chan.moe/login.html
// @exclude *://*.8chan.st/login.html
// @exclude *://*.8chan.cc/login.html
// @exclude *://alephchvkipd2houttjirmgivro5pxullvcgm4c47ptm7mhubbja6kad.onion/login.html
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.xmlHttpRequest
// @connect youtube.com
// @connect i.ytimg.com
// @connect twitch.tv
// @connect static-cdn.jtvnw.net
// @connect clips-media-assets2.twitch.tv
// @connect api.fxtwitter.com
// @connect embed.bsky.app
// @connect bsky.app
// @connect cdn.bsky.app
// @connect *.twimg.com
// @connect pastebin.com
// @connect rentry.co
// @connect rentry.org
// @run-at document-start
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAZlBMVEUAAABdlloAhl758AH58AH58AcAhl758ADj4CYAh14AhV4AhV0Ahl748AcChl4Chl0Ab2H58AIAhl758AD58AAAhl757wL48AD47wL78QL47wcAh1748AF3oFfs5yEAh1/68QDz7BM5qSu8AAAAH3RSTlMA/lg/OYtM8g/onXtoXzAaCdzBsIFzczMeaCXXyrmp9ddA3QAAANpJREFUSMft0tkOgjAQheFjtVCQVVxwnfr+L+kWM5FOC73TxP/6fBedFJwpyx5CtSpqSHXWpns4qYxo1cDtkNp7GoOW9KgSwM4+09KeEhmw4H0IuGJDAbCw79a8nwJYFDQCuO1gT8oLWCiKAXavKA5cZ78I5n/wBx7wfb+1TwOggpD2gxxSpvWBrIbY3AcUPK1lkMNbJ4FV4wd964KsQqBF6oAEwcoh2GAk/QlyjNYx4AeHMicGxxoTOrRvIB5IPtULJJhY+QIFJrd9gCUi0tdZjqgu5yYOGAO5G/kyc3TkciPeAAAAAElFTkSuQmCC
// @downloadURL https://update.greasyfork.icu/scripts/533268/8chanSS.user.js
// @updateURL https://update.greasyfork.icu/scripts/533268/8chanSS.meta.js
// ==/UserScript==
function onReady(fn) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fn, { once: true });
} else {
fn();
}
}
const debounce = (fn, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
};
const observerRegistry = {};
let unloadListenerAdded = false;
function observeSelector(selector, options = { childList: true, subtree: false }) {
if (observerRegistry[selector]) return observerRegistry[selector];
const node = document.querySelector(selector);
if (!node) return null;
const handlers = [];
const observer = new MutationObserver(mutations => {
for (const handler of handlers) {
try {
handler(mutations, node);
} catch (e) {
console.error(`Observer handler error for ${selector}:`, e);
}
}
});
observer.observe(node, options);
if (!unloadListenerAdded) {
unloadListenerAdded = true;
window.addEventListener('beforeunload', () => {
for (const key in observerRegistry) {
observerRegistry[key].observer.disconnect();
}
}, { once: true });
}
observerRegistry[selector] = {
node,
observer,
handlers,
addHandler: fn => handlers.push(fn)
};
return observerRegistry[selector];
}
window.pageType = (() => {
const path = window.location.pathname.toLowerCase();
const currentHost = window.location.hostname.toLowerCase();
return {
isCatalog: /\/catalog\.html$/i.test(path),
isThread: /\/(res|last)\/[^/]+\.html$/i.test(path),
isLast: /\/last\/[^/]+\.html$/i.test(path),
isIndex: /\/[^/]+\/$/i.test(path),
is8chan: /^8chan\.(moe|st|cc)$/.test(currentHost),
host: currentHost,
path: path
};
})();
(function injectCssAsap() {
function doInject() {
if (document.getElementById('8chSShim')) return;
if (!document.head) {
setTimeout(doInject, 1);
return;
}
const style = document.createElement('style');
style.id = '8chSShim';
style.textContent = "#dynamicAnnouncement,#panelMessage,#postingForm{visibility:hidden}:not(.is-catalog) body{margin:0}.innerUtility.top{margin-top:2em;background:0 0!important;color:var(--link-color)!important}.innerUtility.top a{color:var(--link-color)!important}";
document.head.appendChild(style);
}
doInject();
})();
onReady(async function () {
"use strict";
const divThreads = document.getElementById('divThreads');
const innerOP = document.querySelector('.innerOP');
const divPosts = document.querySelector('.divPosts');
const opHeadTitle = document.querySelector('.opHead.title');
const catalogDiv = document.querySelector('.catalogDiv');
const VERSION = "1.60.6";
const scriptSettings = {
site: {
_siteTWTitle: { type: "title", label: ":: Thread Watcher" },
_siteSection1: { type: "separator" },
alwaysShowTW: {
label: "Pin Thread Watcher",
default: false,
subOptions: {
noPinInCatalog: {
label: "Don't pin in Catalog",
default: false,
}
}
},
autoExpandTW: { label: "Auto Expand Thread Watcher", default: false },
_siteSiteTitle: { type: "title", label: ":: Site" },
_siteSection2: { type: "separator" },
customFavicon: {
label: "Custom Favicon",
default: false,
subOptions: {
faviconStyle: {
label: "Favicon Style",
type: "select",
default: "default",
options: [
{ value: "default", label: "Default" },
{ value: "pixel", label: "Pixel" },
{ value: "pixel_alt", label: "Pixel Alt" },
{ value: "eight", label: "Eight" },
{ value: "eight_dark", label: "Eight Dark" }
]
}
}
},
enableBottomHeader: { label: "Bottom Header", default: false },
enableAutoHideHeaderScroll: { label: "Auto-hide Header On Scroll", default: false },
enableHeaderCatalogLinks: {
label: "Header Catalog Links",
default: true,
subOptions: {
openInNewTab: {
label: "Always open in new tab",
default: false,
}
}
},
enableScrollArrows: { label: "Show Up/Down Arrows", default: false },
_siteMediaTitle: { type: "title", label: ":: Media" },
_siteSection3: { type: "separator" },
enableFitImage: { label: "Fit expanded Images and Videos", default: true },
blurSpoilers: {
label: "Blur Spoilers",
default: false,
subOptions: {
removeSpoilers: {
label: "Remove Spoilers",
default: false
}
}
},
enablePNGstop: { label: "Prevent animated PNG images from playing (slow)", default: false },
enableMediaViewer: {
label: "Enable Advanced Media Viewer",
default: false,
subOptions: {
viewerStyle: {
label: "Style",
type: "select",
default: "native",
options: [
{ value: "native", label: "Native" },
{ value: "topright", label: "Pin Top Right" },
{ value: "topleft", label: "Pin Top Left" }
]
}
}
},
trackMediaPlayback: { label: "Track and Restore Hover Media Playback Time", default: true },
hoverVideoVolume: { label: "Hover Media Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }
},
threads: {
_threadsNotiTitle: { type: "title", label: ":: Notifications" },
_threadsSection1: { type: "separator" },
beepOnYou: { label: "Beep on (You)", default: false },
notifyOnYou: {
label: "Tab Notification when (You) (!)",
default: true,
subOptions: {
customMessage: {
label: "Custom Text (max: 8 chars.)",
default: "",
type: "text",
maxLength: 9
}
}
},
_threadsNavTitle: { type: "title", label: ":: Navigation & Others" },
_threadsSection3: { type: "separator" },
enableThreadImageHover: { label: "Thread Image Hover", default: true },
enableBacklinkIcons: { label: "Backlink Icons", default: false },
enableScrollSave: {
label: "Save Scroll Position",
default: true,
subOptions: {
showUnreadLine: {
label: "Show Unread Line",
default: true,
}
}
},
quoteThreading: { label: "Quote Threading (Shift + T to toggle)", default: false },
enableHashNav: { label: "Hash Navigation", default: false },
threadStatsInHeader: { label: "Thread Stats in Header", default: false },
watchThreadOnReply: { label: "Watch Thread on Reply", default: true },
hideHiddenPostStub: { label: "Hide Stubs of Hidden Posts", default: false, },
_miscelIDTitle: { type: "title", label: ":: IDs" },
_miscelSection2: { type: "separator" },
highlightNewIds: {
label: "Highlight New IDs",
default: false,
subOptions: {
idHlStyle: {
label: "Highlight Style",
type: "select",
default: "moetext",
options: [
{ value: "moetext", label: "Moe" },
{ value: "glow", label: "Glow" },
{ value: "dotted", label: "Border" }
]
}
}
},
alwaysShowIdCount: { label: "Always show ID post count", default: false },
enableIdFilters: {
label: "Show all posts by ID when ID is clicked",
type: "checkbox",
default: true,
subOptions: {
idViewMode: {
label: "View Mode",
type: "select",
default: "showPostsOfIdOnly",
options: [
{ value: "showPostsOfIdOnly", label: "Only ID's posts" },
{ value: "showIdLinksOnly", label: "Floating list" },
{ value: "showIdLinksVertical", label: "Vertical list" }
]
}
}
},
enableIdToggle: { label: "Add menu entry to toggle IDs as Yours", type: "checkbox", default: false }
},
catalog: {
enableCatalogImageHover: { label: "Catalog Image Hover", default: true },
enableThreadHiding: {
label: "Enable Thread Hiding (Shift + Click to hide/unhide a thread)",
default: false,
subOptions: {
enableCatalogFiltering: {
label: "Enable Catalog Thread Filtering",
default: false
}
}
},
openCatalogThreadNewTab: { label: "Always Open Threads in New Tab", default: false },
enableLastFifty: { label: "Show Last 50 Posts button", default: false }
},
styling: {
_stylingMascotTitle: { type: "title", label: ":: Mascots" },
_stylingSection1: { type: "separator" },
enableMascots: {
label: "Enable Mascots",
default: false,
subOptions: {
mascotOpacity: {
label: "Mascot Opacity (0-100%)",
default: 30,
type: "number",
min: 0,
max: 100
},
mascotUrls: {
label: "Mascot Image URLs (one per line)",
type: "textarea",
default: "",
placeholder: "Enter image URLs, one per line\nExample:\n/.media/mascot1.png\n/.media/mascot2.png",
rows: 4
}
}
},
_stylingSiteTitle: { type: "title", label: ":: Site Styling" },
_stylingSection2: { type: "separator" },
hideAnnouncement: { label: "Hide Announcement (unhides if new announcement posted)", 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 },
hideNoCookieLink: { label: "Hide No Cookie? Link", default: false },
hideJannyTools: { label: "Hide Janitor Forms", default: false },
hlCurrentBoard: { label: "Highlight Current Board", default: false },
_stylingThreadTitle: { type: "title", label: ":: Thread Styling" },
_stylingSection3: { type: "separator" },
enableSidebar: {
label: "Enable Sidebar",
default: false,
subOptions: {
leftSidebar: {
label: "Sidebar on Left",
default: false
}
}
},
enableFitReplies: { label: "Fit Replies", default: false },
highlightOnYou: { label: "Style (You) posts", default: true },
opBackground: { label: "OP background", default: false },
enableStickyQR: { label: "Sticky Quick Reply", default: false },
fadeQuickReply: { label: "Fade Quick Reply", default: false },
threadHideCloseBtn: { label: "Hide Inline Close Button", default: false },
hideCheckboxes: { label: "Hide Post Checkbox", default: false }
},
miscel: {
enableShortcuts: { label: "Enable Keyboard Shortcuts", type: "checkbox", default: true },
enableUpdateNotif: { label: "8chanSS update notifications", default: true },
switchTimeFormat: { label: "Enable 12-hour Clock (AM/PM)", default: false },
truncFilenames: {
label: "Truncate filenames",
default: false,
subOptions: {
customTrunc: {
label: "Max filename length (min: 5, max: 50)",
default: 15,
type: "number",
min: 5,
max: 50
}
}
},
_miscelThreadTitle: { type: "title", label: ":: Linkification" },
_miscelSection1: { type: "separator" },
enhanceLinks_showIcons: {
label: "Show Icons and Titles",
default: true,
subOptions: {
showIconsYoutube: { label: "Youtube", default: true },
showIconsTwitch: { label: "Twitch", default: true },
showIconsX: { label: "X.com", default: true },
showIconsBsky: { label: "Bluesky", default: true },
showIconsRentry: { label: "Rentry", default: true },
showIconsCatbox: { label: "Catbox", default: true },
showIconsPastebin: { label: "Pastebin", default: true }
}
},
enhanceLinks_showThumbnails: {
label: "Show Thumbnails on Hover",
default: true,
subOptions: {
showThumbnailsYoutube: { label: "Youtube", default: true },
showThumbnailsTwitch: { label: "Twitch", default: true }
}
},
enhanceLinks_enableEmbeds: {
label: "Enable Embedding",
default: false,
subOptions: {
enableEmbedsX: { label: "X.com", default: true },
enableEmbedsBsky: { label: "Bluesky", default: true },
enableEmbedsRentry: { label: "Rentry", default: true },
enableEmbedsPastebin: { label: "Pastebin", default: true }
}
},
enableTheSauce: {
label: "Sauce Links",
default: false,
subOptions: {
iqdb: {
label: "IQDB",
default: false,
},
saucenao: {
label: "Saucenao",
default: false,
},
pixiv: {
label: "Pixiv (only added if filename matches Pixiv ID)",
default: false,
}
}
},
_miscelStorageTitle: { type: "title", label: ":: Storage & Sync" },
_miscelSection2: { type: "separator" },
saveWatchedThreads: {
label: "Save Current Watched Threads",
type: "button"
},
saveFavoriteBoards: {
label: "Save Current Favorite Boards",
type: "button"
},
restoreWatchedThreads: {
label: "Restore Watched Threads",
type: "button"
},
restoreFavoriteBoards: {
label: "Restore Favorite Boards",
type: "button"
},
}
};
Object.freeze(scriptSettings);
function flattenSettings() {
const result = {};
function flattenRecursive(obj, prefix = '') {
Object.keys(obj).forEach((key) => {
if (key.startsWith('_')) return;
const fullKey = prefix ? `${prefix}_${key}` : key;
result[fullKey] = obj[key];
const subOptions = obj[key].subOptions;
if (subOptions && typeof subOptions === "object") {
flattenRecursive(subOptions, fullKey);
}
});
}
Object.keys(scriptSettings).forEach((category) => {
flattenRecursive(scriptSettings[category]);
});
return Object.freeze(result);
}
let flatSettings = flattenSettings();
async function getSetting(key) {
if (!flatSettings[key]) {
console.warn(`Setting key not found: ${key}`);
return false;
}
let val;
try {
val = await GM.getValue("8chanSS_" + key, null);
} catch (err) {
console.error(`Failed to get setting for key ${key}:`, err);
return flatSettings[key]?.default ?? false;
}
if (val === null) return flatSettings[key].default;
switch (flatSettings[key].type) {
case "number":
return Number(val);
case "text":
return String(val).replace(/[<>"']/g, "").slice(0, flatSettings[key].maxLength || 32);
case "textarea":
case "select":
return String(val);
default:
return val === "true";
}
}
async function setSetting(key, value) {
try {
await GM.setValue("8chanSS_" + key, String(value));
} catch (err) {
console.error(`Failed to set setting for key ${key}:`, err);
}
}
async function getStoredObject(key) {
let obj = {};
if (typeof GM !== 'undefined' && GM.getValue) {
obj = await GM.getValue(key, {});
}
return typeof obj === 'object' && obj !== null ? obj : {};
}
async function setStoredObject(key, obj) {
if (typeof GM !== 'undefined' && GM.setValue) {
await GM.setValue(key, obj);
}
}
function getRawIdFromLabelId(labelIdSpan) {
return labelIdSpan ? labelIdSpan.textContent.split(/[|\(]/)[0].trim() : null;
}
(async function featureCssClassToggles() {
document.documentElement.classList.add("8chanSS");
const enableSidebar = await getSetting("enableSidebar");
const enableSidebar_leftSidebar = await getSetting("enableSidebar_leftSidebar");
const classToggles = {
enableFitReplies: "fit-replies",
enableSidebar_leftSidebar: "ss-leftsidebar",
enableStickyQR: "sticky-qr",
fadeQuickReply: "fade-qr",
enableBottomHeader: "bottom-header",
hideHiddenPostStub: "hide-stub",
hideBanner: "disable-banner",
hidePostingForm: "hide-posting-form",
hidePostingForm_showCatalogForm: "show-catalog-form",
hidePanelMessage: "hide-panelmessage",
highlightOnYou: "highlight-yous",
hlCurrentBoard: "hl-currentBoard",
threadHideCloseBtn: "hide-close-btn",
hideCheckboxes: "hide-checkboxes",
hideNoCookieLink: "hide-nocookie",
autoExpandTW: "auto-expand-tw",
hideJannyTools: "hide-jannytools",
opBackground: "op-background",
blurSpoilers: "ss-blur-spoilers",
enableBacklinkIcons: "backlink-icon",
enableFitImage: "fit-images"
};
if (enableSidebar && !enableSidebar_leftSidebar) {
document.documentElement.classList.add("ss-sidebar");
} else {
document.documentElement.classList.remove("ss-sidebar");
}
const settingKeys = Object.keys(classToggles);
const settingValues = await Promise.all(settingKeys.map(getSetting));
settingKeys.forEach((key, i) => {
const className = classToggles[key];
if (settingValues[i]) {
document.documentElement.classList.add(className);
} else {
document.documentElement.classList.remove(className);
}
});
if (window.pageType?.isCatalog) {
document.documentElement.classList.add("is-catalog");
} else {
document.documentElement.classList.remove("is-catalog");
}
if (window.pageType?.isThread) {
document.documentElement.classList.add("is-thread");
} else {
document.documentElement.classList.remove("is-thread");
}
if (window.pageType?.isIndex) {
document.documentElement.classList.add("is-index");
} else {
document.documentElement.classList.remove("is-index");
}
})();
(function injectCustomCss() {
if (document.getElementById('8chSS')) return;
let css = "";
if (window.pageType?.is8chan) {
css += "#dynamicAnnouncement,#panelMessage,#postingForm{visibility:visible}#navFadeEnd,#navFadeMid,.watchedNotification::before,:root.disable-banner #bannerImage,:root.hide-announcement #dynamicAnnouncement,:root.hide-checkboxes .deletionCheckBox,:root.hide-close-btn .inlineQuote>.innerPost>.postInfo.title>a:first-child,:root.hide-jannytools #actionsForm,:root.hide-jannytools #boardContentLinks,:root.hide-nocookie #captchaBody>table:nth-child(2)>tbody:first-child>tr:nth-child(2),:root.hide-panelmessage #panelMessage,:root.hide-posting-form #postingForm{display:none}#sideCatalogDiv{z-index:200;background:var(--background-gradient)}:root.is-catalog.show-catalog-form #postingForm{display:block!important}:root.is-thread footer{visibility:hidden;height:0}:root.op-background .opCell>.innerOP{padding-top:.25em;width:100%;background:var(--contrast-color);border:1px solid var(--horizon-sep-color);border-top-width:0;border-left-width: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)}:root.hl-currentBoard .navBoardsLink--current{font-weight:800;color:var(--link-color)!important;text-transform:uppercase}:root.hl-currentBoard .navBoardsLink--current:hover{color:var(--link-color-hover)!important}:root.highlight-yous .innerOP:has(> .opHead.title > .youName),:root.highlight-yous .innerPost:has(> .postInfo.title > .youName),:root.highlight-yous .yourPost{border-left:dashed #68b723 2px!important}:root.highlight-yous .innerPost:has(>.divMessage>.you),:root.highlight-yous .innerPost:has(>.divMessage>:not(div)>.you),:root.highlight-yous .innerPost:has(>.divMessage>:not(div)>:not(div)>.you),:root.highlight-yous .quotesYou{border-left:solid var(--subject-color) 2px!important}:root.fit-replies :not(.hidden).innerPost{margin-left:10px;display:flow-root}:root.fit-replies :not(.hidden,.inlineQuote).innerPost{margin-left:0}.multipleUploads .uploadCell:not(.expandedCell){max-width:215px}:root.ss-blur-spoilers .quoteTooltip img[src*=\"audioGenericThumb\.png\"],:root.ss-blur-spoilers .quoteTooltip img[src*=\"custom\.spoiler\"],:root.ss-blur-spoilers .quoteTooltip img[src*=\"spoiler\.png\"]{filter:blur(5px)!important;transition:filter .3s ease}a.imgLink img.ss-spoiler-blurred{filter:blur(5px);transition:filter .3s ease}a.imgLink img.ss-spoiler-blurred:hover{filter:none}a.imgLink img.ss-spoiler-border{border:1px dotted var(--border-color)}:root.fit-images :not(ruffle-player).imgExpanded{max-height:90vh!important;object-fit:contain;width:auto!important}:not(:root.auto-expand-tw) #watchedMenu .floatingContainer{overflow-x:hidden;overflow-wrap:break-word}:root.auto-expand-tw #watchedMenu .floatingContainer{height:fit-content!important;padding-bottom:10px;resize:horizontal}.watchedCellLabel a::before{content:attr(data-board);color:#aaa;margin-right:4px;font-weight:700}.watchButton.watched-active::before{color:#dd003e!important}#multiboardMenu,#settingsMenu,#watchedMenu,.mediaViewer{font-size:smaller;padding:5px!important;box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#watchedMenu,#watchedMenu .floatingContainer{max-width:100vw}.watchedNotification::before{padding-right:2px}#watchedMenu .floatingContainer{scrollbar-width:thin;scrollbar-color:var(--link-color) var(--contrast-color)}.ss-active{font-weight:700}.markAllRead::before{content:\"\\e065\"}.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:not(.is-index,.is-catalog).ss-sidebar .scroll-arrow-btn{right:330px!important}:root.ss-sidebar #mainPanel{margin-right:19rem;margin-left:0}:root.ss-leftsidebar #mainPanel{margin-left:19rem;margin-right:0}.scroll-arrow-btn:hover{opacity:1;background:#444}#scroll-arrow-up{bottom:80px}#scroll-arrow-down{bottom:32px}.bumpLockIndicator::after{padding-right:3px}.floatingMenu.focused{z-index:305!important}#quick-reply{padding:0}.mediaViewer.topright{top:26px!important;right:0!important;left:auto!important;max-height:97%!important;max-width:max-content!important}.mediaViewer.topleft{top:26px!important;left:0!important;right:auto!important;max-height:97%!important;max-width:max-content!important}.mediaViewer.topright::after{pointer-events:none}.mediaViewer.topleft::after{pointer-events:none}.ss-chevron{transition:transform .2s;margin-left:6px;font-size:12px;display:inline-block}a.imgLink[data-filemime^='audio/'],a.originalNameLink[href$='.m4a'],a.originalNameLink[href$='.mp3'],a.originalNameLink[href$='.ogg'],a.originalNameLink[href$='.wav']{position:relative}.audio-preview-indicator{display:none;position:absolute;background:rgba(0,0,0,.7);color:#fff;padding:5px;font-size:12px;border-radius:3px;z-index:1000;left:0;top:0;white-space:nowrap;pointer-events:none}a.originalNameLink:hover .audio-preview-indicator,a[data-filemime^='audio/']:hover .audio-preview-indicator{display:block}.yt-icon{width:16px;height:13px;vertical-align:middle;margin-right:2px}.id-glow{box-shadow:0 0 12px var(--subject-color)}.id-dotted{border:2px dotted #fff}.apng-canvas-snapshot{display:block;position:absolute;top:0;left:0;z-index:1}.apng-overlay{position:absolute;top:0;left:0;z-index:2;cursor:pointer;user-select:none}.chSS-mascot{position:fixed;height:auto;object-fit:contain;pointer-events:none;z-index:-1;user-select:none;-webkit-user-drag:none}#setting_enableMascots_mascotUrls{resize:vertical;min-height:80px}:root.backlink-icon .panelBacklinks>a{display:inline-block;width:12px;height:12px;margin:0 2px;text-decoration:none;font-size:0;vertical-align:middle;color:var(--link-color)}:root.backlink-icon .panelBacklinks>a::before{content:'▶';font-size:12px;display:inline-block;line-height:.6;color:var(--link-color);transform:rotate(0);transition:transform .1s ease-in-out,opacity .1s ease-in-out;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI Symbol',Roboto,Ubuntu,Cantarell,sans-serif}:root.backlink-icon .panelBacklinks>a.inlined::before{transform:rotate(90deg);opacity:.7;color:var(--link-hover-color);line-height:.9}:root.show-ID-count .labelId::after{content:attr(data-posts-by-this-id);padding:2px 0 2px 4px;border-left:1px solid #ececec;margin-left:.4em}a.linkSelf{margin-left:.25em}";
}
if (window.pageType?.isThread) {
css += ":root.sticky-qr #quick-reply{display:block;top:auto!important;bottom:0}:root.sticky-qr.ss-sidebar #quick-reply{left:auto!important;right:0!important}:root.sticky-qr.ss-leftsidebar #quick-reply{left:0!important;right:auto!important}:root.sticky-qr #qrbody{resize:vertical;max-height:50vh;height:130px}#selectedDivQr,:root.sticky-qr #selectedDiv{display:inline-flex;overflow:scroll hidden;max-width:300px}#qrbody{min-width:300px}:root.bottom-header #quick-reply{bottom:28px!important}:root.fade-qr #quick-reply{padding:0;opacity:.7;transition:opacity .3s ease}:root.fade-qr #quick-reply:focus-within,:root.fade-qr #quick-reply:hover{opacity:1}#quick-reply{box-shadow:-3px 3px 2px 0 rgba(0,0,0,.19)}#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 -3px auto;width:60%}:root.ss-sidebar #bannerImage{width:19rem;right:0;position:fixed;top:26px}:root.ss-sidebar.bottom-header #bannerImage{top:0!important}:root.ss-leftsidebar #bannerImage{width:19rem;left:0;position:fixed;top:26px}:root.ss-leftsidebar.bottom-header #bannerImage{top:0!important}.quoteTooltip{z-index:999}.nestedQuoteLink{text-decoration:underline dashed!important}:root.hide-stub .unhideButton{display:none}.quoteTooltip .innerPost{overflow:hidden}.inlineQuote .innerPost,.quoteTooltip .innerPost{box-shadow:-1px 1px 2px 0 rgba(0,0,0,.19)}.inlineQuote{margin-top:4px}.postInfo.title>.inlineQuote{margin-left:15px}.postCell.is-hidden-by-filter{display:none}.reply-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.quote-inlined{opacity:.5;text-decoration:underline dashed!important;text-underline-offset:2px}.target-highlight{background:var(--marked-color);border-color:var(--marked-border-color);color:var(--marked-text-color)}.statLabel{color:var(--link-color)}.statNumb{color:var(--text-color)}.postCell::before{display:inline!important;height:auto!important}.threadedReplies{border-left:1px solid #ccc;padding-left:15px}.ss-idlinks-floating{position:absolute;background:var(--background-color);color:var(--text-color);border:1px solid var(--navbar-text-color);padding:8px 15px 10px 10px;box-shadow:0 2px 12px rgba(0,0,0,.25);max-height:60vh;overflow-y:auto;overscroll-behavior:contain;font-size:14px;max-width:56vw;z-index:990;scrollbar-width:thin}.ss-idlinks-floating .innerPost,.ss-vertical-id-list .innerPost{background:0 0;display:block!important;border:none;box-shadow:none;outline:0;padding:0;margin:0}.ss-idlinks-floating .innerPost{padding:0 1px 2px 0!important}";
} else if (window.pageType?.isCatalog) {
css += "#postingForm{margin:2em auto}#divTools>div:nth-child(5),#divTools>div:nth-child(6){float:left!important;margin-top:9px!important;margin-right:8px}";
}
if (!css) return;
const style = document.createElement('style');
style.id = '8chSS';
style.textContent = css;
document.head.appendChild(style);
})();
const faviconManager = (() => {
const STYLES = [
"default",
"eight", "eight_dark",
"pixel", "pixel_alt"
];
const STATES = ["base", "unread", "notif"];
const FAVICON_DATA = {
default: {
base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAq1BMVEUAAACKtMV2lqSE1PWZ1Oyr1eaqzNp7udPO7vuc3ffh8vmG0/OB0fKv3e+N0e3B4e693+56x+bb6e6YzeOgydl7wd3L3OOwytS35fjZ8fuQ2feB1Pbo9vyy5Pjm9Pmp4PW85fbW7feO1/S55Pbk8/mq3/TY7fbn9Pnf7/bQ6vWS1fDa7fWH0vCb0+rY6/Pc7POBzezA4/GNz+vJ4+662ebH1958wNzD6vqm4Ph4HvJJAAAAN3RSTlMADwXliF4dGP7949HLn5mUhWllVCglJBf6+fn5+Pjo4N3a2NfV1MzIxb28rayinJKOioB7UzEt0Y3/cQAAAIBJREFUGNOVjsUOhVAMRG97BXcePHd3Q/7/y0hhQWDHrDpJT86wIQHo1t/xHGLb8aNvzHnYFC2KFHh+mu1PNTW6GuVB0vlaJvTsTl2xWjgxw5upCOB3ZLEzXvve5IlE8DdJ5K7QL0nts61cEw9Q8p82kmDGt4ZgbTCwrS/0Rw9IBRlvB34XFuslAAAAAElFTkSuQmCC",
unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAolBMVEUAAACG2vyC1/iH1vaB1/mC1PSC2PuC2fuD2fyE2fyG3P2I3Pqn3vWB1PWO1fKD2fuE2vqE2vyH3Puk4PaK1vSg2/OP1vJ/0fGB0/SF1vaC1faB1vmF2vyy5PiP2PbF6vnK6vjU7/m/5/ao3fKF0/K55PaW2PKK1PGA0/SP2PWA1/mC2fuE2fyD2fyx4vaT1e+S1/PX8PrO7vqb3fjh9Pu25fiNN6jgAAAAMXRSTlMAHK7Kmf6CYlc/EAnrxOt4OSwV/f3y8OrQv7afI/7+/f379/Pz8OLhvLmRb1pK+/PUqRXqEQAAALhJREFUGNN1j8cSgkAAQ7c3dykiSLFTpaiA/v+vCXrAi7klM5nkgUWrFfjVWgrhqMVDnkQREeuPURBCEevjSaNurrmcsYbo4dQ/NYMA7GxTlhinGj/OZ+QAxc1m9DwrRHad91cJXJIdgsDb+5igyxzImGYowf5r76d9TlwgK7qtbRRat+NwwVwBB1EaGutucFFE9m4aYVu6OYwBaRjj7kzQoSmxYjG9U1+IllQm4XDBUI5o5QTxB/wNI78Nh2SdMuEAAAAASUVORK5CYII=",
notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA4VBMVEUAAACE3vuD1veC1veB2PqB2fuD2/2H2/2I3PqP1vOB1faE2vyN1/WD1PSB2PuE2vzQ7Pak4PaB0/OG0/J/0fGn3vSB1vmC2PqE2vmG2vq54vKf3fWrLzam4PaO1fGD1faP2PWE2fyD2fyD2fzW8PrD5PG11+WXtcW55PVvbnqqOj+XmKaT1e+x3e1+obS4AACBjqGH0/F8vNh5hpuQNj+P2PZyX2+C1/SKEBOC2ftjKjApAAAdAAAJAACD1fXkAADg9Pu05/qb3fi1vsefoq+xhIildHu4PECpMjSkLDDPAQHtACoGAAAAPnRSTlMAEsOumWJVGwnuuT39zIMs/f388erpn3k4I/Tz8/Lmz7laSkH++/v7+vr5+PPv7ezk4M7OzcSdkZFwWTIVCIgxa4YAAADDSURBVBjTdY/VrgJBGIP/cWZm3XE77oLLLm7v/0BAgMANvWuapv3gqlwOboU05wm6esEijClHp1AIwVX49BiSxrFmM8vq0/J8s5yElgDIx/i3Yprl9+zNcUgCiGFj5rqV6n9W+pzUmmBT35DSnRZ/slLhoaZBq9QnkVmcLrK1U6A2NFUadGNSlc/b1UubIdDE80wsP/Df6xeO84cRK0hTYyZpZ9di9pGgQQLP8BUffPfOP+tU4YiJ8XB0wUp4XR/CO+B76XMRs7uqu4cAAAAASUVORK5CYII=",
},
eight: {
base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAxV2oFSGADP1Y1Wm0zWW1EY3Q1VmgpU2g2VWYkUGUyUWIiTWEAKUcFSmErU2c5W24zVWghUWckTmIKSmEZTmUxUmMwUWP1+fxcc4HL6f7j9f/g7PWi1fWWprFJZXUeSl7Z7v6Myu7d4OOjt8SKrcOSrsFMa30/YnT////C5v/Q6PmHz/nt7/Gw1e+bzu+Dx+7A1+d7wOa+ztqtx9m5v8Sds8KDn7J0lqqJl6GEkZtJfZlRb4E5Zn1Ybnw0YHc3V2kPSWAAQVqY0vZpstqMu9mIutjO0tV5rcyru8dfm7xTmLthh51TfpcZngh2AAAAGHRSTlMA1cx8MBoH8fHm5s3Kv5qUiFhYUFDxmpptBtYcAAAAzUlEQVQY0zWOVRKDQBBEB4+77OLB4+5B43b/64QU8P76VddMQwpTyWGFbEAGXTjoaLxWqmlmSRnF9HUl7bSwKO49fO+vyURQsvCMiDZGY+Xfb1JlQ9iENcJDCMf3SqetOe8tgzDaIz0HTH43GfD8sLfcGCI6VKD7laamtYqNIIhygQHudj6qarDi54aMSRrA1jSfqKkWb5WpFhv/cBYzlyNUc7ClkgnF1+hh2x9pcmomou5cRtpUknYlFhKqznsxux7zNGTUi77LdZg0/QBWixhGoJiKLQAAAABJRU5ErkJggg==",
unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAtFBMVEUAAAAAYYUAYYQAXIAAW38AYYMAY4YAXYAAYIQAZIcAY4Udc5MAWX8BZogldJQAWn6L2fIRa4xy0faB2/xt0vqI3Ph+0vBdxO1Wp8c/i6l41fhPwO1Ivu1Lo8dEoMYegaYYcZMEa46X5f1NxvZfyfNZyPOI1OxqwuV3w947rttputpdttldq8porscrmcNRnbtEmLpOmLU9krQFb5MHZIVIueY6tuaAyeJOstp1vNRCp9Auh6qJsLovAAAADnRSTlMAWfLRzujVm5EzHAqLiBrNc7IAAADDSURBVBjTNY9XEsIwEEPd0mFtp4d0eocU+v3vRRic9yeNZldCCtsgIBlGI5aWbMX6JE2lHdoEgRDzh1QZDGGYALzmJ/Y3JhfvyHUCYi1/eWzS2Iv5VAchYLhH4ZpvZvue8yTYEmRrbeT7rjvbx8cwSAxklMuoKA6D43lho9mI7FYl5/3B3cQXoBZCkKW1PuWFn7MJdoYf9T3riM5z/6pasvPiXVWfKKrGknK3SNPVsqWO2mHK8y17lpqFRjCrO2LYSn0BV4wQhK3VgFcAAAAASUVORK5CYII=",
notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABEVBMVEUAAAAAW38AYoUAYoQAYYQAZYgAYYMAZIYAXoEBZom1AwbrAABGAAAALT4AAAAAYYRHAAAAAAAadJTeAAABZohHGymuAABFAACcAAAAAAAAAAAAUnIAZ4oAAAAQao2M2/Qic5Rw0/pKv+1EoscAXoD5AAB31fhfxO0egaYpdpUAa40AWX3/AgK/AAC6AABNAACX5f2B2/xr0fqH2/dNxvZy0fVfyfNZyPN80e+I1OxQv+x7xuA7rttmu9ldttlorsddqsdUpsZLocMrmcNEmLpPm7g+iqkFb5MZcJJYwu1IueY6tuZoweROstp1vNRdsdBCp9Bwt8xAk7U6kbRYnbEuh6oJZYWZdYM1RF0AKDwAHjhOZ3nrAAAAHnRSTlMA0FnyjRzo1psy/v785K2UiVwL+PLu6OPLyL6nm49luDk4AAAA4ElEQVQY0zWP5XLDMBAGT5YpnJRRkiHGOg0zc7lheP8HiTN29t/t7NzMByGCjBlBHFyIiNMWbfRIKryv0KRUojTfImHD2fX6lNm/+R4KBD8p9F0J27RBzj3Ho0FhoPOJw4Iy/x9i4+G30dH3UW+3vgFBXJlFRakY/ffsm3d8Admqfmhat2J0fJFLCIDbNcvVt13l7yyeAJjadCRe15RhPJaNJgHIlzrDkqsVx8/38TQAoFH5fz7fmKZzex27ewXgSLusNmvVJUrnvIeMn6TI6FP9scQIJB8zwRDkzLAsQMAJImsd3S869hIAAAAASUVORK5CYII=",
},
eight_dark: {
base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA2FBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmKCkAAAADBAQAAAD1+fxMTEzM6v4vLy7i9f/E6P/v8PKMye2frbiFo7aNpLQ1P0b////Z7/+I0PrQ6Pmk1vab0/bi7PXe6fKfz+6w1O2Cxu15veS/1OPY2dq8ydOrwdKHtdK1traZqbSQmqF8kZ+QlJhshJSBhIZ7fH0+YnY9R09GREMOIiwfJivp+f/X7fyYzO3g4OBmrtTMzMt0pcKnsrpWjq1Ii6xUcIEyZX8ENUgYNkYAKD1NaNryAAAADnRSTlMAzPGY5tV8WFAwGge/iAmaI18AAADKSURBVBjTNY7XFoIwEEQ3CR1NQi/23rsC9q7//0fiAe7bzLlndyBHI5ibSIYCVdgMmTM1SZ51tGQplZOZOwpvt9YRv1SmKCukuXWjIubMMf++IpU8a0bLYuQwnt5Du4U/qI4opWs2xKAJq65tG/XDaOa12IYA2TY6fjA26tW91V4KGuBJ85XaY2PgzTlSAULX/YplGhhBSVL09Efc732wSH17IWUT0L12DcNnp7tTskJOzjX32GyskA4ZJHn0e5OtoEKBjOI3Jlqefl82Et7gg4sWAAAAAElFTkSuQmCC",
unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA6lBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEAAADb3NxEQ0IAAAAAAABISEgAAAAAAAAlKCk9NzGurq6cnJwAAAD+/v73+Pi6u7tNTExGREMwLy7N6v61trY1QEfE6P/t7u+dzu6Mye2rwdKForaNpLTy+f/h9f/Z7//S6vuI0Pqk1vbg6/Sw1O2Cxu2+0uCHtdLKzM2qtbycqrV8kaCPl51shJV8f4I+YnYOIiwfJiud1PaY0vbo6Oh+vuR0vOPg4OBmrtS8ydN0pcLAwMBWjq1Ii6yjpaZUcIEyZX88R08ENUgYNkYAKD0sty6hAAAAFXRSTlMAzZjyWeUa1owv/Oe/roh8UAf26MtVcWhmAAAA10lEQVQY0zWP1ZLDMBRDr+04SbkLpjgMZeZ2mfn/f2ebqas3aTQaHTCyMVEaWXBWhW6rIg40Nv4CrTsdIfijNp22Go22nprzAJ2C1rq7YA5RItZlv92qL7tL5lI25+q4h75X+bT3zHQg5QcBm27GUeT3ewuPc8kw4N0wyYvM7089wSW1gcxuvxhjmZ+VQR0gTNM/x2WFX1zGPGgA7B8mv8RhebTCNa8JAOht8BKGn8k4rFXvrlwA6/A0SJOb4QY15et1+Rwf3u8nsx2tQMPQWmj/Q7BtOP8BG/MW/0ZJlAQAAAAASUVORK5CYII=",
notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABDlBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAC2AQHrAAA4AAAAAAAAAABIAgIAAAAAAAAlKCneAABDAACuAABFAACcAAADBAQAAAAAAAC8AABNTE3+AQHN6v72+PmKo7QvLy5GAAD+/v/E6P+Myu6rwtLy+f/h9f/Z7//S6vuI0Pqk1vab0/bg6/Sfz+6w1O2Cxu2+0uHc3d2HtdLKzc61tracqrV8kaBshJV8f4I+YnY5R081QUkOIiwfJiv3AADt7u/r7e6YzO1+vuR0vONmrtS8ydN0pcKtucGnsrqQqLeCordWjq1Ii6yjqaqQmqGOk5i3hYhUcIEyZX8ENUgYNkZGREM1PEEAKD1ovUZyAAAAGnRSTlMAzfLlmliSGtYv/v77v66IfFAH+Ozo4suIXxNuy1MAAADcSURBVBjTNY9VjkJBFERvyxMcxud2Pxfc3cfdffa/ESA09VeVk0oOqBicSo9osEuCTbrO+cDTVd8j78Wi4wRdTzEF2WhMfmU/GJDtkH8rPaBJpXPhbfhCPjMsDVFn2A/k+o/MRm6nfIvppPh4pWCwcSsM7Ur53rcsgRz4tNZ8cmO70vHPLMEMoL36FyLGdrwZMgDzKPo3T9C1H9MpK5kFWFy3f6iJbjjiR34OAMhz9W7+99lszQ72U4c6gLa8qUaX9dqY5IQ4PgUAffly1e5NWQKyylYji2/KDeW5AobCG0Lco88iAAAAAElFTkSuQmCC",
},
pixel: {
base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAABiam00WmxcZWlFTE4jQE1GTU8jQU4yV2g8QUIbNkE/WGRWYWY9WGUAAAAAAADg8vnY6fB5yOyA0fW95fi23fB+z/PQ4Od0weNoeYFWcoBSXmNFS00jP0wFxP2oAAAAEnRSTlMAIRHe3v3w8Nzc/ezs/u7u7EbfERAsAAAAcUlEQVQY05VP1wqAMAw0de82TYf7//9SOhBFELy3G1wuyQcYYw8OVVHBzYJUokzBW3VZg+MYFMhI0ThMMm9zlIXnVmmzbFy0iHtSkmq63mhtYuJQthO8X40OHSHR0DwO4UrsoAziDq84HudcO15P/MAJxpAGt0Xg7i0AAAAASUVORK5CYII=",
unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAM2ng8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUJarSxAAAAFHRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkWW9w4AAAB4SURBVBjTlY/ZDoUwCERL3fcNSnsX/f+/tEpjNCYm8nZgMjOoh9FaXxiyJIPTCSKDJoL9lKc5bIx+U3iOmXjoP6asSzSJKjwTWfdrx6lGnFXKVDWds9a1oliImmns/s6Khygq/g69pAQPjiH0kBTPoc7R4/bEi1kBHCgHTIPhydUAAAAASUVORK5CYII=",
notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAAV34AIDg0gKEkdJQjdJUUAAMAAEAyf6AbcJI7dZA/f55MAABAgaBEeZVljaVUhJ4AZIZpkKYAQmp5y/B+0fVrS0224PPf8vnP4uu85vqA0/fY6/NUIivtAABNHynjAAB0xelolKxWj6tNgZpFepUjdJVOTU9fREtAREvaAABiAADosgfnAAAAFHRSTlMAIBHe8NzcIP3s3v7x7u7e7uzeRqhOt1sAAACESURBVBjTlY9ZDoMwDERxCHvp6mASSKH7dv/71YWoAiEh4b83Ho3H3swIIUYchaswGqwgta1N4ccQyABgr4tjoTfA7KPCfKtrFsp63bGqDJ2vloXy5klUcZKRMfRsO8e9Uslhl12IXp8HZ/SOGE+5bN4NX3EZ6IPr0SvMrs6/x+SJBfMF/doItAPX5RIAAAAASUVORK5CYII=",
},
pixel_alt: {
base: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAY1BMVEUAAAAATHcAIGJii6I0gKFFepYjdJVGepYjdJUAXIFeiaEyf6A8dZIbcJJThZ8/f55WhZ89f58AY4YAQmoAJGjg8/p/0vbY6/R5zPC95vq23/TQ5O10xelolKxWj6tFepUjdJUOfB3LAAAAFXRSTlMAIBHe3vDw3Nwj/f3s7P7+7u7sRkaqQLDYAAAAeUlEQVQY05WP6Q6EMAiES73vYxdKu4e+/1NapTEaExP598FkZlA3o7U+MWRJBocTRAZNBNspT3NYGf2m8Bwz8dB/TFmXaBJVeCay7teO7xpxUilT1XTOWteKYiZqXmP3d1Y8RFHxd+glJXhwDKGHpHgOdfYelycezAI9gweHH0yPzQAAAABJRU5ErkJggg==",
unread: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAhFBMVEUAAAAjR1sKChFJeZBBa4FAZ3tCbIEVFRYABA9IeI9OaXlReI1MTExSeo9WcYBxh5RjfYw4WGt0ipcdKTh7y+6A0vdqbm/f8fi85fl+0PTY6vK23fDt7OzP4OdVWFlOUlS44PPj4+NZdITR4+t3xefa2tpyj55jiZ1Ba4BgZ2tiYF9ATFKjqzLCAAAAFHRSTlMAIBHe8O3c3CD93v7x7u7e7uzeRvBdAgMAAACFSURBVBjTlY9ZDoMwDERxIKylqwOBlATo3t7/fjVpVIGQkPDfG4/GY29hGGMTjqNNFI9WkOpOpzAwhEEIcGyKc9HsgNhHgfn+UpNQ1lvLlZDtvdcklL0XoEh4pqRUr846nqLip0P2aNXnbSjj50jwmnNzM3TFZaAProdVBnZ1/j1mT6yYLxHCCNKMC/PDAAAAAElFTkSuQmCC",
notif: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAdVBMVEUAAAAkSFwKChFAaX9JeZAABBhCbIEVAABIeI9yiJVObnxReI1MAABSeo9WcYBjfYw4WGsdKTiA0vd7y+5rS0233/JSHiPf8vnP4um85fnY6/LtAABZdobjAAB3xedyj55jiZ1Ba4BOTU9gREtAREvaAABiAADFYUIqAAAAEnRSTlMAIRHv3iDc3P3e3v7x7u7u7EYDRb2ZAAAAgklEQVQY05WPWQ7CMAxE67jpSlnsOk1Kyw73PyJJFCEQUqX6741H43G2MEqpH9blptRfK2jMbBoIDAUWoGvbH3tbg+ecibudHb0wjNvIRE7Ok/HCMGXIVGErzsl9jo4rER727UXk8bqFjOio+NSheYYrKYNzSD2iEjjV+fT4e2LFvAFuzge5hY5wYgAAAABJRU5ErkJggg==",
}
};
let currentStyle = "default";
let currentState = "base";
let cachedUserStyle = null;
function removeFavicons() {
const head = document.head;
if (!head) return;
head.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(link => link.remove());
}
function insertFavicon(href) {
const head = document.head;
if (!head) return;
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png';
link.href = href;
head.appendChild(link);
}
async function getUserFaviconStyle() {
if (cachedUserStyle) return cachedUserStyle;
let style = "default";
try {
style = await getSetting("customFavicon_faviconStyle");
} catch { }
if (!STYLES.includes(style)) style = "default";
cachedUserStyle = style;
return style;
}
async function setFaviconStyle(style, state = "base") {
if (!STYLES.includes(style)) style = "default";
if (!STATES.includes(state)) state = "base";
if (currentStyle === style && currentState === state) return;
const url = (FAVICON_DATA?.[style]?.[state]) || FAVICON_DATA.default.base;
removeFavicons();
insertFavicon(url);
currentStyle = style;
currentState = state;
document.dispatchEvent(new CustomEvent("faviconStateChanged", {
detail: { style, state }
}));
}
async function setFavicon(state = "base") {
if (!STATES.includes(state)) state = "base";
const style = await getUserFaviconStyle();
await setFaviconStyle(style, state);
}
async function resetFavicon() {
await setFavicon("base");
}
function getCurrentFaviconState() {
return { style: currentStyle, state: currentState };
}
return {
setFavicon,
setFaviconStyle,
resetFavicon,
getCurrentFaviconState,
STYLES,
STATES
};
})();
const featureMap = [
{ key: "enableScrollSave", fn: featureSaveScroll },
{ key: "watchThreadOnReply", fn: featureWatchThreadOnReply },
{ key: "blurSpoilers", fn: featureBlurSpoilers },
{ key: "enableHeaderCatalogLinks", fn: featureHeaderCatalogLinks },
{ key: "openCatalogThreadNewTab", fn: catalogThreadsInNewTab },
{ key: "enableScrollArrows", fn: featureScrollArrows },
{ key: "alwaysShowTW", fn: featureAlwaysShowTW },
{ key: "enableThreadHiding", fn: featureCatalogHiding },
{ key: "switchTimeFormat", fn: featureLabelCreated12h },
{ key: "enableIdFilters", fn: featureIdFiltering },
{ key: "threadStatsInHeader", fn: threadInfoHeader },
{ key: "enableHashNav", fn: hashNavigation },
{ key: "hideAnnouncement", fn: featureHideAnnouncement },
{ key: "enableAutoHideHeaderScroll", fn: autoHideHeaderOnScroll },
{ key: "enableMediaViewer", fn: mediaViewerPositioning },
{ key: "customFavicon", fn: enableFavicon },
{ key: "highlightNewIds", fn: featureHighlightNewIds },
{ key: "quoteThreading", fn: featureQuoteThreading },
{ key: "enableLastFifty", fn: featureLastFifty },
{ key: "enableIdToggle", fn: featureToggleIdAsYours },
{ key: "enableTheSauce", fn: featureSauceLinks },
{ key: "enableUpdateNotif", fn: updateNotif },
{ key: "alwaysShowIdCount", fn: featureShowIDCount },
{ key: "enablePNGstop", fn: featureAPNGStop },
{ key: "enableMascots", fn: featureMascots },
];
for (const { key, fn } of featureMap) {
try {
if (await getSetting(key)) {
fn();
}
} catch (e) {
console.error(`${fn.name || 'Feature'} failed:`, e);
}
}
if (await getSetting("truncFilenames")) {
try {
const filenameLength = await getSetting("truncFilenames_customTrunc");
truncateFilenames(filenameLength);
} catch (e) {
console.error("truncateFilenames failed:", e);
}
}
async function enableFavicon() {
try {
const customFaviconEnabled = await getSetting("customFavicon");
const selectedStyle = await getSetting("customFavicon_faviconStyle");
if (customFaviconEnabled) {
if (selectedStyle && typeof selectedStyle === 'string') {
await faviconManager.setFaviconStyle(selectedStyle);
} else {
console.warn("Invalid favicon style:", selectedStyle);
await faviconManager.setFaviconStyle("eight_dark");
}
} else {
await faviconManager.resetFavicon();
}
} catch (e) {
console.error("Error updating favicon:", e);
}
}
let imageHoverEnabled = false;
try {
if (window.pageType?.isCatalog) {
imageHoverEnabled = await getSetting("enableCatalogImageHover");
} else {
imageHoverEnabled = await getSetting("enableThreadImageHover");
}
if (imageHoverEnabled) {
localStorage.setItem("hoveringImage", "false");
featureImageHover();
}
} catch (e) {
console.error("featureImageHover failed:", e);
}
(function () {
function sanitizeToastHTML(html) {
html = html.replace(/<(\/?)(?!a\b|b\b|i\b|u\b|strong\b|em\b)[^>]*>/gi, '');
html = html.replace(/<(b|i|u|strong|em)[^>]*>/gi, '<$1>');
html = html.replace(/]+)>/gi, function (match, attrs) {
let allowed = '';
attrs.replace(/(\w+)\s*=\s*(['"])(.*?)\2/gi, function (_, name, q, value) {
name = name.toLowerCase();
if (['href', 'target', 'rel'].includes(name)) {
if (name === 'href' && (/^\s*javascript:/i.test(value) || /^\s*data:/i.test(value))) return;
allowed += ` ${name}=${q}${value}${q}`;
}
});
return ``;
});
return html;
}
function showToast(htmlMessage, color = "black", duration = 1200) {
if (document.querySelector('.global-toast-notification')) {
return;
}
const colorMap = {
black: "#222",
orange: "#cc7a00",
green: "#339933",
blue: "#1976d2",
red: "#c62828"
};
const bgColor = colorMap[color] || color;
const icon = document.getElementById("8chanSS-icon");
let toast = document.createElement("span");
toast.className = "global-toast-notification";
toast.innerHTML = sanitizeToastHTML(htmlMessage);
toast.style.position = "absolute";
toast.style.background = bgColor;
toast.style.color = "#fff";
toast.style.padding = "4px 12px";
toast.style.borderRadius = "4px";
toast.style.fontSize = "13px";
toast.style.zIndex = "99999";
toast.style.opacity = "1";
toast.style.transition = "opacity 0.3s";
toast.style.pointerEvents = "auto";
toast.style.boxShadow = "0 2px 8px rgba(0,0,0,0.18)";
let closeBtn = document.createElement("span");
closeBtn.textContent = "✕";
closeBtn.style.marginLeft = "10px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontWeight = "bold";
closeBtn.style.fontSize = "15px";
closeBtn.style.opacity = "0.7";
closeBtn.style.float = "right";
closeBtn.style.userSelect = "none";
closeBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (toast.parentNode) toast.parentNode.removeChild(toast);
if (timeout1) clearTimeout(timeout1);
if (timeout2) clearTimeout(timeout2);
});
closeBtn.addEventListener('mouseover', function () { closeBtn.style.opacity = "1"; });
closeBtn.addEventListener('mouseout', function () { closeBtn.style.opacity = "0.7"; });
toast.appendChild(closeBtn);
if (icon && icon.parentNode) {
toast.style.left = (icon.offsetLeft - 50) + "px";
toast.style.top = "28px";
icon.parentNode.appendChild(toast);
} else {
toast.style.right = "25px";
toast.style.top = "25px";
toast.style.position = "fixed";
document.body.appendChild(toast);
}
let timeout1 = setTimeout(() => { toast.style.opacity = "0"; }, duration - 300);
let timeout2 = setTimeout(() => { toast.remove(); }, duration);
}
window.callPageToast = function (msg, color = 'black', duration = 1200) {
showToast(msg, color, duration);
};
})();
async function featureSaveScroll() {
if (!window.pageType?.isThread) return;
const STORAGE_KEY = "8chanSS_scrollPositions";
const UNREAD_LINE_ID = "unread-line";
const MAX_THREADS = 200;
function getBoardAndThread() {
const match = window.location.pathname.match(/^\/([^/]+)\/res\/([^/.]+)\.html$/i);
if (!match) return null;
return { board: match[1], thread: match[2] };
}
async function getAllSavedScrollData() {
const saved = await GM.getValue(STORAGE_KEY, null);
if (!saved) return {};
try { return JSON.parse(saved); } catch { return {}; }
}
async function setAllSavedScrollData(data) {
await GM.setValue(STORAGE_KEY, JSON.stringify(data));
}
function getCurrentPostCount() {
if (!divPosts) return 0;
return divPosts.querySelectorAll(":scope > .postCell[id]").length;
}
function removeUnreadLineMarker() {
const oldMarker = document.getElementById(UNREAD_LINE_ID);
if (oldMarker && oldMarker.parentNode) {
oldMarker.parentNode.removeChild(oldMarker);
}
}
let lastSeenPostCount = 0;
let unseenCount = 0;
let tabTitleBase = null;
let previousFaviconState = null;
const customFaviconEnabled = await getSetting("customFavicon");
async function updateTabTitle() {
if (window.isNotifying) return;
if (!tabTitleBase) tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
document.title = unseenCount > 0 ? `(${unseenCount}) ${tabTitleBase}` : tabTitleBase;
const { style, state } = faviconManager.getCurrentFaviconState();
if (unseenCount > 0 && customFaviconEnabled) {
if (state !== "unread") {
previousFaviconState = { style, state };
}
faviconManager.setFaviconStyle(style, "unread");
} else if (unseenCount == 0 && customFaviconEnabled) {
if (state === "unread" && previousFaviconState) {
faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
previousFaviconState = null;
} else if (state === "unread") {
faviconManager.setFavicon("base");
}
}
}
async function updateUnseenCountFromSaved() {
const info = getBoardAndThread();
if (!info) return;
const allData = await getAllSavedScrollData();
const key = `${info.board}/${info.thread}`;
const saved = allData[key];
const currentCount = getCurrentPostCount();
lastSeenPostCount = (saved && typeof saved.lastSeenPostCount === "number") ? saved.lastSeenPostCount : 0;
unseenCount = Math.max(0, currentCount - lastSeenPostCount);
if (unseenCount > 0 && (window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 50)) {
lastSeenPostCount = currentCount;
unseenCount = 0;
if (!allData[key]) allData[key] = {};
allData[key].lastSeenPostCount = lastSeenPostCount;
allData[key].timestamp = Date.now();
await setAllSavedScrollData(allData);
}
updateTabTitle();
}
let lastScrollY = window.scrollY;
async function onScrollUpdateSeen() {
const info = getBoardAndThread();
if (!info || !(await getSetting("enableScrollSave"))) return;
const posts = Array.from(document.querySelectorAll(".divPosts > .postCell[id]"));
let maxIndex = -1;
for (let i = 0; i < posts.length; ++i) {
const rect = posts[i].getBoundingClientRect();
if (rect.bottom > 0 && rect.top < window.innerHeight) maxIndex = i;
}
const currentCount = getCurrentPostCount();
let newLastSeen = lastSeenPostCount;
if (window.scrollY > lastScrollY) {
if (maxIndex >= 0 && currentCount > 0) {
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 20)) {
newLastSeen = currentCount;
} else {
newLastSeen = Math.max(lastSeenPostCount, maxIndex + 1);
}
}
if (newLastSeen !== lastSeenPostCount) {
lastSeenPostCount = newLastSeen;
let allData = await getAllSavedScrollData();
const key = `${info.board}/${info.thread}`;
if (!allData[key]) allData[key] = {};
allData[key].lastSeenPostCount = lastSeenPostCount;
allData[key].timestamp = Date.now();
if (
typeof allData[key].position !== "number" ||
window.scrollY > allData[key].position
) {
allData[key].position = window.scrollY;
}
await setAllSavedScrollData(allData);
}
unseenCount = Math.max(0, currentCount - lastSeenPostCount);
updateTabTitle();
}
lastScrollY = window.scrollY;
}
async function saveScrollPosition() {
const info = getBoardAndThread();
if (!info || !(await getSetting("enableScrollSave"))) return;
const scrollPosition = window.scrollY;
const timestamp = Date.now();
let allData = await getAllSavedScrollData();
const keys = Object.keys(allData);
if (keys.length >= MAX_THREADS) {
keys.sort((a, b) => (allData[a].timestamp || 0) - (allData[b].timestamp || 0));
for (let i = 0; i < keys.length - MAX_THREADS + 1; ++i) delete allData[keys[i]];
}
const key = `${info.board}/${info.thread}`;
if (!allData[key]) allData[key] = {};
if (
typeof allData[key].position !== "number" ||
scrollPosition > allData[key].position
) {
allData[key].position = scrollPosition;
allData[key].timestamp = timestamp;
await setAllSavedScrollData(allData);
}
}
function scrollElementToViewportCenter(el) {
if (!el) return;
const rect = el.getBoundingClientRect();
const elTop = rect.top + window.pageYOffset;
const elHeight = rect.height;
const viewportHeight = window.innerHeight;
const scrollTo = elTop - (viewportHeight / 2) + (elHeight / 2);
window.scrollTo({ top: scrollTo, behavior: "auto" });
}
async function restoreScrollPosition() {
const info = getBoardAndThread();
if (!info || !(await getSetting("enableScrollSave"))) return;
const allData = await getAllSavedScrollData();
const key = `${info.board}/${info.thread}`;
const saved = allData[key];
if (!saved || typeof saved.position !== "number") return;
const anchor = window.location.hash ? window.location.hash.replace(/^#/, "") : null;
const safeAnchor = anchor && /^[a-zA-Z0-9_-]+$/.test(anchor) ? anchor : null;
if (safeAnchor) {
setTimeout(() => {
const post = document.getElementById(safeAnchor);
if (post && post.classList.contains("postCell")) {
scrollElementToViewportCenter(post);
}
addUnreadLineAtSavedScrollPosition(saved.position, false);
}, 25);
return;
}
saved.timestamp = Date.now();
await setAllSavedScrollData(allData);
window.scrollTo({ top: saved.position, behavior: "auto" });
setTimeout(() => addUnreadLineAtSavedScrollPosition(saved.position, false), 80);
}
async function addUnreadLineAtSavedScrollPosition(scrollPosition, centerAfter = false) {
if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
if (!divPosts) return;
const margin = 5;
const docHeight = document.body.offsetHeight;
if ((scrollPosition + window.innerHeight) >= (docHeight - margin)) {
return;
}
const posts = Array.from(divPosts.querySelectorAll(":scope > .postCell[id]"));
let targetPost = null;
for (let i = 0; i < posts.length; ++i) {
const postTop = posts[i].offsetTop;
if (postTop > scrollPosition) break;
targetPost = posts[i];
}
if (!targetPost) return;
removeUnreadLineMarker();
const marker = document.createElement("hr");
marker.id = UNREAD_LINE_ID;
if (targetPost.nextSibling) {
divPosts.insertBefore(marker, targetPost.nextSibling);
} else {
divPosts.appendChild(marker);
}
if (centerAfter) {
setTimeout(() => {
const markerElem = document.getElementById(UNREAD_LINE_ID);
if (markerElem) {
const rect = markerElem.getBoundingClientRect();
const desiredY = window.innerHeight / 3;
const scrollY = window.scrollY + rect.top - desiredY;
window.scrollTo({ top: scrollY, behavior: "auto" });
}
}, 25);
}
}
let unseenUpdateTimeout = null;
function debouncedUpdateUnseenCount() {
if (unseenUpdateTimeout) clearTimeout(unseenUpdateTimeout);
unseenUpdateTimeout = setTimeout(() => {
updateUnseenCountFromSaved();
unseenUpdateTimeout = null;
}, 100);
}
const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
if (divPostsObs) {
divPostsObs.addHandler(function saveScrollPostCountHandler() {
debouncedUpdateUnseenCount();
});
}
async function removeUnreadLineIfAtBottom() {
if (!(await getSetting("enableScrollSave_showUnreadLine"))) return;
const margin = 5;
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - margin)) {
removeUnreadLineMarker();
}
}
window.addEventListener("beforeunload", () => {
saveScrollPosition();
});
document.addEventListener("DOMContentLoaded", () => {
tabTitleBase = document.title.replace(/^\(\d+\)\s*/, "");
updateTabTitle();
});
window.addEventListener("load", async () => {
await restoreScrollPosition();
await updateUnseenCountFromSaved();
});
let scrollTimeout = null;
window.addEventListener("scroll", () => {
if (scrollTimeout) return;
scrollTimeout = setTimeout(async () => {
await onScrollUpdateSeen();
await removeUnreadLineIfAtBottom();
scrollTimeout = null;
}, 100);
});
await restoreScrollPosition();
await updateUnseenCountFromSaved();
}
async function featureHeaderCatalogLinks() {
async function appendCatalogToLinks() {
const openInNewTab = await getSetting("enableHeaderCatalogLinks_openInNewTab");
const boardLists = [
document.getElementById("navBoardsTop"),
document.getElementById("navBoardsFavorite")
];
for (const navboardsSpan of boardLists) {
if (navboardsSpan) {
const links = navboardsSpan.getElementsByTagName("a");
for (let link of links) {
if (
link.href &&
!link.href.endsWith("/catalog.html") &&
!link.dataset.catalogLinkProcessed
) {
link.href += "/catalog.html";
link.dataset.catalogLinkProcessed = "1";
if (openInNewTab) {
link.target = "_blank";
link.rel = "noopener noreferrer";
} else {
link.target = "";
link.rel = "";
}
}
}
}
}
}
appendCatalogToLinks();
const debouncedAppend = debounce(appendCatalogToLinks, 100);
const navboardsTopObs = observeSelector('#navBoardsTop', { childList: true, subtree: true });
if (navboardsTopObs) {
navboardsTopObs.addHandler(function headerCatalogLinksHandler() {
debouncedAppend();
});
}
const navboardsFavoriteObs = observeSelector('#navBoardsFavorite', { childList: true, subtree: true });
if (navboardsFavoriteObs) {
navboardsFavoriteObs.addHandler(function headerCatalogLinksHandler() {
debouncedAppend();
});
}
}
function catalogThreadsInNewTab() {
if (!window.pageType?.isCatalog) return;
catalogDiv.querySelectorAll('.catalogCell a.linkThumb').forEach(link => {
if (link.getAttribute('target') !== '_blank') {
link.setAttribute('target', '_blank');
}
});
catalogDiv.addEventListener('click', function (e) {
const link = e.target.closest('.catalogCell a.linkThumb');
if (link && link.getAttribute('target') !== '_blank') {
link.setAttribute('target', '_blank');
}
});
}
function featureImageHover() {
const MEDIA_MAX_WIDTH = "90vw";
const MEDIA_OPACITY_LOADING = "0";
const MEDIA_OPACITY_LOADED = "1";
const MEDIA_OFFSET = 50;
const MEDIA_BOTTOM_MARGIN = 3;
const AUDIO_INDICATOR_TEXT = "▶ Playing audio...";
function getExtensionForMimeType(mime) {
const map = {
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/jxl": ".jxl",
"image/png": ".png",
"image/apng": ".png",
"image/gif": ".gif",
"image/avif": ".avif",
"image/webp": ".webp",
"image/bmp": ".bmp",
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/x-m4v": ".m4v",
"audio/ogg": ".ogg",
"audio/mpeg": ".mp3",
"audio/flac": ".flac",
"audio/opus": ".opus",
"audio/x-m4a": ".m4a",
"audio/x-wav": ".wav",
};
return map[mime.toLowerCase()] || null;
}
function sanitizeUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
parsed.origin === window.location.origin) {
return parsed.href;
}
} catch { }
return "";
}
let floatingMedia = null;
let cleanupFns = [];
let currentAudioIndicator = null;
let lastMouseEvent = null;
const hoverPlaybackTimes = new Map();
let trackMediaPlaybackEnabled = true;
getSetting("trackMediaPlayback").then(val => trackMediaPlaybackEnabled = val);
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
function positionFloatingMedia(event) {
if (!floatingMedia) return;
const vw = window.innerWidth;
const vh = window.innerHeight;
const mw = floatingMedia.offsetWidth || 0;
const mh = floatingMedia.offsetHeight || 0;
const docElement = document.documentElement;
const SCROLLBAR_WIDTH = window.innerWidth - docElement.clientWidth;
const MEDIA_BOTTOM_MARGIN_PX = vh * (MEDIA_BOTTOM_MARGIN / 100);
let x, y;
const rightX = event.clientX + MEDIA_OFFSET;
const leftX = event.clientX - MEDIA_OFFSET - mw;
if (rightX + mw <= vw - SCROLLBAR_WIDTH) {
x = rightX;
}
else if (leftX >= 0) {
x = leftX;
}
else {
x = clamp(rightX, 0, vw - mw - SCROLLBAR_WIDTH);
}
y = event.clientY;
const maxY = vh - mh - MEDIA_BOTTOM_MARGIN_PX;
y = Math.max(0, Math.min(y, maxY));
floatingMedia.style.left = `${x}px`;
floatingMedia.style.top = `${y}px`;
}
function cleanupFloatingMedia() {
cleanupFns.forEach(fn => { try { fn(); } catch { } });
cleanupFns = [];
if (floatingMedia) {
if (["VIDEO", "AUDIO"].includes(floatingMedia.tagName)) {
try {
try {
const key = floatingMedia.dataset && floatingMedia.dataset.previewSrc ? floatingMedia.dataset.previewSrc : (floatingMedia.src || "");
if (key && trackMediaPlaybackEnabled) {
const t = Number(floatingMedia.currentTime);
if (!isNaN(t) && isFinite(t) && t > 0) {
hoverPlaybackTimes.set(key, t);
}
}
} catch (e) { }
try { floatingMedia.pause(); } catch (e) { }
try { floatingMedia.srcObject = null; } catch (e) { }
} catch { }
}
floatingMedia.remove();
floatingMedia = null;
}
if (currentAudioIndicator && currentAudioIndicator.parentNode) {
currentAudioIndicator.parentNode.removeChild(currentAudioIndicator);
currentAudioIndicator = null;
}
}
function getFullMediaSrc(thumbNode, filemime) {
let thumbnailSrc = thumbNode.getAttribute("src");
if (thumbnailSrc) {
thumbnailSrc = thumbnailSrc.split('#')[0];
}
const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
const href = parentA ? parentA.getAttribute("href") : "";
let fileWidth = parentA ? parseInt(parentA.getAttribute("data-filewidth"), 10) : null;
let fileHeight = parentA ? parseInt(parentA.getAttribute("data-fileheight"), 10) : null;
if ((!fileWidth || !fileHeight) && thumbNode.naturalWidth && thumbNode.naturalHeight) {
fileWidth = thumbNode.naturalWidth;
fileHeight = thumbNode.naturalHeight;
}
function hasExtension(str) {
return /\.[a-z0-9]+$/i.test(str);
}
function isTThumb(str) {
return /\/t_/.test(str);
}
function isDirectHash(str) {
return /^\/\.media\/[a-f0-9]{40,}$/i.test(str) && !hasExtension(str);
}
function isSmallImage() {
return (fileWidth && fileWidth <= 220) || (fileHeight && fileHeight <= 220);
}
function isBarePngNoThumb() {
return (
filemime &&
filemime.toLowerCase() === "image/png" &&
parentA &&
!isTThumb(href) &&
!hasExtension(href)
);
}
function isSmallBarePngSrc() {
return (
isSmallImage() &&
filemime &&
filemime.toLowerCase() === "image/png" &&
!isTThumb(thumbnailSrc) &&
!hasExtension(thumbnailSrc)
);
}
function isGenericThumb() {
return (
/\/spoiler\.png$/i.test(thumbnailSrc) ||
/\/custom\.spoiler$/i.test(thumbnailSrc) ||
/\/audioGenericThumb\.png$/i.test(thumbnailSrc)
);
}
if (!filemime) {
if (
thumbNode.closest('.catalogCell') ||
/^\/\.media\/t?_[a-f0-9]{40,}$/i.test(thumbnailSrc.replace(/\\/g, ''))
) {
return thumbnailSrc;
}
return null;
}
if (isBarePngNoThumb()) {
return thumbnailSrc;
}
if (isSmallBarePngSrc()) {
return thumbnailSrc;
}
if (isSmallImage() && hasExtension(thumbnailSrc)) {
return thumbnailSrc;
}
if (isTThumb(thumbnailSrc)) {
let base = thumbnailSrc.replace(/\/t_/, "/");
base = base.replace(/\.(jpe?g|jxl|png|apng|gif|avif|webp|webm|mp4|m4v|ogg|flac|opus|mp3|m4a|wav)$/i, "");
if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
return base;
}
const ext = filemime ? getExtensionForMimeType(filemime) : null;
if (!ext) return null;
return base + ext;
}
if (isDirectHash(thumbnailSrc)) {
if (filemime && (filemime.toLowerCase() === "image/apng" || filemime.toLowerCase() === "video/x-m4v")) {
return thumbnailSrc;
}
const ext = filemime ? getExtensionForMimeType(filemime) : null;
if (!ext) {
return thumbnailSrc;
}
return thumbnailSrc + ext;
}
if (isGenericThumb()) {
if (parentA && parentA.getAttribute("href")) {
return sanitizeUrl(parentA.getAttribute("href"));
}
return null;
}
return null;
}
function leaveHandler() {
cleanupFloatingMedia();
}
function mouseMoveHandler(ev) {
lastMouseEvent = ev;
positionFloatingMedia(ev);
}
async function onThumbEnter(e) {
cleanupFloatingMedia();
lastMouseEvent = e;
const thumb = e.currentTarget;
let filemime = null, fullSrc = null, isVideo = false, isAudio = false;
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",
jxl: "image/jxl",
png: "image/png",
apng: "image/apng",
gif: "image/gif",
avif: "image/avif",
webp: "image/webp",
bmp: "image/bmp",
mp4: "video/mp4",
webm: "video/webm",
m4v: "video/x-m4v",
ogg: "audio/ogg",
flac: "audio/flac",
opus: "audio/opus",
mp3: "audio/mpeg",
m4a: "audio/x-m4a",
wav: "audio/wav",
}[ext];
fullSrc = getFullMediaSrc(thumb, filemime);
if (
/custom\.spoiler$|spoiler\.png$/i.test(thumb.getAttribute("src") || "") &&
parentA && parentA.getAttribute("href")
) {
fullSrc = parentA.getAttribute("href");
}
isVideo = filemime && filemime.startsWith("video/");
isAudio = filemime && filemime.startsWith("audio/");
}
fullSrc = sanitizeUrl(fullSrc);
if (!fullSrc) return;
let volume = 0.5;
try {
if (typeof getSetting === "function") {
const v = await getSetting("hoverVideoVolume");
if (typeof v === "number" && !isNaN(v)) {
volume = Math.max(0, Math.min(1, v / 100));
}
}
} catch { }
if (isAudio) {
const container = thumb.closest("a.linkThumb, a.imgLink");
let audioSrc = fullSrc;
if (
thumb.tagName === "IMG" &&
container &&
/audioGenericThumb\.png$/.test(thumb.getAttribute("src") || "") &&
container.getAttribute("href")
) {
audioSrc = sanitizeUrl(container.getAttribute("href")) || audioSrc;
}
if (container && !container.style.position) {
container.style.position = "relative";
}
floatingMedia = document.createElement("audio");
floatingMedia.dataset.previewSrc = audioSrc;
floatingMedia.src = audioSrc;
floatingMedia.controls = false;
floatingMedia.style.display = "none";
floatingMedia.volume = volume;
document.body.appendChild(floatingMedia);
try {
const saved = trackMediaPlaybackEnabled ? hoverPlaybackTimes.get(audioSrc) : null;
if (typeof saved === 'number' && !isNaN(saved) && isFinite(saved) && saved > 0) {
const onLoadedMeta = function () {
const onSeeked = function () {
try { floatingMedia.play().catch(() => { }); } finally { floatingMedia.removeEventListener('seeked', onSeeked); }
};
floatingMedia.addEventListener('seeked', onSeeked);
try {
const dur = floatingMedia.duration;
if (!isNaN(dur) && isFinite(dur) && dur > 0) {
floatingMedia.currentTime = Math.min(saved, Math.max(0, dur - 0.05));
} else {
floatingMedia.currentTime = saved;
}
} catch (e) {
floatingMedia.removeEventListener('seeked', onSeeked);
floatingMedia.play().catch(() => { });
}
};
floatingMedia.addEventListener('loadedmetadata', onLoadedMeta, { once: true });
} else {
floatingMedia.addEventListener('canplay', function () { floatingMedia.play().catch(() => { }); }, { once: true });
}
} catch (e) {
floatingMedia.play().catch(() => { });
}
const indicator = document.createElement("div");
indicator.classList.add("audio-preview-indicator");
indicator.textContent = AUDIO_INDICATOR_TEXT;
if (container) {
container.appendChild(indicator);
}
currentAudioIndicator = indicator;
thumb.addEventListener("mouseleave", leaveHandler, { once: true });
if (container) container.addEventListener("click", leaveHandler, { once: true });
window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
if (container) cleanupFns.push(() => container.removeEventListener("click", leaveHandler));
cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
return;
}
let videoSrc = fullSrc;
if (
isVideo &&
thumb.tagName === "IMG" &&
thumb.closest("a.linkThumb, a.imgLink") &&
(!/\.(mp4|webm|m4v)$/i.test(fullSrc) || /\/\.media\//.test(fullSrc) && !/\.(mp4|webm|m4v)$/i.test(fullSrc))
) {
const parentA = thumb.closest("a.linkThumb, a.imgLink");
if (parentA && parentA.getAttribute("href")) {
videoSrc = parentA.getAttribute("href");
}
}
floatingMedia = isVideo ? document.createElement("video") : document.createElement("img");
floatingMedia.dataset.previewSrc = isVideo ? videoSrc : fullSrc;
floatingMedia.src = isVideo ? videoSrc : fullSrc;
floatingMedia.id = "hover-preview-media";
floatingMedia.style.position = "fixed";
floatingMedia.style.zIndex = "9999";
floatingMedia.style.pointerEvents = "none";
floatingMedia.style.opacity = MEDIA_OPACITY_LOADING;
floatingMedia.style.visibility = isVideo ? "hidden" : "visible";
floatingMedia.style.left = "-9999px";
floatingMedia.style.top = "-9999px";
floatingMedia.style.maxWidth = MEDIA_MAX_WIDTH;
const availableHeight = window.innerHeight * (1 - MEDIA_BOTTOM_MARGIN / 100);
floatingMedia.style.maxHeight = `${availableHeight}px`;
if (isVideo) {
floatingMedia.autoplay = false;
floatingMedia.loop = true;
floatingMedia.muted = false;
floatingMedia.playsInline = true;
floatingMedia.volume = volume;
}
document.body.appendChild(floatingMedia);
document.addEventListener("mousemove", mouseMoveHandler, { passive: true });
thumb.addEventListener("mouseleave", leaveHandler, { passive: true, once: true });
cleanupFns.push(() => document.removeEventListener("mousemove", mouseMoveHandler));
if (lastMouseEvent) {
positionFloatingMedia(lastMouseEvent);
}
if (isVideo) {
floatingMedia.preload = 'auto';
floatingMedia.onloadeddata = function () {
try {
const key = floatingMedia.dataset && floatingMedia.dataset.previewSrc ? floatingMedia.dataset.previewSrc : (floatingMedia.src || "");
const saved = (trackMediaPlaybackEnabled && key) ? hoverPlaybackTimes.get(key) : null;
function revealAndPlay() {
if (!floatingMedia) return;
floatingMedia.style.visibility = "visible";
floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
try { floatingMedia.play().catch(() => { }); } catch (e) { }
}
if (typeof saved === 'number' && !isNaN(saved) && isFinite(saved) && saved > 0) {
const onSeeked = function () {
try { revealAndPlay(); } finally { floatingMedia.removeEventListener('seeked', onSeeked); }
};
floatingMedia.addEventListener('seeked', onSeeked);
try {
const dur = floatingMedia.duration;
if (!isNaN(dur) && isFinite(dur) && dur > 0) {
floatingMedia.currentTime = Math.min(saved, Math.max(0, dur - 0.05));
} else {
floatingMedia.currentTime = saved;
}
} catch (e) {
floatingMedia.removeEventListener('seeked', onSeeked);
revealAndPlay();
}
} else {
const onCanPlay = function () { try { revealAndPlay(); } finally { floatingMedia.removeEventListener('canplay', onCanPlay); } };
floatingMedia.addEventListener('canplay', onCanPlay);
}
} catch (e) {
try { floatingMedia.style.visibility = "visible"; floatingMedia.style.opacity = MEDIA_OPACITY_LOADED; } catch (e) { }
}
};
} else {
floatingMedia.onload = function () {
if (floatingMedia) {
floatingMedia.style.opacity = MEDIA_OPACITY_LOADED;
if (lastMouseEvent) positionFloatingMedia(lastMouseEvent);
}
};
}
floatingMedia.onerror = cleanupFloatingMedia;
thumb.addEventListener("mouseleave", leaveHandler, { once: true });
window.addEventListener("scroll", leaveHandler, { passive: true, once: true });
cleanupFns.push(() => thumb.removeEventListener("mouseleave", leaveHandler));
cleanupFns.push(() => window.removeEventListener("scroll", leaveHandler));
}
function attachThumbListeners(root = document) {
root.querySelectorAll("a.linkThumb img, a.imgLink img").forEach(thumb => {
if (!thumb._fullImgHoverBound) {
thumb.addEventListener("mouseenter", onThumbEnter);
thumb._fullImgHoverBound = true;
}
});
if (
root.tagName === "IMG" &&
root.parentElement &&
(root.parentElement.closest("a.linkThumb") || root.parentElement.closest("a.imgLink")) &&
!root._fullImgHoverBound
) {
root.addEventListener("mouseenter", onThumbEnter);
root._fullImgHoverBound = true;
}
}
attachThumbListeners();
const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
if (divThreadsObs) {
divThreadsObs.addHandler(function imageHoverHandler(mutations) {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === 1) {
attachThumbListeners(addedNode);
}
}
}
});
}
}
function getExtensionForMimeType(mime) {
const map = {
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/jxl": ".jxl",
"image/png": ".png",
"image/apng": ".png",
"image/gif": ".gif",
"image/avif": ".avif",
"image/webp": ".webp",
"image/bmp": ".bmp",
};
return map[mime.toLowerCase()] || "";
}
async function featureBlurSpoilers() {
if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
return;
}
const removeSpoilers = await getSetting("blurSpoilers_removeSpoilers");
function applyBlurOrRemoveSpoilers(img, removeSpoilers) {
if (removeSpoilers) {
img.classList.add('ss-spoiler-border');
} else {
img.classList.add('ss-spoiler-blurred');
}
}
function processImgLink(link) {
if (link.dataset.blurSpoilerProcessed === "1") {
return;
}
const img = link.querySelector("img");
if (!img) {
return;
}
if (
/\/\.media\/[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src) &&
!/\/\.media\/t_[^\/]+?\.[a-zA-Z0-9]+$/.test(img.src)
) {
link.dataset.blurSpoilerProcessed = "1";
return;
}
const isCustomSpoiler = img.src.includes("/custom.spoiler")
|| img.src.includes("/*/custom.spoiler")
|| img.src.includes("/spoiler.png");
const isNotThumbnail = !img.src.includes("/.media/t_");
const hasFilenameExtension = !isCustomSpoiler && /\.[a-zA-Z0-9]+$/.test(img.src);
if (isNotThumbnail || isCustomSpoiler) {
let href = link.getAttribute("href");
if (!href) {
link.dataset.blurSpoilerProcessed = "1";
return;
}
const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/);
if (!match) {
link.dataset.blurSpoilerProcessed = "1";
return;
}
const fileMime = link.getAttribute("data-filemime") || "";
const ext = getExtensionForMimeType(fileMime);
let fileWidthAttr = link.getAttribute("data-filewidth");
let fileHeightAttr = link.getAttribute("data-fileheight");
let transformedSrc;
if (
(fileWidthAttr && Number(fileWidthAttr) <= 220) ||
(fileHeightAttr && Number(fileHeightAttr) <= 220)
) {
if (fileMime && fileMime.startsWith('video/')) {
link.dataset.blurSpoilerProcessed = "1";
return;
}
transformedSrc = `/.media/${match[1]}${ext}`;
} else if (!hasFilenameExtension && isCustomSpoiler) {
transformedSrc = `/.media/t_${match[1]}`;
} else {
link.dataset.blurSpoilerProcessed = "1";
return;
}
if (isCustomSpoiler && !fileWidthAttr && !fileHeightAttr) {
const uploadCell = img.closest('.uploadCell');
if (uploadCell) {
const dimensionLabel = uploadCell.querySelector('.dimensionLabel');
if (dimensionLabel) {
const dimensions = dimensionLabel.textContent.trim().split(/x|×/);
if (dimensions.length === 2) {
const parsedWidth = parseInt(dimensions[0].trim(), 10);
const parsedHeight = parseInt(dimensions[1].trim(), 10);
if ((parsedWidth <= 220 || parsedHeight <= 220)) {
img.src = href + "#spoiler";
link.dataset.blurSpoilerProcessed = "1";
applyBlurOrRemoveSpoilers(img, removeSpoilers);
return;
}
}
}
}
}
const initialWidth = img.offsetWidth;
const initialHeight = img.offsetHeight;
img.style.width = initialWidth + "px";
img.style.height = initialHeight + "px";
img.src = transformedSrc + "#spoiler";
img.addEventListener('load', function () {
img.style.width = img.naturalWidth + "px";
img.style.height = img.naturalHeight + "px";
});
applyBlurOrRemoveSpoilers(img, removeSpoilers);
link.dataset.blurSpoilerProcessed = "1";
return;
}
link.dataset.blurSpoilerProcessed = "1";
}
document.querySelectorAll("a.imgLink").forEach(link => processImgLink(link));
let pendingImgLinks = new WeakSet();
let debounceTimeout = null;
function processPendingImgLinks() {
const allLinks = document.querySelectorAll("a.imgLink");
allLinks.forEach(link => {
if (pendingImgLinks.has(link)) {
processImgLink(link);
}
});
pendingImgLinks = new WeakSet();
debounceTimeout = null;
}
const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
if (divThreadsObs) {
divThreadsObs.addHandler(function blurSpoilersHandler(mutations) {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType !== 1) continue;
if (addedNode.classList && addedNode.classList.contains('imgLink')) {
pendingImgLinks.add(addedNode);
} else if (addedNode.querySelectorAll) {
addedNode.querySelectorAll('.imgLink').forEach(link => pendingImgLinks.add(link));
}
}
}
if (!debounceTimeout) {
debounceTimeout = setTimeout(processPendingImgLinks, 50);
}
});
}
const bodyObs = observeSelector('body', { childList: true, subtree: true });
if (bodyObs) {
bodyObs.addHandler(function quoteTooltipSpoilerHandler(mutations) {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType !== 1) continue;
if (addedNode.classList && addedNode.classList.contains('quoteTooltip')) {
addedNode.querySelectorAll('a.imgLink').forEach(link => processImgLink(link));
} else if (addedNode.querySelectorAll) {
addedNode.querySelectorAll('.quoteTooltip a.imgLink').forEach(link => processImgLink(link));
}
}
}
});
}
}
function featureAPNGStop() {
function createCanvasSnapshot(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
return canvas;
}
function createOverlay(width, height) {
const overlay = document.createElement('div');
overlay.className = 'apng-overlay';
overlay.style.width = width + 'px';
overlay.style.height = height + 'px';
return overlay;
}
function isAPNG(mime, src) {
if (mime) {
const lower = mime.toLowerCase();
if (lower === 'image/apng' || lower === 'image/png') return true;
}
if (src && /\.(a?png)(\?.*)?$/i.test(src)) return true;
return false;
}
function processThumb(img) {
if (img.dataset.apngProcessed === '1') return;
const link = img.closest('a.linkThumb, a.imgLink');
if (!link) return;
const mime = (link.getAttribute('data-filemime') || '').toLowerCase();
const href = link.getAttribute('href') || '';
if (!isAPNG(mime, href)) return;
const width = parseInt(link.getAttribute('data-filewidth'), 10) || img.width || 0;
const height = parseInt(link.getAttribute('data-fileheight'), 10) || img.height || 0;
if (width > 220 || height > 220) return;
img.style.visibility = 'hidden';
if (!img.complete || img.naturalWidth === 0) {
img.addEventListener('load', () => processThumb(img), { once: true });
return;
}
img.dataset.apngProcessed = '1';
const canvas = createCanvasSnapshot(img);
canvas.className = 'apng-canvas-snapshot';
const overlay = createOverlay(canvas.width, canvas.height);
const isCatalog = !!link.closest('.catalogCell');
const wrapper = document.createElement('div');
Object.assign(wrapper.style, {
position: 'relative',
display: 'inline-block',
width: canvas.width + 'px',
height: canvas.height + 'px',
...(isCatalog ? {} : { marginRight: '1em', marginBottom: '0.7em' })
});
img.parentNode.insertBefore(wrapper, img);
wrapper.append(img, canvas, overlay);
overlay.addEventListener('click', () => {
overlay.remove();
canvas.remove();
img.style.visibility = '';
});
['mouseenter', 'mouseleave'].forEach(type => {
overlay.addEventListener(type, e => {
img.dispatchEvent(new MouseEvent(type, {
bubbles: false,
cancelable: true,
clientX: e.clientX,
clientY: e.clientY
}));
});
});
}
const SEL = 'a.linkThumb img, a.imgLink img';
document.querySelectorAll(SEL).forEach(processThumb);
const obs = observeSelector('body', { childList: true, subtree: true });
if (obs) {
obs.addHandler(function apngStopHandler(mutations) {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.matches && node.matches(SEL)) {
processThumb(node);
} else if (node.querySelectorAll) {
node.querySelectorAll(SEL).forEach(processThumb);
}
}
}
});
}
}
async function featureMascots() {
if (!(window.pageType?.isCatalog || window.pageType?.isThread || window.pageType?.isIndex)) {
return;
}
const opacity = await getSetting("enableMascots_mascotOpacity") || 30;
const urlsText = await getSetting("enableMascots_mascotUrls") || "";
if (!urlsText.trim()) return;
const urlLines = urlsText.split('\n');
const trimmedUrls = urlLines.map(url => url.trim());
const validUrls = trimmedUrls.filter(url => url && (url.startsWith('/') || url.startsWith('http')));
const urls = validUrls.filter(url => /\.(png|apng|jpg|jpeg|gif|webp)$/i.test(url));
if (urls.length === 0) {
console.log('8chanSS: No valid mascot URLs found. Check that URLs start with / or http and have valid image extensions (.png, .jpg, .gif, etc.)');
return;
}
const enableSidebar = await getSetting("enableSidebar");
const leftSidebar = await getSetting("enableSidebar_leftSidebar");
const randomIndex = Math.floor(Math.random() * urls.length);
const selectedUrl = urls[randomIndex];
const mascot = document.createElement('img');
mascot.className = 'chSS-mascot';
mascot.src = selectedUrl;
mascot.style.opacity = opacity / 100;
if (enableSidebar) {
if (leftSidebar) {
mascot.style.left = '0px';
mascot.style.bottom = '0px';
mascot.style.maxWidth = '312px';
} else {
mascot.style.right = '0px';
mascot.style.bottom = '0px';
mascot.style.maxWidth = '312px';
}
} else {
mascot.style.right = '0px';
mascot.style.bottom = '0px';
mascot.style.maxWidth = '70vh';
}
document.body.appendChild(mascot);
return () => {
document.querySelectorAll('.chSS-mascot').forEach(mascot => {
if (mascot.parentNode) {
mascot.parentNode.removeChild(mascot);
}
});
};
}
function autoHideHeaderOnScroll() {
const header = document.getElementById('dynamicHeaderThread');
if (!header) return;
const scrollThreshold = 50;
let lastScrollY = window.scrollY;
let scrollDirection = 'none';
let ticking = false;
function updateHeaderVisibility() {
const currentScrollY = window.scrollY;
scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
lastScrollY = currentScrollY;
const isNearTop = currentScrollY < 100;
if (scrollDirection === 'up' || isNearTop) {
header.classList.remove('nav-hidden');
} else if (scrollDirection === 'down' && currentScrollY > scrollThreshold) {
header.classList.add('nav-hidden');
}
ticking = false;
}
const style = document.createElement('style');
style.textContent = `
#dynamicHeaderThread {
transition: transform 0.3s ease;
}
#dynamicHeaderThread.nav-hidden {
transform: translateY(-100%);
}
:root.bottom-header #dynamicHeaderThread.nav-hidden {
transform: translateY(100%);
}
`;
document.head.appendChild(style);
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(updateHeaderVisibility);
ticking = true;
}
}, { passive: true });
updateHeaderVisibility();
}
const decodeHtmlEntitiesTwice = (() => {
const txt = document.createElement('textarea');
return function (html) {
txt.innerHTML = html;
const once = txt.value;
txt.innerHTML = once;
return txt.value;
};
})();
function highlightMentions() {
const watchedCells = document.querySelectorAll("#watchedMenu .watchedCell");
const watchButton = document.querySelector(".opHead .watchButton");
if (!watchedCells.length) return;
watchedCells.forEach((cell) => {
const notification = cell.querySelector(".watchedNotification");
if (!notification) return;
const labelLink = cell.querySelector(".watchedCellAnchor");
if (!labelLink) return;
if (!labelLink.dataset.board) {
const href = labelLink.getAttribute("href");
const match = href?.match(/^(?:https?:\/\/[^\/]+)?\/([^\/]+)\//);
if (match) {
labelLink.dataset.board = `/${match[1]}/ -`;
}
if (document.location.href.includes(href)) {
if (watchButton) {
watchButton.style.color = "var(--board-title-color)";
watchButton.title = "Watched";
}
}
const originalText = labelLink.textContent;
const decodedText = decodeHtmlEntitiesTwice(originalText);
if (labelLink.textContent !== decodedText) {
labelLink.textContent = decodedText;
}
}
const repliesSpan = notification.querySelector(".watchedNotificationReplies");
const yousSpan = notification.querySelector(".watchedNotificationYous");
const hasYou = yousSpan && !yousSpan.classList.contains("hidden");
const replyCount = repliesSpan ? repliesSpan.textContent.trim() : "";
if (labelLink.dataset.mentionProcessed === "true") {
return;
}
if (hasYou) {
labelLink.style.color = "var(--board-title-color)";
labelLink.style.fontWeight = "bold";
if (repliesSpan) {
repliesSpan.style.color = "var(--board-title-color)";
repliesSpan.style.fontWeight = "";
}
if (yousSpan) {
yousSpan.style.color = "var(--board-title-color)";
yousSpan.style.fontWeight = "";
}
}
else if (replyCount && /^\d+$/.test(replyCount)) {
labelLink.style.color = "var(--link-color)";
if (repliesSpan) {
repliesSpan.style.color = "var(--link-color)";
repliesSpan.style.fontWeight = "";
}
}
labelLink.dataset.mentionProcessed = "true";
});
}
highlightMentions();
highlightActiveWatchedThread();
function highlightActiveWatchedThread() {
const currentPath = window.pageType?.path;
if (!currentPath) return;
document.querySelectorAll('.watchedCellAnchor').forEach(link => {
const href = link.getAttribute('href');
if (!href) return;
const watchedPath = href.replace(/#.*$/, '');
const cell = link.closest('.watchedCell');
const repliesSpan = cell?.querySelector('.watchedNotificationReplies');
const yousSpan = cell?.querySelector('.watchedNotificationYous');
if (watchedPath === currentPath) {
link.classList.add('ss-active');
link.style.fontWeight = "bold";
if (repliesSpan) {
repliesSpan.style.fontWeight = "bold";
}
if (yousSpan) {
yousSpan.style.fontWeight = "bold";
}
} else {
link.classList.remove('ss-active');
link.style.fontWeight = "";
if (repliesSpan) {
repliesSpan.style.fontWeight = "";
}
if (yousSpan) {
yousSpan.style.fontWeight = "";
}
}
});
}
const watchedMenuObs = observeSelector('#watchedMenu', { childList: true, subtree: true });
if (watchedMenuObs) {
watchedMenuObs.addHandler(function highlightMentionsHandler() {
highlightMentions();
highlightActiveWatchedThread();
});
}
async function featureWatchThreadOnReply() {
if ((window.pageType?.isIndex || window.pageType?.isCatalog)) {
return;
}
const getWatchButton = () => document.querySelector(".watchButton");
function watchThreadIfNotWatched() {
const btn = getWatchButton();
if (btn && !btn.classList.contains("watched-active")) {
btn.click();
setTimeout(() => {
btn.classList.add("watched-active");
}, 100);
}
}
const submitButton = document.getElementById("qrbutton");
if (submitButton) {
submitButton.removeEventListener("click", submitButton._watchThreadHandler || (() => { }));
submitButton._watchThreadHandler = async function () {
if (await getSetting("watchThreadOnReply")) {
setTimeout(watchThreadIfNotWatched, 500);
}
};
submitButton.addEventListener("click", submitButton._watchThreadHandler);
}
}
async function featureAlwaysShowTW() {
if (!(await getSetting("alwaysShowTW"))) return;
if ((await getSetting("alwaysShowTW_noPinInCatalog")) && window.pageType.isCatalog) return;
const POSITION_STORAGE_KEY = "8chanSS_threadWatcherPosition";
async function restorePosition() {
const watchedMenu = document.getElementById("watchedMenu");
if (!watchedMenu) return;
try {
const position = await getStoredObject(POSITION_STORAGE_KEY);
if (position.left !== undefined && position.top !== undefined) {
watchedMenu.style.left = position.left + "px";
watchedMenu.style.top = position.top + "px";
}
} catch (err) {
console.error("Failed to restore thread watcher position:", err);
}
}
async function savePosition() {
const watchedMenu = document.getElementById("watchedMenu");
if (!watchedMenu) return;
try {
const left = parseInt(watchedMenu.style.left) || 0;
const top = parseInt(watchedMenu.style.top) || 0;
if (left > 0 || top > 0) {
await setStoredObject(POSITION_STORAGE_KEY, { left, top });
}
} catch (err) {
console.error("Failed to save thread watcher position:", err);
}
}
function showThreadWatcher() {
const watchedMenu = document.getElementById("watchedMenu");
if (watchedMenu) {
restorePosition();
}
}
const watcherButton = document.querySelector("#navLinkSpan > .jsOnly > .watcherButton");
if (watcherButton) {
watcherButton.click();
}
setTimeout(showThreadWatcher, 100);
setTimeout(restorePosition, 500);
const watchedMenuObs = observeSelector('#watchedMenu', { attributes: true, attributeFilter: ['style'] });
if (watchedMenuObs) {
let saveTimeout = null;
watchedMenuObs.addHandler(function positionSaveHandler() {
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(savePosition, 500);
});
}
}
(function markAllThreadsAsRead() {
const handleDiv = document.querySelector('#watchedMenu > div.handle');
if (!handleDiv) return;
if (handleDiv.querySelector('.markAllRead')) return;
const btn = document.createElement('span');
btn.className = 'coloredIcon glowOnHover markAllRead';
btn.title = 'Mark all threads as read';
function hasUnreadThreads() {
const watchedMenu = document.querySelector('#watchedMenu');
if (!watchedMenu) return false;
return watchedMenu.querySelector('.watchedNotification:not(.hidden) .watchedCellDismissButton:not(.markAllRead)') !== null;
}
function updateButtonState() {
if (hasUnreadThreads()) {
btn.classList.remove('disabled');
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
btn.title = 'Mark all threads as read';
} else {
btn.classList.add('disabled');
btn.style.opacity = '0.5';
btn.style.cursor = 'default';
btn.title = 'No unread threads';
}
}
function clickAllMarkAsReadButtons(watchedMenu) {
const markButtons = watchedMenu.querySelectorAll('.watchedNotification:not(.hidden) .watchedCellDismissButton:not(.markAllRead)');
markButtons.forEach(btn => {
try {
btn.click();
} catch (e) {
console.log("Error clicking button:", e);
}
});
return markButtons.length;
}
function markAllThreadsAsReadWithRetry(retriesLeft, callback) {
setTimeout(function () {
const watchedMenu = document.querySelector('#watchedMenu');
if (!watchedMenu) {
if (callback) callback();
return;
}
const clickedCount = clickAllMarkAsReadButtons(watchedMenu);
if (clickedCount === 0) {
updateButtonState();
if (callback) callback();
return;
}
if (retriesLeft > 0) {
setTimeout(() => markAllThreadsAsReadWithRetry(retriesLeft - 1, callback), 200);
} else if (callback) {
callback();
}
}, 100);
}
const watchedMenuObs = observeSelector('#watchedMenu > div.floatingContainer', { childList: true, subtree: true });
if (watchedMenuObs) {
const debouncedUpdate = debounce(updateButtonState, 100);
watchedMenuObs.addHandler(function markAllThreadsAsReadHandler() {
debouncedUpdate();
});
}
updateButtonState();
const closeBtn = handleDiv.querySelector('.close-btn');
if (closeBtn) {
handleDiv.insertBefore(btn, closeBtn);
} else {
handleDiv.appendChild(btn);
}
document.body.addEventListener('click', function (e) {
const closeBtn = e.target.closest('#watchedMenu .close-btn');
if (closeBtn) {
const watchedMenu = document.getElementById("watchedMenu");
if (watchedMenu) watchedMenu.style.display = "none";
return;
}
const markAllBtn = e.target.closest('.markAllRead');
if (markAllBtn) {
e.preventDefault();
e.stopPropagation();
if (markAllBtn.style.pointerEvents === 'none' || markAllBtn.classList.contains('disabled') || markAllBtn.dataset.processing === 'true') return;
markAllBtn.dataset.processing = 'true';
markAllBtn.style.opacity = '0.5';
markAllThreadsAsReadWithRetry(3, function () {
markAllBtn.dataset.processing = 'false';
updateButtonState();
});
}
});
})();
function hashNavigation() {
if (!window.pageType?.isThread) return;
const processedLinks = new WeakSet();
function addHashLinks(container = document) {
const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
links.forEach(link => {
if (
processedLinks.has(link) ||
(link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container'))
) return;
const hashSpan = document.createElement('span');
hashSpan.textContent = ' #';
hashSpan.className = 'hash-link';
hashSpan.style.cursor = 'pointer';
hashSpan.style.color = 'var(--navbar-text-color)';
hashSpan.title = 'Scroll to post';
const wrapper = document.createElement('span');
wrapper.className = 'hash-link-container';
wrapper.appendChild(hashSpan);
link.insertAdjacentElement('afterend', wrapper);
processedLinks.add(link);
});
}
addHashLinks();
const divThreadsObs = observeSelector('#divThreads', { childList: true, subtree: true });
if (divThreadsObs) {
const debouncedAddHashLinks = debounce(() => addHashLinks(), 25);
divThreadsObs.addHandler(function hashNavigationHandler() {
debouncedAddHashLinks();
});
}
const postsContainer = document.getElementById('divThreads') || document.body;
postsContainer.addEventListener('click', function (e) {
if (e.target.classList.contains('hash-link')) {
e.preventDefault();
const link = e.target.closest('.hash-link-container').previousElementSibling;
if (!link || !link.href) return;
const hashMatch = link.href.match(/#(\d+)$/);
if (!hashMatch) return;
const postId = hashMatch[1];
const safePostId = /^[0-9]+$/.test(postId) ? postId : null;
if (!safePostId) return;
const postElem = document.getElementById(safePostId);
if (postElem) {
window.location.hash = `#${safePostId}`;
if (postElem.classList.contains('opCell')) {
const offset = 25;
const rect = postElem.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const targetY = rect.top + scrollTop - offset;
window.scrollTo({ top: targetY, behavior: "smooth" });
} else {
postElem.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}
}, true);
}
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", () => {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
});
document.body.appendChild(upBtn);
document.body.appendChild(downBtn);
}
async function featureHideAnnouncement() {
async function processElement(selector, settingKey, contentKey) {
const el = document.querySelector(selector);
if (!el) return;
const content = (el.textContent || "").replace(/[^\w\s.,!?-]/g, "");
const shouldHide = await GM.getValue(`8chanSS_${settingKey}`, "false") === "true";
const storedContent = await GM.getValue(`8chanSS_${contentKey}`, null);
const root = document.documentElement;
if (shouldHide) {
if (storedContent !== null && storedContent !== content) {
if (typeof window.setSetting === "function") {
await window.setSetting("hideAnnouncement", false);
}
await GM.setValue(`8chanSS_${settingKey}`, "false");
await GM.deleteValue(`8chanSS_${contentKey}`);
return;
}
root.classList.add("hide-announcement");
await GM.setValue(`8chanSS_${contentKey}`, content);
} else {
root.classList.remove("hide-announcement");
await GM.deleteValue(`8chanSS_${contentKey}`);
}
}
await processElement("#dynamicAnnouncement", "hideAnnouncement", "announcementContent");
}
(async function featureBeepOnYou() {
if (!divPosts) return;
let audioContext = null;
let audioContextReady = false;
let audioContextPromise = null;
function ensureAudioContextReady() {
if (audioContextReady) return Promise.resolve();
if (audioContextPromise) return audioContextPromise;
audioContextPromise = new Promise((resolve) => {
function resumeAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
audioContextReady = true;
window.removeEventListener('click', resumeAudioContext);
window.removeEventListener('keydown', resumeAudioContext);
resolve();
});
} else {
audioContextReady = true;
window.removeEventListener('click', resumeAudioContext);
window.removeEventListener('keydown', resumeAudioContext);
resolve();
}
}
window.addEventListener('click', resumeAudioContext);
window.addEventListener('keydown', resumeAudioContext);
});
return audioContextPromise;
}
async function createBeepSound() {
if (!(await getSetting("beepOnYou"))) {
return;
}
await ensureAudioContextReady();
return function playBeep() {
try {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.value = 550;
gainNode.gain.value = 0.1;
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
setTimeout(() => {
oscillator.stop();
}, 100);
} catch (e) {
console.warn("Beep failed:", e);
}
};
}
window.originalTitle = document.title;
window.isNotifying = false;
let beepOnYouSetting = false;
let notifyOnYouSetting = false;
let customMsgSetting = "(!) ";
let previousFaviconState = null;
async function initSettings() {
beepOnYouSetting = await getSetting("beepOnYou");
notifyOnYouSetting = await getSetting("notifyOnYou");
const customMsg = await getSetting("notifyOnYou_customMessage");
if (customMsg) customMsgSetting = customMsg;
}
await initSettings();
let playBeep = null;
createBeepSound().then(fn => { playBeep = fn; });
let scrollHandlerActive = false;
async function notifyOnYou() {
if (!window.isNotifying) {
window.isNotifying = true;
document.title = customMsgSetting + " " + window.originalTitle;
if (await getSetting("customFavicon")) {
const { style, state } = faviconManager.getCurrentFaviconState();
if (state !== "notif") {
previousFaviconState = { style, state };
}
faviconManager.setFaviconStyle(style, "notif");
}
}
}
function setupNotificationScrollHandler() {
if (scrollHandlerActive) return;
scrollHandlerActive = true;
const BOTTOM_OFFSET = 45;
function checkScrollPosition() {
if (!window.isNotifying) return;
const scrollPosition = window.scrollY + window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
if (scrollPosition >= documentHeight - BOTTOM_OFFSET) {
document.title = window.originalTitle;
window.isNotifying = false;
const { state } = faviconManager.getCurrentFaviconState();
if (state === "notif" && previousFaviconState) {
faviconManager.setFaviconStyle(previousFaviconState.style, previousFaviconState.state);
previousFaviconState = null;
} else if (state === "notif") {
faviconManager.setFavicon("base");
}
window.removeEventListener('scroll', checkScrollPosition);
scrollHandlerActive = false;
}
}
window.addEventListener('scroll', checkScrollPosition);
}
window.addEventListener("focus", () => {
if (window.isNotifying) {
setupNotificationScrollHandler();
}
});
const divPostsObs = observeSelector('.divPosts', { childList: true, subtree: false });
if (divPostsObs) {
divPostsObs.addHandler(function beepOnYouHandler(mutations) {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (
node.nodeType === 1 &&
typeof node.matches === "function" &&
(node.matches('.postCell') || node.matches('.opCell')) &&
node.querySelector("a.quoteLink.you") &&
!node.closest('.innerPost')
) {
if (beepOnYouSetting && playBeep) {
playBeep();
}
if (notifyOnYouSetting) {
notifyOnYou();
setupNotificationScrollHandler();
}
}
}
}
});
}
window.addEventListener("8chanSS_settingChanged", async (e) => {
if (e.detail && e.detail.key) {
const key = e.detail.key;
if (key === "beepOnYou") {
beepOnYouSetting = await getSetting("beepOnYou");
createBeepSound().then(fn => { playBeep = fn; });
} else if (key === "notifyOnYou") {
notifyOnYouSetting = await getSetting("notifyOnYou");
} else if (key === "notifyOnYou_customMessage") {
const customMsg = await getSetting("notifyOnYou_customMessage");
if (customMsg) customMsgSetting = customMsg;
}
}
});
})();
(async function enhanceLinks() {
if (!(window.pageType?.isThread || window.pageType?.isIndex)) {
return;
}
const [
enableShowIcons, enableShowThumbnails, enableEmbeds,
showIconsYoutubeRaw, showIconsTwitchRaw, showIconsXRaw, showIconsBskyRaw,
showIconsRentryRaw, showIconsCatboxRaw, showIconsPastebinRaw,
showThumbnailsYoutubeRaw, showThumbnailsTwitchRaw,
enableEmbedsXRaw, enableEmbedsBskyRaw, enableEmbedsRentryRaw, enableEmbedsPastebinRaw
] = await Promise.all([
getSetting("enhanceLinks_showIcons"),
getSetting("enhanceLinks_showThumbnails"),
getSetting("enhanceLinks_enableEmbeds"),
getSetting("enhanceLinks_showIcons_showIconsYoutube"),
getSetting("enhanceLinks_showIcons_showIconsTwitch"),
getSetting("enhanceLinks_showIcons_showIconsX"),
getSetting("enhanceLinks_showIcons_showIconsBsky"),
getSetting("enhanceLinks_showIcons_showIconsRentry"),
getSetting("enhanceLinks_showIcons_showIconsCatbox"),
getSetting("enhanceLinks_showIcons_showIconsPastebin"),
getSetting("enhanceLinks_showThumbnails_showThumbnailsYoutube"),
getSetting("enhanceLinks_showThumbnails_showThumbnailsTwitch"),
getSetting("enhanceLinks_enableEmbeds_enableEmbedsX"),
getSetting("enhanceLinks_enableEmbeds_enableEmbedsBsky"),
getSetting("enhanceLinks_enableEmbeds_enableEmbedsRentry"),
getSetting("enhanceLinks_enableEmbeds_enableEmbedsPastebin")
]);
const showIconsYoutube = enableShowIcons && showIconsYoutubeRaw;
const showIconsTwitch = enableShowIcons && showIconsTwitchRaw;
const showIconsX = enableShowIcons && showIconsXRaw;
const showIconsBsky = enableShowIcons && showIconsBskyRaw;
const showIconsRentry = enableShowIcons && showIconsRentryRaw;
const showIconsCatbox = enableShowIcons && showIconsCatboxRaw;
const showIconsPastebin = enableShowIcons && showIconsPastebinRaw;
const showThumbnailsYoutube = enableShowThumbnails && showThumbnailsYoutubeRaw;
const showThumbnailsTwitch = enableShowThumbnails && showThumbnailsTwitchRaw;
const enableEmbedsX = enableEmbeds && enableEmbedsXRaw;
const enableEmbedsBsky = enableEmbeds && enableEmbedsBskyRaw;
const enableEmbedsRentry = enableEmbeds && enableEmbedsRentryRaw;
const enableEmbedsPastebin = enableEmbeds && enableEmbedsPastebinRaw;
if (!showIconsYoutube && !showIconsTwitch && !showIconsX && !showIconsBsky &&
!showIconsRentry && !showIconsCatbox && !showIconsPastebin &&
!showThumbnailsYoutube && !showThumbnailsTwitch &&
!enableEmbedsX && !enableEmbedsBsky && !enableEmbedsRentry && !enableEmbedsPastebin) {
return;
}
const MAX_CACHE_SIZE = 350;
const ORDER_KEY = "_order";
const TRACKING_PARAMS = [
"si", "feature", "ref", "fsi", "source",
"utm_source", "utm_medium", "utm_campaign", "gclid", "gclsrc", "fbclid"
];
const YT_ICON = ``;
const TWITCH_ICON = ``;
const RENTRY_ICON = ``;
const PASTEBIN_ICON = ``;
const CATBOX_ICON = ``;
const X_ICON = ``;
const BSKY_ICON = ``;
function loadCache(cacheKey) {
try {
const data = localStorage.getItem(cacheKey);
if (data) {
const parsed = JSON.parse(data);
if (!Array.isArray(parsed[ORDER_KEY])) {
parsed[ORDER_KEY] = [];
}
return parsed;
}
} catch (e) { }
return { [ORDER_KEY]: [] };
}
function saveCache(cacheKey, cache) {
try {
localStorage.setItem(cacheKey, JSON.stringify(cache));
} catch (e) { }
}
const ytTitleCache = loadCache('ytTitleCache');
function getYouTubeInfo(url) {
try {
const u = new URL(url);
if (u.hostname === 'youtu.be') {
const id = (u.pathname.slice(1).split('/')[0] || '').split('?')[0];
if (id) return { type: 'video', id: id };
return null;
}
const isYoutube = u.hostname.endsWith('youtube.com') || u.hostname.endsWith('youtube-nocookie.com');
if (isYoutube) {
if (u.pathname === '/watch') {
const videoId = u.searchParams.get('v');
if (videoId) {
return { type: 'video', id: videoId };
}
}
const watchPathMatch = u.pathname.match(/^\/watch\/([a-zA-Z0-9_-]{11})(?:\/|$)/);
if (watchPathMatch) {
return { type: 'video', id: watchPathMatch[1] };
}
const vPathMatch = u.pathname.match(/^\/v\/([a-zA-Z0-9_-]{11})(?:\/|$)/);
if (vPathMatch) {
return { type: 'video', id: vPathMatch[1] };
}
const liveMatch = u.pathname.match(/^\/(live|embed|shorts)\/([a-zA-Z0-9_-]{11})/);
if (liveMatch) {
return { type: 'video', id: liveMatch[2] };
}
const postMatch = u.pathname.match(/^(?:\/@([a-zA-Z0-9_.-]+))?\/post\/([a-zA-Z0-9_-]+)/);
if (postMatch) {
return {
type: 'post',
id: postMatch[2],
channel: postMatch[1] || null
};
}
const channelMatch = u.pathname.match(/^\/(?:@([a-zA-Z0-9_.-]+)|c\/([a-zA-Z0-9_.-]+)|channel\/([a-zA-Z0-9_-]+)|user\/([a-zA-Z0-9_-]+)|([a-zA-Z0-9_.-]+))(?:\/|$)/);
if (channelMatch) {
const channelId = channelMatch[1] || channelMatch[2] || channelMatch[3] || channelMatch[4] || channelMatch[5];
if (channelId) {
return { type: 'channel', id: channelId };
}
}
}
} catch (e) { }
return null;
}
function sanitizeYouTubeId(videoId) {
if (!videoId) return null;
const match = videoId.match(/([a-zA-Z0-9_-]{11})/);
return match ? match[1] : null;
}
function stripTrackingParams(url) {
try {
const u = new URL(url);
let changed = false;
const KEEP_PARAMS = new Set(['t', 'start']);
TRACKING_PARAMS.forEach(param => {
if (u.searchParams.has(param) && !KEEP_PARAMS.has(param)) {
u.searchParams.delete(param);
changed = true;
}
});
if (u.hash && u.hash.includes('?')) {
const [hashPath, hashQuery] = u.hash.split('?');
const hashParams = new URLSearchParams(hashQuery);
let hashChanged = false;
TRACKING_PARAMS.forEach(param => {
if (hashParams.has(param) && !KEEP_PARAMS.has(param)) {
hashParams.delete(param);
hashChanged = true;
}
});
if (hashChanged) {
u.hash = hashParams.toString()
? `${hashPath}?${hashParams.toString()}`
: hashPath;
changed = true;
}
}
return changed ? u.toString() : url;
} catch (e) {
return url;
}
}
async function fetchYouTubeTitle(videoId) {
const cleanId = sanitizeYouTubeId(videoId);
if (!cleanId) return null;
if (ytTitleCache.hasOwnProperty(cleanId)) {
const idx = ytTitleCache[ORDER_KEY].indexOf(cleanId);
if (idx !== -1) {
ytTitleCache[ORDER_KEY].splice(idx, 1);
}
ytTitleCache[ORDER_KEY].push(cleanId);
saveCache('ytTitleCache', ytTitleCache);
return ytTitleCache[cleanId];
}
try {
const data = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
url: `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${cleanId}&format=json`,
timeout: 8000,
onload: (response) => {
if (response.status === 200) {
try {
resolve(JSON.parse(response.responseText));
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: () => reject(new Error('Network error')),
ontimeout: () => reject(new Error('Timeout'))
});
});
const title = data ? data.title : null;
if (title) {
ytTitleCache[cleanId] = title;
ytTitleCache[ORDER_KEY].push(cleanId);
while (ytTitleCache[ORDER_KEY].length > MAX_CACHE_SIZE) {
const oldest = ytTitleCache[ORDER_KEY].shift();
delete ytTitleCache[oldest];
}
saveCache('ytTitleCache', ytTitleCache);
}
return title;
} catch {
return null;
}
}
function getYouTubeThumbnailUrl(videoId) {
return `https://i.ytimg.com/vi_webp/${videoId}/hqdefault.webp`;
}
async function fetchAsDataURL(url) {
return new Promise((resolve) => {
try {
GM.xmlHttpRequest({
method: "GET",
url: url,
responseType: "blob",
timeout: 10000,
onload: (response) => {
if (response.status === 200 && response.response) {
try {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = () => resolve(null);
reader.readAsDataURL(response.response);
} catch (e) {
resolve(null);
}
} else {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
} catch (e) {
resolve(null);
}
});
}
function addThumbnailHover(link, thumbnailUrl, altText = "Thumbnail") {
if (link.dataset.thumbHover) return;
link.dataset.thumbHover = "1";
let thumbDiv = null;
let lastImg = null;
let lastHoverToken = 0;
function showThumb(e) {
if (!thumbDiv) {
thumbDiv = document.createElement('div');
thumbDiv.style.position = 'fixed';
thumbDiv.style.zIndex = '9999';
thumbDiv.style.pointerEvents = 'none';
thumbDiv.style.background = '#222';
thumbDiv.style.border = '1px solid #444';
thumbDiv.style.padding = '2px';
thumbDiv.style.borderRadius = '4px';
thumbDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.4)';
thumbDiv.style.transition = 'opacity 0.1s';
thumbDiv.style.opacity = '0';
thumbDiv.style.maxWidth = '280px';
thumbDiv.style.maxHeight = '200px';
thumbDiv.style.color = '#fff';
const img = document.createElement('img');
img.style.display = 'block';
img.style.maxWidth = '280px';
img.style.maxHeight = '200px';
img.style.borderRadius = '3px';
img.alt = altText;
img.src = "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQv///wAAACH5BAEAAAMALAAAAAAQABAAAAIgjI+py+0Po5yUFQA7";
lastImg = img;
const hoverToken = ++lastHoverToken;
fetchAsDataURL(thumbnailUrl).then(dataUrl => {
if (lastImg === img && hoverToken === lastHoverToken) {
if (dataUrl) {
img.src = dataUrl;
} else {
img.alt = "Failed to load thumbnail";
}
}
});
thumbDiv.appendChild(img);
document.body.appendChild(thumbDiv);
setTimeout(() => {
if (thumbDiv) thumbDiv.style.opacity = '1';
}, 10);
}
const top = Math.min(window.innerHeight - 130, e.clientY + 12);
const left = Math.min(window.innerWidth - 290, e.clientX + 12);
thumbDiv.style.top = `${top}px`;
thumbDiv.style.left = `${left}px`;
}
function moveThumb(e) {
if (thumbDiv) {
const top = Math.min(window.innerHeight - 130, e.clientY + 12);
const left = Math.min(window.innerWidth - 290, e.clientX + 12);
thumbDiv.style.top = `${top}px`;
thumbDiv.style.left = `${left}px`;
}
}
function hideThumb() {
lastHoverToken++;
if (thumbDiv && thumbDiv.parentNode) {
thumbDiv.parentNode.removeChild(thumbDiv);
thumbDiv = null;
}
lastImg = null;
}
link.addEventListener('mouseenter', showThumb);
link.addEventListener('mousemove', moveThumb);
link.addEventListener('mouseleave', hideThumb);
}
function isInsideCodeblock(link) {
return link.closest('.inlineCode, .aa, .katex, .katex-html, .hljs-built_in, .hljs-string, .embedContainer, .embed-wrapper, [data-embed], .embed') !== null;
}
function processYouTubeLink(link) {
if (link.dataset.enhanced) return;
if (isInsideCodeblock(link)) return;
const ytInfo = getYouTubeInfo(link.href);
if (!ytInfo) return;
link.dataset.enhanced = "1";
const cleanUrl = stripTrackingParams(link.href);
if (cleanUrl !== link.href) {
link.href = cleanUrl;
}
if (showIconsYoutube) {
if (ytInfo.type === 'video') {
const cleanId = sanitizeYouTubeId(ytInfo.id);
if (cleanId) {
fetchYouTubeTitle(cleanId).then(title => {
if (title) link.innerHTML = `${YT_ICON} ${title}`;
});
}
} else if (ytInfo.type === 'post') {
link.innerHTML = `${YT_ICON} ${link.textContent.trim()}`;
} else if (ytInfo.type === 'channel') {
const channelName = ytInfo.id.startsWith('@') ? ytInfo.id : `@${ytInfo.id}`;
link.innerHTML = `${YT_ICON} ${channelName}`;
}
}
if (showThumbnailsYoutube && ytInfo.type === 'video') {
const cleanId = sanitizeYouTubeId(ytInfo.id);
if (cleanId) {
const thumbUrl = getYouTubeThumbnailUrl(cleanId);
addThumbnailHover(link, thumbUrl, "YouTube thumbnail");
}
}
}
function extractTitleAndThumbnail(html) {
const result = { title: null, thumbnail: null };
const jsonLdMatch = html.match(/