// ==UserScript== // @name S1 Plus - Stage1st 体验增强套件 // @namespace http://tampermonkey.net/ // @version 4.0.0 // @description 为Stage1st论坛提供帖子/用户屏蔽、导航栏自定义、自动签到、阅读进度跟踪等多种功能,全方位优化你的论坛体验。 // @author moekyo // @match https://stage1st.com/2b/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; const SCRIPT_VERSION = '4.0.0'; const SCRIPT_RELEASE_DATE = '2025-08-02'; // --- 样式注入 --- GM_addStyle(` /* --- 核心修复:禁用论坛自带的用户信息悬浮窗 --- */ #p_pop { display: none !important; } /* --- 关键字屏蔽样式 --- */ .s1plus-hidden-by-keyword { display: none !important; } /* --- 按钮通用样式 --- */ .s1plus-btn { display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; background-color: #f3f4f6; color: #374151; font-size: 12px; font-weight: bold; cursor: pointer; user-select: none; white-space: nowrap; border: none; } .s1plus-btn:hover { background-color: #ef4444; color: white; } /* --- 帖子屏蔽按钮动画与布局 --- */ .thread-block-btn { position: absolute; top: 50%; left: 50%; z-index: 5; padding: 5px 10px 5px 12px; border-radius: 0 30px 30px 0; background-color: #f87171; color: white; font-size: 12px; border: none; box-shadow: 0 1px 3px rgba(0,0,0,0.2); opacity: 0; visibility: hidden; transform: translate(-50%, -50%) scale(0.85); transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } tbody[id^="normalthread_"]:hover .thread-block-btn, tbody[id^="stickthread_"]:hover .thread-block-btn { opacity: 1; visibility: visible; transform: translate(-50%, -50%) scale(1); transition-delay: 0.1s; } .thread-block-btn:hover { background-color: #ef4444; transform: translate(-50%, -50%) scale(1.1); } /* 阅读进度跳转按钮样式 */ .s1plus-progress-jump-btn { display: inline-block; margin: 0 8px; font-size: 12px; font-weight: normal; color: #6b7280; text-decoration: none; border: 1px solid #e5e7eb; background-color: #f9fafb; border-radius: 4px; padding: 1px 6px; transition: all 0.2s ease-in-out; vertical-align: middle; } .s1plus-progress-jump-btn:hover { color: white; background-color: #3b82f6; border-color: #3b82f6; } /* --- 用户屏蔽悬停交互样式 --- */ .s1plus-avatar-overlay-container { position: absolute; display: flex; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.55); opacity: 0; visibility: hidden; transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; pointer-events: auto; z-index: 10; } .pls:hover .s1plus-avatar-overlay-container { opacity: 1; visibility: visible; } .s1plus-avatar-overlay-container .s1plus-btn { color: white; background-color: rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.5); transform: scale(1); transition: all 0.2s ease-in-out; padding: 4px 8px; } .s1plus-avatar-overlay-container .s1plus-btn:hover { background-color: #ef4444; border-color: #ef4444; transform: scale(1.05); } /* --- [MODIFIED] 用户标记悬浮窗 (Style Revamp per Image 2) --- */ .s1plus-tag-popover { position: absolute; z-index: 10001; width: 300px; background-color: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; opacity: 0; visibility: hidden; transform: translateY(5px) scale(0.98); transition: opacity 0.2s ease-out, transform 0.2s ease-out, visibility 0.2s; pointer-events: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .s1plus-tag-popover.visible { opacity: 1; visibility: visible; transform: translateY(0) scale(1); pointer-events: auto; } .s1plus-popover-content { padding: 16px; } .s1plus-popover-main-content { font-size: 14px; line-height: 1.6; color: #1a1a1a; padding: 4px 4px 20px 4px; min-height: 30px; word-wrap: break-word; white-space: pre-wrap; } .s1plus-popover-main-content.empty { text-align: center; color: #888; } .s1plus-popover-hr { border: none; border-top: 1px solid #f0f0f0; margin: 0; } .s1plus-popover-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 16px; } .s1plus-popover-user-container { display: flex; align-items: center; gap: 10px; min-width: 0; } .s1plus-popover-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background-color: #f0f0f0; } .s1plus-popover-user-info { flex-grow: 1; min-width: 0; } .s1plus-popover-username { font-weight: 500; color: #111; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .s1plus-popover-user-id { font-size: 12px; color: #888; white-space: nowrap; } .s1plus-popover-actions { display: flex; gap: 8px; flex-shrink: 0; } .s1plus-popover-btn { padding: 6px 12px; font-size: 13px; font-weight: 500; border-radius: 6px; cursor: pointer; border: none; transition: background-color 0.2s ease, color 0.2s ease; background-color: #f1f2f3; color: #333; white-space: nowrap; } /* [FIX] Blue hover for most buttons */ .s1plus-popover-btn:hover { background-color: #3b82f6; color: white; } /* [FIX] Red hover specifically for the delete button */ .s1plus-popover-btn[data-action="delete-tag"]:hover { background-color: #ef4444; color: white; } .s1plus-edit-mode-header { font-weight: 600; color: #111; font-size: 15px; margin-bottom: 12px; } .s1plus-edit-mode-textarea { width: 100%; height: 90px; border: 1px solid #d1d5db; border-radius: 8px; padding: 10px; font-size: 14px; resize: vertical; box-sizing: border-box; margin-bottom: 12px; } .s1plus-edit-mode-actions { display: flex; justify-content: flex-end; gap: 8px; } /* --- 设置面板样式 --- */ .s1plus-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; } .s1plus-modal-content { background-color: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); width: 600px; max-width: 90%; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; } .s1plus-modal-header { padding: 16px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } .s1plus-modal-title { font-size: 18px; font-weight: bold; color: #111827; } .s1plus-modal-close { width: 12px; height: 12px; cursor: pointer; color: #9ca3af; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 2L14 14M14 2L2 14' stroke='currentColor' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: contain; transition: color 0.2s ease-in-out, transform 0.2s ease-in-out; transform: rotate(0deg); } .s1plus-modal-close:hover { color: #ef4444; transform: rotate(90deg); } .s1plus-modal-body { padding: 0 16px 16px; overflow-y: auto; flex-grow: 1; } .s1plus-modal-footer { padding: 12px 16px; border-top: 1px solid #e5e7eb; text-align: right; font-size: 12px; color: #9ca3af; } .s1plus-tabs { display: flex; border-bottom: 1px solid #e5e7eb; margin-bottom: 16px; } .s1plus-tab-btn { padding: 12px 16px; cursor: pointer; border: none; background-color: transparent; font-size: 14px; color: #6b7280; border-bottom: 2px solid transparent; transition: all 0.2s; } .s1plus-tab-btn:hover { color: #111827; } .s1plus-tab-btn.active { color: #3b82f6; border-bottom-color: #3b82f6; font-weight: 500; } .s1plus-tab-content { display: none; padding-top: 8px; } .s1plus-tab-content.active { display: block; } .s1plus-empty { text-align: center; padding: 24px; color: #6b7280; } .s1plus-list { display: flex; flex-direction: column; gap: 8px; } .s1plus-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px; border-radius: 6px; background-color: #f9fafb; border: 1px solid #e5e7eb; } .s1plus-item-info { flex-grow: 1; min-width: 0; } .s1plus-item-title { font-weight: 500; color: #111827; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .s1plus-item-meta { font-size: 12px; color: #6b7280; margin-bottom: 8px; } .s1plus-item-toggle { font-size: 12px; color: #374151; display: flex; align-items: center; gap: 8px; } .s1plus-item-toggle input { /* Handled by .s1plus-switch */ } .s1plus-unblock-btn { padding: 6px 12px; border-radius: 4px; background-color: #f3f4f6; color: #6b7280; font-size: 14px; cursor: pointer; transition: all 0.2s; border: none; flex-shrink: 0; align-self: center; } .s1plus-unblock-btn:hover { background-color: #10b981; color: white; } .s1plus-sync-title { font-weight: 500; color: #111827; margin-bottom: 8px; } .s1plus-sync-desc { font-size: 14px; color: #6b7280; margin-bottom: 12px; line-height: 1.5; } .s1plus-sync-buttons { display: flex; gap: 8px; margin-bottom: 16px; } .s1plus-sync-btn { padding: 6px 12px; border-radius: 4px; background-color: #f3f4f6; color: #6b7280; font-size: 14px; cursor: pointer; transition: all 0.2s; border: none; } .s1plus-sync-btn:hover { background-color: #3b82f6; color: white; } .s1plus-sync-textarea { width: 100%; min-height: 80px; padding: 8px; border-radius: 4px; border: 1px solid #e5e7eb; font-family: monospace; font-size: 12px; resize: vertical; margin-bottom: 8px; box-sizing: border-box; } .s1plus-message { font-size: 14px; margin-top: 8px; padding: 8px; border-radius: 4px; display:none; text-align: center; } .s1plus-message.success { background-color: #d1fae5; color: #065f46; } .s1plus-message.error { background-color: #fee2e2; color: #ef4444; } /* --- 确认弹窗样式 --- */ @keyframes s1plus-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes s1plus-scale-in { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes s1plus-fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes s1plus-scale-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0.97); opacity: 0; } } .s1plus-confirm-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.65); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: s1plus-fade-in 0.2s ease-out; } .s1plus-confirm-content { background-color: white; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); width: 420px; max-width: 90%; text-align: left; overflow: hidden; animation: s1plus-scale-in 0.25s ease-out; } .s1plus-confirm-body { padding: 20px 24px; font-size: 16px; color: #1f2937; line-height: 1.6; } .s1plus-confirm-body .confirm-title { font-weight: 600; font-size: 18px; margin-bottom: 8px; } .s1plus-confirm-body .confirm-subtitle { font-size: 14px; color: #6b7280; } .s1plus-confirm-footer { padding: 12px 16px; background-color: #f3f3f3ff; display: flex; justify-content: flex-end; gap: 12px; } .s1plus-confirm-btn { padding: 9px 18px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all 0.15s ease-in-out; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .s1plus-confirm-btn:active { transform: translateY(1px); } .s1plus-confirm-btn.cancel { background-color: white; color: #374151; border-color: #d1d5db; } .s1plus-confirm-btn.cancel:hover { background-color: #f9fafb; border-color: #b0b8c2; } .s1plus-confirm-btn.confirm { background-color: #ef4444; color: white; border-color: #ef4444; } .s1plus-confirm-btn.confirm:hover { background-color: #dc2626; border-color: #dc2626; } /* --- Collapsible Section --- */ .s1plus-collapsible-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none; transition: color 0.2s ease; } .s1-settings-group-title.s1plus-collapsible-header { margin-bottom: 0; } .s1plus-collapsible-header:hover { color: #3b82f6; } .s1plus-collapsible-header:hover .s1plus-expander-arrow { color: #3b82f6; } .s1plus-expander-arrow { display: inline-block; width: 12px; height: 12px; color: #6b7280; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 16'%3E%3Cpath d='M2 2L8 8L2 14' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: contain; transition: transform 0.3s ease-in-out, color 0.2s ease; } .s1plus-expander-arrow.expanded { transform: rotate(90deg); } .s1plus-collapsible-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; } .s1plus-collapsible-content.expanded { max-height: 500px; transition: max-height 0.4s ease-in; padding-top: 12px; } /* --- 界面定制设置样式 --- */ .s1-settings-group { margin-bottom: 24px; } .s1-settings-group-title { font-size: 16px; font-weight: 500; color: #111827; border-bottom: 1px solid #e5e7eb; padding-bottom: 8px; margin-bottom: 12px; } .s1-settings-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; } .s1-settings-label { color: #374151; font-size: 14px; } .s1-settings-checkbox { /* Handled by .s1plus-switch */ } .s1plus-setting-desc { font-size: 12px; color: #6b7280; margin: -4px 0 12px 0; padding: 0; line-height: 1.5; } .s1-editor-item { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center; padding: 8px; border-radius: 4px; background: #f9fafb; } .s1-editor-item:not(:last-child) { margin-bottom: 8px; } .s1-editor-item input[type="text"] { width: 100%; border: 1px solid #d1d5db; border-radius: 4px; padding: 6px 8px; font-size: 14px; box-sizing: border-box; } .s1-editor-item-controls { display: flex; align-items: center; gap: 4px; } .s1-editor-btn { padding: 4px; font-size: 18px; line-height: 1; cursor: pointer; border-radius: 4px; border:none; background: transparent; color: #9ca3af; } .s1-editor-btn:hover { background: #e5e7eb; color: #374151; } .s1-editor-btn.keyword-rule-delete, .s1-editor-btn[data-action="delete"] { font-size: 0; width: 26px; height: 26px; padding: 4px; box-sizing: border-box; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23374151'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0' /%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 18px 18px; transition: all 0.2s ease; } .s1-editor-btn.keyword-rule-delete:hover, .s1-editor-btn[data-action="delete"]:hover { background-color: #ef4444; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='white'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0' /%3E%3C/svg%3E"); } .s1-editor-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; } .s1-settings-action-btn { display: inline-block; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; border: none; } .s1-settings-action-btn.primary { background-color: #3b82f6; color: white; } .s1-settings-action-btn.primary:hover { background-color: #2563eb; } .s1-settings-action-btn.secondary { background-color: #e5e7eb; color: #374151; } .s1-settings-action-btn.secondary:hover { background-color: #d1d5db; } /* --- Modern Toggle Switch --- */ .s1plus-switch { position: relative; display: inline-block; width: 40px; height: 22px; vertical-align: middle; flex-shrink: 0; } .s1plus-switch input { opacity: 0; width: 0; height: 0; } .s1plus-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #d1d5db; transition: .3s; border-radius: 22px; } .s1plus-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } input:checked + .s1plus-slider { background-color: #3b82f6; } input:checked + .s1plus-slider:before { transform: translateX(18px); } /* --- Nav Editor Dragging --- */ .s1-editor-item.dragging { opacity: 0.5; } /* --- [NEW] 用户标记设置面板专属样式 --- */ .s1plus-item-meta-id { font-family: monospace; background-color: #e5e7eb; padding: 1px 5px; border-radius: 4px; font-size: 11px; color: #4b5563; } .s1plus-item-content { margin-top: 8px; color: #374151; line-height: 1.6; white-space: pre-wrap; word-break: break-all; } .s1plus-item-editor textarea { width: 100%; min-height: 60px; border: 1px solid #d1d5db; border-radius: 4px; padding: 6px 8px; font-size: 14px; resize: vertical; box-sizing: border-box; margin-top: 8px; } .s1plus-item-actions { display: flex; align-self: flex-start; flex-shrink: 0; gap: 8px; margin-left: 16px; } .s1plus-item-actions .s1plus-btn { padding: 6px 12px; font-size: 14px; background-color: #f3f4f6; color: #6b7280; cursor: pointer; border: none; } .s1plus-item-actions .s1plus-btn:hover { background-color: #e5e7eb; color: #1f2937; } .s1plus-item-actions .s1plus-btn[data-action="edit-tag-item"]:hover { background-color: #2563eb; color: white; } .s1plus-item-actions .s1plus-btn.primary { background-color: #3b82f6; color: white; } .s1plus-item-actions .s1plus-btn.primary:hover { background-color: #2563eb; } .s1plus-item-actions .s1plus-btn.danger:hover { background-color: #ef4444; color: white; } `); let dynamicallyHiddenThreads = {}; // --- 数据处理 & 核心功能 --- const getBlockedThreads = () => GM_getValue('s1plus_blocked_threads', {}); const saveBlockedThreads = (threads) => GM_setValue('s1plus_blocked_threads', threads); const getBlockedUsers = () => GM_getValue('s1plus_blocked_users', {}); const saveBlockedUsers = (users) => GM_setValue('s1plus_blocked_users', users); const saveUserTags = (tags) => GM_setValue('s1plus_user_tags', tags); // [MODIFIED] 升级并获取用户标记,自动迁移旧数据 const getUserTags = () => { const tags = GM_getValue('s1plus_user_tags', {}); let needsMigration = false; const migratedTags = { ...tags }; Object.keys(migratedTags).forEach(id => { if (typeof migratedTags[id] === 'string' || !migratedTags[id].timestamp) { needsMigration = true; const oldTag = typeof migratedTags[id] === 'string' ? migratedTags[id] : migratedTags[id].tag; const oldName = (migratedTags[id] && migratedTags[id].name) ? migratedTags[id].name : `用户 #${id}`; migratedTags[id] = { name: oldName, tag: oldTag, timestamp: (migratedTags[id] && migratedTags[id].timestamp) || Date.now() }; } }); if (needsMigration) { console.log('S1 Plus: 正在将用户标记迁移到新版数据结构...'); saveUserTags(migratedTags); return migratedTags; } return tags; }; const getTitleFilterRules = () => { const rules = GM_getValue('s1plus_title_filter_rules', null); if (rules !== null) return rules; // --- 向下兼容:迁移旧的关键字数据 --- const oldKeywords = GM_getValue('s1plus_title_keywords', null); if (Array.isArray(oldKeywords)) { const newRules = oldKeywords.map(k => ({ pattern: k, enabled: true, id: `rule_${Date.now()}_${Math.random()}` })); saveTitleFilterRules(newRules); GM_setValue('s1plus_title_keywords', null); // 清理旧数据 return newRules; } return []; }; const saveTitleFilterRules = (rules) => GM_setValue('s1plus_title_filter_rules', rules); const blockThread = (id, title, reason = 'manual') => { const b = getBlockedThreads(); if (b[id]) return; b[id] = { title, timestamp: Date.now(), reason }; saveBlockedThreads(b); hideThread(id); }; const unblockThread = (id) => { const b = getBlockedThreads(); delete b[id]; saveBlockedThreads(b); showThread(id); }; const hideThread = (id) => { (document.getElementById(`normalthread_${id}`) || document.getElementById(`stickthread_${id}`))?.setAttribute('style', 'display: none !important'); }; const showThread = (id) => { (document.getElementById(`normalthread_${id}`) || document.getElementById(`stickthread_${id}`))?.removeAttribute('style'); } const hideBlockedThreads = () => Object.keys(getBlockedThreads()).forEach(hideThread); const blockUser = (id, name) => { const settings = getSettings(); const b = getBlockedUsers(); b[id] = { name, timestamp: Date.now(), blockThreads: settings.blockThreadsOnUserBlock }; saveBlockedUsers(b); hideUserPosts(id); if (b[id].blockThreads) applyUserThreadBlocklist(); }; const unblockUser = (id) => { const b = getBlockedUsers(); delete b[id]; saveBlockedUsers(b); showUserPosts(id); unblockThreadsByUser(id); }; const hideUserPosts = (id) => { document.querySelectorAll(`a[href*="space-uid-${id}.html"]`).forEach(l => l.closest('table.plhin')?.setAttribute('style', 'display: none !important')); }; const showUserPosts = (id) => { document.querySelectorAll(`a[href*="space-uid-${id}.html"]`).forEach(l => l.closest('table.plhin')?.removeAttribute('style')); }; const hideBlockedUsersPosts = () => Object.keys(getBlockedUsers()).forEach(hideUserPosts); // [NEW] 屏蔽评分记录 const hideBlockedUserRatings = () => { const blockedUserIds = Object.keys(getBlockedUsers()); if (blockedUserIds.length === 0) return; document.querySelectorAll('tbody.ratl_l tr').forEach(row => { const userLink = row.querySelector('a[href*="space-uid-"]'); if (userLink) { const uidMatch = userLink.href.match(/space-uid-(\d+)/); if (uidMatch && uidMatch[1] && blockedUserIds.includes(uidMatch[1])) { row.style.display = 'none'; const tbody = row.parentElement; const allRows = tbody.querySelectorAll('tr'); const visibleRows = Array.from(allRows).filter(r => r.style.display !== 'none'); if (visibleRows.length === 0) { const rateLogContainer = tbody.closest('dl.rate'); if (rateLogContainer) { rateLogContainer.style.display = 'none'; } } } } }); }; const hideThreadsByTitleKeyword = () => { const rules = getTitleFilterRules().filter(r => r.enabled && r.pattern); const newHiddenThreads = {}; const regexes = rules.map(r => { try { return { regex: new RegExp(r.pattern), pattern: r.pattern }; } catch (e) { console.error(`S1 Plus: 屏蔽规则 "${r.pattern}" 不是一个有效的正则表达式,将被忽略。`, e); return null; } }).filter(Boolean); document.querySelectorAll('tbody[id^="normalthread_"]').forEach(row => { const titleElement = row.querySelector('th a.s.xst'); if (!titleElement) return; const title = titleElement.textContent.trim(); const threadId = row.id.replace('normalthread_', ''); let isHidden = false; if (regexes.length > 0) { const matchingRule = regexes.find(r => r.regex.test(title)); if (matchingRule) { newHiddenThreads[threadId] = { title, pattern: matchingRule.pattern }; row.classList.add('s1plus-hidden-by-keyword'); isHidden = true; } } if (!isHidden) { row.classList.remove('s1plus-hidden-by-keyword'); } }); dynamicallyHiddenThreads = newHiddenThreads; }; const getReadProgress = () => GM_getValue('s1plus_read_progress', {}); const saveReadProgress = (progress) => GM_setValue('s1plus_read_progress', progress); const updateThreadProgress = (threadId, postId, page) => { if (!postId || !page) return; const progress = getReadProgress(); progress[threadId] = { postId, page, timestamp: Date.now() }; saveReadProgress(progress); }; const applyUserThreadBlocklist = () => { const blockedUsers = getBlockedUsers(); const usersToBlockThreads = Object.keys(blockedUsers).filter(uid => blockedUsers[uid].blockThreads); if (usersToBlockThreads.length === 0) return; document.querySelectorAll('tbody[id^="normalthread_"]').forEach(row => { const authorLink = row.querySelector('td.by cite a[href*="space-uid-"]'); if (authorLink) { const uidMatch = authorLink.href.match(/space-uid-(\d+)\.html/); const authorId = uidMatch ? uidMatch[1] : null; if (authorId && usersToBlockThreads.includes(authorId)) { const threadId = row.id.replace('normalthread_', ''); const titleElement = row.querySelector('th a.s.xst'); if (threadId && titleElement) { blockThread(threadId, titleElement.textContent.trim(), `user_${authorId}`); } } } }); }; const unblockThreadsByUser = (userId) => { const allBlockedThreads = getBlockedThreads(); const reason = `user_${userId}`; Object.keys(allBlockedThreads).forEach(threadId => { if (allBlockedThreads[threadId].reason === reason) { unblockThread(threadId); } }); }; const exportData = () => JSON.stringify({ version: 3.2, settings: getSettings(), threads: getBlockedThreads(), users: getBlockedUsers(), user_tags: getUserTags(), title_filter_rules: getTitleFilterRules(), read_progress: getReadProgress() }, null, 2); const importData = (jsonStr) => { try { const imported = JSON.parse(jsonStr); if (typeof imported !== 'object' || imported === null) throw new Error("无效数据格式"); let threadsImported = 0, usersImported = 0, progressImported = 0, rulesImported = 0, tagsImported = 0; const upgradeAndMerge = (type, importedData, getter, saver) => { if (!importedData || typeof importedData !== 'object') return 0; Object.keys(importedData).forEach(id => { const item = importedData[id]; if (type === 'users' && typeof item.blockThreads === 'undefined') item.blockThreads = false; if (type === 'threads' && typeof item.reason === 'undefined') item.reason = 'manual'; }); const merged = { ...getter(), ...importedData }; saver(merged); return Object.keys(importedData).length; }; if(imported.settings) { saveSettings({...getSettings(), ...imported.settings}); } threadsImported = upgradeAndMerge('threads', imported.threads, getBlockedThreads, saveBlockedThreads); usersImported = upgradeAndMerge('users', imported.users, getBlockedUsers, saveBlockedUsers); if (imported.user_tags && typeof imported.user_tags === 'object') { const mergedTags = { ...getUserTags(), ...imported.user_tags }; saveUserTags(mergedTags); tagsImported = Object.keys(imported.user_tags).length; } if (imported.title_filter_rules && Array.isArray(imported.title_filter_rules)) { saveTitleFilterRules(imported.title_filter_rules); rulesImported = imported.title_filter_rules.length; } else if (imported.title_keywords && Array.isArray(imported.title_keywords)) { // 向后兼容导入旧格式 const newRules = imported.title_keywords.map(k => ({ pattern: k, enabled: true, id: `rule_${Date.now()}_${Math.random()}` })); saveTitleFilterRules(newRules); rulesImported = newRules.length; } if (imported.read_progress) { const mergedProgress = { ...getReadProgress(), ...imported.read_progress }; saveReadProgress(mergedProgress); progressImported = Object.keys(imported.read_progress).length; } hideBlockedThreads(); hideBlockedUsersPosts(); applyUserThreadBlocklist(); hideThreadsByTitleKeyword(); initializeNavbar(); applyInterfaceCustomizations(); return { success: true, message: `成功导入 ${threadsImported} 条帖子、${usersImported} 条用户、${tagsImported} 条标记、${rulesImported} 条标题规则、${progressImported} 条阅读进度及相关设置。` }; } catch (e) { return { success: false, message: `导入失败: ${e.message}` }; } }; // --- 设置管理 --- const defaultSettings = { enableNavCustomization: true, changeLogoLink: true, hideBlacklistTip: true, blockThreadsOnUserBlock: true, showBlockedByKeywordList: false, showManuallyBlockedList: false, customNavLinks: [ { name: '论坛', href: 'forum.php' }, { name: '归墟', href: 'forum-157-1.html' }, { name: '漫区', href: 'forum-6-1.html' }, { name: '游戏', href: 'forum-4-1.html' }, { name: '影视', href: 'forum-48-1.html' }, { name: 'PC数码', href: 'forum-51-1.html' }, { name: '黑名单', href: 'home.php?mod=space&do=friend&view=blacklist' } ] }; const getSettings = () => { const saved = GM_getValue('s1plus_settings', {}); return {...defaultSettings, ...saved}; }; const saveSettings = (settings) => GM_setValue('s1plus_settings', settings); // --- 界面定制功能 --- const applyInterfaceCustomizations = () => { const settings = getSettings(); if (settings.changeLogoLink) document.querySelector('#hd h2 a')?.setAttribute('href', './forum.php'); if (settings.hideBlacklistTip) document.getElementById('hiddenpoststip')?.remove(); }; const initializeNavbar = () => { const settings = getSettings(); const navUl = document.querySelector('#nv > ul'); if (!navUl) return; const createManagerLink = () => { const li = document.createElement('li'); li.id = 's1plus-nav-link'; const a = document.createElement('a'); a.href = 'javascript:void(0);'; a.textContent = 'S1 Plus 设置'; a.addEventListener('click', createManagementModal); li.appendChild(a); return li; }; document.getElementById('s1plus-nav-link')?.remove(); if (settings.enableNavCustomization) { navUl.innerHTML = ''; (settings.customNavLinks || []).forEach(link => { if(!link.name || !link.href) return; const li = document.createElement('li'); if (window.location.href.includes(link.href)) li.className = 'a'; const a = document.createElement('a'); a.href = link.href; a.textContent = link.name; a.setAttribute('hidefocus', 'true'); li.appendChild(a); navUl.appendChild(li); }); } navUl.appendChild(createManagerLink()); }; // --- UI 创建 --- const formatDate = (timestamp) => new Date(timestamp).toLocaleString('zh-CN'); const showMessage = (msgEl, message, isSuccess) => { msgEl.textContent = message; msgEl.className = `s1plus-message ${isSuccess ? 'success' : 'error'}`; msgEl.style.display = 'block'; setTimeout(() => { msgEl.style.display = 'none'; }, 3000); }; const createConfirmationModal = (title, subtitle, onConfirm, confirmText = '确定') => { document.querySelector('.s1plus-confirm-modal')?.remove(); const modal = document.createElement('div'); modal.className = 's1plus-confirm-modal'; modal.innerHTML = `
在此集中管理、编辑、导出或导入您为所有用户添加的标记。
提示:顶部总开关仅影响未来新屏蔽用户的默认设置。每个用户下方的独立开关,才是控制该用户主题帖的最终开关,拥有最高优先级。
${userItemIds.length === 0 ? `将自动屏蔽标题匹配已启用规则的帖子,支持正则表达式。修改后请点击“保存规则”以生效。