- // ==UserScript==
- // @name 百度贴吧终极增强套件Pro
- // @namespace http://tampermonkey.net/
- // @version 7.54
- // @description 修复版:彻底解决翻页后过滤失效问题,性能优化,增加网络优化预加载,部分来自(Grok AI)
- // @author YourName
- // @match *://tieba.baidu.com/p/*
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addStyle
- // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
- // @require https://cdn.jsdelivr.net/npm/lz-string@1.4.4/libs/lz-string.min.js
- // @connect tieba.baidu.com
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // 一、关键修复点说明:
- // 1. 增强AJAX拦截,覆盖XMLHttpRequest
- // 2. 改进MutationObserver监听动态内容
- // 3. 持久化屏蔽词配置到localStorage
- // 4. 监听翻页按钮点击触发过滤
- // 5. 使用标记优化过滤性能
- // 6. 修复控制面板拖动不跟手问题
-
- // 二、性能优化常量
- const DEBOUNCE_LEVEL = { QUICK: 100, COMPLEX: 500 };
- const LOG_LEVEL = GM_getValue('logLevel', 'verbose');
- const MAX_LOG_ENTRIES = 100;
- const DATA_VERSION = 2;
- const CACHE_TTL = 60000;
-
- // 三、网络优化预加载资源列表
- const preloadList = [
- '/static/emoji.png',
- '/static/theme-dark.css'
- ];
-
- // 四、增强的日志管理
- const logBuffer = { script: [], pageState: [], pageBehavior: [], userActions: [] };
- const originalConsole = {
- log: console.log.bind(console),
- warn: console.warn.bind(console),
- error: console.error.bind(console)
- };
-
- function logWrapper(category, level, ...args) {
- const debugMode = GM_getValue('debugMode', true);
- const levelMap = { ERROR: 1, WARN: 2, LOG: 3 };
- const currentLevel = levelMap[level.toUpperCase()];
- const minLevel = levelMap[LOG_LEVEL.toUpperCase()] || 3;
-
- if (!debugMode || (currentLevel && minLevel < currentLevel)) return;
-
- const timestamp = new Date().toISOString();
- const formattedArgs = args.map(arg => {
- if (typeof arg === 'object') {
- try { return JSON.stringify(arg); }
- catch { return String(arg); }
- }
- return String(arg);
- }).join(' ');
-
- const message = `[${timestamp}] [${level}] ${formattedArgs}`;
- logBuffer[category].push(message);
- if (logBuffer[category].length > MAX_LOG_ENTRIES) logBuffer[category].shift();
- originalConsole[level.toLowerCase()](message);
- }
-
- const customConsole = {
- log: (...args) => logWrapper('script', 'LOG', ...args),
- warn: (...args) => logWrapper('script', 'WARN', ...args),
- error: (...args) => logWrapper('script', 'ERROR', ...args)
- };
-
- // 五、通用工具类
- class DomUtils {
- static safeQuery(selector, parent = document) {
- try {
- return parent.querySelector(selector) || null;
- } catch (e) {
- customConsole.error(`无效选择器: ${selector}`, e);
- return null;
- }
- }
-
- static safeQueryAll(selector, parent = document) {
- try {
- return parent.querySelectorAll(selector);
- } catch (e) {
- customConsole.error(`无效选择器: ${selector}`, e);
- return [];
- }
- }
- }
-
- // 六、配置管理类
- class ConfigManager {
- static get defaultFilterSettings() {
- return {
- hideInvalid: true,
- hideSpam: true,
- spamKeywords: ["顶", "沙发", "签到"],
- whitelist: [],
- blockedElements: [],
- tempBlockedElements: [],
- autoExpandImages: true,
- blockType: 'perm',
- blockAds: true,
- enhanceImages: true,
- linkifyVideos: true,
- darkMode: false,
- showHiddenFloors: false
- };
- }
-
- static get defaultPanelSettings() {
- return {
- width: 320,
- minHeight: 100,
- maxHeight: '90vh',
- position: { x: 20, y: 20 },
- scale: 1.0,
- minimized: true
- };
- }
-
- static getFilterSettings() {
- const raw = GM_getValue('settings');
- const settings = raw ? decompressSettings(raw) : this.defaultFilterSettings;
- const savedKeywords = loadConfig();
- if (savedKeywords.length > 0) settings.spamKeywords = savedKeywords;
- return settings;
- }
-
- static getPanelSettings() {
- return GM_getValue('panelSettings', this.defaultPanelSettings);
- }
-
- static updateFilterSettings(newSettings) {
- GM_setValue('settings', compressSettings({ ...this.defaultFilterSettings, ...newSettings }));
- saveConfig(newSettings.spamKeywords);
- }
-
- static updatePanelSettings(newSettings) {
- GM_setValue('panelSettings', { ...this.defaultPanelSettings, ...newSettings });
- }
- }
-
- // 七、本地存储压缩与迁移
- function compressSettings(settings) {
- return LZString.compressToUTF16(JSON.stringify(settings));
- }
-
- function decompressSettings(data) {
- try {
- return JSON.parse(LZString.decompressFromUTF16(data));
- } catch {
- return ConfigManager.defaultFilterSettings;
- }
- }
-
- function saveConfig(keywords) {
- localStorage.setItem('filterConfig', JSON.stringify(keywords));
- customConsole.log('保存屏蔽词配置到 localStorage:', keywords);
- }
-
- function loadConfig() {
- return JSON.parse(localStorage.getItem('filterConfig')) || [];
- }
-
- function migrateSettings() {
- const storedVer = GM_getValue('dataVersion', 1);
- if (storedVer < DATA_VERSION) {
- const old = GM_getValue('settings');
- if (storedVer === 1 && old) {
- const decompressed = decompressSettings(old);
- const newSettings = { ...ConfigManager.defaultFilterSettings, ...decompressed };
- GM_setValue('settings', compressSettings(newSettings));
- }
- GM_setValue('dataVersion', DATA_VERSION);
- }
- }
-
- // 八、错误边界
- class ErrorBoundary {
- static wrap(fn, context) {
- return function (...args) {
- try {
- return fn.apply(context, args);
- } catch (e) {
- customConsole.error(`Error in ${fn.name}:`, e);
- context.showErrorToast?.(`${fn.name}出错`, e);
- return null;
- }
- };
- }
- }
-
- // 九、事件管理
- const listenerMap = new WeakMap();
- function addSafeListener(element, type, listener) {
- const wrapped = e => { try { listener(e); } catch (err) { customConsole.error('Listener error:', err); } };
- element.addEventListener(type, wrapped);
- listenerMap.set(listener, wrapped);
- }
-
- function removeSafeListener(element, type, listener) {
- const wrapped = listenerMap.get(listener);
- if (wrapped) {
- element.removeEventListener(type, wrapped);
- listenerMap.delete(listener);
- }
- }
-
- // 十、性能监控类
- class PerformanceMonitor {
- static instance;
- constructor() {
- this.metrics = { memoryUsage: [], processSpeed: [], networkRequests: [] };
- this.maxMetrics = 100;
- }
-
- static getInstance() {
- if (!PerformanceMonitor.instance) PerformanceMonitor.instance = new PerformanceMonitor();
- return PerformanceMonitor.instance;
- }
-
- recordMemory() {
- if ('memory' in performance) {
- const used = performance.memory.usedJSHeapSize;
- this.metrics.memoryUsage.push(used);
- if (this.metrics.memoryUsage.length > this.maxMetrics) this.metrics.memoryUsage.shift();
- logWrapper('pageState', 'LOG', `Memory usage: ${Math.round(used / 1024 / 1024)} MB`);
- }
- }
-
- recordProcessSpeed(time) {
- this.metrics.processSpeed.push(time);
- if (this.metrics.processSpeed.length > this.maxMetrics) this.metrics.processSpeed.shift();
- logWrapper('pageState', 'LOG', `Process speed: ${time.toFixed(2)} ms`);
- }
- }
-
- // 十一、智能空闲任务调度
- const idleQueue = [];
- let idleCallback = null;
-
- function scheduleIdleTask(task) {
- idleQueue.push(task);
- if (!idleCallback) {
- idleCallback = requestIdleCallback(processIdleTasks, { timeout: 1000 });
- }
- }
-
- function processIdleTasks(deadline) {
- while (deadline.timeRemaining() > 0 && idleQueue.length) {
- idleQueue.shift()();
- }
- if (idleQueue.length) {
- idleCallback = requestIdleCallback(processIdleTasks, { timeout: 1000 });
- } else {
- idleCallback = null;
- }
- }
-
- // 十二、网络优化策略
- function prefetchResources() {
- preloadList.forEach(url => {
- const link = document.createElement('link');
- link.rel = 'prefetch';
- link.href = url;
- document.head.appendChild(link);
- });
- customConsole.log('预加载资源完成:', preloadList);
- }
-
- const cacheStore = new Map();
-
- function smartFetch(url) {
- const cached = cacheStore.get(url);
- if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
- customConsole.log('从缓存读取:', url);
- return Promise.resolve(cached.data);
- }
- return fetch(url)
- .then(res => res.json())
- .then(data => {
- cacheStore.set(url, { data, timestamp: Date.now() });
- customConsole.log('缓存新数据:', url);
- return data;
- })
- .catch(err => {
- customConsole.error('请求失败:', url, err);
- throw err;
- });
- }
-
- // 十三、帖子过滤类
- class PostFilter {
- constructor() {
- this.settings = ConfigManager.getFilterSettings();
- this.postContainer = DomUtils.safeQuery('.l_post_list') || DomUtils.safeQuery('.pb_list') || document.body;
- this.postsCache = new Map();
- this.spamPosts = new Set();
- this.originalOrder = new Map();
- this.applyStyles();
- this.saveOriginalOrder();
- this.applyFilters = ErrorBoundary.wrap(this.applyFilters, this);
- this.applyFilters();
- this.autoExpandImages();
- this.observeDOMChanges();
- this.handlePagination();
- this.startSpamEnforcer();
- this.blockAds();
- this.interceptAjax();
- if (this.settings.linkifyVideos) this.linkifyVideos();
- this.cleanupMemory();
- customConsole.log('PostFilter 初始化完成');
- }
-
- applyStyles() {
- GM_addStyle(`
- .l_post {
- transition: opacity 0.3s ease;
- overflow: hidden;
- }
- .spam-hidden {
- display: none !important;
- opacity: 0;
- }
- .invalid-hidden { display: none !important; }
- .pb_list.filtering, .l_post_list.filtering {
- position: relative;
- opacity: 0.8;
- }
- .pb_list.filtering::after, .l_post_list.filtering::after {
- content: "过滤中...";
- position: absolute;
- top: 20px;
- left: 50%;
- transform: translateX(-50%);
- color: #666;
- }
- `);
- }
-
- saveOriginalOrder() {
- const posts = DomUtils.safeQueryAll('.l_post', this.postContainer);
- customConsole.log('保存帖子原始顺序,检测到帖子数量:', posts.length);
- posts.forEach(post => {
- const data = this.safeGetPostData(post);
- if (!this.originalOrder.has(data.pid)) {
- this.originalOrder.set(data.pid, { pid: data.pid, floor: data.floor, element: post.cloneNode(true) });
- }
- });
- customConsole.log('保存帖子原始顺序完成,数量:', this.originalOrder.size);
- logWrapper('pageBehavior', 'LOG', `Saved original order, posts: ${this.originalOrder.size}`);
- }
-
- applyFilters(nodes = DomUtils.safeQueryAll('.l_post:not(.filtered)', this.postContainer)) {
- logWrapper('script', 'LOG', 'Starting filter process', `Keywords: ${this.settings.spamKeywords}`);
- customConsole.log('开始应用过滤器,帖子数量:', nodes.length);
-
- const startTime = performance.now();
- this.postContainer.classList.add('filtering');
- let hiddenCount = 0;
-
- const keywords = this.settings.spamKeywords
- .map(k => k.trim())
- .filter(k => k.length > 0);
- const regex = keywords.length > 0
- ? new RegExp(`(${keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'i')
- : null;
-
- nodes.forEach(post => {
- const data = this.safeGetPostData(post);
- const pid = data.pid;
-
- if (!post || post.classList.contains('filtered')) return;
-
- post.style.display = '';
- post.classList.remove('spam-hidden', 'invalid-hidden');
-
- if (this.settings.hideInvalid && !data.content) {
- post.classList.add('invalid-hidden');
- hiddenCount++;
- logWrapper('pageBehavior', 'LOG', `Hid invalid post: ${pid}`);
- if (this.settings.showHiddenFloors) {
- post.classList.remove('invalid-hidden');
- hiddenCount--;
- logWrapper('pageBehavior', 'LOG', `Restored invalid post: ${pid}`);
- }
- } else if (this.settings.hideSpam && data.content && regex && regex.test(data.content)) {
- post.classList.add('spam-hidden');
- post.style.display = 'none';
- this.spamPosts.add(pid);
- hiddenCount++;
- const matchedKeyword = keywords.find(k => data.content.toLowerCase().includes(k.toLowerCase())) || 'unknown';
- logWrapper('pageBehavior', 'LOG', `Hid spam post: ${pid}, Keyword: ${matchedKeyword}, Content: ${data.content.slice(0, 50)}...`);
- }
-
- post.classList.add('filtered');
- this.postsCache.set(pid, true);
- });
-
- const blockedElements = [...(this.settings.blockedElements || []), ...(this.settings.tempBlockedElements || [])];
- blockedElements.forEach(selector => {
- DomUtils.safeQueryAll(selector + ':not(.filtered)', this.postContainer).forEach(el => {
- const pid = el.dataset.pid || `temp_${Date.now()}`;
- if (!this.postsCache.has(pid)) {
- el.classList.add('spam-hidden');
- el.style.display = 'none';
- hiddenCount++;
- logWrapper('pageBehavior', 'LOG', `Hid blocked element: ${selector}`);
- el.classList.add('filtered');
- this.postsCache.set(pid, true);
- }
- });
- });
-
- setTimeout(() => {
- this.postContainer.classList.remove('filtering');
- if (hiddenCount > 0) this.showToast(`已屏蔽 ${hiddenCount} 条水贴`, 'success');
- customConsole.log(`[LOG] 翻页后过滤完成,处理帖子数:${nodes.length}`);
- logWrapper('pageBehavior', 'LOG', `Filter completed, processed posts: ${nodes.length}, hidden: ${hiddenCount}`);
- }, 500);
-
- const endTime = performance.now();
- PerformanceMonitor.getInstance().recordProcessSpeed(endTime - startTime);
- return hiddenCount;
- }
-
- startSpamEnforcer() {
- const enforce = () => {
- const allPosts = DomUtils.safeQueryAll('.l_post', this.postContainer);
- allPosts.forEach(post => {
- const data = this.safeGetPostData(post);
- const pid = data.pid;
- if (this.spamPosts.has(pid) && document.body.contains(post) && post.style.display !== 'none') {
- post.style.display = 'none';
- post.classList.add('spam-hidden');
- logWrapper('pageBehavior', 'LOG', `Re-enforced spam hiding for post: ${pid}`);
- }
- });
- setTimeout(enforce, 500);
- };
- enforce();
- customConsole.log('Spam enforcer 已启动');
- }
-
- autoExpandImages(nodes = DomUtils.safeQueryAll('.replace_tip:not(.expanded)', this.postContainer)) {
- if (!this.settings.autoExpandImages) return;
- customConsole.log('开始自动展开图片,图片数量:', nodes.length);
- logWrapper('pageBehavior', 'LOG', 'Auto expanding images');
- nodes.forEach(tip => {
- if (tip.style.display !== 'none' && !tip.classList.contains('expanded')) {
- const rect = tip.getBoundingClientRect();
- tip.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: rect.left, clientY: rect.top }));
- tip.classList.add('expanded');
- const img = DomUtils.safeQuery('img', tip.closest('.replace_div'));
- if (this.settings.enhanceImages && img) this.enhanceImage(img);
- this.postsCache.set(tip.dataset.pid || `img_${Date.now()}`, true);
- logWrapper('pageBehavior', 'LOG', `Expanded image for post: ${tip.dataset.pid || 'unknown'}`);
- }
- });
- }
-
- updateFilters() {
- this.settings = ConfigManager.getFilterSettings();
- customConsole.log('更新过滤器配置,屏蔽词:', this.settings.spamKeywords);
- logWrapper('script', 'LOG', 'Updating filters with new settings');
- this.postsCache.clear();
- this.spamPosts.clear();
- this.saveOriginalOrder();
- this.applyFilters();
- this.autoExpandImages();
- this.blockAds();
- this.restoreOriginalOrder();
- if (this.settings.linkifyVideos) this.linkifyVideos();
- }
-
- observeDOMChanges() {
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mut => {
- if (mut.addedNodes.length) {
- this.applyFilters();
- this.autoExpandImages();
- logWrapper('pageBehavior', 'LOG', 'DOM change detected, filters reapplied');
- }
- });
- });
- observer.observe(document.body, { childList: true, subtree: true });
- customConsole.log('MutationObserver 已初始化');
- }
-
- handlePagination() {
- addSafeListener(document, 'click', e => {
- if (e.target.closest('.pagination a, .pager a')) {
- customConsole.log('检测到翻页按钮点击');
- logWrapper('userActions', 'LOG', 'Pagination button clicked');
- setTimeout(() => {
- this.applyFilters();
- this.autoExpandImages();
- }, 500);
- }
- });
- customConsole.log('翻页按钮监听已初始化');
- }
-
- interceptAjax() {
- const originalFetch = window.fetch;
- window.fetch = async (url, options) => {
- customConsole.log('拦截到 fetch 请求:', url);
- const response = await originalFetch(url, options);
- if (/tieba\.baidu\.com\/p\/\d+(\?pn=\d+)?$/.test(url)) {
- customConsole.log('检测到翻页 fetch 请求:', url);
- logWrapper('pageBehavior', 'LOG', `Page change detected via fetch: ${url}`);
- setTimeout(() => {
- this.postsCache.clear();
- this.spamPosts.clear();
- this.saveOriginalOrder();
- this.applyFilters();
- this.autoExpandImages();
- this.blockAds();
- this.restoreOriginalOrder(true);
- }, 500);
- }
- return response;
- };
-
- const originalOpen = XMLHttpRequest.prototype.open;
- XMLHttpRequest.prototype.open = function() {
- this.addEventListener('load', () => {
- if (this.responseURL.includes('tieba.baidu.com/p/')) {
- customConsole.log('检测到翻页 XMLHttpRequest 请求:', this.responseURL);
- logWrapper('pageBehavior', 'LOG', `Page change detected via XMLHttpRequest: ${this.responseURL}`);
- setTimeout(() => {
- this.postsCache.clear();
- this.spamPosts.clear();
- this.saveOriginalOrder();
- this.applyFilters();
- this.autoExpandImages();
- this.blockAds();
- this.restoreOriginalOrder(true);
- }, 300);
- }
- });
- originalOpen.apply(this, arguments);
- };
-
- customConsole.log('AJAX 拦截器已初始化(fetch 和 XMLHttpRequest)');
- }
-
- blockAds() {
- if (!this.settings.blockAds) return;
- customConsole.log('开始屏蔽广告');
- logWrapper('pageBehavior', 'LOG', 'Blocking advertisements');
- const adSelectors = ['.ad_item', '.mediago', '[class*="ad_"]:not([class*="content"])', '.app_download_box', '.right_section .region_bright:not(.content)'];
- adSelectors.forEach(selector => {
- DomUtils.safeQueryAll(selector + ':not(.filtered)', this.postContainer).forEach(el => {
- if (!el.closest('.d_post_content') && !el.closest('.l_container')) {
- el.classList.add('spam-hidden');
- const pid = el.dataset.pid || `ad_${Date.now()}`;
- this.postsCache.set(pid, true);
- el.classList.add('filtered');
- logWrapper('pageBehavior', 'LOG', `Hid ad element: ${selector}`);
- }
- });
- });
- }
-
- restoreOriginalOrder(isPageChange = false) {
- if (!this.postContainer) return;
-
- customConsole.log('开始恢复帖子顺序,原始顺序数量:', this.originalOrder.size);
- logWrapper('pageBehavior', 'LOG', `Restoring original order, isPageChange: ${isPageChange}`);
-
- const newContainer = this.postContainer.cloneNode(false);
- const currentPosts = new Map();
- DomUtils.safeQueryAll('.l_post', this.postContainer).forEach(post => {
- const data = this.safeGetPostData(post);
- currentPosts.set(data.pid, post);
- });
-
- const sortedOrder = Array.from(this.originalOrder.values())
- .sort((a, b) => Number(a.floor) - Number(b.floor));
-
- sortedOrder.forEach(item => {
- const existingPost = currentPosts.get(item.pid);
- if (existingPost) {
- newContainer.appendChild(existingPost);
- } else {
- newContainer.appendChild(item.element.cloneNode(true));
- }
- });
-
- this.postContainer.parentNode.replaceChild(newContainer, this.postContainer);
- this.postContainer = newContainer;
-
- customConsole.log('帖子顺序恢复完成,数量:', sortedOrder.length);
- logWrapper('pageBehavior', 'LOG', `Restored order, posts: ${sortedOrder.length}`);
- if (!isPageChange) this.applyFilters();
- }
-
- linkifyVideos() {
- customConsole.log('开始链接化视频');
- logWrapper('pageBehavior', 'LOG', 'Linking videos');
- const videoRegex = /(?:av\d+|BV\w+)|(?:https?:\/\/(?:www\.)?(youtube\.com|youtu\.be)\/[^\s]+)/gi;
- DomUtils.safeQueryAll('.d_post_content:not(.filtered)', this.postContainer).forEach(post => {
- const pid = this.safeGetPostData(post).pid;
- if (!this.postsCache.has(pid)) {
- post.innerHTML = post.innerHTML.replace(videoRegex, match => {
- return match.startsWith('http') ? `<a href="${match}" target="_blank">${match}</a>` : `<a href="https://bilibili.com/video/${match}" target="_blank">${match}</a>`;
- });
- post.classList.add('filtered');
- this.postsCache.set(pid, true);
- logWrapper('pageBehavior', 'LOG', `Linked videos in post: ${pid}`);
- }
- });
- }
-
- enhanceImage(img) {
- if (!img) return;
- img.style.cursor = 'pointer';
- removeSafeListener(img, 'click', this.handleImageClick);
- addSafeListener(img, 'click', this.handleImageClick.bind(this));
- logWrapper('pageBehavior', 'LOG', `Enhanced image: ${img.src}`);
- }
-
- handleImageClick() {
- const overlay = document.createElement('div');
- overlay.className = 'image-overlay';
- const largeImg = document.createElement('img');
- largeImg.src = this.src;
- largeImg.className = 'large-image';
- overlay.appendChild(largeImg);
- document.body.appendChild(overlay);
- addSafeListener(overlay, 'click', () => overlay.remove());
- addSafeListener(overlay, 'wheel', e => {
- e.preventDefault();
- const scale = e.deltaY > 0 ? 0.9 : 1.1;
- largeImg.style.transform = `scale(${(parseFloat(largeImg.style.transform?.match(/scale\((.*?)\)/)?.[1]) || 1) * scale})`;
- });
- logWrapper('userActions', 'LOG', `Clicked to enlarge image: ${this.src}`);
- }
-
- safeGetPostData(post) {
- const pid = post?.dataset?.pid || post?.getAttribute('data-pid') || `temp_${Date.now()}`;
- const floor = parseInt(post?.dataset?.floor) || 0;
- const contentEle = DomUtils.safeQuery('.d_post_content', post);
- const content = contentEle ? contentEle.textContent.trim() : '';
- const author = DomUtils.safeQuery('.p_author_name', post)?.textContent?.trim() || '匿名';
- return { pid, floor, content, author };
- }
-
- cleanupMemory() {
- setInterval(() => {
- const now = Date.now();
- for (const [pid, data] of this.postsCache) {
- if (typeof data === 'object' && data.timestamp && now - data.timestamp > 300000) {
- this.postsCache.delete(pid);
- }
- }
- customConsole.log('清理内存,剩余缓存条目:', this.postsCache.size);
- }, 60000);
- }
-
- showToast(message, type = 'info') {
- const toast = document.createElement('div');
- toast.textContent = message;
- toast.style.cssText = `
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
- background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
- padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s;
- `;
- document.body.appendChild(toast);
- setTimeout(() => {
- toast.style.opacity = '0';
- setTimeout(() => toast.remove(), 300);
- }, 2000);
- logWrapper('script', 'LOG', `Showed toast: ${message}`);
- }
- }
-
- // 十四、动态面板类
- class DynamicPanel {
- constructor() {
- this.panel = null;
- this.minimizedIcon = null;
- this.isDragging = false;
- this.dragOccurred = false;
- this.lastClickTime = 0;
- this.isResizing = false;
- this.settings = ConfigManager.getFilterSettings();
- this.panelSettings = ConfigManager.getPanelSettings();
- this.postFilter = new PostFilter();
- this.debugPanel = { show: () => {}, hide: () => {}, update: () => {}, remove: () => {} }; // 简化处理
- this.init();
- this.applyDarkMode(this.settings.darkMode);
- migrateSettings();
- if (!GM_getValue('debugMode', true)) this.debugPanel.hide();
- customConsole.log('DynamicPanel 初始化完成');
- }
-
- init() {
- this.createPanel();
- this.createMinimizedIcon();
- document.body.appendChild(this.panel);
- document.body.appendChild(this.minimizedIcon);
- this.loadContent();
- this.setupPanelInteractions();
- this.minimizePanel();
- if (!this.panelSettings.minimized) this.restorePanel();
- this.ensureVisibility();
- this.observer = new ResizeObserver(() => this.adjustPanelHeight());
- this.observer.observe(this.panel.querySelector('.panel-content'));
- this.setupUserActionListeners();
- this.setupCleanup();
- scheduleIdleTask(() => this.startPerformanceMonitoring());
- prefetchResources();
- }
-
- ensureVisibility() {
- this.panel.style.opacity = '1';
- this.panel.style.visibility = 'visible';
- this.minimizedIcon.style.opacity = '1';
- this.minimizedIcon.style.visibility = 'visible';
- customConsole.log('Panel visibility ensured at:', this.panelSettings.position);
- }
-
- createPanel() {
- this.panel = document.createElement('div');
- this.panel.id = 'enhanced-panel';
- GM_addStyle(`
- #enhanced-panel {
- position: fixed; z-index: 9999; top: ${this.panelSettings.position.y}px; left: ${this.panelSettings.position.x}px;
- width: ${this.panelSettings.width}px; min-height: ${this.panelSettings.minHeight}px; max-height: ${this.panelSettings.maxHeight};
- background: rgba(255,255,255,0.98); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);
- transition: opacity 0.3s ease; will-change: left, top; contain: strict; display: none; opacity: 1; visibility: visible; height: auto;
- }
- #minimized-icon {
- position: fixed; z-index: 9999; top: ${this.panelSettings.position.y}px; left: ${this.panelSettings.position.x}px;
- width: 32px; height: 32px; background: #ffffff; border-radius: 50%; box-shadow: 0 4px 16px rgba(0,0,0,0.2);
- display: block; cursor: pointer; text-align: center; line-height: 32px; font-size: 16px; color: #007bff; overflow: hidden;
- }
- .panel-header { padding: 16px; border-bottom: 1px solid #eee; user-select: none; display: flex; justify-content: space-between; align-items: center; cursor: move; }
- .panel-content { padding: 16px; overflow-y: auto; overscroll-behavior: contain; height: auto; max-height: calc(90vh - 80px); }
- .resize-handle { position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; cursor: nwse-resize; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUgNUwxNSAxNSIgc3Ryb2tlPSIjOTk5OTk5IiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSAxNUwxNSAxNSIgc3Ryb2tlPSIjOTk5OTk5IiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=') no-repeat center; }
- .minimize-btn, .scale-btn { cursor: pointer; padding: 0 8px; }
- .minimize-btn:hover, .scale-btn:hover { color: #007bff; }
- .setting-group { display: flex; align-items: center; padding: 10px 0; gap: 10px; }
- .toggle-switch { position: relative; width: 40px; height: 20px; flex-shrink: 0; }
- .toggle-switch input { opacity: 0; width: 0; height: 0; }
- .toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 10px; cursor: pointer; transition: background 0.3s; }
- .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; top: 2px; background: white; border-radius: 50%; transition: transform 0.3s; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
- .toggle-switch input:checked + .toggle-slider { background: #34c759; }
- .toggle-switch input:checked + .toggle-slider:before { transform: translateX(20px); }
- .setting-label { flex: 1; font-size: 14px; color: #333; }
- body.dark-mode .setting-label { color: #ddd !important; }
- select { padding: 4px; border: 1px solid #ddd; border-radius: 4px; }
- .divider { height: 1px; background: #eee; margin: 16px 0; }
- .tool-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
- .tool-card { padding: 12px; background: #f8f9fa; border: 1px solid #eee; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
- .tool-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
- .metric-grid { display: grid; gap: 12px; }
- .progress-bar { height: 4px; background: #e9ecef; border-radius: 2px; overflow: hidden; }
- .progress-fill { height: 100%; background: #28a745; width: 0%; transition: width 0.3s ease; }
- .block-modal, .keyword-modal, .log-modal, .search-modal {
- position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5);
- display: flex; justify-content: center; align-items: center; z-index: 10000; pointer-events: auto;
- }
- .modal-content { background: white; padding: 20px; border-radius: 8px; width: 400px; max-height: 80vh; overflow-y: auto; pointer-events: auto; }
- .modal-content p { color: #666; margin: 5px 0 10px; font-size: 12px; }
- textarea, input[type="text"] { width: 100%; margin: 10px 0; padding: 8px; border: 1px solid #ddd; resize: vertical; }
- .modal-actions { text-align: right; }
- .btn-cancel, .btn-save, .btn-block, .btn-undo, .btn-confirm, .btn-search {
- padding: 6px 12px; margin: 0 5px; border: none; border-radius: 4px; cursor: pointer; pointer-events: auto;
- }
- .btn-cancel { background: #eee; }
- .btn-save, .btn-block, .btn-undo, .btn-confirm, .btn-search { background: #34c759; color: white; }
- .btn-block.active { background: #ff4444; }
- .btn-undo { background: #ff9800; }
- .hover-highlight { outline: 2px solid #ff4444; outline-offset: 2px; }
- .blocked-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid #eee; }
- .blocked-item button { padding: 4px 8px; font-size: 12px; }
- .cursor-circle {
- position: fixed; width: 20px; height: 20px; background: rgba(128, 128, 128, 0.5); border-radius: 50%;
- pointer-events: none; z-index: 10001; transition: transform 0.2s ease;
- }
- .cursor-circle.confirm { background: rgba(52, 199, 89, 0.8); transform: scale(1.5); transition: transform 0.3s ease, background 0.3s ease; }
- body.blocking-mode * { cursor: none !important; }
- .performance-info { display: none; }
- .highlight-match { background-color: yellow; }
- .image-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; justify-content: center; align-items: center; }
- .large-image { max-width: 90%; max-height: 90%; cursor: move; transform: translateZ(0); backface-visibility: hidden; }
- body.dark-mode, body.dark-mode .wrap1, body.dark-mode .l_container, body.dark-mode .pb_content, body.dark-mode .d_post_content, body.dark-mode .left_section, body.dark-mode .right_section {
- background: #222 !important; color: #ddd !important; transition: background 0.3s, color 0.3s;
- }
- body.dark-mode #enhanced-panel { background: rgba(50,50,50,0.98) !important; color: #ddd !important; }
- body.dark-mode a { color: #66b3ff !important; }
- @media (max-width: 768px) {
- #enhanced-panel { width: 90vw !important; left: 5vw !important; }
- .tool-grid { grid-template-columns: 1fr; }
- }
- .quick-actions { margin-top: 10px; }
- .quick-actions button { margin: 5px; padding: 5px 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
- `);
- this.panel.innerHTML = `
- <div class="panel-header"><span>贴吧增强控制台</span><div class="panel-controls"><span class="minimize-btn">—</span><span class="scale-btn" data-scale="0.8">缩小</span><span class="scale-btn" data-scale="1.0">还原</span></div></div>
- <div class="panel-content"></div>
- <div class="resize-handle"></div>
- `;
- }
-
- createMinimizedIcon() {
- this.minimizedIcon = document.createElement('div');
- this.minimizedIcon.id = 'minimized-icon';
- this.minimizedIcon.textContent = '⚙️';
- addSafeListener(this.minimizedIcon, 'click', e => {
- const now = Date.now();
- if (now - this.lastClickTime > 300 && !this.dragOccurred) {
- this.toggleMinimize();
- this.lastClickTime = now;
- }
- this.dragOccurred = false;
- e.stopPropagation();
- });
- }
-
- loadContent() {
- const content = DomUtils.safeQuery('.panel-content', this.panel);
- content.innerHTML = `
- <div class="filter-controls">
- <h3>📊 智能过滤设置</h3>
- <div class="setting-group">
- <label class="toggle-switch">
- <input type="checkbox" data-setting="debugMode" ${GM_getValue('debugMode', true) ? 'checked' : ''}>
- <span class="toggle-slider"></span>
- </label>
- <span class="setting-label">启用调试模式</span>
- </div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="hideInvalid" ${this.settings.hideInvalid ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">隐藏无效楼层</span></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="hideSpam" ${this.settings.hideSpam ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">屏蔽水贴内容</span><button class="btn-config" data-action="editKeywords">✏️ 编辑关键词</button></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="autoExpandImages" ${this.settings.autoExpandImages ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">自动展开图片</span></div>
- <div class="setting-group">
- <button class="btn-block" data-action="toggleBlockMode">🛡️ ${this.isBlockingMode ? '停止选择屏蔽' : '开始选择屏蔽元素'}</button>
- <select data-setting="blockType">
- <option value="perm" ${this.settings.blockType === 'perm' ? 'selected' : ''}>永久屏蔽</option>
- <option value="temp" ${this.settings.blockType === 'temp' ? 'selected' : ''}>临时屏蔽</option>
- </select>
- </div>
- <div class="setting-group"><button class="btn-undo" data-action="showUndoList">🔄 查看并撤回屏蔽</button></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="blockAds" ${this.settings.blockAds ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">自动屏蔽广告</span></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="enhanceImages" ${this.settings.enhanceImages ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">图片交互优化</span></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="linkifyVideos" ${this.settings.linkifyVideos ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">视频链接跳转</span></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="darkMode" ${this.settings.darkMode ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">黑夜模式</span></div>
- <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="showHiddenFloors" ${this.settings.showHiddenFloors ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">显示隐藏楼层</span></div>
- </div>
- <div class="quick-actions">
- <button data-action="toggleAllImages">一键展开/收起图片</button>
- </div>
- <div class="divider"></div>
- <div class="advanced-tools">
- <h3>⚙️ 高级工具</h3>
- <div class="tool-grid">
- <button class="tool-card" data-action="exportSettings"><div class="icon">📤</div><span>导出配置</span></button>
- <button class="tool-card" data-action="importSettings"><div class="icon">📥</div><span>导入配置</span></button>
- <button class="tool-card" data-action="performanceChart"><div class="icon">📈</div><span>性能图表</span></button>
- <button class="tool-card" data-action="quickSearch"><div class="icon">🔍</div><span>快速检索</span></button>
- <button class="tool-card" data-action="saveLogs"><div class="icon">💾</div><span>保存日志</span></button>
- </div>
- </div>
- <div class="divider"></div>
- <div class="performance-info">
- <h3>💻 系统监控</h3>
- <div class="metric-grid">
- <div class="metric-item"><span class="metric-label">内存占用</span><span class="metric-value" id="mem-usage">0 MB</span><div class="progress-bar"><div class="progress-fill" id="mem-progress"></div></div></div>
- <div class="metric-item"><span class="metric-label">处理速度</span><span class="metric-value" id="process-speed">0 ms</span><div class="sparkline" id="speed-chart"></div></div>
- </div>
- </div>
- `;
- this.bindEvents();
- setTimeout(() => this.adjustPanelHeight(), 50);
- }
-
- adjustPanelHeight() {
- if (this.panelSettings.minimized) return;
- const content = DomUtils.safeQuery('.panel-content', this.panel);
- const headerHeight = DomUtils.safeQuery('.panel-header', this.panel)?.offsetHeight || 0;
- const maxHeight = Math.min(content.scrollHeight + headerHeight + 32, window.innerHeight * 0.9);
- this.panel.style.height = `${maxHeight}px`;
- }
-
- bindEvents() {
- DomUtils.safeQueryAll('input[type="checkbox"]', this.panel).forEach(checkbox => {
- addSafeListener(checkbox, 'change', () => {
- if (checkbox.dataset.setting === 'debugMode') {
- GM_setValue('debugMode', checkbox.checked);
- if (checkbox.checked) {
- this.debugPanel.show();
- } else {
- this.debugPanel.hide();
- }
- } else if (checkbox.dataset.setting === 'darkMode') {
- this.settings.darkMode = checkbox.checked;
- ConfigManager.updateFilterSettings(this.settings);
- this.applyDarkMode(checkbox.checked);
- } else {
- this.settings[checkbox.dataset.setting] = checkbox.checked;
- ConfigManager.updateFilterSettings(this.settings);
- this.postFilter.updateFilters();
- }
- logWrapper('userActions', 'LOG', `Toggled ${checkbox.dataset.setting} to ${checkbox.checked}`);
- this.adjustPanelHeight();
- });
- });
-
- const blockTypeSelect = DomUtils.safeQuery('[data-setting="blockType"]', this.panel);
- if (blockTypeSelect) {
- addSafeListener(blockTypeSelect, 'change', e => {
- this.settings.blockType = e.target.value;
- ConfigManager.updateFilterSettings(this.settings);
- logWrapper('userActions', 'LOG', `Changed blockType to ${e.target.value}`);
- });
- }
-
- const actions = {
- editKeywords: () => this.showKeywordEditor(),
- toggleBlockMode: () => this.toggleBlockMode(),
- showUndoList: () => this.showUndoList(),
- exportSettings: () => this.exportConfig(),
- importSettings: () => this.importConfig(),
- performanceChart: () => {
- const perfInfo = DomUtils.safeQuery('.performance-info', this.panel);
- perfInfo.style.display = perfInfo.style.display === 'block' ? 'none' : 'block';
- this.adjustPanelHeight();
- logWrapper('userActions', 'LOG', `Toggled performance chart: ${perfInfo.style.display}`);
- },
- quickSearch: () => this.toggleSearch(),
- saveLogs: () => this.showLogSaveDialog(),
- toggleAllImages: () => {
- const tips = DomUtils.safeQueryAll('.replace_tip', this.postFilter.postContainer);
- const allExpanded = Array.from(tips).every(tip => tip.classList.contains('expanded'));
- tips.forEach(tip => {
- if (allExpanded && tip.classList.contains('expanded')) {
- tip.click();
- tip.classList.remove('expanded');
- } else if (!tip.classList.contains('expanded')) {
- tip.click();
- tip.classList.add('expanded');
- }
- });
- this.showToast(allExpanded ? '已收起所有图片' : '已展开所有图片', 'success');
- logWrapper('userActions', 'LOG', `Toggled all images: ${allExpanded ? 'collapsed' : 'expanded'}`);
- }
- };
-
- DomUtils.safeQueryAll('[data-action]', this.panel).forEach(btn => {
- addSafeListener(btn, 'click', () => actions[btn.dataset.action]?.());
- });
-
- const minimizeBtn = DomUtils.safeQuery('.minimize-btn', this.panel);
- if (minimizeBtn) addSafeListener(minimizeBtn, 'click', e => { this.toggleMinimize(); e.stopPropagation(); });
- }
-
- setupPanelInteractions() {
- const header = DomUtils.safeQuery('.panel-header', this.panel);
- const resizeHandle = DomUtils.safeQuery('.resize-handle', this.panel);
- let startX, startY, startWidth, startHeight, rafId;
-
- const onDragStart = (e, isIcon = false) => {
- if (e.button !== 0) return; // 只响应左键
- this.isDragging = true;
- this.dragOccurred = false;
- const target = isIcon ? this.minimizedIcon : this.panel;
- startX = e.clientX - this.panelSettings.position.x;
- startY = e.clientY - this.panelSettings.position.y;
- e.preventDefault();
- customConsole.log(`Drag started at:`, { x: e.clientX, y: e.clientY, isIcon });
- };
-
- const updatePosition = (x, y) => {
- const target = this.panelSettings.minimized ? this.minimizedIcon : this.panel;
- const panelWidth = target.offsetWidth;
- const panelHeight = target.offsetHeight;
- this.panelSettings.position.x = Math.max(10, Math.min(x - startX, window.innerWidth - panelWidth - 10));
- this.panelSettings.position.y = Math.max(10, Math.min(y - startY, window.innerHeight - panelHeight - 10));
- target.style.left = `${this.panelSettings.position.x}px`;
- target.style.top = `${this.panelSettings.position.y}px`;
- customConsole.log('Dragging to:', { x: this.panelSettings.position.x, y: this.panelSettings.position.y, minimized: this.panelSettings.minimized });
- };
-
- const onDragMove = (e) => {
- if (!this.isDragging) return;
- this.dragOccurred = true;
- cancelAnimationFrame(rafId);
- rafId = requestAnimationFrame(() => updatePosition(e.clientX, e.clientY));
- };
-
- const onDragEnd = (e) => {
- if (!this.isDragging) return;
- this.isDragging = false;
- cancelAnimationFrame(rafId);
- ConfigManager.updatePanelSettings(this.panelSettings);
- this.adjustPanelHeight();
- customConsole.log('Drag ended, position:', this.panelSettings.position);
- setTimeout(() => { this.dragOccurred = false; }, 100);
- };
-
- if (header) {
- addSafeListener(header, 'mousedown', (e) => onDragStart(e, false));
- }
- addSafeListener(this.minimizedIcon, 'mousedown', (e) => onDragStart(e, true));
- addSafeListener(document, 'mousemove', onDragMove);
- addSafeListener(document, 'mouseup', onDragEnd);
-
- if (resizeHandle) {
- addSafeListener(resizeHandle, 'mousedown', (e) => {
- this.isResizing = true;
- startX = e.clientX;
- startY = e.clientY;
- startWidth = this.panelSettings.width;
- startHeight = parseInt(this.panel.style.height) || this.panel.offsetHeight;
- e.preventDefault();
- });
- }
-
- const onResizeMove = (e) => {
- if (this.isResizing) {
- const newWidth = startWidth + (e.clientX - startX);
- const newHeight = startHeight + (e.clientY - startY);
- this.panelSettings.width = Math.max(200, newWidth);
- this.panel.style.width = `${this.panelSettings.width}px`;
- this.panel.style.height = `${Math.max(200, newHeight)}px`;
- }
- };
-
- const onResizeEnd = () => {
- if (this.isResizing) {
- this.isResizing = false;
- ConfigManager.updatePanelSettings(this.panelSettings);
- this.adjustPanelHeight();
- customConsole.log('Resize ended, size:', { width: this.panelSettings.width });
- }
- };
-
- addSafeListener(document, 'mousemove', onResizeMove);
- addSafeListener(document, 'mouseup', onResizeEnd);
-
- DomUtils.safeQueryAll('.scale-btn', this.panel).forEach(btn => {
- addSafeListener(btn, 'click', e => {
- this.panelSettings.scale = parseFloat(btn.dataset.scale);
- this.panel.style.transform = `scale(${this.panelSettings.scale})`;
- ConfigManager.updatePanelSettings(this.panelSettings);
- this.ensureVisibility();
- this.adjustPanelHeight();
- e.stopPropagation();
- });
- });
-
- addSafeListener(window, 'resize', () => {
- const target = this.panelSettings.minimized ? this.minimizedIcon : this.panel;
- const panelWidth = target.offsetWidth * (this.panelSettings.scale || 1);
- const panelHeight = target.offsetHeight * (this.panelSettings.scale || 1);
- this.panelSettings.position.x = Math.min(this.panelSettings.position.x, window.innerWidth - panelWidth - 10);
- this.panelSettings.position.y = Math.min(this.panelSettings.position.y, window.innerHeight - panelHeight - 10);
- target.style.left = `${this.panelSettings.position.x}px`;
- target.style.top = `${this.panelSettings.position.y}px`;
- ConfigManager.updatePanelSettings(this.panelSettings);
- customConsole.log('Adjusted position on resize:', this.panelSettings.position);
- });
- }
- setupUserActionListeners() {
- addSafeListener(document, 'click', e => {
- if (!this.panel.contains(e.target) && !this.minimizedIcon.contains(e.target)) {
- logWrapper('userActions', 'LOG', `Clicked on page at (${e.clientX}, ${e.clientY}), Target: ${e.target.tagName}.${e.target.className || ''}`);
- }
- });
- addSafeListener(document, 'scroll', _.debounce(() => {
- logWrapper('userActions', 'LOG', `Scrolled to (${window.scrollX}, ${window.scrollY})`);
- }, DEBOUNCE_LEVEL.QUICK));
- }
-
- startPerformanceMonitoring() {
- const perfMonitor = PerformanceMonitor.getInstance();
- const update = () => {
- perfMonitor.recordMemory();
- const memUsage = perfMonitor.metrics.memoryUsage.length ? Math.round(_.mean(perfMonitor.metrics.memoryUsage) / 1024 / 1024) : 0;
- const memElement = DomUtils.safeQuery('#mem-usage', this.panel);
- const progElement = DomUtils.safeQuery('#mem-progress', this.panel);
- if (memElement && progElement) {
- memElement.textContent = `${memUsage} MB`;
- progElement.style.width = `${Math.min(memUsage / 100 * 100, 100)}%`;
- }
- this.debugPanel.update({
- posts: DomUtils.safeQueryAll('.l_post', this.postFilter.postContainer).length,
- hidden: DomUtils.safeQueryAll('.spam-hidden', this.postFilter.postContainer).length,
- memory: memUsage,
- cacheSize: this.postFilter.postsCache.size
- });
- requestAnimationFrame(update);
- };
- requestAnimationFrame(update);
- }
-
- toggleMinimize() {
- if (this.panelSettings.minimized) this.restorePanel();
- else this.minimizePanel();
- ConfigManager.updatePanelSettings(this.panelSettings);
- this.ensureVisibility();
- logWrapper('userActions', 'LOG', `Panel minimized: ${this.panelSettings.minimized}`);
- }
-
- minimizePanel() {
- this.panel.style.display = 'none';
- this.minimizedIcon.style.display = 'block';
- this.minimizedIcon.style.left = `${this.panelSettings.position.x}px`;
- this.minimizedIcon.style.top = `${this.panelSettings.position.y}px`;
- this.panelSettings.minimized = true;
- this.ensureVisibility();
- customConsole.log('Minimized panel, icon position:', this.panelSettings.position);
- }
-
- restorePanel() {
- this.panel.style.display = 'block';
- this.minimizedIcon.style.display = 'none';
- this.panel.style.left = `${this.panelSettings.position.x}px`;
- this.panel.style.top = `${this.panelSettings.position.y}px`;
- this.panelSettings.minimized = false;
- this.adjustPanelHeight();
- this.ensureVisibility();
- customConsole.log('Restored panel, position:', this.panelSettings.position);
- }
-
- toggleBlockMode(event) {
- this.isBlockingMode = !this.isBlockingMode;
- const blockBtn = DomUtils.safeQuery('.btn-block', this.panel);
- blockBtn.textContent = `🛡️ ${this.isBlockingMode ? '停止选择屏蔽' : '开始选择屏蔽元素'}`;
- blockBtn.classList.toggle('active', this.isBlockingMode);
-
- if (this.isBlockingMode) {
- document.body.classList.add('blocking-mode');
- this.createCursorCircle();
- this.listeners = {
- move: this.moveCursorCircle.bind(this),
- click: this.handleBlockClick.bind(this)
- };
- addSafeListener(document, 'mousemove', this.listeners.move);
- addSafeListener(document, 'click', this.listeners.click);
- } else {
- document.body.classList.remove('blocking-mode');
- this.removeCursorCircle();
- if (this.listeners) {
- removeSafeListener(document, 'mousemove', this.listeners.move);
- removeSafeListener(document, 'click', this.listeners.click);
- this.listeners = null;
- }
- this.removeHighlight();
- this.selectedTarget = null;
- }
- if (event) event.stopPropagation();
- this.adjustPanelHeight();
- logWrapper('userActions', 'LOG', `Toggled block mode: ${this.isBlockingMode}`);
- }
-
- createCursorCircle() {
- this.cursorCircle = document.createElement('div');
- this.cursorCircle.className = 'cursor-circle';
- document.body.appendChild(this.cursorCircle);
- }
-
- moveCursorCircle(event) {
- if (!this.isBlockingMode || !this.cursorCircle) return;
- this.cursorCircle.style.left = `${event.clientX - 10}px`;
- this.cursorCircle.style.top = `${event.clientY - 10}px`;
- this.highlightElement(event);
- }
-
- removeCursorCircle() {
- if (this.cursorCircle && document.body.contains(this.cursorCircle)) document.body.removeChild(this.cursorCircle);
- this.cursorCircle = null;
- }
-
- highlightElement(event) {
- if (!this.isBlockingMode) return;
- this.removeHighlight();
- const target = event.target;
- if (target === this.panel || this.panel.contains(target) || target.classList.contains('block-modal') || target.closest('.block-modal')) return;
- target.classList.add('hover-highlight');
- }
-
- removeHighlight() {
- const highlighted = DomUtils.safeQuery('.hover-highlight');
- if (highlighted) highlighted.classList.remove('hover-highlight');
- }
-
- handleBlockClick(event) {
- if (!this.isBlockingMode) return;
- event.preventDefault();
- event.stopPropagation();
- const target = event.target;
- if (target === this.panel || this.panel.contains(target) || target.classList.contains('block-modal') || target.closest('.block-modal')) return;
- this.selectedTarget = target;
- this.showConfirmDialog(event.clientX, event.clientY);
- }
-
- showConfirmDialog(x, y) {
- const modal = document.createElement('div');
- modal.className = 'block-modal';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>确认屏蔽</h3>
- <p>确定要屏蔽此元素吗?当前模式:${this.settings.blockType === 'temp' ? '临时' : '永久'}</p>
- <div class="modal-actions">
- <button class="btn-cancel">取消</button>
- <button class="btn-confirm">确定</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- const confirmBtn = DomUtils.safeQuery('.btn-confirm', modal);
- const cancelBtn = DomUtils.safeQuery('.btn-cancel', modal);
- addSafeListener(confirmBtn, 'click', e => {
- e.stopPropagation();
- if (this.selectedTarget) this.blockElement(this.selectedTarget, x, y);
- document.body.removeChild(modal);
- }, { once: true });
- addSafeListener(cancelBtn, 'click', e => {
- e.stopPropagation();
- document.body.removeChild(modal);
- this.toggleBlockMode();
- }, { once: true });
- }
-
- blockElement(target, x, y) {
- if (!target || !document.body.contains(target)) {
- this.showToast('无效的元素', 'error');
- return;
- }
- const selector = this.getUniqueSelector(target);
- const blockList = this.settings.blockType === 'temp' ? this.settings.tempBlockedElements : this.settings.blockedElements;
- if (!blockList.includes(selector)) {
- blockList.push(selector);
- ConfigManager.updateFilterSettings(this.settings);
- this.postFilter.updateFilters();
- logWrapper('userActions', 'LOG', `Blocked element: ${selector}`, `Type: ${this.settings.blockType}`);
- }
- target.classList.add('spam-hidden');
- if (this.cursorCircle) {
- this.cursorCircle.style.left = `${x - 10}px`;
- this.cursorCircle.style.top = `${y - 10}px`;
- this.cursorCircle.classList.add('confirm');
- setTimeout(() => {
- this.cursorCircle.classList.remove('confirm');
- this.toggleBlockMode();
- }, 300);
- } else {
- this.toggleBlockMode();
- }
- this.adjustPanelHeight();
- }
-
- getUniqueSelector(element) {
- if (element.id) return `#${element.id}`;
- const path = [];
- let current = element;
- while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
- let selector = current.tagName.toLowerCase();
- if (current.className) selector += `.${current.className.trim().split(/\s+/).join('.')}`;
- const siblings = Array.from(current.parentNode.children).filter(child => child.tagName === current.tagName);
- if (siblings.length > 1) selector += `:nth-child(${siblings.indexOf(current) + 1})`;
- path.unshift(selector);
- current = current.parentNode;
- }
- return path.join(' > ');
- }
-
- showKeywordEditor() {
- const modal = document.createElement('div');
- modal.className = 'keyword-modal';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>关键词管理</h3>
- <textarea id="keywordInput">${this.settings.spamKeywords.join('\n')}</textarea>
- <div class="modal-actions">
- <button class="btn-cancel">取消</button>
- <button class="btn-save">保存</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- const textarea = DomUtils.safeQuery('#keywordInput', modal);
- const saveBtn = DomUtils.safeQuery('.btn-save', modal);
- const cancelBtn = DomUtils.safeQuery('.btn-cancel', modal);
-
- addSafeListener(saveBtn, 'click', () => {
- const keywords = textarea.value.split('\n').map(k => k.trim()).filter(k => k.length > 0);
- if (keywords.length > 0) {
- this.settings.spamKeywords = keywords;
- ConfigManager.updateFilterSettings(this.settings);
- customConsole.log('保存自定义屏蔽词:', keywords);
- logWrapper('userActions', 'LOG', `Updated spam keywords: ${keywords.join(', ')}`);
- this.postFilter.updateFilters();
- document.body.removeChild(modal);
- this.showToast('关键词已更新', 'success');
- } else {
- this.showToast('请至少输入一个关键词', 'error');
- }
- });
- addSafeListener(cancelBtn, 'click', () => document.body.removeChild(modal));
- }
-
- showUndoList() {
- const modal = document.createElement('div');
- modal.className = 'block-modal';
- const permItems = this.settings.blockedElements.length > 0 ?
- this.settings.blockedElements.map((sel, i) => `
- <div class="blocked-item">
- <span>[永久] ${sel}</span>
- <button class="btn-undo" data-index="${i}" data-type="perm">撤销</button>
- </div>
- `).join('') : '';
- const tempItems = this.settings.tempBlockedElements.length > 0 ?
- this.settings.tempBlockedElements.map((sel, i) => `
- <div class="blocked-item">
- <span>[临时] ${sel}</span>
- <button class="btn-undo" data-index="${i}" data-type="temp">撤销</button>
- </div>
- `).join('') : '';
- const listItems = permItems + tempItems || '<p>暂无屏蔽元素</p>';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>屏蔽元素列表</h3>
- <p>点击“撤销”恢复显示对应元素</p>
- ${listItems}
- <div class="modal-actions">
- <button class="btn-cancel">关闭</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- DomUtils.safeQueryAll('.btn-undo', modal).forEach(btn => {
- addSafeListener(btn, 'click', () => {
- const index = parseInt(btn.dataset.index);
- const type = btn.dataset.type;
- this.undoBlockElement(index, type);
- document.body.removeChild(modal);
- this.showUndoList();
- });
- });
- addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
- }
-
- undoBlockElement(index, type) {
- const blockList = type === 'temp' ? this.settings.tempBlockedElements : this.settings.blockedElements;
- if (index >= 0 && index < blockList.length) {
- const selector = blockList[index];
- blockList.splice(index, 1);
- ConfigManager.updateFilterSettings(this.settings);
- this.postFilter.updateFilters();
- DomUtils.safeQueryAll(selector).forEach(el => el.classList.remove('spam-hidden'));
- this.showToast(`已撤销屏蔽: ${selector}`, 'success');
- logWrapper('userActions', 'LOG', `Undid block (${type}): ${selector}`);
- }
- this.adjustPanelHeight();
- }
-
- exportConfig() {
- const config = { filter: this.settings, panel: this.panelSettings };
- const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `tieba_enhance_config_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- this.showToast('配置已导出', 'success');
- logWrapper('userActions', 'LOG', 'Exported configuration');
- }
-
- importConfig() {
- const modal = document.createElement('div');
- modal.className = 'keyword-modal';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>导入配置</h3>
- <p>请选择配置文件(JSON格式)</p>
- <input type="file" accept=".json" id="configFileInput" />
- <div class="modal-actions">
- <button class="btn-cancel">取消</button>
- <button class="btn-save">导入</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- const fileInput = DomUtils.safeQuery('#configFileInput', modal);
- addSafeListener(DomUtils.safeQuery('.btn-save', modal), 'click', () => {
- const file = fileInput.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const importedConfig = JSON.parse(e.target.result);
- this.settings = { ...ConfigManager.defaultFilterSettings, ...importedConfig.filter };
- this.panelSettings = { ...ConfigManager.defaultPanelSettings, ...importedConfig.panel };
- ConfigManager.updateFilterSettings(this.settings);
- ConfigManager.updatePanelSettings(this.panelSettings);
- this.postFilter.updateFilters();
- this.loadContent();
- if (this.panelSettings.minimized) this.minimizePanel(); else this.restorePanel();
- this.applyDarkMode(this.settings.darkMode);
- this.showToast('配置已导入', 'success');
- logWrapper('userActions', 'LOG', 'Imported configuration');
- } catch (err) {
- this.showToast('配置文件无效', 'error');
- }
- document.body.removeChild(modal);
- };
- reader.readAsText(file);
- } else {
- this.showToast('请选择一个配置文件', 'error');
- }
- });
- addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
- }
-
- toggleSearch() {
- const modal = document.createElement('div');
- modal.className = 'search-modal';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>快速检索</h3>
- <p>输入关键词搜索帖子内容(支持正则表达式)</p>
- <input type="text" id="searchInput" placeholder="请输入关键词" />
- <div class="modal-actions">
- <button class="btn-cancel">关闭</button>
- <button class="btn-search">搜索</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- const searchInput = DomUtils.safeQuery('#searchInput', modal);
- addSafeListener(DomUtils.safeQuery('.btn-search', modal), 'click', () => {
- const keyword = searchInput.value.trim();
- if (keyword) this.performSearch(keyword);
- });
- addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
- searchInput.focus();
- }
-
- performSearch(keyword) {
- DomUtils.safeQueryAll('.highlight-match', this.postFilter.postContainer).forEach(el => el.replaceWith(el.textContent));
- const posts = DomUtils.safeQueryAll('.d_post_content', this.postFilter.postContainer);
- let regex;
- try {
- regex = new RegExp(keyword, 'gi');
- } catch {
- regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
- }
- let found = false;
- posts.forEach(post => {
- if (regex.test(post.textContent)) {
- post.innerHTML = post.innerHTML.replace(regex, match => `<span class="highlight-match">${match}</span>`);
- post.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
- found = true;
- }
- });
- this.showToast(found ? '搜索完成' : '未找到匹配内容', found ? 'success' : 'error');
- logWrapper('userActions', 'LOG', `Searched for keyword: ${keyword}, found: ${found}`);
- }
-
- showLogSaveDialog() {
- const modal = document.createElement('div');
- modal.className = 'log-modal';
- modal.innerHTML = `
- <div class="modal-content">
- <h3>保存日志</h3>
- <p>点击“保存”将日志导出为文件(当前最多 ${MAX_LOG_ENTRIES} 条/分类)</p>
- <div class="modal-actions">
- <button class="btn-cancel">取消</button>
- <button class="btn-save">保存</button>
- </div>
- </div>
- `;
- document.body.appendChild(modal);
- addSafeListener(DomUtils.safeQuery('.btn-save', modal), 'click', () => {
- const fullLog = [
- '=== 脚本运行日志 ===', ...logBuffer.script,
- '\n=== 网页运行状态 ===', ...logBuffer.pageState,
- '\n=== 网页行为 ===', ...logBuffer.pageBehavior,
- '\n=== 用户操作 ===', ...logBuffer.userActions
- ].join('\n');
- const blob = new Blob([fullLog], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `tieba_enhance_log_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- document.body.removeChild(modal);
- this.showToast('日志已保存', 'success');
- logWrapper('userActions', 'LOG', 'Saved logs to file');
- });
- addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
- }
-
- setupCleanup() {
- addSafeListener(window, 'beforeunload', () => {
- this.panel.remove();
- this.minimizedIcon.remove();
- this.debugPanel.remove();
- this.observer?.disconnect();
- if (this.listeners) {
- removeSafeListener(document, 'mousemove', this.listeners.move);
- removeSafeListener(document, 'click', this.listeners.click);
- }
- });
- }
-
- applyDarkMode(enable) {
- if (enable) document.body.classList.add('dark-mode');
- else document.body.classList.remove('dark-mode');
- logWrapper('pageBehavior', 'LOG', `Applied dark mode: ${enable}`);
- }
-
- showToast(message, type = 'info') {
- const toast = document.createElement('div');
- toast.textContent = message;
- toast.style.cssText = `
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
- background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
- padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
- background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
- padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s;
- `;
- document.body.appendChild(toast);
- setTimeout(() => {
- toast.style.opacity = '0';
- setTimeout(() => toast.remove(), 300);
- }, 2000);
- logWrapper('script', 'LOG', `Showed toast: ${message}`);
- }
-
- showErrorToast(methodName, error) {
- this.showToast(`${methodName}出错: ${error.message}`, 'error');
- }
- }
-
- // 十五、初始化
- addSafeListener(document, 'DOMContentLoaded', () => {
- new DynamicPanel();
- customConsole.log('DOM content loaded, script initialized');
- });
-
- if (document.readyState === 'complete' || document.readyState === 'interactive') {
- setTimeout(() => {
- if (!DomUtils.safeQuery('#enhanced-panel') && !DomUtils.safeQuery('#minimized-icon')) {
- new DynamicPanel();
- customConsole.log('Fallback initialization triggered');
- }
- }, 50);
- }
- })();