您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Standardize font size, revert justified text, replace fonts with sans-serif/serif/monospace, dodge colored text from the background and lightly pastelize, detect centered text and add Uncenter toggle. Auto-hiding navbar sticky with style button (Light/Dark style selector + font size + padding widget).
// ==UserScript== // @name AH/QQ/SB/SV Mobile readability // @description Standardize font size, revert justified text, replace fonts with sans-serif/serif/monospace, dodge colored text from the background and lightly pastelize, detect centered text and add Uncenter toggle. Auto-hiding navbar sticky with style button (Light/Dark style selector + font size + padding widget). // @version 1.28 // @author C89sd // @namespace https://greasyfork.org/users/1376767 // @match https://*.spacebattles.com/* // @match https://*.sufficientvelocity.com/* // @match https://*.questionablequesting.com/* // @match https://*.alternatehistory.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @noframes // ==/UserScript== 'use strict'; const DISABLE_STICKY_HIDE = false; const FONT_SCALE_MONO = 0.8917; const FONT_SCALE_SERIF = 0.9108; const IS_THREAD = document.URL.includes('/threads/'); const toPx = (base, factor) => (base * factor).toFixed(2) + 'px'; const applyStyle = (styleNode, templateFn, basePx) => styleNode && (styleNode.textContent = templateFn(basePx)); let styleBbTable, styleSbStats; /* script el */ let cssBbTable, cssSbStats; /* templates */ if (IS_THREAD) { styleBbTable = document.head.appendChild(document.createElement('style')); cssBbTable = px => ` .bbTable{ font-size:${toPx(px, 14.0 / 15)}; overflow:scroll; table-layout:auto; } .bbTable td{ vertical-align:top; white-space:normal; word-wrap:normal; }`; /* 2) SB weekly-stats */ if (document.URL.includes('.1140820/')) { styleSbStats = document.head.appendChild(document.createElement('style')); cssSbStats = px => ` /* Indicator */ .bbTable td > b:first-of-type:has(code.bbCodeInline){display:inline-block;padding:2px 0;} .bbTable td > b:first-of-type code.bbCodeInline {font-size:${toPx(px, 11 / 15)};} /* Name / Metric */ .bbTable span:has(.username) {font-size:${toPx(px, 11.5 / 15)};} .bbTable span:has(.bbc-abbr) {font-size:${toPx(px, 12.5 / 15)};} /* Bottom Tags */ .bbTable td > span:last-child:has(>code.bbCodeInline){display:inline-flex;flex-wrap:wrap;gap:1px;padding-top:6px;} .bbTable td > span:last-child > code.bbCodeInline {font-size:${toPx(px, 9 / 15)};padding:1px 3px;}`; const width = { 2: '50%', 3: '33.33%' }; document.querySelectorAll('.bbTable table').forEach(t => { const cells = t.rows[0]?.cells; const n = cells?.length; if (width[n]) { t.style.cssText = 'table-layout:fixed;width:100%'; [...cells].forEach(c => (c.style.width = width[n])); } }); } } // ======================== Style + Font selector + Sticky Scroll Nav ======================== // /* ───── 1. Site-specific style IDs + path ───── */ const site = location.hostname.split('.').slice(-2, -1)[0]; const [LIGHT_ID, DARK_ID] = { spacebattles : [ 6, 2], sufficientvelocity : [19, 20], questionablequesting: [ 1, 31], alternatehistory : [13, 15] }[site]; const STYLE_PATH = site === 'alternatehistory' ? '/forum/misc/style' : '/misc/style'; /* ───── 2. CSS ───── */ let css = ` .ffss { font-family: Roboto, Segoe UI, Ubuntu, sans-serif !important; } .ffm { font-family: monospace !important; } .ffs { font-family: serif !important; } .p-nav { border-top: none !important; } .gm-navStyleBtn{ cursor:pointer; -webkit-user-select:none; /* stop long-press text selection */ user-select:none; } /* popup */ #gmStylePopup{ position:fixed;top:64px;right:20px;background:#fff;color:#000; border:1px solid #666;border-radius:8px;padding:16px;z-index:9999; box-shadow:0 4px 14px rgba(0,0,0,.25);display:none; flex-direction:column;gap:16px;min-width:280px;font-size:14px} @media (max-width:600px){#gmStylePopup{transform:scale(1.1);transform-origin:top right}} /* Light / Dark tiles */ .gm-tile-row{display:flex;width:100%;gap:8px} .gm-style-tile{ flex:1 1 50%;height:60px;line-height:60px;font-size:1.05em; border-radius:6px;text-align:center;cursor:pointer;user-select:none; border:1px solid #666;text-decoration:none} .gm-light{background:#eaeaea;color:#000}.gm-light:hover{background:#dbdbdb} .gm-dark {background:#222;color:#fff}.gm-dark:hover {background:#2d2d2d} /* Font-size block */ .gm-font-row{display:flex;align-items:center;gap:6px;justify-content:center} .gm-font-row button{ width:41.1px;height:50px;font-size:15px;cursor:pointer; border:1px solid #666;border-radius:6px;background:#f5f5f5;color:#000} .gm-font-row button:hover{background:#e4e4e4} .gm-font-row input{ width:70px;height:50px;text-align:center;font-size:16px; border:1px solid #666;border-radius:6px} .gmStickyNav{ position:sticky; top:0; /* docks here once we reach it */ z-index:1010; will-change:transform; /* we only translate it, no repaint */ } `; document.head.appendChild(Object.assign(document.createElement('style'), {textContent: css})); /* ───── HELPER FUNCTIONS (MOVED UP FOR CORRECT INITIALIZATION ORDER) ───── */ function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } const colorCache = new Map(); const adjustTextColor = (textRGBStr) => { if (colorCache.has(textRGBStr)) { return colorCache.get(textRGBStr); } function toRGB(str) { // can return up to 3 values [] to [r,g,b] const nums = str.match(/\d+/g); return nums ? nums.map(Number).slice(0, 3) : []; } let fg = toRGB(textRGBStr); // The background color for comparison is hardcoded here (rgb(39, 39, 39)). // Consider making this dynamic based on the actual page background if needed. const bg = [39, 39, 39]; // Represents the dark background color value (from "rgb(39, 39, 39)") if (fg.length !== 3) { return textRGBStr } const pastelize = ([r, g, b]) => { const neutral = 128; const whiteBoost = Math.pow((r + g + b) / (3 * 255), 10); const factor = 0.8 + 0.1*whiteBoost; return [ neutral + factor * (r - neutral), neutral + factor * (g - neutral), neutral + factor * (b - neutral) ].map(clamp); }; fg = pastelize(fg); let positiveDiff = 0; positiveDiff += Math.max(0, fg[0] - bg[0]); positiveDiff += Math.max(0, fg[1] - bg[1]); positiveDiff += Math.max(0, fg[2] - bg[2]); const threshold = 180; if (positiveDiff < threshold) { fg[0] = Math.max(fg[0], bg[0]); fg[1] = Math.max(fg[1], bg[1]); fg[2] = Math.max(fg[2], bg[2]); const avg = (fg[0]+fg[1]+fg[2])/3; const boost = Math.min(1.0, (Math.abs(fg[0]-avg)+Math.abs(fg[1]-avg)+Math.abs(fg[2]-avg))/255/0.2); // grays = 0, colors >= 1 let correction = Math.round((threshold - positiveDiff) / 3 + boost*40); fg = fg.map(c => Math.min(c + correction, 255)); } const result = `rgb(${fg[0]}, ${fg[1]}, ${fg[2]})`; colorCache.set(textRGBStr, result); return result; }; function applyFontSize(px) { px = px + 'px' const SANS_SERIF = /arial|tahoma|trebuchet ms|verdana/; const MONOSPACE = /courier new/; const SERIF = /times new roman|georgia|book antiqua/; const scale = (val, factor) => (parseFloat(val) * factor).toFixed(2) + "px"; document.querySelector('html').classList.add('ffss') const wrappers = document.getElementsByClassName('bbWrapper'); for (let wrapper of wrappers) { wrapper.style.fontSize = px; wrapper.classList.add('ffss') const children = wrapper.querySelectorAll('*'); for (let child of children) { const style = child.style; if (style.fontSize) { style.fontSize = ''; // Reset explicit font sizes for consistency } if (style.fontFamily) { const font = style.fontFamily; if (SANS_SERIF.test(font)) { child.classList.add('ffss') } else if (MONOSPACE.test(font)) { child.classList.add('ffm') style.fontSize = scale(px, FONT_SCALE_MONO); } else if (SERIF.test(font)) { child.classList.add('ffs') style.fontSize = scale(px, FONT_SCALE_SERIF); } } if (style.textAlign) { // Keep 'center' and 'right', clear others for readability if (style.textAlign !== 'center' && style.textAlign !== 'right') { style.textAlign = ''; } } if (style.color && style.color.startsWith('rgb')) { style.color = adjustTextColor(style.color); } } } /* Update dependent styles */ const basePx = parseFloat(px); applyStyle(styleBbTable, cssBbTable, basePx); applyStyle(styleSbStats, cssSbStats, basePx); } function applyPadding(px) { px = clamp(px, 0, 80) + 'px'; const cells = document.querySelectorAll('.message-cell.message-cell--main'); for (const cell of cells) { cell.style.paddingLeft = px; cell.style.paddingRight = px; } } /* ───── END HELPER FUNCTIONS ───── */ /* ───── Sticky bar injection ───── */ /* 1. pick the nav and keep only our class */ const sticky = document.querySelector('.p-navSticky.p-navSticky--primary') || document.querySelector('.p-nav'); sticky.classList.add('gmStickyNav'); sticky.classList.remove('p-navSticky', 'p-navSticky--primary', 'is-sticky'); sticky.removeAttribute('data-xf-init'); /* 2. choose algorithm */ const useOld = location.origin .includes('alternatehistory'); /* 3. state common to both algorithms */ let barH = sticky.offsetHeight; let offset = 0; let lastY = scrollY; /* ────────────────────────────────────────────────────────────── A) OLD algorithm (once-only measurement of page position) ────────────────────────────────────────────────────────────────*/ if (!DISABLE_STICKY_HIDE && useOld) { /* a. measure the element’s true document position */ function measure() { const prev = sticky.style.position; sticky.style.position = 'static'; // neutralise sticky const originTop = sticky.getBoundingClientRect().top + scrollY; sticky.style.position = prev; // restore return { originTop, h: sticky.offsetHeight }; } let { originTop } = measure(); function onScrollOld() { const y = scrollY; /* still above the slot → keep bar fully visible */ if (y < originTop) { offset = 0; sticky.style.transform = ''; lastY = y; return; } /* bar is docked → pixel-for-pixel retract / reveal */ const dy = y - lastY; // + down, – up lastY = y; offset = clamp(offset + dy, 0, barH); sticky.style.transform = `translateY(${-offset}px)`; } addEventListener('scroll', onScrollOld, { passive:true }); addEventListener('resize', () => { ({ originTop, h:barH } = measure()); }); } /* ────────────────────────────────────────────────────────────── B) NEW algorithm (live getBoundingClientRect check) ────────────────────────────────────────────────────────────────*/ else if (!DISABLE_STICKY_HIDE) { let stuck = false; // “is the bar currently touching the viewport top?” function onScrollNew() { const y = scrollY; const top = sticky.getBoundingClientRect().top; // distance to viewport top /* bar still in normal flow */ if (top > 0) { if (stuck) { stuck = false; offset = 0; sticky.style.transform = ''; } lastY = y; return; } /* bar just became sticky */ if (!stuck) { stuck = true; lastY = y; // reset baseline → no initial jump return; } /* regular retract / reveal */ const dy = y - lastY; // + down, – up lastY = y; offset = clamp(offset + dy, 0, barH); sticky.style.transform = `translateY(${-offset}px)`; } addEventListener('scroll', onScrollNew, { passive:true }); addEventListener('resize', () => { barH = sticky.offsetHeight; }); } /* 4. Remove old XF button, add our own to the right of Jump */ let Tbtn = document.querySelector('.p-nav-opposite > div[data-xf-click="sv-font-size-chooser-form"]') Tbtn?.remove(); const navOpposite = document.querySelector('#top .p-nav-opposite'); if (navOpposite) { navOpposite.querySelector('#js-XFUniqueId1')?.closest('div')?.remove(); /* ─── NEW: use <a> instead of <div> ─── */ const btn = document.createElement('div'); btn.className = 'p-navgroup-link gm-navStyleBtn'; btn.setAttribute('role', 'button'); // accessibility btn.textContent = 'Style'; const firstLink = navOpposite.querySelector('.p-navgroup-link'); if (firstLink && firstLink.textContent.trim() === 'Jump') firstLink.after(btn); else navOpposite.insertBefore(btn, navOpposite.firstChild); btn.onclick = (event) => { event.preventDefault(); event.stopPropagation(); togglePopup(); }; } /* ───── 5. Build popup once ───── */ let popup; popup = buildPopup(); // This call now happens *after* `applyFontSize` is defined document.body.appendChild(popup); /* ───── 6. Popup builder ───── */ function buildPopup() { const p = Object.assign(document.createElement('div'), { id: 'gmStylePopup' }); /* Light / Dark */ const tiles = Object.assign(document.createElement('div'), { className: 'gm-tile-row' }); tiles.append(makeTile('Light', LIGHT_ID, 'gm-light'), makeTile('Dark', DARK_ID, 'gm-dark')); p.appendChild(tiles); /* Font size */ const fsRow = Object.assign(document.createElement('div'), { className: 'gm-font-row' }); const stored = clamp(Number(GM_getValue('fontSize', 15)), 10, 25); const num = Object.assign(document.createElement('input'), { type: 'number', step: '0.1', min: '10', max: '25', value: stored.toFixed(1) }); fsRow.append( makeFSBtn('-1', -1), makeFSBtn('-0.1', -.1), num, makeFSBtn('+0.1', .1),makeFSBtn('+1', 1) ); p.appendChild(fsRow); applyFS(stored); num.addEventListener('change', () => applyFS(parseFloat(num.value))); /* Padding size */ const padRow = Object.assign(document.createElement('div'), { className: 'gm-font-row' }); const storedPad = clamp(Number(GM_getValue('paddingSize', 10)), 0, 80); const padNum = Object.assign(document.createElement('input'), { type: 'number', step: '0.1', min: '0', max: '80', value: storedPad.toFixed(1) }); padRow.append( makePadBtn('-1', -1), makePadBtn('-0.1', -.1), padNum, makePadBtn('+0.1', .1),makePadBtn('+1', 1) ); p.appendChild(padRow); applyPad(storedPad); padNum.addEventListener('change', () => applyPad(parseFloat(padNum.value))); function makePadBtn(txt, delta) { const b = document.createElement('button'); b.textContent = txt; b.addEventListener('click', () => applyPad(parseFloat(padNum.value) + delta)); return b; } function applyPad(px) { px = clamp(Math.round(px * 10) / 10, 0, 80); padNum.value = px.toFixed(1); GM_setValue('paddingSize', px); applyPadding(px); } function makeFSBtn(txt, delta) { const b = document.createElement('button'); b.textContent = txt; b.addEventListener('click', () => applyFS(parseFloat(num.value) + delta)); return b; } function applyFS(px) { px = clamp(Math.round(px * 10) / 10, 10, 25); num.value = px.toFixed(1); GM_setValue('fontSize', px); applyFontSize(px); } return p; } /* ───── 7. Style tiles ───── */ function makeTile(label, id, cls) { const token = document.querySelector('input[name="_xfToken"]')?.value || ''; const url = `${STYLE_PATH}?style_id=${id}&_xfRedirect=/&t=${encodeURIComponent(token)}`; const a = Object.assign(document.createElement('a'), { className: `gm-style-tile ${cls}`, textContent: label, href: url }); a.addEventListener('click', ev => { ev.preventDefault(); fetch(url, { credentials: 'same-origin', mode: 'same-origin' }) .then(r => { if (!r.ok) throw 0; return r.text(); }) .then(() => location.reload()) .catch(() => (location.href = url)); }); return a; } /* ───── 8. Popup show / hide ───── */ function togglePopup() { popup.style.display = popup.style.display === 'flex' ? 'none' : 'flex'; } document.addEventListener('click', e => { if (!popup.contains(e.target) && !e.target.closest('.gm-navStyleBtn')) popup.style.display = 'none'; }); // ========================================================== // if (!IS_THREAD) return; // Centered text toggle const OP = document.querySelector('.username.u-concealed')?.textContent || '!'; const messages = document.getElementsByClassName('message'); let foundCentered = false; for (let message of messages) { if (message.getAttribute('data-author') === OP || message.classList.contains('hasThreadmark')) { const centeredDiv = message.querySelector('.bbWrapper:first-of-type > div[style*="text-align: center"], .bbWrapper > div.uncentered'); if (centeredDiv) { foundCentered = true; break; } } } if (foundCentered) { let buttonGrp = document.querySelector('.block-outer-threadmarks .buttonGroup'); let centered = true; if (buttonGrp) { buttonGrp.insertAdjacentHTML('afterbegin', '<div class="buttonGroup buttonGroup-buttonWrapper"><span class="button--link button" style="cursor:pointer" title="Toggle center text">Uncenter</span></div>'); buttonGrp.firstElementChild.firstElementChild.addEventListener('click', function() { document.querySelectorAll(centered ? '.bbWrapper > div[style*="text-align: center"]' : '.bbWrapper > div.uncentered').forEach(el => { if (centered) { el.classList.add('uncentered'); el.style.textAlign = ''; } else { el.style.textAlign = 'center'; } }); centered = !centered; }); } }