// ==UserScript== // @name NodeSeek Enhance // @namespace http://www.nodeseek.com/ // @version 1.3 // @description 【NodeSeek增强脚本】全面增强 NodeSeek/DeepFlood 论坛体验。核心功能:自动签到(支持随机/固定鸡腿)、无缝翻页(帖子列表和评论自动加载)、快捷回复(浮动编辑器)、代码高亮(支持亮色/暗色主题)、图片滑动查看。内容管理:屏蔽用户/帖子/分类(支持关键词和正则)、帖子排序(按回复数/查看数/已访问置底)、隐藏元素(页脚/分类标签/帖子信息/推荐轮播/用户统计面板)。界面优化:紧凑模式(1-4列多栏布局,自定义间距/字体/头像大小)、自定义背景图(支持URL/本地上传)、Header/Frame透明度(支持模糊和饱和度)、字体排版自定义(标题/Tag/元数据/内容)、已访问链接标记(可隐藏/置底)、默认头像替换(自动识别系统头像)。增强功能:用户卡片扩展(显示@我/私信/回复通知)、等级标签显示(楼主等级和统计信息)、浏览历史记录(下拉菜单快速访问)、键盘快捷键(左右箭头翻页、Ctrl+Enter提交)、强制新标签页打开帖子、自动跳转外部链接、平滑滚动、即时页面预加载。设置管理:可视化设置面板(集成到导航栏)、设置导入导出、一键恢复默认样式。支持深色模式自动适配。 // @author bbb // @match *://www.nodeseek.com/* // @match *://www.deepflood.com/* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACz0lEQVR4Ae3B32tVdQAA8M85u7aVHObmzJVD0+ssiphstLEM62CBlCBEIAYhUoGGD/kiRUo+9CIEElFZgZJFSApBVhCUX2WFrVQKf5Qy26SgdK4pN7eZu+cbtyfJ/gLx83HD9SAhlEyXupiPhUSTeonRfNw1ws2aRJeN5jHcolFhJJ9M8Zj99piDTnv12SjzfzIb9dmrC7Pttt8ykjDVLsu8ZZ1GH1oqeDofJLtJh4fMEw3Y72jlCuEO2+W+sNJFr3vOZ1YIi8NIGA29hDWhGgZDJ2Rt2ZvZSBazmMUsZsPZ1qwVQmcYDNWwhtAbRsNIWJx6WLPDfgxNVkm9nR8hm+XduLba7F9RtcXztmUzyY/YJrUqNPvBYc0eSS3CwXxMl4WG7CarsyEuvU2HOkRNujSw3PosxR6DFurKxx3E/akFohPo0aDfEO61os5LdrtLVWG1TzxokifdiSH9GnTjuGhBqsWE39GOo3kVi8wsmeVW00SJ200zA9r0kFcdQzv+MKElVW/S+L5EE86pmUth3BV/SzCOCUjMVXMWzfsSYybVl1SlSlESkagpuOI1nzshFX1gyAF1UKhJEKOkJFVNXVBv+pJoBK1qBkh86z1/SaR+9o5zEgoDaloxsiSart6F1Bkl83ESHWEKvvEbqZJETaokgSH9hCk6cBLtSs6kDqEb/cZ0K+MnO0X/VdhRGUBZjzH9uA+HUl+a0BvmO+J7bVZSKWz1kehqhfe9oWalNoccDmW9JnyV+toxsy3PK3aY9Gx4gMp567ziV4WawpCXra+MEhZ5xqTtecVycxzXlxA22OK4ZYbt9LjvrM5PkNUp6zVPdNpBv1QKwt126Paxp8zwqXu8kG8pYZdHlT2Rvxo2aVG2ObyYn65UnXLKVULZZrP02ZRfCms1OmAXCSHRYqrLzuZFaDFV6s/8omuERs0Kl/LzITVTvTHDeXTD9eAftAsSYhXYOWUAAAAASUVORK5CYII= // @require https://s4.zstatic.net/ajax/libs/layui/2.9.9/layui.min.js // @resource highlightStyle https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css // @resource highlightStyle_dark https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_notification // @grant GM_getResourceURL // @grant GM_addElement // @grant GM_addStyle // @grant GM_openInTab // @grant unsafeWindow // @run-at document-start // @license GPL-3.0 License // @downloadURL https://update.greasyfork.icu/scripts/555408/NodeSeek%20Enhance.user.js // @updateURL https://update.greasyfork.icu/scripts/555408/NodeSeek%20Enhance.meta.js // ==/UserScript== (function () { 'use strict'; const { version, author, name, icon } = GM_info.script; const BASE_URL = location.origin; const SITE_MAP = { "www.nodeseek.com": { code: "ns", name: "NodeSeek", url: "https://www.nodeseek.com/" }, "www.deepflood.com": { code: "df", name: "DeepFlood", url: "https://www.deepflood.com/" } }; const DEFAULT_COMPACT_MODE = Object.freeze({ enabled: true, columns: 1, padding: "6px 10px", avatarSize: 26, titleFontSize: 13, infoFontSize: 10, marginBottom: 2 }); const COMPACT_MODE_STYLE_ID = 'nsx-compact-mode-style'; const CUSTOM_BACKGROUND_STYLE_ID = 'nsx-custom-background-style'; const VISITED_LINK_STYLE_ID = 'nsx-visited-links-style'; const HEADER_OPACITY_STYLE_ID = 'nsx-header-opacity-style'; const FRAME_OPACITY_STYLE_ID = 'nsx-frame-opacity-style'; const TYPOGRAPHY_STYLE_IDS = { title: 'nsx-typography-title-style', tag: 'nsx-typography-tag-style', meta: 'nsx-typography-meta-style', content: 'nsx-typography-content-style' }; const escapeCssValue = (value = '') => `${value}`.replace(/"/g, '\\"'); const INITIAL_OVERLAY_CLASS = 'nsx-initial-hide'; const INITIAL_OVERLAY_DURATION_DEFAULT = 1200; let initialOverlayTimer = null; const toggleRootClass = (className, enabled = true) => { const root = document.documentElement; root.classList.toggle(className, enabled); const body = document.body; if (body) { body.classList.toggle(className, enabled); return; } if (!enabled) return; const applyToBody = () => { if (document.body) { document.body.classList.toggle(className, enabled); return true; } return false; }; if (typeof MutationObserver === 'function') { const observer = new MutationObserver((mutations, obs) => { if (applyToBody()) { obs.disconnect(); } }); observer.observe(root, { childList: true }); } else { const onReady = () => { applyToBody(); document.removeEventListener('DOMContentLoaded', onReady); }; document.addEventListener('DOMContentLoaded', onReady, { once: true }); } }; const addCompactPendingClass = () => { const apply = () => { document.documentElement.classList.add('nsx-compact-mode-pending'); document.documentElement.classList.add('nsx-body-hidden'); document.body?.classList.add('nsx-compact-mode-pending'); document.body?.classList.add('nsx-body-hidden'); }; if (document.body) { apply(); } else { document.addEventListener('DOMContentLoaded', apply, { once: true }); } }; const removeCompactPendingClass = () => { document.documentElement.classList.remove('nsx-compact-mode-pending'); document.documentElement.classList.remove('nsx-body-hidden'); document.body?.classList.remove('nsx-compact-mode-pending'); document.body?.classList.remove('nsx-body-hidden'); }; const getOverlayStoredSettings = () => { const stored = getStoredSettings(); const overlay = stored && stored.loading_overlay ? stored.loading_overlay : {}; return { enabled: overlay.enabled !== false, duration: parseInt(overlay.duration, 10) || INITIAL_OVERLAY_DURATION_DEFAULT }; }; const createInitialOverlay = (() => { let cssInjected = false; return (_forceDuration = null, forceEnabled = null) => { const storedSettings = getOverlayStoredSettings(); const enabled = forceEnabled !== null ? forceEnabled : storedSettings.enabled; if (!enabled) return; if (!cssInjected) { GM_addStyle(` html.${INITIAL_OVERLAY_CLASS} body { opacity: 0 !important; transition: opacity 0.4s ease; } `); cssInjected = true; } document.documentElement.classList.add(INITIAL_OVERLAY_CLASS); }; })(); const removeInitialOverlay = () => { document.documentElement.classList.remove(INITIAL_OVERLAY_CLASS); initialOverlayTimer && clearTimeout(initialOverlayTimer); }; const scheduleRemoveInitialOverlay = (() => { let loadHandlerRegistered = false; let pendingDuration = INITIAL_OVERLAY_DURATION_DEFAULT; let overlayEnabled = true; const reveal = () => { removeInitialOverlay(); removeCompactPendingClass(); }; const runRemoval = () => { initialOverlayTimer && clearTimeout(initialOverlayTimer); if (!overlayEnabled) { reveal(); return; } initialOverlayTimer = setTimeout(() => { if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(reveal); } else { reveal(); } }, pendingDuration); }; return (duration = INITIAL_OVERLAY_DURATION_DEFAULT, enabled = true) => { const delay = Number.isFinite(duration) ? duration : parseInt(duration, 10); pendingDuration = Number.isFinite(delay) && delay >= 0 ? delay : INITIAL_OVERLAY_DURATION_DEFAULT; overlayEnabled = enabled !== false; if (!overlayEnabled) { reveal(); return; } if (document.readyState === 'complete') { runRemoval(); return; } if (!loadHandlerRegistered) { loadHandlerRegistered = true; window.addEventListener('load', runRemoval, { once: true }); } }; })(); const ensureStyleElement = (id) => { let style = document.getElementById(id); if (!style) { style = document.createElement('style'); style.id = id; (document.head || document.documentElement).appendChild(style); } return style; }; const applyCustomBackgroundStyle = (config) => { if (!config) { const existing = document.getElementById(CUSTOM_BACKGROUND_STYLE_ID); if (existing) existing.remove(); return; } const style = ensureStyleElement(CUSTOM_BACKGROUND_STYLE_ID); style.textContent = ` body { background-image: url("${escapeCssValue(config.url)}") !important; background-repeat: ${config.repeat} !important; background-position: ${config.position} !important; background-size: ${config.size} !important; background-attachment: ${config.attachment} !important; } `; }; const applyVisitedLinkStyle = (config) => { if (!config) { const existing = document.getElementById(VISITED_LINK_STYLE_ID); if (existing) existing.remove(); return; } const { lightLink, lightVisited, darkLink, darkVisited } = config; const style = ensureStyleElement(VISITED_LINK_STYLE_ID); const lightVars = []; if (lightLink) lightVars.push(` --link-color: ${lightLink};`); lightVars.push(` --link-visited-color: ${lightVisited};`); const darkVars = []; if (darkLink) darkVars.push(` --link-color: ${darkLink};`); darkVars.push(` --link-visited-color: ${darkVisited};`); style.textContent = ` :root { ${lightVars.join('\n')} } body.dark-layout { ${darkVars.join('\n')} } .post-list .post-title a:visited { color: var(--link-visited-color); } `; }; const applyHeaderOpacityCSS = (config) => { const style = ensureStyleElement(HEADER_OPACITY_STYLE_ID); if (!config) { style.textContent = ''; return; } const clamped = Math.min(1, Math.max(0, config.value ?? 0.92)); const darkValue = Math.min(1, Math.max(0, clamped - 0.1)); const blurEnabled = config.blur_enabled !== false; const saturateEnabled = config.saturate_enabled !== false; const blur = blurEnabled ? Math.max(0, parseFloat(config.blur) || 0) : 0; const saturate = saturateEnabled ? Math.max(0, parseFloat(config.saturate) || 0) : 0; const blurCSS = blurEnabled ? ` blur(${blur}px)` : ''; const saturateCSS = saturateEnabled ? ` saturate(${saturate}%)` : ''; const effectCSS = (blurEnabled || saturateEnabled) ? ` backdrop-filter:${saturateCSS || ''}${blurCSS || ''}; -webkit-backdrop-filter:${saturateCSS || ''}${blurCSS || ''}; ` : ''; style.textContent = ` html.nsx-header-opacity body > header, html.nsx-header-opacity body > header #nsk-head { background-color: rgba(255,255,255, ${clamped}) !important; ${effectCSS}} html.nsx-header-opacity body.dark-layout > header, html.nsx-header-opacity body.dark-layout > header #nsk-head { background-color: rgba(20,20,20, ${darkValue}) !important; ${effectCSS}} `; }; const applyFrameOpacityCSS = (config) => { const style = ensureStyleElement(FRAME_OPACITY_STYLE_ID); if (!config) { style.textContent = ''; return; } const clamped = Math.min(1, Math.max(0, config.value ?? 0.95)); const darkValue = Math.min(1, Math.max(0, clamped - 0.1)); const blurEnabled = config.blur_enabled !== false; const saturateEnabled = config.saturate_enabled !== false; const blur = blurEnabled ? Math.max(0, parseFloat(config.blur) || 0) : 0; const saturate = saturateEnabled ? Math.max(0, parseFloat(config.saturate) || 0) : 0; const blurCSS = blurEnabled ? ` blur(${blur}px)` : ''; const saturateCSS = saturateEnabled ? ` saturate(${saturate}%)` : ''; const effectCSS = (blurEnabled || saturateEnabled) ? ` backdrop-filter:${saturateCSS || ''}${blurCSS || ''}; -webkit-backdrop-filter:${saturateCSS || ''}${blurCSS || ''}; ` : ''; style.textContent = ` html.nsx-frame-opacity #nsk-body, html.nsx-frame-opacity .nsk-body, html.nsx-frame-opacity #nsk-frame, html.nsx-frame-opacity .nsk-frame { background-color: rgba(255,255,255, ${clamped}) !important; ${effectCSS}} html.nsx-frame-opacity body.dark-layout #nsk-body, html.nsx-frame-opacity body.dark-layout .nsk-body, html.nsx-frame-opacity body.dark-layout #nsk-frame, html.nsx-frame-opacity body.dark-layout .nsk-frame { background-color: rgba(20,20,20, ${darkValue}) !important; ${effectCSS}} `; }; const buildTypographyCSS = (type, config) => { const selectors = { title: '.post-list .post-title a, .post-title a', tag: '.post-list .post-category, .post-category, .post-list .nsk-badge', meta: '.post-list .post-info, .post-info, .post-meta-info, .content-info', content: '.post-content, .post-content *:not(svg):not(path)' }; const selector = selectors[type]; if (!selector) return ''; const declarations = []; const normalize = (val) => typeof val === 'string' ? val.trim() : ''; const fontFamily = normalize(config.fontFamily); const fontSize = normalize(config.fontSize); const color = normalize(config.color); const fontStyle = normalize(config.fontStyle); const letterSpacing = normalize(config.letterSpacing); const ligatures = normalize(config.ligatures); if (fontFamily) declarations.push(`font-family: ${fontFamily} !important;`); if (fontSize) declarations.push(`font-size: ${fontSize} !important;`); if (color) declarations.push(`color: ${color} !important;`); if (fontStyle) declarations.push(`font-style: ${fontStyle} !important;`); if (letterSpacing) declarations.push(`letter-spacing: ${letterSpacing} !important;`); if (ligatures) declarations.push(`font-variant-ligatures: ${ligatures} !important;`); if (declarations.length === 0) return ''; return ` ${selector} { ${declarations.join('\n ')} } `; }; const applyTypographyStyle = (type, config) => { const styleId = TYPOGRAPHY_STYLE_IDS[type]; if (!styleId) return; if (!config || config.enabled === false) { const existing = document.getElementById(styleId); if (existing) existing.remove(); return; } const css = buildTypographyCSS(type, config); if (!css.trim()) { document.getElementById(styleId)?.remove(); return; } const style = ensureStyleElement(styleId); style.textContent = css; }; const normalizeCompactConfig = (config = {}) => ({ enabled: config.enabled !== false, columns: parseInt(config.columns, 10) || DEFAULT_COMPACT_MODE.columns, padding: config.padding || DEFAULT_COMPACT_MODE.padding, avatarSize: parseInt(config.avatarSize, 10) || DEFAULT_COMPACT_MODE.avatarSize, titleFontSize: parseInt(config.titleFontSize, 10) || DEFAULT_COMPACT_MODE.titleFontSize, infoFontSize: parseInt(config.infoFontSize, 10) || DEFAULT_COMPACT_MODE.infoFontSize, marginBottom: parseInt(config.marginBottom, 10) || DEFAULT_COMPACT_MODE.marginBottom }); const buildCompactModeCSS = (config) => { const normalized = normalizeCompactConfig(config); const columns = normalized.columns; const padding = normalized.padding; const avatarSize = normalized.avatarSize; const titleFontSize = normalized.titleFontSize; const infoFontSize = normalized.infoFontSize; const marginBottom = normalized.marginBottom; const containerWidth = columns > 1 ? `${1080 + (columns - 1) * 400}px` : '1080px'; const containerWidthNum = columns > 1 ? 1080 + (columns - 1) * 400 : 1080; const gridGapX = Math.max(8, marginBottom * 2); const iconSize = Math.max(12, infoFontSize - 2); return ` /* 紧凑模式 - 多列布局 */ html.nsx-compact-mode .nsk-container { width: ${containerWidth} !important; max-width: 98% !important; } html.nsx-compact-mode #nsk-body-left { flex: 1 !important; min-width: 0 !important; } html.nsx-compact-mode #nsk-body-left .post-list { display: grid !important; grid-template-columns: repeat(${columns}, 1fr) !important; gap: ${marginBottom}px ${gridGapX}px !important; padding: 4px 0 !important; } html.nsx-compact-mode #nsk-head { position: relative !important; } html.nsx-compact-mode #nsk-head .search-box { position: absolute !important; left: 50% !important; transform: translateX(-50%) !important; margin-left: 0 !important; flex: 0 1 170px !important; max-width: 290px !important; z-index: 2 !important; pointer-events: auto !important; transition: none !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item { padding: ${padding} !important; margin-bottom: ${marginBottom}px !important; border-bottom: 1px solid rgba(0,0,0,.05) !important; display: flex !important; flex-direction: row !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-title { font-size: ${titleFontSize}px !important; line-height: 1.35 !important; margin-bottom: 3px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-title a { font-size: inherit !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-info { font-size: ${infoFontSize}px !important; line-height: 1.25 !important; margin-top: 2px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .avatar-wrapper, html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .avatar-normal { width: ${avatarSize}px !important; height: ${avatarSize}px !important; margin-right: 6px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-list-content { margin-left: 6px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .nsk-badge, html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .role-tag { font-size: ${infoFontSize}px !important; padding: 1px 4px !important; margin: 0 2px !important; line-height: 1.1 !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .content-info, html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-meta-info { margin-top: 1px !important; gap: 3px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .content-info > span, html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .post-meta-info > span { margin-right: 4px !important; } html.nsx-compact-mode #nsk-body-left .post-list .post-list-item svg, html.nsx-compact-mode #nsk-body-left .post-list .post-list-item .iconpark-icon { width: ${iconSize}px !important; height: ${iconSize}px !important; } html.nsx-compact-mode .nsx-user-stats { font-size: ${infoFontSize - 1}px !important; margin-top: 1px !important; gap: 3px !important; } html.nsx-compact-mode .nsx-user-stats span { font-size: inherit !important; } @media screen and (max-width: 1400px) { html.nsx-compact-mode .nsk-container { width: auto !important; max-width: 98% !important; } html.nsx-compact-mode #nsk-body-left .post-list { grid-template-columns: repeat(${Math.min(columns, 3)}, 1fr) !important; } } @media screen and (max-width: 1200px) { html.nsx-compact-mode #nsk-body-left .post-list { grid-template-columns: repeat(${Math.min(columns, 2)}, 1fr) !important; } } @media screen and (max-width: 800px) { html.nsx-compact-mode #nsk-body-left .post-list { grid-template-columns: 1fr !important; } html.nsx-compact-mode #nsk-head .search-box { position: static !important; transform: none !important; left: auto !important; margin-left: auto !important; flex: 0 1 auto !important; transition: none !important; } } html.nsx-compact-mode #nsk-body-left .post-list .blocked-post, html.nsx-compact-mode #nsk-body-left .post-list-item.blocked-post { display: none !important; } /* 紧凑模式下调整快速导航按钮位置,根据容器宽度动态计算 */ html.nsx-compact-mode #fast-nav-button-group { right: calc(50% - ${Math.floor(containerWidthNum / 2)}px + 20px) !important; } /* 响应式调整 */ @media screen and (max-width: 1400px) { html.nsx-compact-mode #fast-nav-button-group { right: calc(50% - ${Math.floor(Math.min(containerWidthNum, 1480) / 2)}px + 20px) !important; } } @media screen and (max-width: 1200px) { html.nsx-compact-mode #fast-nav-button-group { right: calc(50% - ${Math.floor(Math.min(containerWidthNum, 1280) / 2)}px + 20px) !important; } } @media screen and (max-width: 800px) { html.nsx-compact-mode #fast-nav-button-group { right: 30px !important; } } `; }; const applyCompactModeStyleFromConfig = (config, options = {}) => { const { delayReveal = false } = options; if (!config) { const existing = document.getElementById(COMPACT_MODE_STYLE_ID); if (existing) existing.remove(); removeCompactPendingClass(); return; } const style = ensureStyleElement(COMPACT_MODE_STYLE_ID); style.textContent = buildCompactModeCSS(config); if (!delayReveal) { requestAnimationFrame(removeCompactPendingClass); } }; const injectBaseUtilityStyles = (() => { let injected = false; return () => { if (injected) return; injected = true; GM_addStyle(` html.nsx-hide-footer footer { display: none !important; } html.nsx-hide-post-category .post-category, html.nsx-hide-post-category a.info-item.post-category { display: none !important; } html.nsx-hide-post-info .post-info { display: none !important; } html.nsx-hide-topic-carousel .topic-carousel-wrapper { display: none !important; } html.nsx-body-hidden, body.nsx-body-hidden { visibility: hidden !important; } .nsx-right-panel-highlight { background-color: #ffeb3b !important; color: #000 !important; padding: 1px 2px !important; border-radius: 2px !important; font-weight: inherit !important; text-decoration: inherit !important; display: inline !important; } body.dark-layout .nsx-right-panel-highlight { background-color: #f57f17 !important; color: #fff !important; } /* 确保在链接中也能正确显示 */ a .nsx-right-panel-highlight, .post-title .nsx-right-panel-highlight, .post-title a .nsx-right-panel-highlight, mark.nsx-right-panel-highlight { background-color: #ffeb3b !important; color: #000 !important; padding: 1px 2px !important; border-radius: 2px !important; display: inline !important; } body.dark-layout a .nsx-right-panel-highlight, body.dark-layout .post-title .nsx-right-panel-highlight, body.dark-layout .post-title a .nsx-right-panel-highlight, body.dark-layout mark.nsx-right-panel-highlight { background-color: #f57f17 !important; color: #fff !important; } .nsx-highlight-input-panel textarea { font-family: inherit; width: 100%; box-sizing: border-box; } .nsx-highlight-input-panel label { user-select: none; } .nsx-highlight-input-panel code { background-color: rgba(0, 0, 0, 0.05); border-radius: 3px; padding: 2px 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 11px; color: #e83e8c; } body.dark-layout .nsx-highlight-input-panel code { background-color: rgba(255, 255, 255, 0.1); color: #ff6b9d; } `); }; })(); // 早期注入紧凑模式样式,避免页面闪烁 // 在脚本最开始就读取配置并注入样式 const getStoredSettings = () => { try { const stored = GM_getValue('settings'); return stored || null; } catch (e) { return null; } }; const getCompactModeConfig = () => { const stored = getStoredSettings(); if (!stored) return normalizeCompactConfig({ ...DEFAULT_COMPACT_MODE }); const compactMode = stored.compact_mode ?? {}; if (compactMode.enabled === false) return null; return normalizeCompactConfig({ ...DEFAULT_COMPACT_MODE, ...compactMode }); }; const getVisitedLinksStoredConfig = () => { const stored = getStoredSettings(); if (!stored) return null; const cfg = stored.visited_links || {}; if (cfg.enabled === false) return null; return { lightLink: (cfg.link_color || '').trim(), lightVisited: (cfg.visited_color || '').trim() || '#afb9c1', darkLink: (cfg.dark_link_color || '').trim(), darkVisited: (cfg.dark_visited_color || '').trim() || cfg.visited_color || '#393f4e' }; }; const getCustomBackgroundStoredConfig = () => { const stored = getStoredSettings(); if (!stored) return null; const cfg = stored.custom_background || {}; if (cfg.enabled === false || !cfg.url) return null; return { url: cfg.url, repeat: cfg.repeat || 'repeat', position: cfg.position || 'center', size: cfg.size || 'cover', attachment: cfg.attachment || 'scroll' }; }; const getHeaderOpacityStoredConfig = () => { const stored = getStoredSettings(); if (!stored) return null; return stored.header_opacity || null; }; const getFrameOpacityStoredConfig = () => { const stored = getStoredSettings(); if (!stored) return null; return stored.frame_opacity || null; }; const applyEarlyVisibilityClasses = () => { const stored = getStoredSettings(); if (!stored) return; if (stored.hide_footer?.enabled) toggleRootClass('nsx-hide-footer', true); if (stored.hide_post_category?.enabled) toggleRootClass('nsx-hide-post-category', true); if (stored.hide_post_info?.enabled) toggleRootClass('nsx-hide-post-info', true); if (stored.hide_topic_carousel?.enabled) toggleRootClass('nsx-hide-topic-carousel', true); if (stored.remove_promotions?.enabled) toggleRootClass('nsx-remove-promotions', true); if (stored.visited_links?.hide_visited) toggleRootClass('nsx-hide-visited', true); }; const applyEarlyVisitedLinksStyle = () => { const config = getVisitedLinksStoredConfig(); if (!config) return; applyVisitedLinkStyle(config); }; const applyEarlyCustomBackgroundStyle = () => { const config = getCustomBackgroundStoredConfig(); if (!config) return; applyCustomBackgroundStyle(config); }; const applyEarlyHeaderOpacityStyle = () => { const config = getHeaderOpacityStoredConfig(); if (!config || config.enabled !== true) return; toggleRootClass('nsx-header-opacity', true); applyHeaderOpacityCSS(config); }; const applyEarlyFrameOpacityStyle = () => { const config = getFrameOpacityStoredConfig(); if (!config || config.enabled !== true) return; toggleRootClass('nsx-frame-opacity', true); applyFrameOpacityCSS(config); }; // 立即注入早期样式 injectBaseUtilityStyles(); const injectEarlyCompactModeStyle = () => { const overlaySettings = getOverlayStoredSettings(); const overlayEnabled = overlaySettings.enabled; if (overlayEnabled) { createInitialOverlay(null, true); } else { removeInitialOverlay(); } const config = getCompactModeConfig(); if (!config) { removeCompactPendingClass(); return; } addCompactPendingClass(); toggleRootClass('nsx-compact-mode', true); applyCompactModeStyleFromConfig(config, { delayReveal: overlayEnabled }); if (overlayEnabled) { scheduleRemoveInitialOverlay(overlaySettings.duration, true); } }; // 立即执行 injectEarlyCompactModeStyle(); applyEarlyVisibilityClasses(); applyEarlyVisitedLinksStyle(); applyEarlyCustomBackgroundStyle(); applyEarlyHeaderOpacityStyle(); applyEarlyFrameOpacityStyle(); const onDocumentReady = (fn) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn, { once: true }); } else { fn(); } }; class BroadcastManager { static instances = new Map(); constructor(channelName = "nsx_channel") { if (BroadcastManager.instances.has(channelName)) { return BroadcastManager.instances.get(channelName); } this.channelName = channelName; this.myId = `${Date.now()}-${Math.random()}`; this.receivers = []; this.ch = new BroadcastChannel(channelName); this.KEY = `only_last_tab_${channelName}`; // 广播接收 this.ch.onmessage = e => this.receivers.forEach(fn => fn(e.data)); // 主控权管理 localStorage.setItem(this.KEY, this.myId); this.updateActive(); // 事件监听 addEventListener("storage", e => { if (e.key === this.KEY) { e.newValue || localStorage.setItem(this.KEY, this.myId); this.updateActive(); } }); addEventListener("beforeunload", () => { this.active && localStorage.removeItem(this.KEY); }); BroadcastManager.instances.set(channelName, this); } updateActive() { this.active = localStorage.getItem(this.KEY) === this.myId; } registerReceiver(fn) { this.receivers.push(fn); } broadcast(data) { const message = { sender: this.myId, data }; this.ch.postMessage(message); this.receivers.forEach(fn => fn(message)); } startTask(taskFn, interval) { setInterval(async () => { if (this.active) { try { this.broadcast(await taskFn()); } catch (err) { // console.error(`[Tab ${this.myId}] 任务出错:`, err); } } }, interval); } } const util = { clog:(c) => { // console.group(`%c %c [${name}]-v${version} by ${author}`, `background:url(${icon}) center/12px no-repeat;padding:3px`, ""); // console.log(c); // console.groupEnd(); }, getValue: (name, defaultValue) => GM_getValue(name, defaultValue), setValue: (name, value) => GM_setValue(name, value), sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), addStyle(id, tag, css) { tag = tag || 'style'; let doc = document, styleDom = doc.head.querySelector(`#${id}`); if (styleDom) return; let style = doc.createElement(tag); style.rel = 'stylesheet'; style.id = id; tag === 'style' ? style.innerHTML = css : style.href = css; doc.head.appendChild(style); }, removeStyle(id,tag){ tag = tag || 'style'; let doc = document, styleDom = doc.head.querySelector(`#${id}`); if (styleDom) { doc.head.removeChild(styleDom) }; }, getAttrsByPrefix(element, prefix) { return Array.from(element.attributes).reduce((acc, { name, value }) => { if (name.startsWith(prefix)) acc[name] = value; return acc; }, {}); }, data(element, key, value) { if (arguments.length < 2) return undefined; if (value !== undefined) element.dataset[key] = value; return element.dataset[key]; }, async post(url, data, headers, responseType = 'json') { return this.fetchData(url, 'POST', data, headers, responseType); }, async get(url, headers, responseType = 'json') { return this.fetchData(url, 'GET', null, headers, responseType); }, async fetchData(url, method='GET', data=null, headers={}, responseType='json') { const options = { method, headers: { 'Content-Type':'application/json',...headers}, body: data ? JSON.stringify(data) : undefined }; const response = await fetch(url.startsWith("http") ? url : BASE_URL + url, options); const result = await response[responseType]().catch(() => null); return response.ok ? result : Promise.reject(result); }, getCurrentDate() { const localTimezoneOffset = (new Date()).getTimezoneOffset(); const beijingOffset = 8 * 60; const beijingTime = new Date(Date.now() + (localTimezoneOffset + beijingOffset) * 60 * 1000); const timeNow = `${beijingTime.getFullYear()}/${(beijingTime.getMonth() + 1)}/${beijingTime.getDate()}`; return timeNow; }, createElement(tagName, options = {}, childrens = [], doc = document, namespace = null) { if (Array.isArray(options)) { if (childrens.length !== 0) { throw new Error("If options is an array, childrens should not be provided."); } childrens = options; options = {}; } const { staticClass = '', dynamicClass = '', attrs = {}, on = {} } = options; const ele = namespace ? doc.createElementNS(namespace, tagName) : doc.createElement(tagName); if (staticClass) { staticClass.split(' ').forEach(cls => ele.classList.add(cls.trim())); } if (dynamicClass) { dynamicClass.split(' ').forEach(cls => ele.classList.add(cls.trim())); } Object.entries(attrs).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.entries(value).forEach(([styleKey, styleValue]) => { ele.style[styleKey] = styleValue; }); } else { if (value !== undefined) ele.setAttribute(key, value); } }); Object.entries(on).forEach(([event, handler]) => { ele.addEventListener(event, handler); }); childrens.forEach(child => { if (typeof child === 'string') { child = doc.createTextNode(child); } ele.appendChild(child); }); return ele; }, b64DecodeUnicode(str) { // Going backwards: from bytestream, to percent-encoding, to original string. return decodeURIComponent(atob(str).split('').map(function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } }; const opts = { curSite : SITE_MAP[location.host] || null, post: { pathPattern: /^\/(categories\/|page|award|search|$)/, scrollThreshold: 1500, nextPagerSelector: '.nsk-pager a.pager-next', postListSelector: 'ul.post-list:not(.topic-carousel-panel)', topPagerSelector: 'div.nsk-pager.pager-top', bottomPagerSelector: 'div.nsk-pager.pager-bottom', }, comment: { pathPattern: /^\/post-/, scrollThreshold: 690, nextPagerSelector: '.nsk-pager a.pager-next', postListSelector: 'ul.comments', topPagerSelector: 'div.nsk-pager.post-top-pager', bottomPagerSelector: 'div.nsk-pager.post-bottom-pager', }, settings:{ "version": version, "sign_in": {"ns":{ "enabled": false, "method": 0, "last_date": "", "ignore_date": "" },"df":{ "enabled": false, "method": 0, "last_date": "", "ignore_date": "" }}, "signin_tips": { "enabled": true }, "re_signin": { "enabled": true }, "auto_jump_external_links": { "enabled": true }, "loading_post": { "enabled": true }, "loading_comment": { "enabled": true }, "quick_comment": { "enabled": true }, "open_post_in_new_tab": { "enabled": false }, "block_members": { "enabled": true }, "block_posts": { "enabled": true,"keywords":[] }, "block_categories": { "enabled": false, "categories": [] }, "level_tag": { "enabled": true, "low_lv_alarm":false, "low_lv_max_days":30 }, "show_all_users_stats": { "enabled": false, "position": "below" }, "code_highlight": { "enabled": true }, "image_slide":{ "enabled":true }, "visited_links":{ "enabled": true, "link_color":"","visited_color":"","dark_link_color":"","dark_visited_color":"", "hide_visited": false }, "user_card_ext": { "enabled":true }, "compact_mode": { ...DEFAULT_COMPACT_MODE }, "custom_background": { "enabled": false, "url": "", "repeat": "repeat", "position": "center", "size": "cover", "attachment": "scroll" }, "merge_category_to_nav": { "enabled": false }, "remove_promotions": { "enabled": false }, "header_opacity": { "enabled": false, "value": 0.92, "effect": true, "blur": 16, "saturate": 180 }, "frame_opacity": { "enabled": false, "value": 0.95, "blur_enabled": true, "saturate_enabled": true, "blur": 12, "saturate": 180 }, "default_avatar": { "enabled": false, "url": "", "auto_detect": true }, "loading_overlay": { "enabled": true, "duration": INITIAL_OVERLAY_DURATION_DEFAULT }, "typography": { "title": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "tag": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "meta": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "content": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" } }, "hide_user_stats_panel": { "enabled": false }, "hide_footer": { "enabled": false }, "hide_post_category": { "enabled": false }, "hide_post_info": { "enabled": false }, "hide_topic_carousel": { "enabled": false }, "post_sort": { "enabled": false, "mode": "none", "visited_to_bottom": false }, "right_panel_highlight": { "enabled": false, "keywords": [] } } }; onDocumentReady(() => { layui.use(function () { const layer = layui.layer; const dropdown = layui.dropdown; const message = { info: (text) => message.__msg(text, { "background-color": "#4D82D6" }), success: (text) => message.__msg(text, { "background-color": "#57BF57" }), warning: (text) => message.__msg(text, { "background-color": "#D6A14D" }), error: (text) => message.__msg(text, { "background-color": "#E1715B" }), __msg: (text, style) => { let index = layer.msg(text, { offset: 't', area: ['100%', 'auto'], anim: 'slideDown' }); layer.style(index, Object.assign({ opacity: 0.9 }, style)); } }; const Config = { // 初始化配置数据 initializeConfig() { const defaultConfig = opts.settings; if (!util.getValue('settings')) { util.setValue('settings', defaultConfig); return; } if(this.getConfig('version')===version) return; // 从存储中获取当前配置 let storedConfig = util.getValue('settings'); // 递归地删除不在默认配置中的项 const cleanDefaults = (stored, defaults) => { Object.keys(stored).forEach(key => { if (defaults[key] === undefined) { delete stored[key]; // 如果默认配置中没有这个键,删除它 } else if (typeof stored[key] === 'object' && stored[key] !== null && !(stored[key] instanceof Array)) { cleanDefaults(stored[key], defaults[key]); // 递归检查 } }); }; // 递归地将默认配置中的新项合并到存储的配置中 const mergeDefaults = (stored, defaults) => { Object.keys(defaults).forEach(key => { if (typeof defaults[key] === 'object' && defaults[key] !== null && !(defaults[key] instanceof Array)) { if (!stored[key]) stored[key] = {}; mergeDefaults(stored[key], defaults[key]); } else { if (stored[key] === undefined) { stored[key] = defaults[key]; } } }); }; mergeDefaults(storedConfig, defaultConfig); //...这里将旧设置项的值迁移到新设置项 cleanDefaults(storedConfig, defaultConfig); storedConfig.version = version; util.setValue('settings',storedConfig); },updateConfig(path, value) { let config = util.getValue('settings'); if (!config) { config = opts.settings; util.setValue('settings', config); } let keys = path.split('.'); let lastKey = keys.pop(); let lastObj = keys.reduce((obj, key) => { if (!obj[key]) { obj[key] = {}; } return obj[key]; }, config); lastObj[lastKey] = value; util.setValue('settings', config); }, getConfig(path) { let config = GM_getValue('settings'); if (!config) return undefined; let keys = path.split('.'); let result = keys.reduce((obj, key) => { if (obj === null || obj === undefined) return undefined; return obj[key]; }, config); return result; } }; const getRuntimeOverlaySettings = () => ({ enabled: Config.getConfig('loading_overlay.enabled') !== false, duration: parseInt(Config.getConfig('loading_overlay.duration'), 10) || INITIAL_OVERLAY_DURATION_DEFAULT }); const FeatureFlags={ isEnabled(featureName) { if (Config.getConfig(featureName)) { return Config.getConfig(`${featureName}.enabled`); } else { // console.error(`Feature '${featureName}' does not exist.`); return false; } } }; const main = { loginStatus: false, blockKeywordRules: [], blockedCategorySet: new Set(), rightPanelHighlightRules: [], rightPanelHighlightObserver: null, avatarObserver: null, avatarDefaultCache: new Map(), //检查是否登陆 checkLogin() { if (unsafeWindow.__config__ && unsafeWindow.__config__.user) { this.loginStatus = true; util.clog(`当前登录用户 ${unsafeWindow.__config__.user.member_name} (ID ${unsafeWindow.__config__.user.member_id})`); } }, // 自动签到 autoSignIn(rand) { if(!FeatureFlags.isEnabled(`sign_in.${opts.curSite.code}`)) return; if (!this.loginStatus) return if (Config.getConfig(`sign_in.${opts.curSite.code}.enabled`) !== true) return; rand = rand || (Config.getConfig(`sign_in.${opts.curSite.code}.method`) === 1); let timeNow = util.getCurrentDate(), timeOld = Config.getConfig(`sign_in.${opts.curSite.code}.last_date`); if (!timeOld || timeOld != timeNow) { // 是新的一天 Config.updateConfig(`sign_in.${opts.curSite.code}.last_date`, timeNow); this.signInRequest(rand); } }, // 重新签到 reSignIn() { if (!this.loginStatus) return; if (Config.getConfig(`sign_in.${opts.curSite.code}.enabled`) !== true) { unsafeWindow.mscAlert('提示', '自动签到已关闭,不支持重新签到!'); return; } Config.updateConfig(`sign_in.${opts.curSite.code}.last_date`, '1753/1/1'); location.reload(); }, addSignTips() { if(!FeatureFlags.isEnabled('signin_tips')) return; if (!this.loginStatus) return if (Config.getConfig(`sign_in.${opts.curSite.code}.enabled`) === true) return; const timeNow = util.getCurrentDate(); const timeIgnore = Config.getConfig(`sign_in.${opts.curSite.code}.ignore_date`); const timeOld = Config.getConfig(`sign_in.${opts.curSite.code}.last_date`); if (timeNow === timeIgnore || timeNow === timeOld) return; const _this = this; let tip = util.createElement("div", { staticClass: 'nsplus-tip' }); let tip_p = util.createElement('p'); tip_p.innerHTML = '今天你还没有签到哦! 【随机抽个鸡腿】 【只要5个鸡腿】 【今天不再提示】'; tip.appendChild(tip_p); tip.querySelectorAll('.sign_in_btn').forEach(function (item) { item.addEventListener("click", function (e) { const rand = util.data(this, 'rand'); _this.signInRequest(rand); tip.remove(); Config.updateConfig(`sign_in.${opts.curSite.code}.last_date`, timeNow); }) }); tip.querySelector('#sign_in_ignore').addEventListener("click", function (e) { tip.remove(); Config.updateConfig(`sign_in.${opts.curSite.code}.ignore_date`, timeNow); }); document.querySelector('header').append(tip); }, async signInRequest(rand) { await util.post('/api/attendance?random=' + (rand || false), {}, { "Content-Type": "application/json" }).then(json => { if (json.success) { message.success(`签到成功!今天午饭+${json.gain}个鸡腿; 积攒了${json.current}个鸡腿了`); } else { message.info(json.message); } }).catch(error => { message.info(error.message || "发生未知错误"); util.clog(error); }); util.clog(`[${name}] 签到完成`); }, is_show_quick_comment: false, quickComment() { if (!this.loginStatus || !opts.comment.pathPattern.test(location.pathname)) return; if (Config.getConfig('loading_comment.enabled') === false) return; const _this = this; const onClick = (e) => { if (_this.is_show_quick_comment) { return; } e.preventDefault(); const mdEditor = document.querySelector('.md-editor'); const clientHeight = document.documentElement.clientHeight, clientWidth = document.documentElement.clientWidth; const mdHeight = mdEditor.clientHeight, mdWidth = mdEditor.clientWidth; const top = (clientHeight / 2) - (mdHeight / 2), left = (clientWidth / 2) - (mdWidth / 2); mdEditor.style.cssText = `position: fixed; top: ${top}px; left: ${left}px; margin: 30px 0px; width: 100%; max-width: ${mdWidth}px; z-index: 999;`; const moveEl = mdEditor.querySelector('.tab-select.window_header'); moveEl.style.cursor = "move"; moveEl.addEventListener('mousedown', startDrag); addEditorCloseButton(); _this.is_show_quick_comment = true; }; const commentDiv = document.querySelector('#fast-nav-button-group #back-to-parent').cloneNode(true); commentDiv.id = 'back-to-comment'; commentDiv.innerHTML = ''; commentDiv.addEventListener("click", onClick); document.querySelector('#back-to-parent').before(commentDiv); document.querySelectorAll('.nsk-post .comment-menu,.comment-container .comments').forEach(x=>x.addEventListener("click",(event) =>{ if(!["引用", "回复", "编辑"].includes(event.target.textContent)) return; onClick(event);},true));//使用冒泡法给按钮引用、回复添加事件 function addEditorCloseButton() { const fullScreenToolbar = document.querySelector('#editor-body .window_header > :last-child'); const cloneToolbar = fullScreenToolbar.cloneNode(true); cloneToolbar.setAttribute('title', '关闭'); cloneToolbar.querySelector('span').classList.replace('i-icon-full-screen-one', 'i-icon-close'); cloneToolbar.querySelector('span').innerHTML = ''; cloneToolbar.addEventListener("click", function (e) { const mdEditor = document.querySelector('.md-editor'); mdEditor.style = ""; const moveEl = mdEditor.querySelector('.tab-select.window_header'); moveEl.style.cursor = ""; moveEl.removeEventListener('mousedown', startDrag); this.remove(); _this.is_show_quick_comment = false; }); fullScreenToolbar.after(cloneToolbar); } function startDrag(event) { if (event.button !== 0) return; const draggableElement = document.querySelector('.md-editor'); const parentMarginTop = parseInt(window.getComputedStyle(draggableElement).marginTop); const initialX = event.clientX - draggableElement.offsetLeft; const initialY = event.clientY - draggableElement.offsetTop + parentMarginTop; document.onmousemove = function (event) { const newX = event.clientX - initialX; const newY = event.clientY - initialY; draggableElement.style.left = newX + 'px'; draggableElement.style.top = newY + 'px'; }; document.onmouseup = function () { document.onmousemove = null; document.onmouseup = null; }; } }, // 切换官方打开链接设置 async switchOpenPostInNewTab(stateName, states){ try { unsafeWindow.indexedDB.open('ns-preference-db').onsuccess = e => { const db = e.target.result; const store = db.transaction('ns-preference-store', 'readwrite').objectStore('ns-preference-store'); store.get('configuration').onsuccess = e => { const cfg = e.target.result || { openPostInNewPage: false }; cfg.openPostInNewPage = !cfg.openPostInNewPage; store.put(cfg, 'configuration'); Config.updateConfig(`${stateName}.enabled`, cfg.openPostInNewPage); unsafeWindow.mscAlert(`已${cfg.openPostInNewPage?'开启':'关闭'}新标签页打开链接`); }; }; } catch (error) { // console.error(error); } }, // 强制新标签页打开帖子链接 enforceNewTabForPosts() { // 使用事件委托监听所有链接点击 const clickHandler = function(e) { // 检查配置是否启用(每次检查以确保实时性,但只在启用时才进行后续处理) if (!Config.getConfig('open_post_in_new_tab.enabled')) return; const link = e.target.closest('a'); if (!link || !link.href) return; // 检查是否是帖子链接 const href = link.href; const isPostLink = /\/post-\d+/.test(href) && href.startsWith(BASE_URL); // 排除特殊链接(如跳转链接、锚点、已有target的链接等) if (link.hasAttribute('data-no-instant') || link.href.includes('/jump?to=') || link.href.startsWith('#') || link.target === '_blank') { return; } // 如果是帖子链接,强制在新标签页打开 if (isPostLink) { e.preventDefault(); e.stopPropagation(); window.open(link.href, '_blank'); } }; document.addEventListener('click', clickHandler, true); // 使用捕获阶段确保优先执行 }, //自动点击跳转页链接 autoJump() { document.querySelectorAll('a[href*="/jump?to="]').forEach(link => { try { const urlObj = new URL(link.href); const encodedUrl = urlObj.searchParams.get('to'); if (encodedUrl) { const decodedUrl = decodeURIComponent(encodedUrl); link.href = decodedUrl; } } catch (e) { // console.error('处理链接时出错:', e); } }); if (!/^\/jump/.test(location.pathname)) return; document.querySelector('.btn').click(); }, refreshBlockKeywordRules() { const stored = Config.getConfig('block_posts.keywords'); const list = Array.isArray(stored) ? stored : []; this.blockKeywordRules = list.map(rule => { if (!rule) return null; if (typeof rule === 'string') { const value = rule.trim(); if (!value) return null; return { type: 'text', value, lower: value.toLowerCase() }; } const type = rule.type || 'text'; if (type === 'regex') { const pattern = rule.value || rule.pattern || ''; if (!pattern) return null; const flags = rule.flags || ''; try { const regex = new RegExp(pattern, flags || 'i'); return { type: 'regex', regex, value: pattern, flags }; } catch (err) { // console.warn('[NodeSeek X] 无效的正则规则:', pattern, err); return null; } } const value = (rule.value || '').trim(); if (!value) return null; return { type: 'text', value, lower: value.toLowerCase() }; }).filter(Boolean); }, getBlockKeywordInputValue() { const stored = Config.getConfig('block_posts.keywords'); if (!Array.isArray(stored) || stored.length === 0) return ''; return stored.map(rule => { if (!rule) return ''; if (typeof rule === 'string') return rule; if (rule.type === 'regex') { const flags = rule.flags || ''; return `/${rule.value || ''}/${flags}`; } return rule.value || ''; }).filter(Boolean).join('\n'); }, parseBlockKeywordInput(inputValue) { if (!inputValue) return []; return inputValue.split(/\r?\n/).map(line => line.trim()).filter(Boolean).map(line => { if (line.startsWith('/') && line.lastIndexOf('/') > 0) { const lastSlash = line.lastIndexOf('/'); const pattern = line.slice(1, lastSlash); const flags = line.slice(lastSlash + 1); try { new RegExp(pattern, flags || 'i'); return { type: 'regex', value: pattern, flags }; } catch (err) { // console.warn('[NodeSeek X] 忽略无效正则:', line, err); return null; } } return { type: 'text', value: line }; }).filter(Boolean); }, shouldBlockTitle(title) { if (!this.blockKeywordRules || this.blockKeywordRules.length === 0) return false; const lowerTitle = title.toLowerCase(); return this.blockKeywordRules.some(rule => { if (rule.type === 'regex') { try { return rule.regex.test(title); } catch (err) { // console.warn('[NodeSeek X] 正则匹配失败:', rule, err); return false; } } return lowerTitle.includes(rule.lower); }); }, blockPost(ele) { if (Config.getConfig('block_posts.enabled') === false) return; if (!this.blockKeywordRules || this.blockKeywordRules.length === 0) return; ele = ele || document; ele.querySelectorAll('.post-title a[href], .post-list-item a.post-title').forEach(item => { const title = (item.textContent || '').trim(); if (!title || !this.shouldBlockTitle(title)) return; const li = item.closest('.post-list-item'); if (li) { li.classList.add('blocked-post'); } else { const fallback = item.closest('li, article, .post-card, .post-item, .post, .post-list-content') || item.parentElement; fallback?.classList.add('blocked-post'); } }); }, refreshBlockedCategories() { const stored = Config.getConfig('block_categories.categories'); const list = Array.isArray(stored) ? stored : []; this.blockedCategorySet = new Set(list.map(item => (item || '').trim().toLowerCase()).filter(Boolean)); }, getBlockedCategoryInputValue() { const stored = Config.getConfig('block_categories.categories'); if (!Array.isArray(stored) || stored.length === 0) return ''; return stored.join('\n'); }, parseBlockedCategoryInput(inputValue) { if (!inputValue) return []; return inputValue.split(/\r?\n/).map(line => line.trim()).filter(Boolean); }, blockPostsByCategory(ele) { if (Config.getConfig('block_categories.enabled') === false) return; if (!this.blockedCategorySet || this.blockedCategorySet.size === 0) return; ele = ele || document; ele.querySelectorAll('.post-list-item').forEach(item => { if (item.classList.contains('blocked-post')) return; const categoryAnchor = item.querySelector('.post-category'); if (!categoryAnchor) return; const text = (categoryAnchor.textContent || '').trim().toLowerCase(); if (this.blockedCategorySet.has(text)) { item.classList.add('blocked-post'); } }); }, togglePromotions() { const enabled = Config.getConfig('remove_promotions.enabled') === true; toggleRootClass('nsx-remove-promotions', enabled); }, async isDefaultAvatar(img) { const config = Config.getConfig('default_avatar'); if (!config || config.auto_detect === false) return true; const src = img.currentSrc || img.src; if (this.avatarDefaultCache.has(src)) { return this.avatarDefaultCache.get(src); } try { const response = await fetch(src, { credentials: 'include', headers: { 'Accept': 'text/html' } }); if (!response.ok) throw new Error(response.status); const text = await response.text(); const isDefault = /vue-color-avatar/i.test(text); this.avatarDefaultCache.set(src, isDefault); return isDefault; } catch (err) { // console.warn('[NodeSeek X] avatar detect failed', err); this.avatarDefaultCache.set(src, false); return false; } }, replaceDefaultAvatars(target = document) { const config = Config.getConfig('default_avatar'); if (!config || config.enabled !== true) { target.querySelectorAll('img.avatar-normal[data-nsx-original-avatar]').forEach(img => { img.src = img.dataset.nsxOriginalAvatar; delete img.dataset.nsxOriginalAvatar; delete img.dataset.nsxAvatarProcessed; }); return; } const fallbackUrl = (config.url || '').trim(); if (!fallbackUrl) return; const autoDetect = config.auto_detect !== false; const process = (img) => { if (img.dataset.nsxAvatarProcessed) return; const finalize = (shouldReplace) => { if (shouldReplace) { if (!img.dataset.nsxOriginalAvatar) { img.dataset.nsxOriginalAvatar = img.src; } img.src = fallbackUrl; img.dataset.nsxAvatarProcessed = '1'; } else { img.dataset.nsxAvatarProcessed = 'checked'; } }; const runDetect = () => { if (!autoDetect) { finalize(true); return; } this.isDefaultAvatar(img).then(isDefault => finalize(isDefault)); }; if (img.complete && img.naturalWidth) { runDetect(); } else { img.addEventListener('load', runDetect, { once: true }); } }; target.querySelectorAll('img.avatar-normal').forEach(process); }, startAvatarObserver() { if (this.avatarObserver) this.avatarObserver.disconnect(); if (!document.body) return; this.avatarObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches && node.matches('img.avatar-normal')) { this.replaceDefaultAvatars(node); } else if (node.querySelector) { this.replaceDefaultAvatars(node); } }); }); }); this.avatarObserver.observe(document.body, { childList: true, subtree: true }); }, updateHeaderOpacityStyle() { const config = Config.getConfig('header_opacity'); if (!config || config.enabled !== true) { toggleRootClass('nsx-header-opacity', false); document.getElementById(HEADER_OPACITY_STYLE_ID)?.remove(); return; } toggleRootClass('nsx-header-opacity', true); applyHeaderOpacityCSS(config); }, updateFrameOpacityStyle() { const config = Config.getConfig('frame_opacity'); if (!config || config.enabled !== true) { toggleRootClass('nsx-frame-opacity', false); document.getElementById(FRAME_OPACITY_STYLE_ID)?.remove(); return; } toggleRootClass('nsx-frame-opacity', true); applyFrameOpacityCSS(config); }, blockPostsByViewLevel(ele) { ele = ele || document; let level=0; if (this.loginStatus) level = unsafeWindow.__config__.user.rank; [...ele.querySelectorAll('.post-list-item use[href="#lock"]')].forEach(el => { const n = +el.closest('span')?.textContent.match(/\d+/)?.[0] || 0; if (n > level) el.closest('.post-list-item')?.classList.add('blocked-post'); }); }, //屏蔽用户 blockMemberDOMInsert() { if (!this.loginStatus) return; const _this = this; // 调整用户卡片位置,确保不会超出页面底部 const adjustUserCardPosition = (userCard) => { if (!userCard) return; const rect = userCard.getBoundingClientRect(); const viewportHeight = window.innerHeight; const cardBottom = rect.bottom; // 如果卡片底部超出视口,调整位置 if (cardBottom > viewportHeight - 20) { const currentTop = parseInt(userCard.style.top) || rect.top; const cardHeight = rect.height; const newTop = viewportHeight - cardHeight - 20; // 确保新位置不会太高(至少距离顶部100px) if (newTop < 100) { userCard.style.top = '100px'; userCard.style.maxHeight = `${viewportHeight - 120}px`; userCard.style.overflowY = 'auto'; } else { userCard.style.top = `${newTop}px`; } } }; Array.from(document.querySelectorAll(".post-list .post-list-item,.content-item")).forEach((function (t, n) { const r = t.querySelector('.avatar-normal'); if (!r) return; r.addEventListener("click", (function (n) { n.preventDefault(); let intervalId = setInterval(() => { const userCard = document.querySelector('div.user-card.hover-user-card'); const pmButton = document.querySelector('div.user-card.hover-user-card a.btn'); if (userCard && pmButton) { clearInterval(intervalId); // 调整用户卡片位置 adjustUserCardPosition(userCard); // 监听窗口大小变化和滚动,动态调整位置 const adjustHandler = () => adjustUserCardPosition(userCard); window.addEventListener('resize', adjustHandler); window.addEventListener('scroll', adjustHandler); // 当用户卡片消失时,移除监听器 const observer = new MutationObserver(() => { if (!document.contains(userCard)) { window.removeEventListener('resize', adjustHandler); window.removeEventListener('scroll', adjustHandler); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); const dataVAttrs = util.getAttrsByPrefix(userCard, 'data-v'); const userName = userCard.querySelector('a.Username').textContent; dataVAttrs.style = "float:left; background-color:rgba(0,0,0,.3)"; const blockBtn = util.createElement("a", { staticClass: "btn", attrs: dataVAttrs, on: { click: function (e) { e.preventDefault(); unsafeWindow.mscConfirm(`确定要屏蔽"${userName}"吗?`, '你可以在本站的 设置=>屏蔽用户 中解除屏蔽', function () { blockMember(userName); }) } } }, ["屏蔽"]); pmButton.after(blockBtn); // 添加屏蔽按钮后,再次调整位置(因为按钮可能增加了卡片高度) setTimeout(() => adjustUserCardPosition(userCard), 100); } }, 50); // 添加超时保护,5秒后自动清理 setTimeout(() => clearInterval(intervalId), 5000); })) })) function blockMember(userName) { util.post("/api/block-list/add", { "block_member_name": userName }, { "Content-Type": "application/json" }).then(function (data) { if (data.success) { let msg = '屏蔽用户【' + userName + '】成功!'; unsafeWindow.mscAlert(msg); util.clog(msg); } else { let msg = '屏蔽用户【' + userName + '】失败!' + data.message; unsafeWindow.mscAlert(msg); util.clog(msg); } }).catch(function (err) { util.clog(err); }); } }, addImageSlide() { if (!opts.comment.pathPattern.test(location.pathname)) return; const posts = document.querySelectorAll('article.post-content'); posts.forEach(function (post, i) { const images = post.querySelectorAll('img:not(.sticker)'); if (images.length === 0) return; images.forEach(function (image, i) { const newImg = image.cloneNode(true); image.parentNode.replaceChild(newImg, image); newImg.addEventListener('click', function (e) { e.preventDefault(); const imgArr = Array.from(post.querySelectorAll('img:not(.sticker)')); const clickedIndex = imgArr.indexOf(this); const photoData = imgArr.map((img, i) => ({ alt: img.alt, pid: i + 1, src: img.src })); layer.photos({ photos: { "title": "图片预览", "start": clickedIndex, "data": photoData } }); }, true); }); }); }, addLevelTag() {//添加等级标签 if (!this.loginStatus) return; if (!opts.comment.pathPattern.test(location.pathname)) return; let _this=this; this.getUserInfo(unsafeWindow.__config__.postData.op.uid).then((user) => { let warningInfo = ''; const daysDiff = Math.floor((new Date() - new Date(user.created_at)) / (1000 * 60 * 60 * 24)); if (daysDiff < 30) { warningInfo = `⚠️`; } // console.log(user); let rank = _this.getRankByCoin(user.coin); const span = util.createElement("span", { staticClass: `nsk-badge role-tag user-level user-lv${rank}` }, [util.createElement("span", [`${warningInfo}Lv ${rank}`])]); const authorLink = document.querySelector('#nsk-body .nsk-post .nsk-content-meta-info .author-info>a'); if (authorLink != null) { authorLink.after(span); } // 在 content-info 中添加用户统计信息 const contentInfo = document.querySelector('#nsk-body .nsk-post .nsk-content-meta-info .content-info'); const mainPost = document.querySelector('#nsk-body .nsk-post'); if (contentInfo) { // 检查是否已添加过统计信息,避免重复 if (contentInfo.querySelector('.nsx-user-stats')) return; // 获取位置设置 const position = Config.getConfig('show_all_users_stats.position') || 'below'; // 创建统计信息容器 const statsContainer = util.createElement("div", { staticClass: "nsx-user-stats", attrs: { style: position === 'right' ? "display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.9em; margin-left: 8px;" : "margin-top: 4px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.9em;" } }, [ util.createElement("span", { attrs: { title: "注册天数", style: "color: #2ea44f;" } }, [`📅 注册 ${daysDiff} 天`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "鸡腿数量", style: "color: #ff9800;" } }, [`🍗 ${user.coin || 0}`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "帖子数", style: "color: #2196f3;" } }, [`📝 帖子 ${user.nPost || 0}`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "评论数", style: "color: #9c27b0;" } }, [`💬 评论 ${user.nComment || 0}`]) ]); // 根据位置设置插入 if (position === 'right' && mainPost) { // 插入到作者名字右边 const authorLink = mainPost.querySelector('.nsk-content-meta-info .author-info a'); if (authorLink) { authorLink.after(statsContainer); } else { // 如果找不到作者链接,回退到下方 contentInfo.appendChild(statsContainer); } } else { // 默认插入到下方 contentInfo.appendChild(statsContainer); } } }); }, getUserInfo(uid) { return new Promise((resolve, reject) => { util.get(`/api/account/getInfo/${uid}`, {}, 'json').then((data) => { if (!data.success) { util.clog(data); return; } resolve(data.detail); }).catch((err) => reject(err)); }) }, // 为所有用户显示统计信息 showAllUsersStats() { if (!this.loginStatus) return; const _this = this; // 缓存用户数据,避免重复请求(Map) if (!_this.userDataCache) { _this.userDataCache = new Map(); } // 记录正在请求的 UID,避免重复请求(Set) if (!_this.pendingUserRequests) { _this.pendingUserRequests = new Set(); } // 记录等待用户数据的楼层(Map>) if (!_this.pendingContentItems) { _this.pendingContentItems = new Map(); } // 创建用户统计信息的辅助函数 const createUserStats = (user, contentInfo, item) => { if (!user || !contentInfo) return; // 获取位置设置 const position = Config.getConfig('show_all_users_stats.position') || 'below'; // 检查是否已添加过统计信息 const existingStats = contentInfo.querySelector('.nsx-user-stats'); if (existingStats) { // 如果位置设置改变,需要移除旧的并重新添加 existingStats.remove(); } // 计算注册天数 let daysDiff = 0; if (user.created_at) { const registerDate = new Date(user.created_at); const now = new Date(); if (!isNaN(registerDate.getTime())) { daysDiff = Math.floor((now - registerDate) / (1000 * 60 * 60 * 24)); } } // 如果计算失败,显示为 0 天 if (isNaN(daysDiff) || daysDiff < 0) { daysDiff = 0; } // 创建统计信息容器 const statsContainer = util.createElement("div", { staticClass: "nsx-user-stats", attrs: { style: position === 'right' ? "display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.9em; margin-left: 8px;" : "margin-top: 4px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.9em;" } }, [ util.createElement("span", { attrs: { title: "注册天数", style: "color: #2ea44f;" } }, [`📅 注册 ${daysDiff} 天`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "鸡腿数量", style: "color: #ff9800;" } }, [`🍗 ${user.coin || 0}`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "帖子数", style: "color: #2196f3;" } }, [`📝 帖子 ${user.nPost || 0}`]), util.createElement("span", { attrs: { style: "color: var(--text-tertiary-color, #ccc);" } }, ["·"]), util.createElement("span", { attrs: { title: "评论数", style: "color: #9c27b0;" } }, [`💬 评论 ${user.nComment || 0}`]) ]); // 根据位置设置插入 if (position === 'right' && item) { // 插入到作者名字右边 const authorLink = item.querySelector('.nsk-content-meta-info .author-info a'); if (authorLink) { authorLink.after(statsContainer); } else { // 如果找不到作者链接,回退到下方 contentInfo.appendChild(statsContainer); } } else { // 默认插入到下方 contentInfo.appendChild(statsContainer); } }; // 处理单个内容项(帖子或评论) const processContentItem = (item) => { // 排除主帖,主帖的统计信息由 addLevelTag 处理 // 检查 item 是否在主帖内(#nsk-body .nsk-post) const mainPost = document.querySelector('#nsk-body .nsk-post'); if (mainPost && mainPost.contains(item)) { return; } // 查找 content-info 元素 const contentInfo = item.querySelector('.nsk-content-meta-info .content-info'); if (!contentInfo) return; // 如果已经有统计信息,跳过 if (contentInfo.querySelector('.nsx-user-stats')) return; // 尝试从头像获取 UID const avatar = item.querySelector('.avatar-normal'); let uid = null; if (avatar) { // 尝试从 data-uid 属性获取 uid = avatar.getAttribute('data-uid'); // 如果没找到,尝试从头像 URL 提取(/avatar/12345.png) if (!uid && avatar.src) { const match = avatar.src.match(/\/avatar\/(\d+)\.png/); if (match) { uid = match[1]; } } } // 如果还是没找到,尝试从作者链接获取 if (!uid) { const authorLink = item.querySelector('.nsk-content-meta-info .author-info a'); if (authorLink && authorLink.href) { const match = authorLink.href.match(/\/space\/(\d+)/); if (match) { uid = match[1]; } } } if (!uid) return; // 如果用户数据已经在缓存中,直接使用 if (_this.userDataCache.has(uid)) { const user = _this.userDataCache.get(uid); createUserStats(user, contentInfo, item); return; } // 如果正在请求该用户的数据,将当前楼层加入等待列表 if (_this.pendingUserRequests.has(uid)) { if (!_this.pendingContentItems.has(uid)) { _this.pendingContentItems.set(uid, []); } _this.pendingContentItems.get(uid).push({ contentInfo, item }); return; } // 标记为正在请求 _this.pendingUserRequests.add(uid); // 获取用户信息 _this.getUserInfo(uid).then((user) => { // 缓存用户数据 _this.userDataCache.set(uid, user); // 为当前楼层显示统计信息 createUserStats(user, contentInfo, item); // 为所有等待该用户数据的楼层显示统计信息 if (_this.pendingContentItems.has(uid)) { const pendingItems = _this.pendingContentItems.get(uid); pendingItems.forEach(({ contentInfo: pendingContentInfo, item: pendingItem }) => { createUserStats(user, pendingContentInfo, pendingItem); }); _this.pendingContentItems.delete(uid); } // 移除请求标记 _this.pendingUserRequests.delete(uid); }).catch((err) => { // 忽略错误,移除请求标记 _this.pendingUserRequests.delete(uid); _this.pendingContentItems.delete(uid); }); }; // 处理所有内容项 const processAllItems = () => { // 处理所有评论(包括主帖,但主帖的统计信息由 addLevelTag 处理,这里会跳过) const contentItems = document.querySelectorAll('.content-item'); contentItems.forEach(item => { processContentItem(item); }); }; // 初始处理 processAllItems(); // 监听动态加载的内容 if (!_this.allUsersStatsObserver) { _this.allUsersStatsObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element node // 检查是否是新的内容项 if (node.matches && node.matches('.content-item')) { processContentItem(node); } // 检查子元素中是否有新的内容项 const newItems = node.querySelectorAll ? node.querySelectorAll('.content-item') : []; newItems.forEach(item => processContentItem(item)); } }); }); }); const commentsContainer = document.querySelector('.comments, .comment-container'); if (commentsContainer) { _this.allUsersStatsObserver.observe(commentsContainer, { childList: true, subtree: true }); } } }, // 隐藏所有用户统计信息 hideAllUsersStats() { // 移除所有用户统计信息(除了楼主的,因为那是 level_tag 功能的一部分) document.querySelectorAll('.nsx-user-stats').forEach(stats => { // 只移除非楼主区域的统计信息 const isMainPost = stats.closest('#nsk-body .nsk-post'); if (!isMainPost) { stats.remove(); } }); // 断开观察器 if (this.allUsersStatsObserver) { this.allUsersStatsObserver.disconnect(); this.allUsersStatsObserver = null; } }, getRankByCoin(coin){ // 处理无效值:负数、NaN、undefined 都返回 0 if (!coin || coin < 0) { return 0; } // 计算等级:使用平方根公式,最大等级为6 const level = Math.floor(Math.sqrt(coin) / 10); return Math.min(6, level); }, fakeLevel(){ let coin = unsafeWindow.__config__.user.coin; if(coin < 4900) return;//不足7级直接返回 let rank = this.getRankByCoin(coin); const userCard = document.querySelector(".user-card .user-stat"); userCard.querySelector('use[href="#level"]').closest('svg').nextElementSibling.innerText='等级 Lv '+ rank; }, userCardEx() { if (!this.loginStatus) return; if (!(opts.post.pathPattern.test(location.pathname)|| opts.comment.pathPattern.test(location.pathname))) return; const bn = new BroadcastManager("notification_sync"); const updateNotificationElement = (element, href, iconHref, text, count) => { element.querySelector("a").setAttribute("href", `${href}`); element.querySelector("a > svg > use").setAttribute("href", `${iconHref}`); element.querySelector("a > :nth-child(2)").textContent = `${text} `; element.querySelector("a > :last-child").textContent = count; const countEl = element.querySelector("a > :last-child"); countEl.classList.toggle("notify-count", count > 0); // 通知(只在主控标签页且有新消息时发送) if (count > 0 && bn.active) { GM_notification({ text: `你有 ${count} 条新 ${text === '我' ? '@' : text},点击查看`, tag: 'notice_count', onclick: e => (e.preventDefault(), GM_openInTab(`${BASE_URL}${href}`, {active: true})) }); } return element; }; const userCard = document.querySelector(".user-card .user-stat"); const lastElement = userCard.querySelector(".stat-block:first-child > :last-child"); const atMeElement = lastElement.cloneNode(true); const msgElement = lastElement.cloneNode(true); lastElement.after(atMeElement); userCard.querySelector(".stat-block:last-child").append(msgElement); // 初始化通知显示 const updateAllCounts = (counts) => { updateNotificationElement(atMeElement, "/notification#/atMe", "#at-sign", "我", counts.atMe); updateNotificationElement(msgElement, "/notification#/message?mode=list", "#envelope-one", "私信", counts.message); updateNotificationElement(lastElement, "/notification#/reply", "#remind-6nce9p47", "回复", counts.reply); }; // 注册数据接收器 bn.registerReceiver(({ sender, data }) => { if (data.type === 'unreadCount' && data.counts) { // console.log(`接收到来自 ${sender} 的广播数据:${JSON.stringify(data.counts)}`, new Date(data.timestamp).toLocaleString()); updateAllCounts(data.counts); } }); // 首次加载 bn.broadcast({ type: 'unreadCount', counts: unsafeWindow.__config__.user.unViewedCount, timestamp: Date.now() }); let interval = 5000; // 启动定时任务(只在主控标签页执行) bn.startTask(async () => { const response = await fetch("/api/notification/unread-count", { credentials: "include" }); if (!response.ok) throw new Error(response.status); const data = await response.json(); if (data.success && data.unreadCount) { // console.log(`${bn.myId} 发送一条广播数据:${JSON.stringify(data.unreadCount)}`); return { type: 'unreadCount', counts: data.unreadCount, timestamp: Date.now() }; } throw new Error('Invalid response'); }, interval); }, // 自动翻页 autoLoading() { if (Config.getConfig('loading_post.enabled') === false && Config.getConfig('loading_comment.enabled') === false) return; let opt = {}; if (opts.post.pathPattern.test(location.pathname)) { opt = opts.post; } else if (opts.comment.pathPattern.test(location.pathname)) { opt = opts.comment; } else { return; } let is_requesting = false; let _this = this; this.windowScroll(function (direction, e) { if (direction === 'down') { // 下滑才准备翻页 let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; if (document.documentElement.scrollHeight <= document.documentElement.clientHeight + scrollTop + opt.scrollThreshold && !is_requesting) { if (!document.querySelector(opt.nextPagerSelector)) return; let nextUrl = document.querySelector(opt.nextPagerSelector).attributes.href.value; is_requesting = true; // 显示加载遮罩(无缝翻页时) const overlay = document.getElementById('nsx-loading-overlay'); if (overlay) overlay.classList.add('show'); util.get(nextUrl, {}, 'text').then(function (data) { let doc = new DOMParser().parseFromString(data, "text/html"); _this.blockPost(doc);//过滤帖子 _this.blockPostsByViewLevel(doc); _this.blockPostsByCategory(doc); if (opts.comment.pathPattern.test(location.pathname)){ // 取加载页的评论数据追加到原评论数据 let el = doc.getElementById('temp-script') let jsonText = el.textContent; if (jsonText) { let conf = JSON.parse(util.b64DecodeUnicode(jsonText)) unsafeWindow.__config__.postData.comments.push(...conf.postData.comments); } } if (name === 'block_posts') { const keywordsBox = layero[0].querySelector('#block_post_keywords_box'); if (keywordsBox) keywordsBox.style.display = data.elem.checked ? '' : 'none'; if (data.elem.checked) { _this.refreshBlockKeywordRules(); _this.blockPost(); } } document.querySelector(opt.postListSelector).append(...doc.querySelector(opt.postListSelector).childNodes); document.querySelector(opt.topPagerSelector).innerHTML = doc.querySelector(opt.topPagerSelector).innerHTML; document.querySelector(opt.bottomPagerSelector).innerHTML = doc.querySelector(opt.bottomPagerSelector).innerHTML; history.pushState(null, null, nextUrl); _this.replaceDefaultAvatars(doc); // 确保新加载的内容也应用紧凑模式样式 if (Config.getConfig('compact_mode.enabled') !== false) { _this.updateCompactModeStyle(); } // 确保新加载的内容也应用隐藏设置 _this.togglePostCategory(); _this.togglePostInfo(); // 应用关键词高亮 if (Config.getConfig('right_panel_highlight.enabled') === true) { _this.applyRightPanelHighlight(); } // 应用排序 _this.sortPostList(); // 如果启用了显示所有用户统计,为新加载的内容添加统计信息 if (Config.getConfig('show_all_users_stats.enabled') === true) { setTimeout(() => { _this.showAllUsersStats(); }, 300); } // 隐藏加载遮罩 const overlay = document.getElementById('nsx-loading-overlay'); if (overlay) overlay.classList.remove('show'); // 评论菜单条 if (opts.comment.pathPattern.test(location.pathname)){ const vue = document.querySelector('.comment-menu').__vue__; Array.from(document.querySelectorAll(".content-item")).forEach(function (t,e) { const n = t.querySelector(".comment-menu-mount"); if(!n) return; const o = new vue.$root.constructor(vue.$options); o.setIndex(e); o.$mount(n); }); // 如果启用了显示所有用户统计,为新加载的评论添加统计信息 if (Config.getConfig('show_all_users_stats.enabled') === true) { setTimeout(() => { _this.showAllUsersStats(); }, 500); } } is_requesting = false; }).catch(function (err) { // 出错时也隐藏遮罩 const overlay = document.getElementById('nsx-loading-overlay'); if (overlay) overlay.classList.remove('show'); is_requesting = false; util.clog(err); }); } } }); }, // 滚动条事件 windowScroll(fn1) { let beforeScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop, fn = fn1 || function () { }; setTimeout(function () { // 延时执行,避免刚载入到页面就触发翻页事件 window.addEventListener('scroll', function (e) { const afterScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop, delta = afterScrollTop - beforeScrollTop; if (delta == 0) return false; fn(delta > 0 ? 'down' : 'up', e); beforeScrollTop = afterScrollTop; }, false); }, 1000) }, // 平滑滚动 smoothScroll(){ const scroll = (selector, top = 0) => { const btn = document.querySelector(selector); if (btn) { // 移除现有事件监听器 btn.onclick = null; btn.removeAttribute('onclick'); // 添加新的事件处理器 btn.addEventListener('click', e => { e.preventDefault(); e.stopImmediatePropagation(); if(e.target.querySelector('use[href="#down"]')){ top = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); } window.scrollTo({ top, behavior: 'smooth' }); }, true); } }; scroll('#back-to-top', 0); scroll('#back-to-bottom', Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)); }, history: ()=>{ const STORAGE_KEY = 'nsx_browsing_history'; const PAGE_SIZE = 10; let saveLimit = 'all'; let sortedCache = null; // 缓存排序后的数据 const POST_URL_PATTERN = /\/post-(\d+)-\d+.*$/; const getCurrentTime = () => layui.util.toDateString(new Date(),"yyyy-MM-ddTHH:mm:ss.SSS"); const getBrowsingHistory = () => { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }; const saveBrowsingHistory = (history) => { if (saveLimit !== 'all') { history = history.slice(-saveLimit); } // 保存时排序,并清除缓存 sortedCache = null; localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); }; const addOrUpdateHistory = (url, title) => { const match = url.match(POST_URL_PATTERN); if (!match) return; // 只保存匹配的帖子记录 const normalizedUrl = `/post-${match[1]}-1`; // 只判断第1页,即不区分页码 const history = getBrowsingHistory(); const index = history.findIndex(item => item.url === normalizedUrl); const entry = { url: normalizedUrl, title, time: getCurrentTime() }; if (index > -1) { history[index] = entry; } else { history.push(entry); } saveBrowsingHistory(history); }; const getHistory = (page = 1) => { // 使用缓存或重新排序 if (!sortedCache) { const history = getBrowsingHistory(); sortedCache = history.sort((a, b) => new Date(b.time) - new Date(a.time)); } if(page===0) return sortedCache; return sortedCache.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); }; const showHistory = (page = 1) => { const history = getBrowsingHistory(); const totalPages = Math.ceil(history.length / PAGE_SIZE); const pageHistory = getHistory(page); // console.clear(); // console.log(`浏览历史 - 第 ${page} 页,共 ${totalPages} 页`); pageHistory.forEach((item, i) => { // console.log(`${(page - 1) * PAGE_SIZE + i + 1}. [${item.time}] ${item.title} - ${item.url}`); }); if (page < totalPages) { // console.log(`输入 showHistory(${page + 1}) 查看下一页`); } }; const setSaveLimit = (limit) => { if (typeof limit === 'number' && limit > 0 || limit === 'all') { saveLimit = limit; // console.log(`保存限制已设置为:${limit === 'all' ? '全部' : `最近 ${limit} 条`}`); } else { // console.error('无效的保存限制。请输入正整数或 "all"'); } }; const injectDom=()=>{ const svg = util.createElement("svg", { staticClass: "iconpark-icon", attrs: { "style": "width: 17px;height: 17px;" }},[ util.createElement("use",{ attrs: { "href": "#history"} }, [], document, "http://www.w3.org/2000/svg") ], document, "http://www.w3.org/2000/svg"); const originalSwitcher = document.querySelector('#nsk-head .color-theme-switcher'); if (originalSwitcher) { const svgWrap = originalSwitcher.cloneNode(); svgWrap.classList.replace('color-theme-switcher', 'history-dropdown-on'); svgWrap.setAttribute('lay-options', '{trigger:"hover"}'); // 判断是否为移动端(li 元素)并移除 SVG 的 style 属性 if (originalSwitcher.tagName.toLowerCase() === 'li') { svg.removeAttribute('style'); } svgWrap.appendChild(svg); originalSwitcher.insertAdjacentElement('beforebegin', svgWrap); } const history=getHistory(0); const maxLength=20; // 按天分组 const grouped = history.reduce((result, item, i) => { const date = item.time.split("T")[0]; if (!result[date]) { result[date] = []; } const truncatedTitle = item.title.length > maxLength ? item.title.slice(0, maxLength) + "..." : item.title; result[date].push({ id: 1000+i+1, title: `${truncatedTitle}(${layui.util.toDateString(item.time,'HH:mm')})`, href: item.url, time: item.time }); return result; }, {}); // 转换为目标结构 const result = Object.entries(grouped).map(([day, items], index) => ({ id: index + 1, title: day, type: "group", child: items // 将子项包裹在数组中 })); // console.log(result); dropdown.render({ elem: '.history-dropdown-on', // trigger: 'click' // trigger 已配置在元素 `lay-options` 属性上 data: result, style: 'width: 370px; height: 200px;' }); }; addOrUpdateHistory(window.location.href, document.title); injectDom(); }, initInstantPage:() => { const prefetchedUrls = new Set(); // 用于存储已经尝试预加载的 URL let prefetcher = document.createElement('link'); prefetcher.rel = 'prefetch'; document.body.addEventListener('mouseover', (event) => { const target = event.target.closest('a'); if (!target || !target.href || target.hasAttribute('data-no-instant')) { return; } const href = target.href; if (!href.startsWith(`${BASE_URL}/post-`)) { return; } if (prefetchedUrls.has(href)) { // console.log('跳过已预加载链接:', href); return; } setTimeout(() => { if (target.matches(':hover')) { prefetcher.href = href; document.head.appendChild(prefetcher); prefetchedUrls.add(href); // console.log('预加载链接已启动:', href); } }, 65); // 65毫秒延迟 }); }, // 在导航栏添加设置按钮 addSettingsButton() { const header = document.querySelector('#nsk-head'); if (!header) return; const colorSwitcher = header.querySelector('.color-theme-switcher'); if (!colorSwitcher) return; // 检查是否已存在设置按钮 if (header.querySelector('.nsx-settings-btn')) return; const settingsBtn = colorSwitcher.cloneNode(true); settingsBtn.classList.remove('color-theme-switcher'); settingsBtn.classList.add('nsx-settings-btn'); settingsBtn.setAttribute('title', 'NodeSeek X 设置'); settingsBtn.querySelector('use').setAttribute('href', '#setting'); settingsBtn.addEventListener('click', (e) => { e.preventDefault(); this.advancedSettings(); }); colorSwitcher.insertAdjacentElement('beforebegin', settingsBtn); }, moveSearchBoxToCenter() { // 搜索框定位逻辑已提前通过紧凑模式样式处理,避免运行时闪烁 }, advancedSettings() { const _this = this; const layerWidth = layui.device().mobile ? '100%' : '700px'; const siteCode = opts.curSite ? opts.curSite.code : 'ns'; // 生成设置内容 const generateSettingsContent = () => { const blockPostsEnabled = (Config.getConfig('block_posts.enabled') ?? true) !== false; const blockCategoriesEnabled = Config.getConfig('block_categories.enabled') === true; const blockPostKeywordsValue = (_this.getBlockKeywordInputValue() || '') .replace(/&/g, '&') .replace(//g, '>'); const blockCategoriesValue = (_this.getBlockedCategoryInputValue() || '') .replace(/&/g, '&') .replace(//g, '>'); const signInMethod = Config.getConfig(`sign_in.${siteCode}.method`) ?? 0; const signInEnabled = Config.getConfig(`sign_in.${siteCode}.enabled`) === true; const typographyTitle = Config.getConfig('typography.title') || {}; const typographyTag = Config.getConfig('typography.tag') || {}; const typographyMeta = Config.getConfig('typography.meta') || {}; const typographyContent = Config.getConfig('typography.content') || {}; const escape = (val) => (val || '').replace(/&/g, '&').replace(//g, '>'); return ` 基本设置 增强功能 显示设置 配置 基本设置 自动签到 移除推广 隐藏页面中的推广位(.promotation-item)。 默认头像 识别系统随机头像并替换为自定义图片。 头像设置 若禁用自动识别则所有未自定义头像的用户都会替换为上面的 URL。 加载遮罩 控制开屏时的模糊加载页。 显示时长 ${(Config.getConfig('loading_overlay.duration') ?? INITIAL_OVERLAY_DURATION_DEFAULT)}ms 签到提示 自动跳转外部链接 增强功能 下拉加载翻页 自动加载帖子和评论 新标签页打开帖子 快捷评论 屏蔽用户 屏蔽帖子 标题关键词 ${blockPostKeywordsValue} 示例:羊了个羊、/^\\[广告\\]/i。匹配命中时会直接隐藏该帖子。 屏蔽分类 分类列表 ${blockCategoriesValue} 与帖子上显示的分类文字一致即可,例如“日常”“技术”“情报”“测评”等。 用户卡片扩展 显示所有用户统计 在帖子内所有用户(包括评论者)的元数据区域显示注册天数、鸡腿数量、帖子数、评论数等统计信息 显示位置 选择统计信息的显示位置 显示设置 等级标签 代码高亮 图片滑动查看 已访问链接标记 启用后,点击过的帖子会直接在列表中隐藏。 启用后,已访问的帖子会排在列表最后。 帖子排序 对帖子列表进行排序,按回复数或查看数从高到低排列 紧凑模式 增大信息密度,一页显示更多帖子 列数 自定义设置 内边距 (px): 头像大小 (px): 标题字体 (px): 信息字体 (px): 间距 (px): 自定义背景图 自定义页面背景图片 Header透明 调节顶栏透明度,美化视觉。 透明度 ${(Config.getConfig('header_opacity.value') ?? 0.92).toFixed(2)} 框体透明 调节 #nsk-frame 等主体容器透明度。 透明度 ${(Config.getConfig('frame_opacity.value') ?? 0.95).toFixed(2)} 标题字体 设置 Tag 字体 设置 Meta 字体 设置 内容字体 设置 背景图设置 上传图片: 选择图片 清除图片 或输入URL: 重复方式: 重复 不重复 横向重复 纵向重复 位置: 大小: 滚动方式: 随页面滚动 固定背景 合并分类到导航栏 将分类面板合并到导航栏,节省空间。多列模式下自动启用 隐藏用户统计面板 隐藏右侧面板中的"用户数目"和"欢迎新用户"面板 隐藏页脚 隐藏页面底部的 footer 区域 隐藏分类标签 隐藏帖子列表中的分类标签(如"日常"、"技术"等) 隐藏帖子信息 隐藏帖子列表中的作者、浏览量、评论数等信息 隐藏推荐轮播 隐藏页面顶部的推荐帖子轮播图 右侧面板关键词高亮 在右侧面板显示关键词输入框,可在帖子标题和右侧面板中高亮显示关键词 配置 导出设置 导出到剪贴板 复制当前全部设置,方便备份或同步。 导入设置 导入前会自动校验格式,并覆盖现有设置。 导入并应用 默认样式 一键设置默认样式 将设置恢复为推荐的默认样式配置(不会覆盖version字段)。 键盘快捷键 当前可用的快捷键: ← 或 ArrowLeft 跳转到上一页 → 或 ArrowRight 跳转到下一页 Ctrl + Enter 提交回复(在回复编辑器中) 💡 提示: • 翻页快捷键仅在非输入框状态下生效 • 提交快捷键仅在回复编辑器中生效 • 快捷键不会与网站原有功能冲突 `; }; layer.open({ type: 1, offset: 'r', anim: 'slideLeft', area: [layerWidth, '100%'], scrollbar: false, shade: 0.1, shadeClose: true, btn: ["保存设置", "取消"], btnAlign: 'r', title: ' NodeSeek X 设置', id: 'nsx-settings-layer', content: generateSettingsContent(), success: function(layero, index) { // 初始化表单 layui.form.render(); // 菜单导航事件处理 const menuLinks = layero[0].querySelectorAll('#nsx-settings-menu a'); menuLinks.forEach(function(link) { link.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const targetId = this.getAttribute('data-target'); const target = layero[0].querySelector('#' + targetId); if (target) { layero[0].querySelectorAll('#nsx-settings-menu li').forEach(li => li.classList.remove('layui-menu-item-checked')); this.closest('li').classList.add('layui-menu-item-checked'); const contentArea = layero[0].querySelector('#nsx-settings-content'); if (contentArea) { contentArea.scrollTo({ top: target.offsetTop - 20, behavior: 'smooth' }); } } return false; }); }); // 滚动时高亮菜单 const docContent = layero[0].querySelector('#nsx-settings-content'); if (docContent) { docContent.addEventListener('scroll', function() { const scrollPos = docContent.scrollTop; layero[0].querySelectorAll('#nsx-settings-content fieldset').forEach(function(el) { const topPos = el.offsetTop - 30; if (scrollPos >= topPos) { const id = el.getAttribute('id'); layero[0].querySelectorAll('#nsx-settings-menu li').forEach(li => li.classList.remove('layui-menu-item-checked')); const navItem = layero[0].querySelector('#nsx-settings-menu a[data-target="' + id + '"]'); if (navItem) { navItem.closest('li').classList.add('layui-menu-item-checked'); } } }); }); } // 处理签到方式切换 layui.form.on('radio(sign_in_method)', function(data) { const value = parseInt(data.value, 10); if (value === 0) { Config.updateConfig(`sign_in.${siteCode}.enabled`, false); return; } Config.updateConfig(`sign_in.${siteCode}.enabled`, true); if (value === 1) { // 随机抽鸡腿 Config.updateConfig(`sign_in.${siteCode}.method`, 1); } else if (value === 2) { // 固定 5 个鸡腿 Config.updateConfig(`sign_in.${siteCode}.method`, 0); } }); // 处理所有开关 const switchNames = [ 'signin_tips', 'auto_jump_external_links', 'loading_post', 'open_post_in_new_tab', 'quick_comment', 'block_members', 'block_posts', 'block_categories', 'remove_promotions', 'default_avatar', 'user_card_ext', 'level_tag', 'code_highlight', 'image_slide', 'visited_links', 'compact_mode', 'custom_background', 'header_opacity', 'frame_opacity', 'typography_title', 'typography_tag', 'typography_meta', 'typography_content', 'loading_overlay', 'merge_category_to_nav', 'hide_user_stats_panel', 'hide_footer', 'hide_post_category', 'hide_post_info', 'hide_topic_carousel', 'right_panel_highlight', 'show_all_users_stats' ]; switchNames.forEach(name => { layui.form.on('switch(' + name + ')', function(data) { Config.updateConfig(`${name}.enabled`, data.elem.checked); // 紧凑模式实时切换,无需刷新 if (name === 'compact_mode') { const optionsDiv = layero[0].querySelector('#compact_mode_options'); const customDiv = layero[0].querySelector('#compact_mode_custom_options'); if (optionsDiv) optionsDiv.style.display = data.elem.checked ? '' : 'none'; if (customDiv) customDiv.style.display = data.elem.checked ? '' : 'none'; _this.updateCompactModeStyle(); } if (name === 'block_posts') { const keywordsBox = layero[0].querySelector('#block_post_keywords_box'); if (keywordsBox) keywordsBox.style.display = data.elem.checked ? '' : 'none'; if (data.elem.checked) { _this.refreshBlockKeywordRules(); _this.blockPost(); } } if (name === 'block_categories') { const categoriesBox = layero[0].querySelector('#block_categories_box'); if (categoriesBox) categoriesBox.style.display = data.elem.checked ? '' : 'none'; if (data.elem.checked) { _this.refreshBlockedCategories(); _this.blockPostsByCategory(); } } if (name === 'remove_promotions') { _this.togglePromotions(); } if (name === 'default_avatar') { const box = layero[0].querySelector('#default_avatar_box'); if (box) box.style.display = data.elem.checked ? '' : 'none'; _this.replaceDefaultAvatars(); } if (name === 'loading_overlay') { const box = layero[0].querySelector('#loading_overlay_box'); if (box) box.style.display = data.elem.checked ? '' : 'none'; if (!data.elem.checked) { removeInitialOverlay(); } } if (name === 'header_opacity') { const box = layero[0].querySelector('#header_opacity_box'); if (box) box.style.display = data.elem.checked ? '' : 'none'; _this.updateHeaderOpacityStyle(); } if (name === 'frame_opacity') { const box = layero[0].querySelector('#frame_opacity_box'); if (box) box.style.display = data.elem.checked ? '' : 'none'; _this.updateFrameOpacityStyle(); } if (name === 'typography_title' || name === 'typography_tag' || name === 'typography_meta' || name === 'typography_content') { const type = name.split('_')[1]; Config.updateConfig(`typography.${type}.enabled`, data.elem.checked); _this.updateTypography(type); } // 自定义背景图实时切换 if (name === 'custom_background') { const optionsDiv = layero[0].querySelector('#custom_background_options'); if (data.elem.checked) { if (optionsDiv) optionsDiv.style.display = ''; _this.updateCustomBackground(); } else { if (optionsDiv) optionsDiv.style.display = 'none'; _this.updateCustomBackground(); } } if (name === 'visited_links') { _this.updateVisitedLinkStyle(); } if (name === 'show_all_users_stats') { const positionBox = document.getElementById('show_all_users_stats_position_box'); if (positionBox) { positionBox.style.display = data.elem.checked ? '' : 'none'; } if (data.elem.checked) { _this.showAllUsersStats(); } else { _this.hideAllUsersStats(); } } // 合并分类到导航栏实时切换 if (name === 'merge_category_to_nav') { _this.mergeCategoryToNav(); } // 隐藏用户统计面板实时切换 if (name === 'hide_user_stats_panel') { _this.toggleUserStatsPanel(); } // 隐藏页脚实时切换 if (name === 'hide_footer') { _this.toggleFooter(); } // 隐藏分类标签实时切换 if (name === 'hide_post_category') { _this.togglePostCategory(); } // 隐藏帖子信息实时切换 if (name === 'hide_post_info') { _this.togglePostInfo(); } // 隐藏推荐轮播实时切换 if (name === 'hide_topic_carousel') { _this.toggleTopicCarousel(); } // 右侧面板关键词高亮实时切换 if (name === 'right_panel_highlight') { if (data.elem.checked) { // 显示右侧面板输入框 _this.addRightPanelHighlightInput(); _this.applyRightPanelHighlight(); _this.startRightPanelHighlightObserver(); } else { // 隐藏右侧面板输入框并移除高亮 const inputPanel = document.querySelector('.nsx-highlight-input-panel'); if (inputPanel) { inputPanel.remove(); } _this.applyRightPanelHighlight(); // 移除高亮 if (_this.rightPanelHighlightObserver) { _this.rightPanelHighlightObserver.disconnect(); _this.rightPanelHighlightObserver = null; } } } }); }); const blockKeywordTextarea = layero[0].querySelector('textarea[name="block_post_keywords"]'); if (blockKeywordTextarea) { let keywordTimer = null; const persistKeywords = () => { const parsed = _this.parseBlockKeywordInput(blockKeywordTextarea.value); Config.updateConfig('block_posts.keywords', parsed); _this.refreshBlockKeywordRules(); _this.blockPost(); }; const schedulePersist = () => { clearTimeout(keywordTimer); keywordTimer = setTimeout(persistKeywords, 400); }; blockKeywordTextarea.addEventListener('input', schedulePersist); blockKeywordTextarea.addEventListener('blur', schedulePersist); } const blockCategoriesTextarea = layero[0].querySelector('textarea[name="block_categories_list"]'); if (blockCategoriesTextarea) { let categoryTimer = null; const persistCategories = () => { const parsed = _this.parseBlockedCategoryInput(blockCategoriesTextarea.value); Config.updateConfig('block_categories.categories', parsed); _this.refreshBlockedCategories(); _this.blockPostsByCategory(); }; const scheduleCategoriesPersist = () => { clearTimeout(categoryTimer); categoryTimer = setTimeout(persistCategories, 400); }; blockCategoriesTextarea.addEventListener('input', scheduleCategoriesPersist); blockCategoriesTextarea.addEventListener('blur', scheduleCategoriesPersist); } const headerOpacityRange = layero[0].querySelector('input[name="header_opacity_value"]'); if (headerOpacityRange) { headerOpacityRange.addEventListener('input', function() { const parsed = parseFloat(this.value); const value = Number.isNaN(parsed) ? 0.92 : parsed; const display = layero[0].querySelector('#header_opacity_value_display'); if (display) display.textContent = value.toFixed(2); Config.updateConfig('header_opacity.value', value); _this.updateHeaderOpacityStyle(); }); } layui.form.on('checkbox(header_opacity_blur_toggle)', function(data) { Config.updateConfig('header_opacity.blur_enabled', data.elem.checked); _this.updateHeaderOpacityStyle(); }); const headerBlurInput = layero[0].querySelector('input[name="header_opacity_blur"]'); if (headerBlurInput) { const handler = () => { Config.updateConfig('header_opacity.blur', headerBlurInput.value); _this.updateHeaderOpacityStyle(); }; headerBlurInput.addEventListener('input', handler); headerBlurInput.addEventListener('blur', handler); } const headerSaturateInput = layero[0].querySelector('input[name="header_opacity_saturate"]'); if (headerSaturateInput) { const handler = () => { Config.updateConfig('header_opacity.saturate', headerSaturateInput.value); _this.updateHeaderOpacityStyle(); }; headerSaturateInput.addEventListener('input', handler); headerSaturateInput.addEventListener('blur', handler); } const frameOpacityRange = layero[0].querySelector('input[name="frame_opacity_value"]'); if (frameOpacityRange) { frameOpacityRange.addEventListener('input', function() { const parsed = parseFloat(this.value); const value = Number.isNaN(parsed) ? 0.95 : parsed; const display = layero[0].querySelector('#frame_opacity_value_display'); if (display) display.textContent = value.toFixed(2); Config.updateConfig('frame_opacity.value', value); _this.updateFrameOpacityStyle(); }); } layui.form.on('checkbox(frame_opacity_blur_toggle)', function(data) { Config.updateConfig('frame_opacity.blur_enabled', data.elem.checked); _this.updateFrameOpacityStyle(); }); layui.form.on('checkbox(header_opacity_saturate_toggle)', function(data) { Config.updateConfig('header_opacity.saturate_enabled', data.elem.checked); _this.updateHeaderOpacityStyle(); }); layui.form.on('checkbox(frame_opacity_saturate_toggle)', function(data) { Config.updateConfig('frame_opacity.saturate_enabled', data.elem.checked); _this.updateFrameOpacityStyle(); }); const frameBlurInput = layero[0].querySelector('input[name="frame_opacity_blur"]'); if (frameBlurInput) { const handler = () => { Config.updateConfig('frame_opacity.blur', frameBlurInput.value); _this.updateFrameOpacityStyle(); }; frameBlurInput.addEventListener('input', handler); frameBlurInput.addEventListener('blur', handler); } const frameSaturateInput = layero[0].querySelector('input[name="frame_opacity_saturate"]'); if (frameSaturateInput) { const handler = () => { Config.updateConfig('frame_opacity.saturate', frameSaturateInput.value); _this.updateFrameOpacityStyle(); }; frameSaturateInput.addEventListener('input', handler); frameSaturateInput.addEventListener('blur', handler); } const loadingOverlayRange = layero[0].querySelector('input[name="loading_overlay_duration"]'); if (loadingOverlayRange) { const handler = () => { const value = parseInt(loadingOverlayRange.value, 10) || INITIAL_OVERLAY_DURATION_DEFAULT; const display = layero[0].querySelector('#loading_overlay_duration_display'); if (display) display.textContent = `${value}ms`; Config.updateConfig('loading_overlay.duration', value); }; loadingOverlayRange.addEventListener('input', handler); loadingOverlayRange.addEventListener('blur', handler); } const defaultAvatarUrlInput = layero[0].querySelector('input[name="default_avatar_url"]'); if (defaultAvatarUrlInput) { const handler = () => { Config.updateConfig('default_avatar.url', defaultAvatarUrlInput.value.trim()); _this.replaceDefaultAvatars(); }; defaultAvatarUrlInput.addEventListener('input', handler); defaultAvatarUrlInput.addEventListener('blur', handler); } layui.form.on('checkbox(default_avatar_auto)', function(data) { Config.updateConfig('default_avatar.auto_detect', data.elem.checked); _this.replaceDefaultAvatars(); }); const exportBtn = layero[0].querySelector('#nsx-export-settings'); if (exportBtn) { exportBtn.addEventListener('click', () => { const settings = util.getValue('settings') || {}; const json = JSON.stringify(settings, null, 2); navigator.clipboard.writeText(json).then(() => { message.success('设置已复制到剪贴板'); }).catch(err => { // console.error(err); message.error('复制失败,请手动复制文本域内容'); }); }); } const importBtn = layero[0].querySelector('#nsx-import-apply'); const importTextarea = layero[0].querySelector('#nsx-import-settings'); if (importBtn && importTextarea) { importBtn.addEventListener('click', () => { const text = importTextarea.value.trim(); if (!text) { message.warning('请先粘贴配置 JSON'); return; } try { const parsed = JSON.parse(text); // 合并默认配置,确保所有配置项都存在 const defaultConfig = opts.settings; const mergeDefaults = (stored, defaults) => { Object.keys(defaults).forEach(key => { if (typeof defaults[key] === 'object' && defaults[key] !== null && !(defaults[key] instanceof Array)) { if (!stored[key]) stored[key] = {}; mergeDefaults(stored[key], defaults[key]); } else { if (stored[key] === undefined) { stored[key] = defaults[key]; } } }); }; mergeDefaults(parsed, defaultConfig); // 保留version字段 const currentSettings = util.getValue('settings') || {}; parsed.version = currentSettings.version || version; util.setValue('settings', parsed); message.success('配置导入成功,正在刷新...'); setTimeout(() => location.reload(), 800); } catch (err) { // console.error(err); message.error('导入失败:JSON 格式不正确'); } }); } // 一键设置默认样式 const resetToDefaultBtn = layero[0].querySelector('#nsx-reset-to-default'); if (resetToDefaultBtn) { resetToDefaultBtn.addEventListener('click', () => { const defaultConfig = { "sign_in": { "ns": { "enabled": true, "method": 0, "last_date": "", "ignore_date": "" }, "df": { "enabled": true, "method": 0, "last_date": "", "ignore_date": "" } }, "signin_tips": { "enabled": true }, "re_signin": { "enabled": true }, "auto_jump_external_links": { "enabled": true }, "loading_post": { "enabled": false }, "loading_comment": { "enabled": false }, "quick_comment": { "enabled": true }, "open_post_in_new_tab": { "enabled": false }, "block_members": { "enabled": true }, "block_posts": { "enabled": true, "keywords": [] }, "block_categories": { "enabled": false, "categories": [] }, "level_tag": { "enabled": true, "low_lv_alarm": false, "low_lv_max_days": 30 }, "code_highlight": { "enabled": true }, "image_slide": { "enabled": true }, "visited_links": { "enabled": true, "link_color": "", "visited_color": "", "dark_link_color": "", "dark_visited_color": "", "hide_visited": false }, "user_card_ext": { "enabled": true }, "compact_mode": { "enabled": true, "columns": 4, "padding": "6px 10px", "avatarSize": 26, "titleFontSize": 13, "infoFontSize": 10, "marginBottom": 2 }, "custom_background": { "enabled": false, "url": "", "repeat": "repeat", "position": "center", "size": "cover", "attachment": "scroll" }, "merge_category_to_nav": { "enabled": true }, "remove_promotions": { "enabled": true }, "header_opacity": { "enabled": true, "value": 0.18, "effect": true, "blur": 16, "saturate": 180, "blur_enabled": false, "saturate_enabled": false }, "frame_opacity": { "enabled": false, "value": 1, "blur_enabled": false, "saturate_enabled": false, "blur": 12, "saturate": 180 }, "default_avatar": { "enabled": true, "url": "/avatar/14049.png", "auto_detect": true }, "loading_overlay": { "enabled": false, "duration": 300 }, "typography": { "title": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "tag": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "meta": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" }, "content": { "enabled": false, "fontFamily": "", "fontSize": "", "color": "", "fontStyle": "", "letterSpacing": "", "ligatures": "" } }, "hide_user_stats_panel": { "enabled": true }, "hide_footer": { "enabled": true }, "hide_post_category": { "enabled": true }, "hide_post_info": { "enabled": false }, "hide_topic_carousel": { "enabled": true }, "post_sort": { "enabled": false, "mode": "none", "visited_to_bottom": true }, "show_all_users_stats": { "enabled": false, "position": "below" }, "right_panel_highlight": { "enabled": false, "keywords": [] } }; // 保留当前的version const currentSettings = util.getValue('settings') || {}; defaultConfig.version = currentSettings.version || version; // 确认对话框 unsafeWindow.mscConfirm('确定要恢复默认样式吗?', '这将覆盖您当前的所有设置(除了版本号),是否继续?', function() { util.setValue('settings', defaultConfig); message.success('默认样式已应用,正在刷新页面...'); setTimeout(() => location.reload(), 800); }); }); } const typographyFields = ['fontFamily','fontSize','color','fontStyle','letterSpacing','ligatures']; const typographyTypes = ['title','tag','meta','content']; typographyTypes.forEach(type => { typographyFields.forEach(field => { const input = layero[0].querySelector(`input[name="typography_${type}_${field}"]`); if (input) { const handler = () => { Config.updateConfig(`typography.${type}.${field}`, input.value); _this.updateTypography(type); }; input.addEventListener('input', handler); input.addEventListener('blur', handler); } }); }); // 紧凑模式列数切换 layui.form.on('radio(compact_columns)', function(data) { const columns = parseInt(data.value); Config.updateConfig('compact_mode.columns', columns); _this.updateCompactModeStyle(); // 如果列数 >= 2,自动启用合并分类到导航栏 if (columns >= 2) { Config.updateConfig('merge_category_to_nav.enabled', true); // 更新开关状态 const switchElem = layero[0].querySelector('input[name="merge_category_to_nav"]'); if (switchElem && !switchElem.checked) { switchElem.checked = true; layui.form.render('checkbox'); } _this.mergeCategoryToNav(); } }); // 帖子排序模式切换 layui.form.on('radio(post_sort_mode)', function(data) { const mode = data.value; Config.updateConfig('post_sort.mode', mode); Config.updateConfig('post_sort.enabled', mode !== 'none'); _this.sortPostList(); }); // 用户统计信息位置切换 layui.form.on('radio(show_all_users_stats_position)', function(data) { const position = data.value; Config.updateConfig('show_all_users_stats.position', position); // 如果功能已启用,重新渲染所有统计信息 if (Config.getConfig('show_all_users_stats.enabled') === true) { _this.hideAllUsersStats(); setTimeout(() => { _this.showAllUsersStats(); }, 100); } }); // 已访问帖子置底切换 layui.form.on('checkbox(visited_to_bottom)', function(data) { Config.updateConfig('post_sort.visited_to_bottom', data.elem.checked); _this.sortPostList(); }); // 紧凑模式自定义数值变化 const compactInputs = ['compact_padding', 'compact_avatarSize', 'compact_titleFontSize', 'compact_infoFontSize', 'compact_marginBottom']; compactInputs.forEach(inputName => { const input = layero[0].querySelector(`input[name="${inputName}"]`); if (input) { input.addEventListener('input', function() { let value = this.value; if (inputName === 'compact_padding') { Config.updateConfig('compact_mode.padding', value); } else if (inputName === 'compact_avatarSize') { Config.updateConfig('compact_mode.avatarSize', parseInt(value)); } else if (inputName === 'compact_titleFontSize') { Config.updateConfig('compact_mode.titleFontSize', parseInt(value)); } else if (inputName === 'compact_infoFontSize') { Config.updateConfig('compact_mode.infoFontSize', parseInt(value)); } else if (inputName === 'compact_marginBottom') { Config.updateConfig('compact_mode.marginBottom', parseInt(value)); } _this.updateCompactModeStyle(); }); } }); // 自定义背景图文件上传 const uploadBtn = layero[0].querySelector('#custom_bg_upload_btn'); const fileInput = layero[0].querySelector('#custom_bg_upload'); const clearBtn = layero[0].querySelector('#custom_bg_clear_btn'); const previewDiv = layero[0].querySelector('#custom_bg_preview'); const previewImg = layero[0].querySelector('#custom_bg_preview_img'); const urlInput = layero[0].querySelector('input[name="custom_bg_url"]'); if (uploadBtn && fileInput) { uploadBtn.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; // 检查文件类型 if (!file.type.startsWith('image/')) { message.warning('请选择图片文件'); return; } // 提示:大图片可能会因为GM存储限制而保存失败 if (file.size > 5 * 1024 * 1024) { const proceed = confirm(`图片较大(${(file.size / 1024 / 1024).toFixed(2)}MB),转换为base64后可能超过GM存储限制(通常5-10MB),是否继续?\n\n建议:使用小于3MB的图片以获得最佳体验。`); if (!proceed) { fileInput.value = ''; return; } } const reader = new FileReader(); reader.onload = function(e) { const base64 = e.target.result; // 尝试保存,如果失败会抛出错误 try { Config.updateConfig('custom_background.url', base64); } catch (err) { message.error('图片数据过大,无法保存。请使用较小的图片(建议小于3MB)。'); fileInput.value = ''; return; } // 更新预览 if (previewImg) { previewImg.src = base64; } if (previewDiv) { previewDiv.style.display = 'block'; } if (clearBtn) { clearBtn.style.display = 'inline-block'; } if (urlInput) { urlInput.value = ''; } // 立即应用 if (Config.getConfig('custom_background.enabled')) { _this.updateCustomBackground(); } message.success('图片上传成功'); }; reader.onerror = function() { message.error('图片读取失败'); }; reader.readAsDataURL(file); }); } // 清除图片 if (clearBtn) { clearBtn.addEventListener('click', function() { Config.updateConfig('custom_background.url', ''); if (previewDiv) { previewDiv.style.display = 'none'; } if (previewImg) { previewImg.src = ''; } if (fileInput) { fileInput.value = ''; } if (urlInput) { urlInput.value = ''; } this.style.display = 'none'; if (Config.getConfig('custom_background.enabled')) { _this.updateCustomBackground(); } message.success('图片已清除'); }); } // 自定义背景图设置变化 const bgInputs = ['custom_bg_url', 'custom_bg_position', 'custom_bg_size']; bgInputs.forEach(inputName => { const input = layero[0].querySelector(`input[name="${inputName}"]`); if (input) { input.addEventListener('input', function() { let value = this.value; if (inputName === 'custom_bg_url') { // 如果输入了URL,清除base64预览 if (value && !value.startsWith('data:')) { Config.updateConfig('custom_background.url', value); if (previewDiv) { previewDiv.style.display = 'none'; } if (clearBtn) { clearBtn.style.display = 'inline-block'; } } } else if (inputName === 'custom_bg_position') { Config.updateConfig('custom_background.position', value); } else if (inputName === 'custom_bg_size') { Config.updateConfig('custom_background.size', value); } if (Config.getConfig('custom_background.enabled')) { _this.updateCustomBackground(); } }); } }); // 自定义背景图下拉框变化 const bgSelects = ['custom_bg_repeat', 'custom_bg_attachment']; bgSelects.forEach(selectName => { const select = layero[0].querySelector(`select[name="${selectName}"]`); if (select) { select.addEventListener('change', function() { let value = this.value; if (selectName === 'custom_bg_repeat') { Config.updateConfig('custom_background.repeat', value); } else if (selectName === 'custom_bg_attachment') { Config.updateConfig('custom_background.attachment', value); } if (Config.getConfig('custom_background.enabled')) { _this.updateCustomBackground(); } }); } }); }, yes: function(index, layero) { // 同步下拉加载设置 const loadingPost = layero.find('input[name="loading_post"]').prop('checked'); Config.updateConfig('loading_post.enabled', loadingPost); Config.updateConfig('loading_comment.enabled', loadingPost); // 保存紧凑模式设置 const compactColumns = layero.find('input[name="compact_columns"]:checked').val(); if (compactColumns) { Config.updateConfig('compact_mode.columns', parseInt(compactColumns)); } const compactPadding = layero.find('input[name="compact_padding"]').val(); if (compactPadding) { Config.updateConfig('compact_mode.padding', compactPadding); } const compactAvatarSize = layero.find('input[name="compact_avatarSize"]').val(); if (compactAvatarSize) { Config.updateConfig('compact_mode.avatarSize', parseInt(compactAvatarSize)); } const compactTitleFontSize = layero.find('input[name="compact_titleFontSize"]').val(); if (compactTitleFontSize) { Config.updateConfig('compact_mode.titleFontSize', parseInt(compactTitleFontSize)); } const compactInfoFontSize = layero.find('input[name="compact_infoFontSize"]').val(); if (compactInfoFontSize) { Config.updateConfig('compact_mode.infoFontSize', parseInt(compactInfoFontSize)); } const compactMarginBottom = layero.find('input[name="compact_marginBottom"]').val(); if (compactMarginBottom) { Config.updateConfig('compact_mode.marginBottom', parseInt(compactMarginBottom)); } // 保存自定义背景图设置 const customBgUrl = layero.find('input[name="custom_bg_url"]').val(); // 如果URL输入框有值,使用输入框的值;否则保留已保存的base64数据 if (customBgUrl && customBgUrl.trim() !== '') { Config.updateConfig('custom_background.url', customBgUrl.trim()); } else { // 如果URL输入框为空,检查是否已有base64数据,如果有则保留 const currentUrl = Config.getConfig('custom_background.url'); if (currentUrl && currentUrl.startsWith('data:')) { // 保留已上传的base64数据,不做任何操作 } else if (customBgUrl === '') { // 如果用户清空了URL输入框,且没有base64数据,则清空配置 Config.updateConfig('custom_background.url', ''); } } const customBgRepeat = layero.find('select[name="custom_bg_repeat"]').val(); if (customBgRepeat) { Config.updateConfig('custom_background.repeat', customBgRepeat); } const customBgPosition = layero.find('input[name="custom_bg_position"]').val(); if (customBgPosition !== undefined) { Config.updateConfig('custom_background.position', customBgPosition); } const customBgSize = layero.find('input[name="custom_bg_size"]').val(); if (customBgSize !== undefined) { Config.updateConfig('custom_background.size', customBgSize); } const customBgAttachment = layero.find('select[name="custom_bg_attachment"]').val(); if (customBgAttachment) { Config.updateConfig('custom_background.attachment', customBgAttachment); } // 更新紧凑模式样式 main.updateCompactModeStyle(); // 更新自定义背景图 main.updateCustomBackground(); // 同步新标签页设置到 IndexedDB const openInNewTab = layero.find('input[name="open_post_in_new_tab"]').prop('checked'); if (openInNewTab !== Config.getConfig('open_post_in_new_tab.enabled')) { _this.switchOpenPostInNewTab('open_post_in_new_tab', []); } // 保存用户统计信息位置设置 const statsPosition = layero.find('input[name="show_all_users_stats_position"]:checked').val(); if (statsPosition) { Config.updateConfig('show_all_users_stats.position', statsPosition); } message.success('设置已保存'); layer.close(index); } }); }, addCodeHighlight() { const codes = document.querySelectorAll(".post-content pre code"); if (codes) { codes.forEach(function (code) { const copyBtn = util.createElement("span", { staticClass: "copy-code", attrs: { title: "复制代码" }, on: { click: copyCode } }, [util.createElement("svg", { staticClass: 'iconpark-icon' }, [util.createElement("use", { attrs: { href: "#copy" } }, [], document, "http://www.w3.org/2000/svg")], document, "http://www.w3.org/2000/svg")]); code.after(copyBtn); }); } function copyCode(e) { const pre = this.closest('pre'); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(pre.querySelector("code")); selection.removeAllRanges(); selection.addRange(range); document.execCommand('copy'); selection.removeAllRanges(); updateCopyButton(this); layer.tips(`复制成功`, this, { tips: 4, time: 1000 }) } function updateCopyButton(ele) { ele.querySelector("use").setAttribute("href", "#check"); util.sleep(1000).then(() => ele.querySelector("use").setAttribute("href", "#copy")); } }, updateCompactModeStyle() { const enabled = Config.getConfig('compact_mode.enabled'); if (enabled === false) { toggleRootClass('nsx-compact-mode', false); applyCompactModeStyleFromConfig(null); removeCompactPendingClass(); removeInitialOverlay(); // 禁用紧凑模式时,也需要更新分类合并 this.mergeCategoryToNav(); return; } const config = normalizeCompactConfig({ columns: Config.getConfig('compact_mode.columns'), padding: Config.getConfig('compact_mode.padding'), avatarSize: Config.getConfig('compact_mode.avatarSize'), titleFontSize: Config.getConfig('compact_mode.titleFontSize'), infoFontSize: Config.getConfig('compact_mode.infoFontSize'), marginBottom: Config.getConfig('compact_mode.marginBottom') }); // 先添加类名,再调用 mergeCategoryToNav,确保能正确判断状态 toggleRootClass('nsx-compact-mode', true); if (config.columns >= 2 && !Config.getConfig('merge_category_to_nav.enabled')) { Config.updateConfig('merge_category_to_nav.enabled', true); } // 更新分类合并(需要知道当前的紧凑模式状态) this.mergeCategoryToNav(); applyCompactModeStyleFromConfig(config); const overlaySettings = getRuntimeOverlaySettings(); scheduleRemoveInitialOverlay(overlaySettings.duration, overlaySettings.enabled); }, updateCustomBackground() { const config = Config.getConfig('custom_background'); if (!config || config.enabled === false || !config.url) { applyCustomBackgroundStyle(null); return; } applyCustomBackgroundStyle({ url: config.url, repeat: config.repeat || 'repeat', position: config.position || 'center', size: config.size || 'cover', attachment: config.attachment || 'scroll' }); }, updateVisitedLinkStyle() { const cfg = Config.getConfig('visited_links'); if (!cfg || cfg.enabled === false) { applyVisitedLinkStyle(null); toggleRootClass('nsx-hide-visited', false); return; } const normalized = { lightLink: (cfg.link_color || '').trim(), lightVisited: (cfg.visited_color || '').trim() || '#afb9c1', darkLink: (cfg.dark_link_color || '').trim(), darkVisited: (cfg.dark_visited_color || '').trim() || cfg.visited_color || '#393f4e' }; applyVisitedLinkStyle(normalized); toggleRootClass('nsx-hide-visited', cfg.hide_visited === true); }, updateTypography(type) { const cfg = Config.getConfig(`typography.${type}`); applyTypographyStyle(type, cfg); }, updateAllTypography() { ['title', 'tag', 'meta', 'content'].forEach(type => this.updateTypography(type)); }, toggleUserStatsPanel() { const enabled = Config.getConfig('hide_user_stats_panel.enabled'); const rightPanel = document.querySelector('#nsk-right-panel-container'); if (!rightPanel) return; // 查找包含"用户数目"或"欢迎新用户"的面板 const panels = rightPanel.querySelectorAll('.nsk-panel'); panels.forEach(panel => { const h4 = panel.querySelector('h4'); if (h4) { const text = h4.textContent || ''; // 检查是否包含"用户数目"或"欢迎新用户" if (text.includes('用户数目') || text.includes('欢迎新用户') || text.includes('📈') || text.includes('🎉')) { panel.style.display = enabled ? 'none' : ''; } } }); }, // 在右侧面板添加关键词高亮输入框 addRightPanelHighlightInput() { const enabled = Config.getConfig('right_panel_highlight.enabled'); if (enabled !== true) { // 如果开关关闭,移除输入框 const existingPanel = document.querySelector('.nsx-highlight-input-panel'); if (existingPanel) { existingPanel.remove(); } return; } const rightPanel = document.querySelector('#nsk-right-panel-container'); if (!rightPanel) return; // 检查是否已存在 if (rightPanel.querySelector('.nsx-highlight-input-panel')) return; // 创建面板 const panel = document.createElement('div'); panel.className = 'nsk-panel nsx-highlight-input-panel'; panel.innerHTML = ` 🔍 关键词高亮 使用说明: • 每行输入一个关键词,使用换行符分隔 • 支持普通文本匹配(不区分大小写) • 支持正则表达式:使用 /正则/flags 格式 • 示例:VPS、服务器、/\\d+GB/i 输入关键词后会自动保存并高亮显示(需在设置中启用) `; // 插入到右侧面板顶部 const firstPanel = rightPanel.querySelector('.nsk-panel'); if (firstPanel) { rightPanel.insertBefore(panel, firstPanel); } else { rightPanel.appendChild(panel); } // 获取元素 const textarea = panel.querySelector('#nsx-right-panel-keywords-input'); const helpText = panel.querySelector('#nsx-highlight-help-text'); const statusText = panel.querySelector('#nsx-highlight-status-text'); // 加载当前配置 const currentKeywords = this.getRightPanelHighlightKeywords(); if (textarea) textarea.value = currentKeywords; // 根据输入框内容显示/隐藏帮助文本 const updateHelpVisibility = () => { if (!textarea || !helpText || !statusText) return; const hasContent = textarea.value.trim().length > 0; helpText.style.display = hasContent ? 'none' : 'block'; statusText.style.display = hasContent ? 'block' : 'none'; }; // 初始状态 updateHelpVisibility(); // 输入框事件处理 if (textarea) { let timer = null; const saveKeywords = () => { const value = textarea.value.trim(); updateHelpVisibility(); // 更新帮助文本显示状态 const parsed = this.parseRightPanelHighlightKeywords(value); // console.log('[NodeSeek X] 解析的关键词:', parsed); Config.updateConfig('right_panel_highlight.keywords', parsed); this.refreshRightPanelHighlightRules(); // console.log('[NodeSeek X] 刷新后的规则:', this.rightPanelHighlightRules); // 如果功能已启用,立即应用高亮 if (Config.getConfig('right_panel_highlight.enabled') === true) { // console.log('[NodeSeek X] 关键词已保存,规则数量:', this.rightPanelHighlightRules.length); this.applyRightPanelHighlight(); } }; textarea.addEventListener('input', () => { updateHelpVisibility(); // 实时更新帮助文本显示状态 clearTimeout(timer); timer = setTimeout(saveKeywords, 500); }); textarea.addEventListener('blur', saveKeywords); } }, toggleFooter() { const enabled = Config.getConfig('hide_footer.enabled'); toggleRootClass('nsx-hide-footer', enabled === true); }, togglePostCategory() { const enabled = Config.getConfig('hide_post_category.enabled'); toggleRootClass('nsx-hide-post-category', enabled === true); }, togglePostInfo() { const enabled = Config.getConfig('hide_post_info.enabled'); toggleRootClass('nsx-hide-post-info', enabled === true); }, toggleTopicCarousel() { const enabled = Config.getConfig('hide_topic_carousel.enabled'); toggleRootClass('nsx-hide-topic-carousel', enabled === true); }, // 获取右侧面板高亮关键词输入值 getRightPanelHighlightKeywords() { const stored = Config.getConfig('right_panel_highlight.keywords'); if (!Array.isArray(stored) || stored.length === 0) return ''; return stored.map(rule => { if (!rule) return ''; if (typeof rule === 'string') return rule; if (rule.type === 'regex') { const flags = rule.flags || ''; return `/${rule.value || ''}/${flags}`; } return rule.value || ''; }).filter(Boolean).join('\n'); }, // 解析右侧面板高亮关键词输入 parseRightPanelHighlightKeywords(inputValue) { if (!inputValue) return []; return inputValue.split(/\r?\n/).map(line => line.trim()).filter(Boolean).map(line => { if (line.startsWith('/') && line.lastIndexOf('/') > 0) { const lastSlash = line.lastIndexOf('/'); const pattern = line.slice(1, lastSlash); const flags = line.slice(lastSlash + 1); try { new RegExp(pattern, flags || 'i'); return { type: 'regex', value: pattern, flags }; } catch (err) { // console.warn('[NodeSeek X] 忽略无效正则:', line, err); return null; } } return { type: 'text', value: line }; }).filter(Boolean); }, // 刷新右侧面板高亮关键词规则 refreshRightPanelHighlightRules() { const stored = Config.getConfig('right_panel_highlight.keywords'); const list = Array.isArray(stored) ? stored : []; this.rightPanelHighlightRules = list.map(rule => { if (!rule) return null; if (typeof rule === 'string') { const value = rule.trim(); if (!value) return null; return { type: 'text', value, lower: value.toLowerCase() }; } const type = rule.type || 'text'; if (type === 'regex') { const pattern = rule.value || rule.pattern || ''; if (!pattern) return null; const flags = rule.flags || ''; try { const regex = new RegExp(pattern, flags || 'i'); return { type: 'regex', regex, value: pattern, flags }; } catch (err) { // console.warn('[NodeSeek X] 无效的正则规则:', pattern, err); return null; } } const value = (rule.value || '').trim(); if (!value) return null; return { type: 'text', value, lower: value.toLowerCase() }; }).filter(Boolean); }, // 高亮文本节点中的关键词 highlightTextNode(textNode, rules) { if (!rules || rules.length === 0) return; if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return; const text = textNode.textContent; if (!text || text.trim().length === 0) return; // 检查是否已经高亮过(避免重复处理) // 如果父元素已经包含高亮标记,说明已经处理过 if (textNode.parentElement && textNode.parentElement.querySelector('.nsx-right-panel-highlight')) { return; } const fragments = []; let lastIndex = 0; // 收集所有匹配位置 const matches = []; rules.forEach(rule => { if (rule.type === 'regex') { try { const regex = new RegExp(rule.regex.source, rule.regex.flags + 'g'); let match; while ((match = regex.exec(text)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length, text: match[0] }); } } catch (err) { // console.warn('[NodeSeek X] 正则匹配失败:', rule, err); } } else { // 普通文本匹配(不区分大小写) const lowerText = text.toLowerCase(); const lowerKeyword = rule.lower; if (!lowerKeyword) return; let index = 0; while ((index = lowerText.indexOf(lowerKeyword, index)) !== -1) { matches.push({ start: index, end: index + rule.value.length, text: text.substring(index, index + rule.value.length) }); index += rule.value.length; } } }); // 按位置排序并合并重叠 matches.sort((a, b) => a.start - b.start); const mergedMatches = []; matches.forEach(match => { if (mergedMatches.length === 0) { mergedMatches.push(match); } else { const last = mergedMatches[mergedMatches.length - 1]; if (match.start <= last.end) { // 重叠,合并 last.end = Math.max(last.end, match.end); last.text = text.substring(last.start, last.end); } else { mergedMatches.push(match); } } }); if (mergedMatches.length === 0) return; // 创建高亮片段 mergedMatches.forEach(match => { // 添加匹配前的文本 if (match.start > lastIndex) { fragments.push(document.createTextNode(text.substring(lastIndex, match.start))); } // 添加高亮的文本 const mark = document.createElement('mark'); mark.className = 'nsx-right-panel-highlight'; mark.textContent = match.text; fragments.push(mark); lastIndex = match.end; }); // 添加剩余的文本 if (lastIndex < text.length) { fragments.push(document.createTextNode(text.substring(lastIndex))); } if (fragments.length > 1) { // 替换文本节点 const parent = textNode.parentNode; const markCount = fragments.filter(f => f.tagName === 'MARK').length; fragments.forEach(fragment => { parent.insertBefore(fragment, textNode); }); parent.removeChild(textNode); if (markCount > 0) { // console.log('[NodeSeek X] 已高亮文本节点,创建了', markCount, '个高亮标记,文本:', text.substring(0, 50), '匹配的关键词:', mergedMatches.map(m => m.text).join(', ')); } } }, // 应用右侧面板关键词高亮 applyRightPanelHighlight() { const enabled = Config.getConfig('right_panel_highlight.enabled'); if (enabled !== true) { // 移除所有高亮 this.removeAllHighlights(); return; } this.refreshRightPanelHighlightRules(); // console.log('[NodeSeek X] 应用高亮,规则数量:', this.rightPanelHighlightRules ? this.rightPanelHighlightRules.length : 0); if (!this.rightPanelHighlightRules || this.rightPanelHighlightRules.length === 0) { this.removeAllHighlights(); return; } // 先移除已存在的高亮标记(避免重复) this.removeAllHighlights(); // 处理右侧面板 const rightPanel = document.querySelector('#nsk-right-panel-container'); if (rightPanel) { // console.log('[NodeSeek X] 处理右侧面板'); this.highlightInContainer(rightPanel); } // 处理帖子列表 - 使用配置的选择器 let postList = document.querySelector(opts.post.postListSelector); if (!postList) { // 备用选择器 postList = document.querySelector('#nsk-body-left .post-list'); } if (!postList) { postList = document.querySelector('.post-list:not(.topic-carousel-panel)'); } if (!postList) { postList = document.querySelector('#nsk-body-left'); } if (postList) { // console.log('[NodeSeek X] 处理帖子列表,容器:', postList.className || postList.id || postList.tagName, '选择器:', opts.post.postListSelector); this.highlightInContainer(postList); } else { // console.warn('[NodeSeek X] 未找到帖子列表容器,尝试的选择器:', opts.post.postListSelector, '#nsk-body-left .post-list', '.post-list:not(.topic-carousel-panel)'); } }, // 移除所有高亮标记 removeAllHighlights() { const containers = [ document.querySelector('#nsk-right-panel-container'), document.querySelector(opts.post.postListSelector), document.querySelector('#nsk-body-left .post-list'), document.querySelector('.post-list:not(.topic-carousel-panel)') ].filter(Boolean); containers.forEach(container => { if (!container) return; container.querySelectorAll('.nsx-right-panel-highlight').forEach(mark => { const parent = mark.parentNode; parent.replaceChild(document.createTextNode(mark.textContent), mark); parent.normalize(); }); container.querySelectorAll('.nsx-highlight-processed').forEach(el => { el.classList.remove('nsx-highlight-processed'); }); }); }, // 在指定容器中应用高亮 highlightInContainer(container) { if (!container) { // console.warn('[NodeSeek X] highlightInContainer: 容器为空'); return; } // console.log('[NodeSeek X] highlightInContainer: 开始处理容器', container.tagName, container.className || container.id); // 遍历所有文本节点 const walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // 排除已处理的节点和某些特殊元素 if (node.parentElement && ( node.parentElement.tagName === 'SCRIPT' || node.parentElement.tagName === 'STYLE' || node.parentElement.classList.contains('nsx-highlight-input-panel') )) { return NodeFilter.FILTER_REJECT; } // 如果父元素已经包含高亮标记,跳过(避免重复处理) // 注意:这里检查的是父元素是否包含高亮标记,而不是当前节点 const parent = node.parentElement; if (parent && parent.querySelector && parent.querySelector('.nsx-right-panel-highlight')) { // 如果父元素已经包含高亮标记,说明已经处理过,跳过 return NodeFilter.FILTER_REJECT; } // 特别处理帖子标题:确保处理 .post-title 内的文本节点 const isInPostTitle = parent && ( parent.classList.contains('post-title') || parent.closest('.post-title') ); if (isInPostTitle && node.textContent.trim()) { // console.log('[NodeSeek X] 找到帖子标题文本节点:', node.textContent.substring(0, 50), '父元素:', parent.tagName, parent.className); } return NodeFilter.FILTER_ACCEPT; } } ); const textNodes = []; let node; while (node = walker.nextNode()) { textNodes.push(node); } // console.log('[NodeSeek X] 找到文本节点数量:', textNodes.length, '规则数量:', this.rightPanelHighlightRules.length); if (textNodes.length === 0) { // console.warn('[NodeSeek X] highlightInContainer: 未找到任何文本节点'); return; } // 处理文本节点(从后往前,避免位置偏移) let processedCount = 0; for (let i = textNodes.length - 1; i >= 0; i--) { const beforeLength = textNodes[i].textContent.length; this.highlightTextNode(textNodes[i], this.rightPanelHighlightRules); // 检查是否被处理了(如果被处理,文本节点会被替换) if (textNodes[i].parentNode && textNodes[i].parentNode.querySelector('.nsx-right-panel-highlight')) { processedCount++; } } // console.log('[NodeSeek X] 处理完成,处理了', processedCount, '个文本节点'); }, // 启动右侧面板高亮观察器 startRightPanelHighlightObserver() { const enabled = Config.getConfig('right_panel_highlight.enabled'); if (enabled !== true) { if (this.rightPanelHighlightObserver) { this.rightPanelHighlightObserver.disconnect(); this.rightPanelHighlightObserver = null; } return; } if (this.rightPanelHighlightObserver) { this.rightPanelHighlightObserver.disconnect(); } const _this = this; const containers = [ document.querySelector('#nsk-right-panel-container'), document.querySelector('#nsk-body-left .post-list') ].filter(Boolean); if (containers.length === 0) return; this.rightPanelHighlightObserver = new MutationObserver(() => { // 延迟应用高亮,避免频繁触发 setTimeout(() => { _this.applyRightPanelHighlight(); }, 100); }); // 观察所有容器 containers.forEach(container => { this.rightPanelHighlightObserver.observe(container, { childList: true, subtree: true, characterData: true }); }); }, mergeCategoryToNav() { const enabled = Config.getConfig('merge_category_to_nav.enabled'); const navMenu = document.querySelector('#nsk-head .nav-menu'); const categoryPanel = document.querySelector('#nsk-right-panel-container .category-list'); const leftCategoryPanel = document.querySelector('#nsk-left-panel-container .category-list'); if (!navMenu) return; // 移除旧的合并样式 const oldStyle = document.getElementById('nsx-merge-category-style'); if (oldStyle) oldStyle.remove(); // 移除已存在的合并分类列表(可能在 nav-menu 中,也可能在 header 中) const existingMerged = document.querySelector('#nsk-head .nsx-merged-categories, header ~ .nsx-merged-categories-wrapper'); if (existingMerged) { existingMerged.remove(); } if (!enabled) { // 恢复右侧面板显示 if (categoryPanel) { categoryPanel.style.display = ''; } if (leftCategoryPanel) { leftCategoryPanel.style.display = ''; } return; } // 隐藏右侧和左侧的分类面板 if (categoryPanel) { categoryPanel.style.display = 'none'; } if (leftCategoryPanel) { leftCategoryPanel.style.display = 'none'; } // 获取分类列表(优先使用右侧面板) const categoryList = categoryPanel || leftCategoryPanel; if (!categoryList) return; const categoryItems = categoryList.querySelectorAll('ul li'); if (categoryItems.length === 0) return; // 获取导航栏中已有的链接URL集合(用于去重) const existingNavLinks = new Set(); navMenu.querySelectorAll('li a').forEach(link => { const href = link.getAttribute('href'); if (href) { // 标准化URL,移除查询参数和锚点 const normalizedHref = href.split('?')[0].split('#')[0]; existingNavLinks.add(normalizedHref); } }); // 也检查已存在的合并分类链接 const header = document.querySelector('#nsk-head'); if (header) { const existingMerged = header.querySelector('.nsx-merged-categories'); if (existingMerged) { existingMerged.querySelectorAll('a').forEach(link => { const href = link.getAttribute('href'); if (href) { const normalizedHref = href.split('?')[0].split('#')[0]; existingNavLinks.add(normalizedHref); } }); } } // 创建合并后的分类菜单 const mergedCategories = document.createElement('div'); mergedCategories.className = 'nsx-merged-categories'; // 检查是否是紧凑模式,如果不是,设置 flex-wrap: nowrap 防止换行 const isCompactMode = document.documentElement.classList.contains('nsx-compact-mode'); const compactColumns = Config.getConfig('compact_mode.columns') || 1; // 只有在紧凑模式且列数>=2时才允许换行,否则单行显示 if (isCompactMode && compactColumns >= 2) { // 紧凑模式多列:允许换行 mergedCategories.style.cssText = 'display: flex; align-items: center; margin-left: 10px; gap: 5px; flex-wrap: wrap;'; } else { // 单列模式或紧凑模式单列:不换行,隐藏超出部分 mergedCategories.style.cssText = 'display: flex; align-items: center; margin-left: 10px; gap: 5px; flex-wrap: nowrap; overflow: hidden;'; } categoryItems.forEach((item) => { const link = item.querySelector('a'); if (!link) return; // 检查是否已存在于导航栏中 const href = link.getAttribute('href'); if (href) { const normalizedHref = href.split('?')[0].split('#')[0]; if (existingNavLinks.has(normalizedHref)) { // 如果导航栏中已有此链接,跳过(保留导航栏中的带图标版本) return; } } const clonedLink = link.cloneNode(true); clonedLink.style.cssText = 'display: flex; align-items: center; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s, color 0.2s; white-space: nowrap;'; // 保持当前分类的样式 if (item.classList.contains('current-category')) { clonedLink.style.backgroundColor = '#f1f3f5'; clonedLink.style.color = '#0dbc79'; } // 添加悬停效果 clonedLink.addEventListener('mouseenter', function() { if (!item.classList.contains('current-category')) { this.style.backgroundColor = '#f1f3f5'; this.style.color = '#0dbc79'; } }); clonedLink.addEventListener('mouseleave', function() { if (!item.classList.contains('current-category')) { this.style.backgroundColor = ''; this.style.color = ''; } }); mergedCategories.appendChild(clonedLink); }); // 只有当有新的分类链接时才添加到导航栏 if (mergedCategories.children.length > 0) { // 将合并的分类链接添加到 header 中,搜索框之后 const header = document.querySelector('#nsk-head'); const searchBox = header ? header.querySelector('.search-box') : null; if (searchBox && searchBox.nextSibling) { header.insertBefore(mergedCategories, searchBox.nextSibling); } else if (searchBox) { searchBox.insertAdjacentElement('afterend', mergedCategories); } else { // 如果没有搜索框,添加到 nav-menu 后面 navMenu.insertAdjacentElement('afterend', mergedCategories); } } // 添加样式 const style = document.createElement('style'); style.id = 'nsx-merge-category-style'; style.textContent = ` /* 增加导航栏宽度,确保能容纳所有内容 */ #nsk-head { max-width: 100% !important; width: 100% !important; overflow: visible !important; } /* 导航栏内的分类链接 */ #nsk-head .nsx-merged-categories { display: flex; align-items: center; flex-wrap: nowrap; gap: 5px; margin-left: 10px; flex-shrink: 0; overflow: hidden; } /* 单列模式下,分类链接从搜索框右边开始,限制最大宽度避免覆盖设置按钮 */ html:not(.nsx-compact-mode) #nsk-head .nsx-merged-categories { position: absolute; left: calc(50% + 180px); right: 120px; top: 50%; transform: translateY(-50%); max-width: calc(50% - 300px); z-index: 0; } /* 紧凑模式多列:分类链接在搜索框右侧 */ html.nsx-compact-mode #nsk-head .nsx-merged-categories { position: absolute; left: calc(50% + 150px); top: 50%; transform: translateY(-50%); max-width: calc(50% - 150px); z-index: 0; } @media screen and (max-width: 1200px) { html:not(.nsx-compact-mode) #nsk-head .nsx-merged-categories { left: calc(50% + 150px) !important; right: 120px !important; max-width: calc(50% - 270px) !important; } html.nsx-compact-mode #nsk-head .nsx-merged-categories { left: calc(50% + 150px) !important; right: 120px !important; max-width: calc(50% - 270px) !important; } } @media screen and (max-width: 800px) { #nsk-head .nsx-merged-categories { position: static !important; transform: none !important; left: auto !important; right: auto !important; max-width: 100% !important; margin-left: 0 !important; margin-top: 5px !important; } } .nsx-merged-categories a { font-size: 13px; text-decoration: none; color: var(--link-color); display: flex; align-items: center; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s, color 0.2s; white-space: nowrap; flex-shrink: 0; } .nsx-merged-categories a:hover { color: var(--link-hover-color); } .nsx-merged-categories .iconpark-icon { width: 14px; height: 14px; margin-right: 4px; } `; document.head.appendChild(style); }, addPluginStyle() { let style = ` .nsplus-tip { background-color: rgba(255, 217, 0, 0.8); border: 0px solid black; padding: 3px; text-align: center;animation: blink 5s cubic-bezier(.68,.05,.46,.96) infinite;} /* @keyframes blink{ 0%{background-color: red;} 25%{background-color: yellow;} 50%{background-color: blue;} 75%{background-color: green;} 100%{background-color: red;} } */ .nsplus-tip p,.nsplus-tip p a { color: #f00 } .nsplus-tip p a:hover {color: #0ff} #back-to-comment{display:flex;} #fast-nav-button-group .nav-item-btn:nth-last-child(4){bottom:120px;} header div.history-dropdown-on { color: var(--link-hover-color); cursor: pointer; padding: 0 5px; position: absolute; right: 50px} header div.nsx-settings-btn { color: var(--link-hover-color); cursor: pointer; padding: 0 5px; position: absolute; right: 70px; transition: color 0.3s; z-index: 10 !important; pointer-events: auto !important; } header div.nsx-settings-btn:hover { color: var(--link-color);} /* 导航栏链接美化 - 添加悬浮效果 */ #nsk-head .nav-menu a { padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s, color 0.2s; text-decoration: none; } #nsk-head .nav-menu a:hover { color: var(--link-hover-color); background-color: rgba(0, 0, 0, 0.05); } body.dark-layout #nsk-head .nav-menu a:hover { background-color: rgba(255, 255, 255, 0.1); } .user-card .close, .hover-user-card .close, .closeBtn, .closeBtn * { cursor: pointer !important; } /* 单列模式下,确保用户卡片不会超出页面底部 */ html:not(.nsx-compact-mode) .hover-user-card, html.nsx-compact-mode[data-compact-columns="1"] .hover-user-card { max-height: calc(100vh - 100px) !important; overflow-y: auto !important; max-width: calc(100vw - 40px) !important; } /* 如果用户卡片底部超出视口,调整位置 */ html:not(.nsx-compact-mode) .hover-user-card[style*="top"] { max-height: calc(100vh - 100px) !important; } html.nsx-compact-mode[data-compact-columns="1"] .hover-user-card[style*="top"] { max-height: calc(100vh - 100px) !important; } .blocked-post { display: none !important; } html.nsx-remove-promotions .promotation-item { display: none !important; } /* 紧凑模式样式由 updateCompactModeStyle() 动态生成 */ /* 设置面板快捷键列表样式 */ #nsx-settings-layer .layui-form-item table kbd { background-color: var(--bg-main-color, #fff) !important; border: 1px solid rgba(0,0,0,.2) !important; border-radius: 3px !important; box-shadow: 0 1px 0 rgba(0,0,0,.2) !important; padding: 3px 8px !important; font-family: monospace !important; font-size: 12px !important; color: var(--text-color, #333) !important; display: inline-block !important; margin: 0 2px !important; } body.dark-layout #nsx-settings-layer .layui-form-item table kbd { background-color: var(--bg-sub-color, #3b3b3b) !important; border-color: rgba(255,255,255,.2) !important; color: var(--text-color, #aaa) !important; } #nsx-settings-layer .layui-form-item table td { color: var(--text-color, #333) !important; } body.dark-layout #nsx-settings-layer .layui-form-item table td { color: var(--text-color, #aaa) !important; } #nsx-settings-layer .layui-form-item table { background-color: transparent !important; } /* 用户统计信息颜色样式 - 鲜艳配色 */ .nsx-user-stats span { font-weight: 500; } .nsx-user-stats span[title="注册天数"] { color: #2ea44f !important; } .nsx-user-stats span[title="鸡腿数量"] { color: #ff9800 !important; } .nsx-user-stats span[title="帖子数"] { color: #2196f3 !important; } .nsx-user-stats span[title="评论数"] { color: #9c27b0 !important; } /* 深色模式下的鲜艳配色 */ body.dark-layout .nsx-user-stats span[title="注册天数"] { color: #45ca6b !important; } body.dark-layout .nsx-user-stats span[title="鸡腿数量"] { color: #ffb74d !important; } body.dark-layout .nsx-user-stats span[title="帖子数"] { color: #64b5f6 !important; } body.dark-layout .nsx-user-stats span[title="评论数"] { color: #ba68c8 !important; } /* Lv0 - Lv2: 灰到橙 */ .role-tag.user-level.user-lv0 {background-color: #c7c2c2; border: 1px solid #c7c2c2; color: #fafafa;} .role-tag.user-level.user-lv1 {background-color: #ffb74d; border: 1px solid #ffb74d; color: #fafafa;} .role-tag.user-level.user-lv2 {background-color: #ff9400; border: 1px solid #ff9400; color: #fafafa;} /* Lv3 - Lv4: 红到深红 */ .role-tag.user-level.user-lv3 {background-color: #ff5252; border: 1px solid #ff5252; color: #fafafa;} .role-tag.user-level.user-lv4 {background-color: #e53935; border: 1px solid #e53935; color: #fafafa;} /* Lv5 - Lv6: 紫到深紫 */ .role-tag.user-level.user-lv5 {background-color: #ab47bc; border: 1px solid #ab47bc; color: #fafafa;} .role-tag.user-level.user-lv6 {background-color: #8e24aa; border: 1px solid #8e24aa; color: #fafafa;} /* Lv7 - Lv8: 蓝到深蓝 */ .role-tag.user-level.user-lv7 {background-color: #42a5f5; border: 1px solid #42a5f5; color: #fafafa;} .role-tag.user-level.user-lv8 {background-color: #1e88e5; border: 1px solid #1e88e5; color: #fafafa;} /* Lv9 - Lv10: 绿到深绿 */ .role-tag.user-level.user-lv9 {background-color: #66bb6a; border: 1px solid #66bb6a; color: #fafafa;} .role-tag.user-level.user-lv10 {background-color: #2e7d32; border: 1px solid #2e7d32; color: #fafafa;} /* Lv11 - Lv12: 金橙到深金橙 */ .role-tag.user-level.user-lv11 {background-color: #ffca28; border: 1px solid #ffca28; color: #fafafa;} .role-tag.user-level.user-lv12 {background-color: #ffb300; border: 1px solid #ffb300; color: #fafafa;} /* Lv13 - Lv14: 紫金到深紫金 */ .role-tag.user-level.user-lv13 {background-color: #b388ff; border: 1px solid #b388ff; color: #fafafa;} .role-tag.user-level.user-lv14 {background-color: #7c4dff; border: 1px solid #7c4dff; color: #fafafa;} /* Lv15: 黑金至尊 */ .role-tag.user-level.user-lv15 {background-color: #000000; border: 1px solid #000000; color: #ffd700;} .post-content pre { position: relative; } .post-content pre span.copy-code { position: absolute; right: .5em; top: .5em; cursor: pointer;color: #c1c7cd; } .post-content pre .iconpark-icon {width:16px;height:16px;margin:3px;} .post-content pre .iconpark-icon:hover {color:var(--link-hover-color)} .dark-layout .post-content pre code.hljs { padding: 1em !important; } html.nsx-hide-visited .post-list .post-title a:visited, html.nsx-hide-visited .post-list-item .post-title a:visited { display: none !important; } `; if (document.head) { util.addStyle('nsplus-style', 'style', style); util.addStyle('layui-style', 'link', 'https://s.cfn.pp.ua/layui/2.9.9/css/layui.css'); util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle")); } }, addPluginScript() { GM_addElement(document.body, 'script', { src: 'https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/highlight.min.js' }); GM_addElement(document.body, 'script', { textContent: 'window.onload = function(){hljs.highlightAll();}' }); GM_addElement(document.body, "script", { textContent: `!function(e){var t,n,d,o,i,a,r='';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window);` }); }, darkMode(){ // 选择要监视的目标元素(body元素) const targetNode = document.querySelector('body'); // 进入页面时判断是否是深色模式 if(targetNode.classList.contains('dark-layout')){ util.addStyle('layuicss-theme-dark','link','https://s.cfn.pp.ua/layui/theme-dark/2.9.7/css/layui-theme-dark.css'); util.removeStyle('hightlight-style'); util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark")); } // 配置MutationObserver的选项 const observerConfig = { attributes: true, // 监视属性变化 attributeFilter: ['class'], // 只监视类属性 }; // 创建一个新的MutationObserver,并指定触发变化时的回调函数 const observer = new MutationObserver((mutationsList, observer) => { for(let mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if(targetNode.classList.contains('dark-layout')){ util.addStyle('layuicss-theme-dark','link','https://s.cfn.pp.ua/layui/theme-dark/2.9.7/css/layui-theme-dark.css'); util.removeStyle('hightlight-style'); util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle_dark")); }else{ util.removeStyle('layuicss-theme-dark'); util.removeStyle('hightlight-style'); util.addStyle('hightlight-style', 'link', GM_getResourceURL("highlightStyle")); } } } }); // 使用给定的配置选项开始观察目标节点 observer.observe(targetNode, observerConfig); }, // 键盘快捷键翻页 initKeyboardNavigation() { document.addEventListener('keydown', (e) => { // 如果用户在输入框中(input, textarea, contenteditable),不触发快捷键 const activeElement = document.activeElement; const isInputFocused = activeElement && ( activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable || activeElement.closest('.CodeMirror') // CodeMirror 编辑器 ); if (isInputFocused) return; // 左箭头键:前一页 if (e.key === 'ArrowLeft' || e.key === '←') { e.preventDefault(); this.navigateToPage('prev'); } // 右箭头键:后一页 else if (e.key === 'ArrowRight' || e.key === '→') { e.preventDefault(); this.navigateToPage('next'); } }); }, // 导航到上一页或下一页 navigateToPage(direction) { // 查找所有分页器(可能有顶部和底部两个) const pagers = document.querySelectorAll('div[role="navigation"][aria-label="pagination"]'); if (pagers.length === 0) return; // 优先使用第一个可见的分页器 let targetPager = null; for (const pager of pagers) { const rect = pager.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { targetPager = pager; break; } } if (!targetPager) targetPager = pagers[0]; let targetLink = null; if (direction === 'prev') { // 查找前一页按钮 const prevBtn = targetPager.querySelector('.pager-prev'); if (prevBtn) { // 检查是否可用(不是 disabled) const isDisabled = prevBtn.getAttribute('aria-disabled') === 'true' || prevBtn.classList.contains('disabled') || prevBtn.hasAttribute('disabled'); if (!isDisabled) { // 如果按钮本身是链接 if (prevBtn.tagName === 'A' && prevBtn.href) { targetLink = prevBtn; } else { // 尝试查找内部的链接或父级链接 const link = prevBtn.querySelector('a') || prevBtn.closest('a'); if (link && link.href) { targetLink = link; } } } } } else if (direction === 'next') { // 查找后一页按钮 const nextBtn = targetPager.querySelector('.pager-next'); if (nextBtn) { // 检查是否可用(不是 disabled) const isDisabled = nextBtn.getAttribute('aria-disabled') === 'true' || nextBtn.classList.contains('disabled') || nextBtn.hasAttribute('disabled'); if (!isDisabled) { // 如果按钮本身是链接 if (nextBtn.tagName === 'A' && nextBtn.href) { targetLink = nextBtn; } else { // 尝试查找内部的链接或父级链接 const link = nextBtn.querySelector('a') || nextBtn.closest('a'); if (link && link.href) { targetLink = link; } } } } } if (targetLink && targetLink.href) { // 显示加载遮罩 const overlay = document.getElementById('nsx-loading-overlay'); if (overlay) overlay.classList.add('show'); window.location.href = targetLink.href; } }, // 排序帖子列表 sortPostList() { const postList = document.querySelector(opts.post.postListSelector); if (!postList) return; const sortMode = Config.getConfig('post_sort.mode') || 'none'; const visitedToBottom = Config.getConfig('post_sort.visited_to_bottom') === true; // 如果没有启用排序且不需要置底,直接返回 if (sortMode === 'none' && !visitedToBottom) return; const items = Array.from(postList.querySelectorAll('.post-list-item')); if (items.length === 0) return; // 获取已访问的链接集合(从 localStorage) const visitedLinks = new Set(); if (visitedToBottom) { try { const visited = localStorage.getItem('nsx_visited_posts'); if (visited) { const visitedArray = JSON.parse(visited); visitedArray.forEach(url => visitedLinks.add(url)); } } catch (e) { // console.warn('[NodeSeek X] 读取访问记录失败:', e); } } // 提取数据并排序 const itemsWithData = items.map(item => { const titleLink = item.querySelector('.post-title a'); const href = titleLink ? titleLink.href : ''; // 检查是否已访问:优先使用 localStorage,其次使用 :visited 伪类 let isVisited = false; if (visitedToBottom && href) { // 标准化 URL(移除查询参数和锚点) const normalizedUrl = href.split('?')[0].split('#')[0]; isVisited = visitedLinks.has(normalizedUrl); } // 如果 localStorage 中没有,尝试使用 :visited 伪类(作为后备) if (!isVisited && titleLink) { try { // 注意:浏览器安全限制,无法直接读取 :visited 状态 // 但可以通过计算样式来检测(虽然不总是可靠) const computedStyle = window.getComputedStyle(titleLink); // 如果链接颜色与未访问链接不同,可能是已访问 // 这里我们主要依赖 localStorage } catch (e) { // 忽略错误 } } // 提取回复数 let comments = 0; const commentsEl = item.querySelector('.info-comments-count span[title]'); if (commentsEl) { const title = commentsEl.getAttribute('title') || ''; const match = title.match(/(\d+)\s*comments?/i); if (match) { comments = parseInt(match[1], 10) || 0; } else { // 如果没有title,尝试从文本内容提取 const text = commentsEl.textContent.trim(); const numMatch = text.match(/\d+/); if (numMatch) { comments = parseInt(numMatch[0], 10) || 0; } } } // 提取查看数 let views = 0; const viewsEl = item.querySelector('.info-views span[title]'); if (viewsEl) { const title = viewsEl.getAttribute('title') || ''; const match = title.match(/(\d+)\s*views?/i); if (match) { views = parseInt(match[1], 10) || 0; } else { // 如果没有title,尝试从文本内容提取 const text = viewsEl.textContent.trim(); const numMatch = text.match(/\d+/); if (numMatch) { views = parseInt(numMatch[0], 10) || 0; } } } return { item, href, isVisited, comments, views }; }); // 排序 itemsWithData.sort((a, b) => { // 如果启用已访问置底,先按访问状态排序 if (visitedToBottom) { if (a.isVisited && !b.isVisited) return 1; if (!a.isVisited && b.isVisited) return -1; } // 然后按排序模式排序 if (sortMode === 'comments') { return b.comments - a.comments; } else if (sortMode === 'views') { return b.views - a.views; } // 如果只是置底,保持原有顺序(在已访问置底后) return 0; }); // 重新插入排序后的元素 itemsWithData.forEach(({ item }) => { postList.appendChild(item); }); }, // 记录帖子访问 recordPostVisit(url) { if (!url) return; try { const normalizedUrl = url.split('?')[0].split('#')[0]; const visited = localStorage.getItem('nsx_visited_posts'); let visitedArray = visited ? JSON.parse(visited) : []; // 如果不存在,添加到数组 if (!visitedArray.includes(normalizedUrl)) { visitedArray.push(normalizedUrl); // 限制数组大小,避免占用过多存储空间(保留最近1000条) if (visitedArray.length > 1000) { visitedArray = visitedArray.slice(-1000); } localStorage.setItem('nsx_visited_posts', JSON.stringify(visitedArray)); } } catch (e) { // console.warn('[NodeSeek X] 记录访问失败:', e); } }, // 初始化帖子访问监听,用于自动重新排序 initPostVisitListener() { const visitedToBottom = Config.getConfig('post_sort.visited_to_bottom') === true; if (!visitedToBottom) return; const _this = this; // 监听帖子链接点击 document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (!link || !link.href) return; // 检查是否是帖子链接 const href = link.href; const isPostLink = /\/post-\d+/.test(href) && href.startsWith(BASE_URL); if (isPostLink) { // 立即记录访问 _this.recordPostVisit(href); // 延迟重新排序,确保记录已保存 setTimeout(() => { _this.sortPostList(); }, 50); } }, true); // 监听页面可见性变化(从新标签页返回时重新排序) document.addEventListener('visibilitychange', () => { if (!document.hidden) { // 页面重新可见时,延迟重新排序 setTimeout(() => { _this.sortPostList(); }, 200); } }); // 监听焦点变化(切换标签页返回时) window.addEventListener('focus', () => { setTimeout(() => { _this.sortPostList(); }, 200); }); }, init() { Config.initializeConfig(); this.addPluginStyle(); this.checkLogin(); this.refreshBlockKeywordRules(); this.refreshBlockedCategories(); this.refreshRightPanelHighlightRules(); // 应用紧凑模式(类名已在早期添加,这里只需要更新样式) this.updateCompactModeStyle(); const codeMirrorElement = document.querySelector('.CodeMirror'); if (codeMirrorElement) { const codeMirrorInstance = codeMirrorElement.CodeMirror; if (codeMirrorInstance) { let btnSubmit = document.querySelector('.md-editor button.submit.btn.focus-visible'); btnSubmit.innerText=btnSubmit.innerText+'(Ctrl+Enter)'; codeMirrorInstance.addKeyMap({"Ctrl-Enter":function(cm){ btnSubmit.click();}}); } } this.autoSignIn();//自动签到 this.addSignTips();//签到提示 this.enforceNewTabForPosts();//强制新标签页打开帖子 this.autoJump();//自动点击跳转页 this.autoLoading();//无缝加载帖子和评论 this.blockMemberDOMInsert();//屏蔽用户 this.blockPost();//屏蔽帖子 this.blockPostsByCategory(); this.blockPostsByViewLevel(); this.quickComment();//快捷评论 this.addLevelTag();//添加等级标签 this.userCardEx();//用户卡片扩展 //this.fakeLevel(); this.addPluginScript(); this.addCodeHighlight(); this.addImageSlide(); this.darkMode(); this.history(); this.initInstantPage(); this.smoothScroll(); this.addSettingsButton(); this.updateCustomBackground(); this.updateVisitedLinkStyle(); this.updateAllTypography(); this.togglePromotions(); this.updateHeaderOpacityStyle(); this.updateFrameOpacityStyle(); this.replaceDefaultAvatars(); this.startAvatarObserver(); this.moveSearchBoxToCenter(); this.mergeCategoryToNav(); this.toggleUserStatsPanel(); this.toggleFooter(); // 应用页脚隐藏设置 this.togglePostCategory(); // 应用分类标签隐藏设置 this.togglePostInfo(); // 应用帖子信息隐藏设置 this.toggleTopicCarousel(); // 应用推荐轮播隐藏设置 this.addRightPanelHighlightInput(); // 在右侧面板添加关键词输入框 // 延迟应用高亮,确保DOM完全加载 setTimeout(() => { this.applyRightPanelHighlight(); }, 200); this.startRightPanelHighlightObserver(); // 启动右侧面板高亮观察器 this.initKeyboardNavigation(); // 初始化键盘快捷键 this.initPostVisitListener(); // 初始化访问监听,用于自动重新排序 // 延迟执行排序,确保DOM完全加载 // 使用多种方式确保排序能执行 setTimeout(() => { this.sortPostList(); // 同时重新应用高亮 this.applyRightPanelHighlight(); }, 100); // 如果页面还在加载,等待load事件 if (document.readyState === 'loading') { window.addEventListener('load', () => { setTimeout(() => { this.sortPostList(); // 同时重新应用高亮 this.applyRightPanelHighlight(); }, 200); }, { once: true }); } // 使用MutationObserver监听帖子列表的出现 const postListObserver = new MutationObserver(() => { const postList = document.querySelector(opts.post.postListSelector); if (postList && postList.children.length > 0) { setTimeout(() => { this.sortPostList(); // 同时重新应用高亮 this.applyRightPanelHighlight(); }, 100); postListObserver.disconnect(); } }); // 如果帖子列表还不存在,开始观察 const postList = document.querySelector(opts.post.postListSelector); if (!postList || postList.children.length === 0) { if (document.body) { postListObserver.observe(document.body, { childList: true, subtree: true }); // 5秒后自动停止观察 setTimeout(() => postListObserver.disconnect(), 5000); } } else { // 如果帖子列表已经存在,立即应用高亮 setTimeout(() => { this.applyRightPanelHighlight(); }, 300); } // 如果启用了显示所有用户统计,则执行 if (Config.getConfig('show_all_users_stats.enabled') === true) { setTimeout(() => { this.showAllUsersStats(); }, 500); } } }; main.init(); }); }); })(); layui.form.on('checkbox(hide_visited_links)', function(data) { Config.updateConfig('visited_links.hide_visited', data.elem.checked); _this.updateVisitedLinkStyle(); });
羊了个羊
/^\\[广告\\]/i
/正则/flags
VPS
服务器
/\\d+GB/i