您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动浏览、点赞、只看楼主、楼层号、保存帖子到本地、清爽模式、黑白灰模式、用户信息展示(批量展示)、查看用户话题。支持拖动和最小化控制面板(50×50圆形图标,统一边距,左右展开方向优化)。支持 linux.do 和 idcflare.com
// ==UserScript== // @name linux.do 小助手(增强版) // @description 自动浏览、点赞、只看楼主、楼层号、保存帖子到本地、清爽模式、黑白灰模式、用户信息展示(批量展示)、查看用户话题。支持拖动和最小化控制面板(50×50圆形图标,统一边距,左右展开方向优化)。支持 linux.do 和 idcflare.com // @namespace https://example.com/userscripts // @match https://linux.do/* // @match https://idcflare.com/* // @grant GM_xmlhttpRequest // @connect connect.linux.do // @connect linux.do // @connect * // @version 1.6.0 // @author quantumcat & nulluser & enhanced & idear // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // ==/UserScript== // 获取当前站点域名 const CURRENT_DOMAIN = window.location.hostname; const BASE_URL = `https://${CURRENT_DOMAIN}`; // 配置项 const CONFIG = { scroll: { minSpeed: 10, maxSpeed: 15, minDistance: 2, maxDistance: 4, checkInterval: 500, fastScrollChance: 0.08, fastScrollMin: 80, fastScrollMax: 200 }, time: { browseTime: 3600000, restTime: 600000, minPause: 300, maxPause: 500, loadWait: 1500, }, article: { commentLimit: 5000, topicListLimit: 100, retryLimit: 3 }, levelRequirements: { 0: { // 0级升1级 topics_entered: 5, posts_read_count: 30, time_read: 600 // 10分钟 = 600秒 }, 1: { // 1级升2级 days_visited: 15, likes_given: 1, likes_received: 1, post_count: 3, topics_entered: 20, posts_read_count: 100, time_read: 3600 // 60分钟 = 3600秒 } }, mustRead: { posts: [ { id: '1051', url: 'https://linux.do/t/topic/1051/' }, { id: '5973', url: 'https://linux.do/t/topic/5973' }, { id: '102770', url: 'https://linux.do/t/topic/102770' }, { id: '154010', url: 'https://linux.do/t/topic/154010' }, { id: '149576', url: 'https://linux.do/t/topic/149576' }, { id: '22118', url: 'https://linux.do/t/topic/22118' }, ], likesNeeded: 5 } }; // 工具函数 const Utils = { random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)), isPageLoaded: () => { const loadingElements = document.querySelectorAll('.loading, .infinite-scroll'); return loadingElements.length === 0; }, isNearBottom: () => { const {scrollHeight, clientHeight, scrollTop} = document.documentElement; return (scrollTop + clientHeight) >= (scrollHeight - 200); }, debounce: (func, wait) => { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } }; // 存储管理 const Storage = { get: (key, defaultValue = null) => { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error('Storage error:', error); return false; } } }; // 用户信息助手类 class UserInfoHelper { constructor() { this.userInfoCache = new Map(); this.pendingRequests = new Map(); this.TRUST_LEVEL_LABELS = { 0: 'Lv0', 1: 'Lv1', 2: 'Lv2', 3: 'Lv3', 4: 'Lv4' }; this.DAY_IN_MS = 24 * 60 * 60 * 1000; this.revealInProgress = false; this.isEnabled = true; // 用户信息展示是否启用 this.observer = null; this.init(); } enable() { this.isEnabled = true; this.init(); } disable() { this.isEnabled = false; if (this.observer) { this.observer.disconnect(); this.observer = null; } } init() { if (!this.isEnabled) return; // 如果已有观察器,先断开 if (this.observer) { this.observer.disconnect(); } // 使用防抖,避免频繁触发 const debouncedEnhance = this.debounce(() => { if (this.isEnabled) { this.enhanceUserInfo(); } }, 300); // 监听页面变化,自动为新加载的用户添加信息 this.observer = new MutationObserver(() => { if (this.isEnabled) { debouncedEnhance(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); // 初始增强 this.enhanceUserInfo(); } debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } isTopicPage() { return window.location.pathname.includes('/t/topic/'); } async enhanceUserInfo() { if (!this.isTopicPage()) return; const articles = document.querySelectorAll('.topic-post article'); for (const article of articles) { const anchor = article.querySelector('.names a[data-user-card]'); if (!anchor) continue; const slug = anchor.getAttribute('data-user-card'); if (!slug) continue; const normalizedSlug = slug.trim().toLowerCase(); // 检查是否已经添加过信息 if (article.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`)) { continue; } // 检查是否是第一楼(楼主) const postWrapper = article.closest('.topic-post'); const postNumber = postWrapper?.getAttribute('data-post-number'); const isFirstPost = postNumber === '1'; // 第一楼直接显示,其他楼添加按钮 if (isFirstPost) { await this.loadAndDisplayUserInfo(anchor, slug, normalizedSlug); } else { this.addInfoButton(anchor, slug, normalizedSlug); } } } addInfoButton(anchor, rawSlug, normalizedSlug) { const namesContainer = anchor.closest('.names'); if (!namesContainer) return; // 检查是否已有按钮或信息 if (namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`)) { return; } // 如果已经有信息节点,不添加按钮 if (namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`)) { return; } const button = document.createElement('button'); button.className = 'user-info-btn'; button.setAttribute('data-user', normalizedSlug); button.setAttribute('data-raw-slug', rawSlug); button.textContent = '📊'; button.title = '点击查看用户注册信息'; button.style.cssText = ` margin-left: 6px; font-size: 14px; cursor: pointer; background: none; border: none; padding: 2px 4px; opacity: 0.6; transition: opacity 0.2s; vertical-align: middle; `; button.addEventListener('mouseenter', () => { button.style.opacity = '1'; }); button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; }); button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (button.disabled) return; button.disabled = true; button.textContent = '⏳'; try { await this.loadAndDisplayUserInfo(anchor, rawSlug, normalizedSlug); // 成功后按钮会被 loadAndDisplayUserInfo 中移除 } catch (error) { console.error('加载用户信息失败:', error); button.textContent = '📊'; button.disabled = false; } }); anchor.insertAdjacentElement('afterend', button); // 添加"查看话题"按钮 this.addTopicsButton(anchor, rawSlug, normalizedSlug); } addTopicsButton(anchor, rawSlug, normalizedSlug) { const namesContainer = anchor.closest('.names'); if (!namesContainer) return; // 检查是否已有话题按钮 if (namesContainer.querySelector(`.user-topics-btn[data-user="${normalizedSlug}"]`)) { return; } const topicsBtn = document.createElement('a'); topicsBtn.className = 'user-topics-btn'; topicsBtn.setAttribute('data-user', normalizedSlug); topicsBtn.href = `${BASE_URL}/u/${rawSlug}/activity/topics`; topicsBtn.target = '_blank'; topicsBtn.textContent = '查看话题'; topicsBtn.title = '查看该用户的话题'; topicsBtn.style.cssText = ` margin-left: 6px; font-size: 12px; cursor: pointer; text-decoration: none; padding: 2px 6px; opacity: 0.7; transition: all 0.2s; vertical-align: middle; display: inline-block; color: #667eea; background: rgba(102, 126, 234, 0.1); border-radius: 4px; `; topicsBtn.addEventListener('mouseenter', () => { topicsBtn.style.opacity = '1'; topicsBtn.style.background = 'rgba(102, 126, 234, 0.2)'; }); topicsBtn.addEventListener('mouseleave', () => { topicsBtn.style.opacity = '0.7'; topicsBtn.style.background = 'rgba(102, 126, 234, 0.1)'; }); // 插入到信息按钮后面 const infoBtn = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (infoBtn) { infoBtn.insertAdjacentElement('afterend', topicsBtn); } else { anchor.insertAdjacentElement('afterend', topicsBtn); } } async loadAndDisplayUserInfo(anchor, slug, normalizedSlug) { const namesContainer = anchor.closest('.names'); if (!namesContainer) return; // 再次检查是否已经存在,避免重复 const existingInfo = namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`); if (existingInfo) { console.log(`用户 ${normalizedSlug} 信息已存在,跳过`); // 确保按钮被移除 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) button.remove(); return; } const info = await this.fetchUserInfo(slug, normalizedSlug); if (!info) { // 获取失败,恢复按钮 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) { button.textContent = '📊'; button.disabled = false; } return; } const infoNode = this.buildInfoNode(info, normalizedSlug); if (!infoNode) { // 构建失败,恢复按钮 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) { button.textContent = '📊'; button.disabled = false; } return; } // 最后一次检查,确保在异步等待期间没有被其他调用添加 const finalCheck = namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`); if (finalCheck) { console.log(`用户 ${normalizedSlug} 信息在等待期间已被添加,跳过`); // 移除按钮 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) button.remove(); return; } // 先移除信息按钮 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) button.remove(); // 添加信息节点 anchor.insertAdjacentElement('afterend', infoNode); // 确保话题按钮存在(如果还没有添加) if (!namesContainer.querySelector(`.user-topics-btn[data-user="${normalizedSlug}"]`)) { this.addTopicsButton(anchor, slug, normalizedSlug); } } async fetchUserInfo(slug, normalizedSlug) { // 检查缓存 if (this.userInfoCache.has(normalizedSlug)) { return this.userInfoCache.get(normalizedSlug); } // 检查是否正在请求 if (this.pendingRequests.has(normalizedSlug)) { return this.pendingRequests.get(normalizedSlug); } // 创建请求 const requestPromise = this.doFetchUserInfo(slug, normalizedSlug); this.pendingRequests.set(normalizedSlug, requestPromise); try { const info = await requestPromise; if (info) { this.userInfoCache.set(normalizedSlug, info); } return info; } finally { this.pendingRequests.delete(normalizedSlug); } } async doFetchUserInfo(slug, normalizedSlug) { try { // 使用两个API并行请求,与原脚本保持一致 const PROFILE_API_BUILDERS = [ (s) => `${BASE_URL}/u/${encodeURIComponent(s)}.json`, (s) => `${BASE_URL}/users/${encodeURIComponent(s)}.json`, ]; const SUMMARY_API_BUILDERS = [ (s) => `${BASE_URL}/u/${encodeURIComponent(s)}/summary.json`, (s) => `${BASE_URL}/users/${encodeURIComponent(s)}/summary.json`, ]; const [profileData, summaryData] = await Promise.all([ this.fetchFirstAvailable(PROFILE_API_BUILDERS, slug), this.fetchFirstAvailable(SUMMARY_API_BUILDERS, slug), ]); if (!profileData && !summaryData) { return null; } const user = profileData && (profileData.user || profileData); const summary = summaryData && (summaryData.user_summary || summaryData.summary || summaryData); const createdAt = this.pickCreatedAt(user) || (summary && this.pickCreatedAt(summary)); if (!createdAt) { return null; } const topicCount = this.pickFirstNumber( user && (user.topic_count ?? user.topicCount), summary && (summary.topic_count ?? summary.topics_count), ); const totalPostCount = this.pickFirstNumber( user && (user.post_count ?? user.postCount), summary && (summary.post_count ?? summary.posts_count), ); let repliesCount = this.pickFirstNumber( summary && (summary.replies_count ?? summary.reply_count), ); if (repliesCount === null && totalPostCount !== null && topicCount !== null) { repliesCount = Math.max(0, totalPostCount - topicCount); } const trustLevelRaw = this.pickFirstValue( user && (user.trust_level ?? user.trustLevel), summary && (summary.trust_level ?? summary.trustLevel), ); const trustLevel = this.normalizeTrustLevel(trustLevelRaw); const days = this.calcDays(createdAt); return { slug: normalizedSlug, createdAt, days, topicCount: typeof topicCount === 'number' && Number.isFinite(topicCount) ? topicCount : undefined, repliesCount: typeof repliesCount === 'number' && Number.isFinite(repliesCount) ? repliesCount : undefined, trustLevel }; } catch (error) { console.error('获取用户信息失败:', slug, error); return null; } } async fetchFirstAvailable(builders, slug) { for (const builder of builders) { const url = builder(slug); const data = await this.safeFetchJson(url); if (data) { return data; } } return null; } async safeFetchJson(url) { try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) { return null; } return await response.json(); } catch (error) { return null; } } pickFirstNumber(...values) { for (const value of values) { const numberValue = Number(value); if (!Number.isNaN(numberValue)) { return numberValue; } } return null; } pickFirstValue(...values) { for (const value of values) { if (value !== undefined && value !== null) { return value; } } return null; } normalizeTrustLevel(raw) { if (raw === undefined || raw === null) { return undefined; } if (typeof raw === 'number' && Number.isFinite(raw)) { return raw; } if (typeof raw === 'string') { const TRUST_LEVEL_ALIAS = { newuser: 0, basic: 1, member: 2, regular: 3, leader: 4, }; const alias = TRUST_LEVEL_ALIAS[raw.toLowerCase()]; if (alias !== undefined) { return alias; } const numeric = Number(raw); if (!Number.isNaN(numeric)) { return numeric; } } return undefined; } pickCreatedAt(source) { if (!source) { return null; } return ( source.created_at || source.createdAt || source.registration_date || source.registrationDate || source.joined || source.joinedAt || null ); } calcDays(createdAt) { const createdTime = new Date(createdAt).getTime(); if (Number.isNaN(createdTime)) { return 0; } const diff = Date.now() - createdTime; return Math.max(0, Math.floor(diff / this.DAY_IN_MS)); } buildInfoNode(info, normalizedSlug) { const segments = [`注册 ${this.formatNumber(info.days)} 天`]; if (typeof info.topicCount === 'number' && Number.isFinite(info.topicCount)) { segments.push(`发帖 ${this.formatNumber(info.topicCount)}`); } if (typeof info.repliesCount === 'number' && Number.isFinite(info.repliesCount)) { segments.push(`回帖 ${this.formatNumber(info.repliesCount)}`); } if (typeof info.trustLevel === 'number' && Number.isFinite(info.trustLevel)) { const FULL_TRUST_LEVEL_LABELS = { 0: 'Lv0 新手', 1: 'Lv1 入门', 2: 'Lv2 成员', 3: 'Lv3 常驻', 4: 'Lv4 领袖', }; const label = FULL_TRUST_LEVEL_LABELS[info.trustLevel] || `信任级别 Lv${info.trustLevel}`; segments.push(label); } if (!segments.length) { return null; } const span = document.createElement('span'); span.className = 'user-reg-info'; span.setAttribute('data-user', normalizedSlug); span.textContent = ` · ${segments.join(' · ')}`; span.style.cssText = ` margin-left: 6px; font-size: 12px; color: #1a4c7c; `; return span; } formatNumber(value) { return Number(value).toLocaleString('zh-CN'); } // 批量展示所有已加载的回复用户信息 async revealAllVisibleReplies() { if (!this.isTopicPage()) return; if (this.revealInProgress) return; this.revealInProgress = true; try { const articles = document.querySelectorAll('.topic-post article'); for (let index = 0; index < articles.length; index++) { const article = articles[index]; // 跳过第一楼(楼主) const postWrapper = article.closest('.topic-post'); const postNumber = postWrapper?.getAttribute('data-post-number'); if (postNumber === '1') continue; const anchor = article.querySelector('.names a[data-user-card]'); if (!anchor) continue; const slug = anchor.getAttribute('data-user-card'); if (!slug) continue; const normalizedSlug = slug.trim().toLowerCase(); const namesContainer = anchor.closest('.names'); if (!namesContainer) continue; // 检查是否已经展示过 const hasInfo = namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`); if (hasInfo) { // 移除可能残留的按钮 const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) button.remove(); continue; } // 加载并显示用户信息 await this.loadAndDisplayUserInfo(anchor, slug, normalizedSlug); } } catch (error) { console.error('批量展示用户信息失败:', error); } finally { this.revealInProgress = false; } } } class BrowseController { constructor() { this.isScrolling = false; this.scrollInterval = null; this.pauseTimeout = null; this.trustLevelMonitorInterval = null; // 等级监控定时器 this.navigationTimeout = null; // 导航超时定时器 this.navigationGuardInterval = null; // 导航守护定时器 // 使用 sessionStorage 存储窗口独立的状态 this.accumulatedTime = this.getSessionStorage('accumulatedTime', 0); this.lastActionTime = Date.now(); this.isTopicPage = window.location.href.includes("/t/topic/"); this.autoRunning = this.getSessionStorage('autoRunning', false); this.topicList = this.getSessionStorage('topicList', []); // 使用 localStorage 存储全局共享的状态 this.firstUseChecked = Storage.get('firstUseChecked', false); this.likesCount = Storage.get('likesCount', 0); this.selectedPost = Storage.get('selectedPost', null); this.autoLikeEnabled = Storage.get('autoLikeEnabled', false); this.cleanModeEnabled = Storage.get('cleanModeEnabled', false); this.grayscaleModeEnabled = Storage.get('grayscaleModeEnabled', false); this.readUnreadEnabled = Storage.get('readUnreadEnabled', false); this.likedTopics = Storage.get('likedTopics', []); this.panelMinimized = Storage.get('panelMinimized', false); this.panelPosition = Storage.get('panelPosition', { x: null, y: null }); this.likeResumeTime = Storage.get('likeResumeTime', null); this.currentUsername = null; // 当前用户名 this.lastDetectedUser = null; // 上次检测到的用户名(用于账号切换检测) this.readTopics = []; // 当前用户的已阅读帖子列表,初始化后会加载 // 检查是否到达恢复点赞的时间 this.checkLikeResumeTime(); // 监听点赞限制弹窗 this.observeLikeLimit(); this.setupButton(); this.loadUserTrustLevel(); // 加载用户信任等级 this.loadUserReadHistory(); // 加载当前用户的阅读历史 this.startUserSwitchMonitoring(); // 启动账号切换监控 this.initFloorNumberDisplay(); this.setupWindowResizeHandler(); // 设置窗口大小调整处理 this.applyCleanModeStyles(); this.applyGrayscaleModeStyles(); this.initOnlyOwnerView(); if (!this.firstUseChecked) { this.handleFirstUse(); } else if (this.autoRunning) { if (this.isTopicPage) { this.startScrolling(); if (this.autoLikeEnabled) { this.autoLikeTopic(); } } else { this.getLatestTopics().then(() => this.navigateNextTopic()); } } // 启动导航守护程序 - 防止卡住 this.startNavigationGuard(); if (this.autoLikeEnabled && this.isTopicPage) { this.autoLikeTopic(); } // 初始化用户信息助手 - 默认启用,让每个窗口独立工作 this.userInfoHelper = new UserInfoHelper(); // 启动等级监控(60秒刷新一次)- 默认启用 this.startTrustLevelMonitor(); } // 启动等级监控(60秒刷新一次) startTrustLevelMonitor() { // 如果已经有定时器在运行,先清除 if (this.trustLevelMonitorInterval) { clearInterval(this.trustLevelMonitorInterval); } this.trustLevelMonitorInterval = setInterval(() => { console.log('自动刷新等级信息...'); this.loadUserTrustLevel(false); }, 60000); // 60秒 console.log('等级监控已启动(60秒刷新一次)'); } // 停止等级监控 stopTrustLevelMonitor() { if (this.trustLevelMonitorInterval) { clearInterval(this.trustLevelMonitorInterval); this.trustLevelMonitorInterval = null; console.log('等级监控已停止'); } } // 启动导航守护程序 - 检测页面是否卡住 startNavigationGuard() { if (this.navigationGuardInterval) { clearInterval(this.navigationGuardInterval); } // 记录页面加载时间 this.pageLoadTime = Date.now(); this.lastPageUrl = window.location.href; // 每5秒检查一次页面状态 this.navigationGuardInterval = setInterval(() => { if (!this.autoRunning) return; const currentTime = Date.now(); const timeOnPage = currentTime - this.pageLoadTime; const currentUrl = window.location.href; // 检测URL是否改变 if (currentUrl !== this.lastPageUrl) { console.log('✅ 页面已跳转,重置守护定时器'); this.pageLoadTime = currentTime; this.lastPageUrl = currentUrl; return; } // 如果在同一个文章页面停留超过60秒且正在自动运行,说明可能卡住了 if (this.isTopicPage && timeOnPage > 60000 && !this.isScrolling) { console.warn('⚠️ 检测到页面可能卡住(60秒未跳转且未滚动),尝试恢复...'); this.recoverFromStuck(); } // 如果不是文章页且停留超过30秒,也可能卡住 if (!this.isTopicPage && timeOnPage > 30000) { console.warn('⚠️ 检测到在非文章页卡住,尝试恢复...'); this.recoverFromStuck(); } }, 5000); console.log('🛡️ 导航守护程序已启动'); } // 从卡住状态恢复 async recoverFromStuck() { console.log('🔧 开始恢复流程...'); // 停止当前滚动 this.stopScrolling(); await Utils.sleep(1000); // 尝试继续流程 if (this.isTopicPage) { console.log('📖 在文章页,重新开始滚动'); this.startScrolling(); } else { console.log('📋 在列表页,尝试导航到下一篇'); if (this.topicList.length === 0) { await this.getLatestTopics(); } await this.navigateNextTopic(); } // 重置页面加载时间 this.pageLoadTime = Date.now(); } // 停止导航守护 stopNavigationGuard() { if (this.navigationGuardInterval) { clearInterval(this.navigationGuardInterval); this.navigationGuardInterval = null; console.log('🛡️ 导航守护程序已停止'); } } // sessionStorage 辅助方法(用于窗口独立状态) getSessionStorage(key, defaultValue = null) { try { const value = sessionStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } } setSessionStorage(key, value) { try { sessionStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error('SessionStorage error:', error); return false; } } addGlobalStyles() { const style = document.createElement('style'); style.textContent = ` :root { --panel-expanded-width: auto; --panel-minimized-size: 50px; --panel-edge-margin: 30px; --panel-border-radius: 16px; } .section-collapsible { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; } .section-collapsible .collapse-icon { transition: transform 0.3s; font-size: 10px; } .section-collapsible.collapsed .collapse-icon { transform: rotate(-90deg); } .section-collapsible-content { max-height: 1000px; overflow: hidden; transition: max-height 0.3s ease-out, opacity 0.3s ease-out; opacity: 1; } .section-collapsible-content.collapsed { max-height: 0; opacity: 0; } /* 当折叠区域收起时,隐藏其后的分隔线 */ .section-collapsible.collapsed + .section-collapsible-content + .section-divider { display: none; } .linuxdo-helper-panel { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); width: fit-content; min-width: 280px; max-width: 450px; max-height: calc(100vh - 40px); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--panel-border-radius); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; overflow-y: auto; overflow-x: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); will-change: transform; } .linuxdo-helper-panel:hover { box-shadow: 0 15px 50px rgba(0, 0, 0, 0.4); } .linuxdo-helper-panel.minimized { width: var(--panel-minimized-size); height: var(--panel-minimized-size); min-width: var(--panel-minimized-size); border-radius: 50%; overflow: hidden; cursor: pointer; opacity: 0.7; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .linuxdo-helper-panel.minimized:hover { transform: scale(1.15); box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6); opacity: 1; } /* 左边展开:从左向右 */ .linuxdo-helper-panel.on-left { transform-origin: left center; } /* 右边展开:从右向左 */ .linuxdo-helper-panel.on-right { transform-origin: right center; } .panel-header { background: rgba(255, 255, 255, 0.15); padding: 12px 16px; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; border-bottom: 1px solid rgba(255, 255, 255, 0.2); transition: opacity 0.3s; } .linuxdo-helper-panel.minimized .panel-header { opacity: 0; pointer-events: none; padding: 0; height: 0; overflow: hidden; } .panel-header:active { cursor: grabbing; } .panel-title { color: white; font-weight: 600; font-size: 14px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .panel-controls { display: flex; gap: 8px; } .panel-control-btn { width: 24px; height: 24px; border-radius: 6px; border: none; background: rgba(255, 255, 255, 0.2); color: white; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; padding: 0; line-height: 1; } .panel-control-btn:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.1); } .panel-control-btn:active { transform: scale(0.95); } .minimized-icon { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; align-items: center; justify-content: center; pointer-events: none; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 20px; font-weight: 700; color: white; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); letter-spacing: -1px; } .linuxdo-helper-panel.minimized .minimized-icon { display: flex; } .linuxdo-helper-panel.minimized:hover .minimized-icon { transform: scale(1.1); text-shadow: 0 3px 8px rgba(255, 255, 255, 0.6); } .panel-content { padding: 12px; display: flex; flex-direction: column; gap: 6px; transition: all 0.3s; overflow: hidden; width: 100%; box-sizing: border-box; } .panel-content.hidden { max-height: 0; padding: 0; opacity: 0; } .linuxdo-helper-panel.minimized .panel-content { display: none; } .main-action-btn { width: 100%; padding: 8px 12px; font-size: 13px; font-weight: 600; background: white; color: #667eea; border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; justify-content: center; gap: 5px; white-space: nowrap; overflow: hidden; min-height: 32px; line-height: 1.1; } .main-action-btn .btn-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; } .main-action-btn .btn-icon { flex-shrink: 0; font-size: 14px; } .main-action-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); } .main-action-btn:active { transform: translateY(0); } .main-action-btn.running { background: #ff6b6b; color: white; } .btn-icon { font-size: 18px; } .trust-level-row { background: rgba(255, 255, 255, 0.15); padding: 8px 12px; border-radius: 10px; margin-top: 8px; } .trust-level-header { color: white; font-size: 13px; font-weight: 600; margin-bottom: 6px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; } .trust-level-refresh { background: rgba(255, 255, 255, 0.2); border: none; color: white; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: all 0.2s; } .trust-level-refresh:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.05); } .trust-level-refresh:disabled { opacity: 0.5; cursor: not-allowed; } .trust-level-item { display: flex; justify-content: space-between; align-items: center; color: rgba(255, 255, 255, 0.9); font-size: 11px; margin: 4px 0; padding: 3px 0; white-space: nowrap; } .trust-level-name { flex: 1; margin-right: 8px; white-space: nowrap; } .trust-level-progress { display: flex; align-items: center; gap: 6px; } .trust-level-bar { width: 60px; height: 6px; background: rgba(255, 255, 255, 0.2); border-radius: 3px; overflow: hidden; } .trust-level-bar-fill { height: 100%; background: linear-gradient(90deg, #48bb78 0%, #68d391 100%); transition: width 0.3s; } .trust-level-bar-fill.completed { background: linear-gradient(90deg, #4299e1 0%, #63b3ed 100%); } .trust-level-value { font-size: 10px; color: rgba(255, 255, 255, 0.8); min-width: 50px; text-align: right; } .trust-level-loading { color: rgba(255, 255, 255, 0.7); font-size: 11px; text-align: center; padding: 8px 0; } .random-floor-btn, .reveal-users-btn { width: 100%; padding: 7px 12px; font-size: 12px; font-weight: 600; background: rgba(255, 255, 255, 0.95); color: #667eea; border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 28px; line-height: 1.2; margin-bottom: 6px; } .reveal-users-btn { margin-bottom: 0; } .random-floor-btn .btn-text, .reveal-users-btn .btn-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; } .random-floor-btn .btn-icon, .reveal-users-btn .btn-icon { flex-shrink: 0; font-size: 13px; } .random-floor-btn:hover, .reveal-users-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); background: rgba(255, 255, 255, 1); } .random-floor-btn:active, .reveal-users-btn:active { transform: translateY(0); } .reveal-users-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none !important; } .toggle-row { background: rgba(255, 255, 255, 0.15); padding: 5px 10px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; transition: all 0.2s; min-height: 26px; } .toggle-row:hover { background: rgba(255, 255, 255, 0.22); } .toggle-label { color: white; font-size: 12px; font-weight: 500; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .toggle-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.3); transition: 0.3s; border-radius: 26px; } .toggle-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: 0.3s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .toggle-switch input:checked + .toggle-slider { background-color: rgba(76, 175, 80, 0.8); } .toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); } .section-divider { height: 1px; background: rgba(255, 255, 255, 0.2); margin: 6px 0; } .section-title { color: rgba(255, 255, 255, 0.9); font-size: 12px; font-weight: 600; margin: 4px 0 4px 0; padding: 0 4px; text-transform: uppercase; letter-spacing: 0.5px; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } .linuxdo-helper-panel { animation: fadeIn 0.3s ease-out; } `; document.head.appendChild(style); } setupButton() { this.addGlobalStyles(); // 创建主容器 this.container = document.createElement("div"); this.container.className = "linuxdo-helper-panel"; if (this.panelMinimized) { this.container.classList.add('minimized'); } // 如果有保存的位置,使用保存的位置;否则默认右上角 if (this.panelPosition.x !== null && this.panelPosition.y !== null) { this.applyPanelPosition(this.panelPosition.x, this.panelPosition.y); } else { // 默认位置:右上角 const defaultX = window.innerWidth - 300; // 280px 宽度 + 20px 边距 const defaultY = 20; this.applyPanelPosition(defaultX, defaultY); } // 创建最小化图标 - 使用简洁的文字标识 const minimizedIcon = document.createElement("div"); minimizedIcon.className = "minimized-icon"; minimizedIcon.textContent = "助手"; minimizedIcon.title = "点击展开控制面板"; // 创建面板头部 const header = document.createElement("div"); header.className = "panel-header"; header.innerHTML = ` <span class="panel-title">📚 Linux.do 助手</span> <div class="panel-controls"> <button class="panel-control-btn minimize-btn" title="最小化">─</button> </div> `; // 创建面板内容区 const content = document.createElement("div"); content.className = "panel-content"; if (this.panelMinimized) { content.classList.add('hidden'); } // 主按钮 this.button = document.createElement("button"); this.button.className = "main-action-btn" + (this.autoRunning ? " running" : ""); this.button.innerHTML = this.autoRunning ? '<span class="btn-icon">⏸</span><span class="btn-text">停止阅读</span>' : '<span class="btn-icon">▶</span><span class="btn-text">开始阅读</span>'; this.button.addEventListener("click", () => this.handleButtonClick()); // 随机楼层按钮 this.randomBtn = document.createElement("button"); this.randomBtn.className = "random-floor-btn"; this.randomBtn.innerHTML = '<span class="btn-icon">🎲</span><span class="btn-text">随机楼层</span>'; this.randomBtn.addEventListener("click", () => this.randomJump()); this.randomBtn.style.display = this.isTopicPage ? 'flex' : 'none'; this.randomBtn.title = '随机跳转到某个楼层(抽奖用)'; // 批量展示用户信息按钮 this.revealUsersBtn = document.createElement("button"); this.revealUsersBtn.className = "reveal-users-btn"; this.revealUsersBtn.innerHTML = '<span class="btn-icon">📊</span><span class="btn-text">批量展示信息</span>'; this.revealUsersBtn.addEventListener("click", () => this.handleRevealUsersClick()); this.revealUsersBtn.style.display = this.isTopicPage ? 'flex' : 'none'; this.revealUsersBtn.title = '批量展示当前页面所有已加载回复的用户信息'; // 自动点赞开关 const autoLikeRow = this.createToggleRow( "👍 自动点赞主题", this.autoLikeEnabled, (checked) => { // 检查是否在冷却期 if (checked && this.likeResumeTime && Date.now() < this.likeResumeTime) { const now = Date.now(); const remainingHours = Math.ceil((this.likeResumeTime - now) / (1000 * 60 * 60)); const resumeDate = new Date(this.likeResumeTime); this.showNotification(`点赞功能冷却中,将在 ${resumeDate.toLocaleTimeString()} 恢复`); console.log(`点赞冷却中,还需约 ${remainingHours} 小时,无法开启`); // 恢复开关状态为关闭 setTimeout(() => { const toggleRows = this.container.querySelectorAll('.toggle-row'); for (const row of toggleRows) { const label = row.querySelector('.toggle-label'); if (label && label.textContent.includes('自动点赞')) { const input = row.querySelector('input[type="checkbox"]'); if (input) { input.checked = false; } break; } } }, 100); return; } this.autoLikeEnabled = checked; Storage.set('autoLikeEnabled', this.autoLikeEnabled); console.log(`自动点赞主题: ${this.autoLikeEnabled ? '开启' : '关闭'}`); if (this.autoLikeEnabled && this.isTopicPage) { this.autoLikeTopic(); } } ); // 清爽模式开关 const cleanModeRow = this.createToggleRow( "✨ 清爽模式", this.cleanModeEnabled, (checked) => { this.cleanModeEnabled = checked; Storage.set('cleanModeEnabled', this.cleanModeEnabled); console.log(`清爽模式: ${this.cleanModeEnabled ? '开启' : '关闭'}`); this.toggleCleanMode(); } ); // 黑白灰模式开关 const grayscaleModeRow = this.createToggleRow( "🎨 黑白灰模式", this.grayscaleModeEnabled, (checked) => { this.grayscaleModeEnabled = checked; Storage.set('grayscaleModeEnabled', this.grayscaleModeEnabled); console.log(`黑白灰模式: ${this.grayscaleModeEnabled ? '开启' : '关闭'}`); this.toggleGrayscaleMode(); } ); // 读取未读帖子开关 const readUnreadRow = this.createToggleRow( "📬 读取未读帖子", this.readUnreadEnabled, (checked) => { this.readUnreadEnabled = checked; Storage.set('readUnreadEnabled', this.readUnreadEnabled); console.log(`读取未读帖子: ${this.readUnreadEnabled ? '开启' : '关闭'}`); // 切换模式时清空话题列表,强制重新获取 this.topicList = []; this.setSessionStorage('topicList', []); console.log('已清空话题列表,下次将获取' + (this.readUnreadEnabled ? '未读' : '最新') + '帖子'); } ); // 清除点赞冷却按钮 this.clearCooldownBtn = document.createElement("button"); this.clearCooldownBtn.className = "reveal-users-btn"; this.clearCooldownBtn.innerHTML = '<span class="btn-icon">🔥</span><span class="btn-text">清除点赞冷却</span>'; this.clearCooldownBtn.addEventListener("click", () => this.handleClearCooldown()); this.clearCooldownBtn.title = '清除点赞冷却时间,立即恢复点赞功能'; this.clearCooldownBtn.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'; // 信任等级显示容器 this.trustLevelContainer = document.createElement("div"); this.trustLevelContainer.className = "trust-level-row"; this.trustLevelContainer.innerHTML = '<div class="trust-level-loading">加载等级信息...</div>'; // 组装面板 - 按功能分类 // 📖 自动阅读区(包含阅读按钮和相关设置) const autoSection = document.createElement("div"); autoSection.className = "section-collapsible"; autoSection.innerHTML = '<div class="section-title"><span class="collapse-icon">▼</span> 📖 自动阅读</div>'; content.appendChild(autoSection); // 自动阅读内容区 this.autoSectionContent = document.createElement("div"); this.autoSectionContent.className = "section-collapsible-content"; // 根据运行状态决定初始折叠状态:停止时折叠,运行时展开 if (!this.autoRunning) { autoSection.classList.add('collapsed'); this.autoSectionContent.classList.add('collapsed'); } this.autoSectionContent.appendChild(this.button); this.autoSectionContent.appendChild(autoLikeRow); this.autoSectionContent.appendChild(this.clearCooldownBtn); this.autoSectionContent.appendChild(readUnreadRow); content.appendChild(this.autoSectionContent); // 自动阅读区折叠点击事件 autoSection.addEventListener('click', () => { autoSection.classList.toggle('collapsed'); this.autoSectionContent.classList.toggle('collapsed'); }); // 分隔线1 this.divider1 = document.createElement("div"); this.divider1.className = "section-divider"; content.appendChild(this.divider1); // ⚙️ 模式设置区(清爽模式、黑白灰模式,默认折叠) const settingsSection = document.createElement("div"); settingsSection.className = "section-collapsible collapsed"; settingsSection.innerHTML = '<div class="section-title"><span class="collapse-icon">▼</span> ⚙️ 模式设置</div>'; content.appendChild(settingsSection); // 设置内容区(默认折叠) this.settingsSectionContent = document.createElement("div"); this.settingsSectionContent.className = "section-collapsible-content collapsed"; this.settingsSectionContent.appendChild(cleanModeRow); this.settingsSectionContent.appendChild(grayscaleModeRow); content.appendChild(this.settingsSectionContent); // 设置区折叠点击事件 settingsSection.addEventListener('click', () => { settingsSection.classList.toggle('collapsed'); this.settingsSectionContent.classList.toggle('collapsed'); }); // 分隔线2 this.divider2 = document.createElement("div"); this.divider2.className = "section-divider"; content.appendChild(this.divider2); // 📖 文章页功能区(只在文章页显示,默认折叠) this.toolSectionContainer = document.createElement("div"); this.toolSectionContainer.className = "tool-section-container"; const toolSection = document.createElement("div"); toolSection.className = "section-collapsible collapsed"; toolSection.innerHTML = '<div class="section-title"><span class="collapse-icon">▼</span> 📖 文章页功能</div>'; this.toolSectionContainer.appendChild(toolSection); // 文章页功能内容区(默认折叠) this.toolSectionContent = document.createElement("div"); this.toolSectionContent.className = "section-collapsible-content collapsed"; this.toolSectionContent.appendChild(this.randomBtn); this.toolSectionContent.appendChild(this.revealUsersBtn); this.toolSectionContainer.appendChild(this.toolSectionContent); // 文章页功能区折叠点击事件 toolSection.addEventListener('click', () => { toolSection.classList.toggle('collapsed'); this.toolSectionContent.classList.toggle('collapsed'); }); content.appendChild(this.toolSectionContainer); // 分隔线3 this.divider3 = document.createElement("div"); this.divider3.className = "section-divider"; content.appendChild(this.divider3); // 📊 账号信息区 const accountSection = document.createElement("div"); accountSection.innerHTML = '<div class="section-title">📊 账号信息</div>'; content.appendChild(accountSection); content.appendChild(this.trustLevelContainer); this.container.appendChild(minimizedIcon); this.container.appendChild(header); this.container.appendChild(content); document.body.appendChild(this.container); // 添加拖动功能(只在展开状态可拖动) this.makeDraggable(header); // 添加最小化功能 header.querySelector('.minimize-btn').addEventListener('click', (e) => { e.stopPropagation(); this.toggleMinimize(); }); // 点击最小化图标展开 minimizedIcon.addEventListener('click', (e) => { e.stopPropagation(); if (this.panelMinimized) { this.toggleMinimize(); } }); // 点击最小化的面板也可以展开 this.container.addEventListener('click', (e) => { if (this.panelMinimized && e.target === this.container) { this.toggleMinimize(); } }); // 给最小化面板添加拖动功能 this.makeMinimizedDraggable(); } createToggleRow(label, checked, onChange) { const row = document.createElement("div"); row.className = "toggle-row"; const labelEl = document.createElement("span"); labelEl.className = "toggle-label"; labelEl.textContent = label; const toggleSwitch = document.createElement("label"); toggleSwitch.className = "toggle-switch"; const input = document.createElement("input"); input.type = "checkbox"; input.checked = checked; input.addEventListener("change", (e) => onChange(e.target.checked)); const slider = document.createElement("span"); slider.className = "toggle-slider"; toggleSwitch.appendChild(input); toggleSwitch.appendChild(slider); row.appendChild(labelEl); row.appendChild(toggleSwitch); return row; } // 应用面板位置(带吸附效果) applyPanelPosition(x, y, snap = false) { let finalX = x; let finalY = y; if (snap) { // 吸附逻辑:判断靠近哪一边 const windowWidth = window.innerWidth; const edgeMargin = 30; // 使用统一的边距变量 const panelWidth = this.panelMinimized ? 50 : (this.container.offsetWidth || 280); const centerX = windowWidth / 2; // 判断在左边还是右边 const isOnLeft = x < centerX; // 如果在左半边,吸附到左边;否则吸附到右边 if (isOnLeft) { finalX = edgeMargin; this.container.classList.add('on-left'); this.container.classList.remove('on-right'); } else { finalX = windowWidth - panelWidth - edgeMargin; this.container.classList.add('on-right'); this.container.classList.remove('on-left'); } // Y 轴始终吸附到顶部 finalY = 70; } // 应用位置 this.container.style.position = 'fixed'; this.container.style.left = finalX + 'px'; this.container.style.top = finalY + 'px'; this.container.style.right = 'auto'; this.container.style.bottom = 'auto'; this.container.style.transform = 'none'; // 保存当前位置 this.currentTranslateX = finalX; this.currentTranslateY = finalY; return { x: finalX, y: finalY }; } makeDraggable(header) { let isDragging = false; let hasMoved = false; let currentX; let currentY; let initialX; let initialY; let rafId = null; // 禁用过渡效果以提高拖动流畅度 const disableTransition = () => { this.container.style.transition = 'none'; }; const enableTransition = () => { this.container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; }; header.addEventListener('mousedown', (e) => { if (e.target.classList.contains('panel-control-btn') || e.target.closest('.panel-control-btn')) { return; } isDragging = true; hasMoved = false; disableTransition(); const rect = this.container.getBoundingClientRect(); initialX = e.clientX - rect.left; initialY = e.clientY - rect.top; // 使用捕获阶段,提高响应速度 document.addEventListener('mousemove', onMouseMove, true); document.addEventListener('mouseup', onMouseUp, true); // 防止文本选择 e.preventDefault(); }); const updatePosition = () => { // 限制在视窗内 const maxX = window.innerWidth - this.container.offsetWidth; const maxY = window.innerHeight - this.container.offsetHeight; currentX = Math.max(0, Math.min(currentX, maxX)); currentY = Math.max(0, Math.min(currentY, maxY)); // 实时更新位置(拖动时不吸附) this.container.style.position = 'fixed'; this.container.style.left = currentX + 'px'; this.container.style.top = currentY + 'px'; this.container.style.right = 'auto'; this.container.style.bottom = 'auto'; this.container.style.transform = 'none'; }; const onMouseMove = (e) => { if (!isDragging) return; e.preventDefault(); e.stopPropagation(); hasMoved = true; currentX = e.clientX - initialX; currentY = e.clientY - initialY; // 使用 requestAnimationFrame 确保流畅渲染 if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(updatePosition); }; const onMouseUp = () => { if (isDragging) { isDragging = false; enableTransition(); // 取消未完成的动画帧 if (rafId) { cancelAnimationFrame(rafId); rafId = null; } // 只有在真正移动过才吸附 if (hasMoved) { // 松开鼠标时吸附到最近的边角 const snappedPos = this.applyPanelPosition(currentX, currentY, true); // 保存吸附后的位置 this.panelPosition = snappedPos; Storage.set('panelPosition', this.panelPosition); } } document.removeEventListener('mousemove', onMouseMove, true); document.removeEventListener('mouseup', onMouseUp, true); }; } makeMinimizedDraggable() { let isDragging = false; let hasMoved = false; let currentX; let currentY; let initialX; let initialY; let rafId = null; this.container.addEventListener('mousedown', (e) => { // 只在最小化状态下才能拖动整个容器 if (!this.panelMinimized) return; isDragging = true; hasMoved = false; this.container.style.transition = 'none'; const rect = this.container.getBoundingClientRect(); initialX = e.clientX - rect.left; initialY = e.clientY - rect.top; document.addEventListener('mousemove', onMouseMove, true); document.addEventListener('mouseup', onMouseUp, true); e.preventDefault(); e.stopPropagation(); }); const updatePosition = () => { const maxX = window.innerWidth - 50; const maxY = window.innerHeight - 50; currentX = Math.max(0, Math.min(currentX, maxX)); currentY = Math.max(0, Math.min(currentY, maxY)); this.container.style.position = 'fixed'; this.container.style.left = currentX + 'px'; this.container.style.top = currentY + 'px'; this.container.style.right = 'auto'; this.container.style.bottom = 'auto'; this.container.style.transform = 'none'; }; const onMouseMove = (e) => { if (!isDragging) return; e.preventDefault(); e.stopPropagation(); hasMoved = true; currentX = e.clientX - initialX; currentY = e.clientY - initialY; if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(updatePosition); }; const onMouseUp = (e) => { if (isDragging) { isDragging = false; this.container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; if (rafId) { cancelAnimationFrame(rafId); rafId = null; } if (hasMoved) { // 松开鼠标时吸附 const snappedPos = this.applyPanelPosition(currentX, currentY, true); this.panelPosition = snappedPos; Storage.set('panelPosition', this.panelPosition); // 阻止点击事件触发展开 e.stopPropagation(); } else { // 没有移动,触发展开 // 不阻止事件,让点击事件继续冒泡 } } document.removeEventListener('mousemove', onMouseMove, true); document.removeEventListener('mouseup', onMouseUp, true); }; } toggleMinimize() { const wasMinimized = this.panelMinimized; this.panelMinimized = !this.panelMinimized; Storage.set('panelMinimized', this.panelMinimized); const content = this.container.querySelector('.panel-content'); // 判断当前在左边还是右边 const windowWidth = window.innerWidth; const isOnRight = this.container.classList.contains('on-right'); if (this.panelMinimized) { // 缩小:从 280px -> 50px content.classList.add('hidden'); this.container.classList.add('minimized'); // 如果在右边,需要调整 left 值以保持右边缘位置不变 if (isOnRight) { const currentLeft = parseInt(this.container.style.left); // 280px 变成 50px,差值是 230px,需要向右移动 230px this.container.style.left = (currentLeft + 230) + 'px'; this.currentTranslateX = currentLeft + 230; } setTimeout(() => { const snappedPos = this.applyPanelPosition(this.currentTranslateX, this.currentTranslateY, true); this.panelPosition = snappedPos; Storage.set('panelPosition', this.panelPosition); }, 100); } else { // 展开:从 50px -> 280px content.classList.remove('hidden'); this.container.classList.remove('minimized'); // 如果在右边,需要调整 left 值以保持右边缘位置不变 if (isOnRight) { const currentLeft = parseInt(this.container.style.left); // 50px 变成 280px,差值是 230px,需要向左移动 230px this.container.style.left = (currentLeft - 230) + 'px'; this.currentTranslateX = currentLeft - 230; } setTimeout(() => { // 强制浏览器重排 void this.container.offsetWidth; const snappedPos = this.applyPanelPosition(this.currentTranslateX, this.currentTranslateY, true); this.panelPosition = snappedPos; Storage.set('panelPosition', this.panelPosition); }, 350); } } setupWindowResizeHandler() { // 监听窗口大小变化,确保面板始终在可见区域内 let resizeTimer; const adjustPosition = () => { if (this.currentTranslateX !== null && this.currentTranslateY !== null) { // 重新应用吸附位置(窗口大小变化时重新计算) const snappedPos = this.applyPanelPosition(this.currentTranslateX, this.currentTranslateY, true); // 保存新位置 this.panelPosition = snappedPos; Storage.set('panelPosition', this.panelPosition); } }; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(adjustPosition, 100); }); // 初始调整一次 setTimeout(adjustPosition, 500); } checkLikeResumeTime() { if (this.likeResumeTime) { const now = Date.now(); if (now >= this.likeResumeTime) { // 时间到了,清除冷却时间 console.log('点赞冷却时间已过,可以正常使用点赞功能'); this.likeResumeTime = null; Storage.set('likeResumeTime', null); this.updateClearCooldownButton(); // 不自动开启点赞,由用户决定 } else { // 还在冷却期,记录状态但不修改开关 const remainingHours = Math.ceil((this.likeResumeTime - now) / (1000 * 60 * 60)); const resumeDate = new Date(this.likeResumeTime); console.log(`点赞功能冷却中,将在 ${resumeDate.toLocaleString()} (还需约 ${remainingHours} 小时) 后恢复`); console.log(`提示:可以点击"清除点赞冷却"按钮立即恢复点赞功能`); this.updateClearCooldownButton(); } } else { this.updateClearCooldownButton(); } } updateClearCooldownButton() { if (!this.clearCooldownBtn) return; if (this.likeResumeTime && Date.now() < this.likeResumeTime) { const remainingHours = Math.ceil((this.likeResumeTime - Date.now()) / (1000 * 60 * 60)); this.clearCooldownBtn.innerHTML = `<span class="btn-icon">🔥</span><span class="btn-text">清除冷却 (${remainingHours}h)</span>`; this.clearCooldownBtn.style.display = 'flex'; } else { this.clearCooldownBtn.style.display = 'none'; } } handleClearCooldown() { if (!this.likeResumeTime) { this.showNotification('当前没有点赞冷却'); return; } // 清除冷却时间 this.likeResumeTime = null; Storage.set('likeResumeTime', null); // 更新按钮显示 this.updateClearCooldownButton(); // 显示成功提示 this.showNotification('✅ 点赞冷却已清除,可以正常点赞了!'); console.log('[清除冷却] 点赞冷却时间已清除'); } observeLikeLimit() { // 监听 DOM 变化,检测点赞限制弹窗 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // 检测弹窗内容 const text = node.textContent || ''; // 只处理点赞限制弹窗,排除回复限制弹窗 // 点赞限制关键词:点赞上限、点赞、分享很多爱 // 回复限制关键词:回复数量、创建更多新回复 const isLikeLimit = ( (text.includes('点赞上限') || text.includes('分享很多爱') || text.includes('点赞') && text.includes('小时后再次点赞')) && !text.includes('回复') && !text.includes('创建更多新回复') ); if (isLikeLimit) { this.handleLikeLimit(text); // 自动关闭弹窗 setTimeout(() => { const confirmBtn = document.querySelector('.modal-footer .btn-primary, .dialog-footer .btn-primary, button.btn-primary'); if (confirmBtn && confirmBtn.textContent.includes('确定')) { confirmBtn.click(); } }, 1000); break; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); } handleLikeLimit(text) { console.log('检测到点赞限制提示:', text); let waitMinutes = 0; // 等待时间(分钟) // 优先匹配 "在 X 分钟后" 格式 const minuteMatch = text.match(/[在|可以在]\s*(\d+)\s*分钟后/); if (minuteMatch) { waitMinutes = parseInt(minuteMatch[1]); console.log(`从 "X分钟后" 提取到等待时间: ${waitMinutes} 分钟`); } else { // 匹配 "在 X 小时后" 格式 const hourMatch = text.match(/[在|可以在]\s*(\d+)\s*小时后/); if (hourMatch) { waitMinutes = parseInt(hourMatch[1]) * 60; console.log(`从 "X小时后" 提取到等待时间: ${hourMatch[1]} 小时 = ${waitMinutes} 分钟`); } else { // 尝试匹配最后一个数字+单位的组合 const allMinuteMatches = text.match(/(\d+)\s*分钟/g); const allHourMatches = text.match(/(\d+)\s*小时/g); if (allMinuteMatches && allMinuteMatches.length > 0) { // 取最后一个分钟匹配 const lastMatch = allMinuteMatches[allMinuteMatches.length - 1].match(/(\d+)/); if (lastMatch) { waitMinutes = parseInt(lastMatch[1]); console.log(`从最后一个匹配提取到等待时间: ${waitMinutes} 分钟`); } } else if (allHourMatches && allHourMatches.length > 0) { // 取最后一个小时匹配 const lastMatch = allHourMatches[allHourMatches.length - 1].match(/(\d+)/); if (lastMatch) { waitMinutes = parseInt(lastMatch[1]) * 60; console.log(`从最后一个匹配提取到等待时间: ${lastMatch[1]} 小时 = ${waitMinutes} 分钟`); } } else { // 默认10小时 waitMinutes = 10 * 60; console.log(`未能提取等待时间,使用默认值: 10 小时 = ${waitMinutes} 分钟`); } } } // 计算恢复时间 const resumeTime = Date.now() + (waitMinutes * 60 * 1000); this.likeResumeTime = resumeTime; Storage.set('likeResumeTime', resumeTime); // 关闭自动点赞 this.autoLikeEnabled = false; Storage.set('autoLikeEnabled', false); // 更新UI - 更精确地定位到自动点赞开关 const toggleRows = this.container.querySelectorAll('.toggle-row'); for (const row of toggleRows) { const label = row.querySelector('.toggle-label'); if (label && label.textContent.includes('自动点赞')) { const input = row.querySelector('input[type="checkbox"]'); if (input) { input.checked = false; } break; } } const resumeDate = new Date(resumeTime); const displayTime = waitMinutes >= 60 ? `${Math.floor(waitMinutes / 60)} 小时 ${waitMinutes % 60 > 0 ? (waitMinutes % 60) + ' 分钟' : ''}`.trim() : `${waitMinutes} 分钟`; console.log(`已达到点赞上限,自动关闭点赞功能,将在 ${resumeDate.toLocaleString()} (${displayTime}后) 恢复`); // 显示提示 - 使用提取到的实际时间 this.showNotification(`点赞已达上限,将在 ${displayTime}后自动恢复`); } showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 100000; font-size: 14px; max-width: 300px; animation: slideIn 0.3s ease-out; `; notification.textContent = message; // 添加动画样式 const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(style); document.body.appendChild(notification); // 3秒后自动消失 setTimeout(() => { notification.style.transition = 'all 0.3s'; notification.style.opacity = '0'; notification.style.transform = 'translateX(400px)'; setTimeout(() => notification.remove(), 300); }, 3000); } // 获取当前用户名 async getCurrentUsername() { if (this.currentUsername) return this.currentUsername; try { // 方法1:从用户菜单获取 const userMenuBtn = document.querySelector('.header-dropdown-toggle.current-user'); if (userMenuBtn) { const img = userMenuBtn.querySelector('img[alt]'); if (img && img.alt) { this.currentUsername = img.alt; return this.currentUsername; } } // 方法2:从 API 获取 const response = await fetch(`${BASE_URL}/session/current.json`); if (response.ok) { const data = await response.json(); if (data.current_user && data.current_user.username) { this.currentUsername = data.current_user.username; return this.currentUsername; } } } catch (error) { console.error('获取用户名失败:', error); } return null; } // 加载用户信任等级 async loadUserTrustLevel(isManualRefresh = false) { const username = await this.getCurrentUsername(); if (!username) { this.trustLevelContainer.innerHTML = '<div class="trust-level-loading">未登录</div>'; return; } // 如果不是手动刷新,显示加载状态 if (isManualRefresh) { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.textContent = '刷新中...'; refreshBtn.disabled = true; } } try { // 域名判断:idcflare.com 使用原逻辑,linux.do 使用新逻辑 if (CURRENT_DOMAIN === 'idcflare.com') { // idcflare.com 使用原来的 summary.json 逻辑 const summaryResponse = await fetch(`${BASE_URL}/u/${username}/summary.json`); if (summaryResponse.ok) { const data = await summaryResponse.json(); if (data.user_summary) { this.renderTrustLevel(data, username); return; } } throw new Error('无法获取等级数据'); } else if (CURRENT_DOMAIN === 'linux.do') { // linux.do: 完全使用 1.js 的逻辑(使用GM_xmlhttpRequest跨域请求) await this.fetchLinuxDoDataWithGM(username); } } catch (error) { console.error('加载信任等级失败:', error); this.trustLevelContainer.innerHTML = ` <div class="trust-level-header"> 📊 信任等级 <button class="trust-level-refresh" onclick="window.browseController.loadUserTrustLevel(true)">🔄 刷新</button> </div> <div class="trust-level-loading">加载失败,请点击刷新重试</div> `; } finally { // 恢复刷新按钮状态 if (isManualRefresh) { setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.textContent = '🔄 刷新'; refreshBtn.disabled = false; } }, 1000); } } } // 使用 GM_xmlhttpRequest 获取 linux.do 数据(完全按照1.js的逻辑) async fetchLinuxDoDataWithGM(username) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://connect.linux.do/", timeout: 15000, onload: (response) => { if (response.status === 200) { const responseText = response.responseText; const tempDiv = document.createElement('div'); tempDiv.innerHTML = responseText; // 1. 解析全局用户名和当前等级 (从 <h1>) let globalUsername = username; let currentLevel = '未知'; const h1 = tempDiv.querySelector('h1'); if (h1) { const h1Text = h1.textContent.trim(); // 例如: "你好,一剑万生 (YY_WD) 2级用户" 或 "你好, (yy2025) 0级用户" const welcomeMatch = h1Text.match(/你好,\s*([^(\s]*)\s*\(?([^)]*)\)?\s*(\d+)级用户/i); if (welcomeMatch) { // 优先使用括号内的用户名,如果没有则使用前面的 globalUsername = welcomeMatch[2] || welcomeMatch[1] || username; currentLevel = welcomeMatch[3]; console.log(`从<h1>解析: 用户名='${globalUsername}', 当前等级='${currentLevel}'`); } } // 检查用户等级,决定使用哪种数据获取方式 const userLevel = parseInt(currentLevel); if (userLevel === 0 || userLevel === 1) { console.log(`检测到${userLevel}级用户,使用summary.json获取数据`); this.fetchLowLevelUserData(username, userLevel).then(resolve).catch(reject); } else if (userLevel >= 2) { console.log(`检测到${userLevel}级用户,使用connect.linux.do页面数据`); this.processHighLevelUserData(tempDiv, globalUsername, currentLevel); resolve(); } else { reject(new Error('无法确定用户等级')); } } else { reject(new Error(`请求失败,状态码: ${response.status}`)); } }, onerror: (error) => { console.error('GM_xmlhttpRequest 错误:', error); reject(new Error('网络请求错误')); }, ontimeout: () => { console.error('GM_xmlhttpRequest 超时'); reject(new Error('请求超时')); } }); }); } // 处理0级和1级用户数据 async fetchLowLevelUserData(username, currentLevel) { const summaryResponse = await fetch(`${BASE_URL}/u/${username}/summary.json`); if (summaryResponse.ok) { const data = await summaryResponse.json(); const userSummary = data.user_summary; this.renderTrustLevelNew(username, currentLevel, userSummary); } else { throw new Error('无法获取用户summary数据'); } } // 处理2级及以上用户数据 processHighLevelUserData(tempDiv, globalUsername, currentLevel) { let targetInfoDiv = null; const potentialDivs = tempDiv.querySelectorAll('div.bg-white.p-6.rounded-lg'); for (let i = 0; i < potentialDivs.length; i++) { const div = potentialDivs[i]; const h2 = div.querySelector('h2'); if (h2 && h2.textContent.includes('信任级别')) { targetInfoDiv = div; break; } } if (!targetInfoDiv) { throw new Error('未找到信任级别数据块'); } // 解析标题获取目标等级 const h2 = targetInfoDiv.querySelector('h2'); const titleMatch = h2.textContent.match(/信任级别\s*(\d+)\s*的要求/); const targetLevel = titleMatch ? titleMatch[1] : '未知'; // 解析表格数据 const tableRows = targetInfoDiv.querySelectorAll('table tbody tr'); const requirements = []; tableRows.forEach((row, index) => { if (index === 0) return; // 跳过表头 const cells = row.querySelectorAll('td'); if (cells.length >= 3) { const name = cells[0].textContent.trim(); const current = cells[1].textContent.trim(); const required = cells[2].textContent.trim(); const isMet = cells[1].classList.contains('text-green-500'); requirements.push({ name, current, required, isMet }); } }); // 渲染高级等级信息 this.renderAdvancedTrustLevel(globalUsername, targetLevel, requirements); } // 新的渲染方法(基于1.js的逻辑,用于0级和1级用户) renderTrustLevelNew(username, currentLevel, userSummary) { const targetLevel = currentLevel + 1; const requirements = CONFIG.levelRequirements[currentLevel]; if (!requirements) { this.trustLevelContainer.innerHTML = '<div class="trust-level-loading">无配置数据</div>'; return; } const trustLevelDetails = { items: [], achievedCount: 0, totalCount: 0 }; // 检查各项要求 Object.entries(requirements).forEach(([key, requiredValue]) => { let currentValue = 0; let label = ''; let isMet = false; switch (key) { case 'topics_entered': currentValue = userSummary.topics_entered || 0; label = '浏览的话题'; isMet = currentValue >= requiredValue; break; case 'posts_read_count': currentValue = userSummary.posts_read_count || 0; label = '已读帖子'; isMet = currentValue >= requiredValue; break; case 'time_read': currentValue = Math.floor((userSummary.time_read || 0) / 60); label = '阅读时长(分)'; isMet = (userSummary.time_read || 0) >= requiredValue; requiredValue = Math.floor(requiredValue / 60); break; case 'days_visited': currentValue = userSummary.days_visited || 0; label = '访问天数'; isMet = currentValue >= requiredValue; break; case 'likes_given': currentValue = userSummary.likes_given || 0; label = '给出的赞'; isMet = currentValue >= requiredValue; break; case 'likes_received': currentValue = userSummary.likes_received || 0; label = '收到的赞'; isMet = currentValue >= requiredValue; break; case 'post_count': currentValue = userSummary.post_count || 0; label = '帖子数量'; isMet = currentValue >= requiredValue; break; } if (label) { trustLevelDetails.items.push({ name: label, current: currentValue, required: requiredValue, isMet: isMet }); if (isMet) { trustLevelDetails.achievedCount++; } trustLevelDetails.totalCount++; } }); const achievedCount = trustLevelDetails.achievedCount; const totalCount = trustLevelDetails.totalCount; const allMet = achievedCount === totalCount; const levelNames = { 0: 'Lv0 → Lv1', 1: 'Lv1 → Lv2' }; let html = ` <div class="trust-level-header"> <span>📊 ${levelNames[currentLevel] || `Lv${currentLevel} → Lv${targetLevel}`} (${username})</span> <button class="trust-level-refresh" data-action="refresh">🔄 刷新</button> </div> `; trustLevelDetails.items.forEach(req => { const progress = Math.min((req.current / req.required) * 100, 100); const isCompleted = req.isMet; const fillClass = isCompleted ? 'completed' : ''; html += ` <div class="trust-level-item"> <span class="trust-level-name">${req.name}</span> <div class="trust-level-progress"> <div class="trust-level-bar"> <div class="trust-level-bar-fill ${fillClass}" style="width: ${progress}%"></div> </div> <span class="trust-level-value">${req.current}/${req.required}</span> </div> </div> `; }); if (allMet) { html += ` <div style="background: rgba(255, 255, 255, 0.25); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: #fff; font-size: 11px; font-weight: 600; text-align: center;"> ✅ 已满足 Lv${targetLevel} 要求 </div> </div> `; } else { const unmetCount = totalCount - achievedCount; html += ` <div style="background: rgba(255, 255, 255, 0.15); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: rgba(255, 255, 255, 0.9); font-size: 11px; font-weight: 500; text-align: center;"> 还需完成 ${unmetCount} 项升级到 Lv${targetLevel} </div> </div> `; } this.trustLevelContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadUserTrustLevel(true)); } }, 100); } // 从 connect.linux.do 加载等级信息(适用于TL2+) async loadTrustLevelFromConnect(username) { try { const response = await fetch('https://connect.linux.do/'); if (!response.ok) { throw new Error('无法访问 connect.linux.do'); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 查找包含"信任级别"的区块 const trustLevelSection = Array.from(doc.querySelectorAll('div.bg-white.p-6.rounded-lg')).find(div => { const h2 = div.querySelector('h2'); return h2 && h2.textContent.includes('信任级别'); }); if (!trustLevelSection) { throw new Error('未找到信任级别数据'); } // 解析标题获取目标等级 const h2 = trustLevelSection.querySelector('h2'); const titleMatch = h2.textContent.match(/信任级别\s*(\d+)\s*的要求/); const targetLevel = titleMatch ? titleMatch[1] : '未知'; // 解析表格数据 const tableRows = trustLevelSection.querySelectorAll('table tbody tr'); const requirements = []; tableRows.forEach((row, index) => { if (index === 0) return; // 跳过表头 const cells = row.querySelectorAll('td'); if (cells.length >= 3) { const name = cells[0].textContent.trim(); const current = cells[1].textContent.trim(); const required = cells[2].textContent.trim(); const isMet = cells[1].classList.contains('text-green-500'); requirements.push({ name, current, required, isMet }); } }); // 渲染高级等级信息 this.renderAdvancedTrustLevel(username, targetLevel, requirements); } catch (error) { console.error('从 connect.linux.do 加载失败:', error); throw error; } } // 渲染信任等级信息(支持 TL0->TL1 和 TL1->TL2 - 基于 summary.json) renderTrustLevel(data, username) { const summary = data.user_summary; if (!summary) { this.trustLevelContainer.innerHTML = '<div class="trust-level-loading">无数据</div>'; return; } // 获取当前信任等级 // 优先从 user_summary 中获取,如果没有则从外层获取 const currentLevel = summary.trust_level !== undefined ? summary.trust_level : (data.user && data.user.trust_level !== undefined ? data.user.trust_level : 1); const targetLevel = currentLevel + 1; // 根据当前等级获取对应的升级要求 const levelConfig = CONFIG.levelRequirements[currentLevel]; if (!levelConfig) { // 如果没有配置(比如已经是最高等级),使用原来的逻辑 this.renderDefaultTrustLevel(summary, username); return; } const requirements = []; // 根据配置动态构建要求列表 Object.entries(levelConfig).forEach(([key, requiredValue]) => { let currentValue = 0; let label = ''; switch (key) { case 'topics_entered': currentValue = summary.topics_entered || 0; label = '浏览的话题'; break; case 'posts_read_count': currentValue = summary.posts_read_count || 0; label = '已读帖子'; break; case 'time_read': currentValue = Math.floor((summary.time_read || 0) / 60); label = '阅读时长(分)'; requiredValue = Math.floor(requiredValue / 60); break; case 'days_visited': currentValue = summary.days_visited || 0; label = '访问天数'; break; case 'likes_given': currentValue = summary.likes_given || 0; label = '给出的赞'; break; case 'likes_received': currentValue = summary.likes_received || 0; label = '收到的赞'; break; case 'post_count': currentValue = summary.post_count || 0; label = '帖子数量'; break; } if (label) { requirements.push({ name: label, current: currentValue, required: requiredValue }); } }); // 计算达标数量 const achievedCount = requirements.filter(req => req.current >= req.required).length; const totalCount = requirements.length; const allMet = achievedCount === totalCount; const levelNames = { 0: 'Lv0 → Lv1', 1: 'Lv1 → Lv2', 2: 'Lv2 → Lv3', 3: 'Lv3 → Lv4' }; let html = ` <div class="trust-level-header"> <span>📊 ${levelNames[currentLevel] || `Lv${currentLevel} → Lv${targetLevel}`} (${username})</span> <button class="trust-level-refresh" data-action="refresh">🔄 刷新</button> </div> `; requirements.forEach(req => { const progress = Math.min((req.current / req.required) * 100, 100); const isCompleted = req.current >= req.required; const fillClass = isCompleted ? 'completed' : ''; html += ` <div class="trust-level-item"> <span class="trust-level-name">${req.name}</span> <div class="trust-level-progress"> <div class="trust-level-bar"> <div class="trust-level-bar-fill ${fillClass}" style="width: ${progress}%"></div> </div> <span class="trust-level-value">${req.current}/${req.required}</span> </div> </div> `; }); // 在数据下方添加总结信息 if (allMet) { html += ` <div style="background: rgba(255, 255, 255, 0.25); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: #fff; font-size: 11px; font-weight: 600; text-align: center;"> ✅ 已满足 Lv${targetLevel} 要求 </div> </div> `; } else { const unmetCount = totalCount - achievedCount; html += ` <div style="background: rgba(255, 255, 255, 0.15); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: rgba(255, 255, 255, 0.9); font-size: 11px; font-weight: 500; text-align: center;"> 还需完成 ${unmetCount} 项升级到 Lv${targetLevel} </div> </div> `; } this.trustLevelContainer.innerHTML = html; // 添加刷新按钮事件监听 setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadUserTrustLevel(true)); } }, 100); } // 默认渲染方法(用于没有配置的等级) renderDefaultTrustLevel(summary, username) { const requirements = [ { name: '访问天数', current: summary.days_visited, required: 15 }, { name: '给出的赞', current: summary.likes_given, required: 1 }, { name: '收到的赞', current: summary.likes_received, required: 1 }, { name: '帖子数量', current: summary.post_count, required: 3 }, { name: '进入主题', current: summary.topics_entered, required: 20 }, { name: '阅读帖子', current: summary.posts_read_count, required: 100 }, { name: '阅读时长(分)', current: Math.floor(summary.time_read / 60), required: 60 } ]; // 计算达标数量 const achievedCount = requirements.filter(req => req.current >= req.required).length; const totalCount = requirements.length; const allMet = achievedCount === totalCount; let html = ` <div class="trust-level-header"> <span>📊 等级 (L2+) (${username || ''})</span> <button class="trust-level-refresh" data-action="refresh">🔄 刷新</button> </div> `; // 添加总结信息 if (allMet) { html += ` <div style="background: rgba(16, 185, 129, 0.2); padding: 6px 8px; border-radius: 6px; margin: 6px 0;"> <div style="color: #10b981; font-size: 11px; font-weight: 600; text-align: center;"> 🎉 所有要求已达标! </div> </div> `; } else { const unmetCount = totalCount - achievedCount; html += ` <div style="background: rgba(251, 146, 60, 0.2); padding: 6px 8px; border-radius: 6px; margin: 6px 0;"> <div style="color: #ea580c; font-size: 11px; font-weight: 600; text-align: center;"> 还需完成 ${unmetCount} 项要求 </div> </div> `; } requirements.forEach(req => { const progress = Math.min((req.current / req.required) * 100, 100); const isCompleted = req.current >= req.required; const fillClass = isCompleted ? 'completed' : ''; html += ` <div class="trust-level-item"> <span class="trust-level-name">${req.name}</span> <div class="trust-level-progress"> <div class="trust-level-bar"> <div class="trust-level-bar-fill ${fillClass}" style="width: ${progress}%"></div> </div> <span class="trust-level-value">${req.current}/${req.required}</span> </div> </div> `; }); this.trustLevelContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadUserTrustLevel(true)); } }, 100); } // 渲染高级信任等级信息(从 connect.linux.do 获取的TL2+数据) renderAdvancedTrustLevel(username, targetLevel, requirements) { const achievedCount = requirements.filter(r => r.isMet).length; const totalCount = requirements.length; // 计算当前等级 const currentLevel = parseInt(targetLevel) - 1; // 等级名称映射(简化显示) const levelNames = { 2: 'Lv1 → Lv2', 3: 'Lv2 → Lv3', 4: 'Lv3 → Lv4' }; let html = ` <div class="trust-level-header"> <span>📊 ${levelNames[targetLevel] || `Lv${currentLevel} → Lv${targetLevel}`} (${username})</span> <button class="trust-level-refresh" data-action="refresh">🔄 刷新</button> </div> `; requirements.forEach(req => { // 尝试从文本中提取数字 const currentMatch = req.current.match(/(\d+)/); const requiredMatch = req.required.match(/(\d+)/); const currentNum = currentMatch ? parseInt(currentMatch[1]) : 0; const requiredNum = requiredMatch ? parseInt(requiredMatch[1]) : 1; const progress = Math.min((currentNum / requiredNum) * 100, 100); const isCompleted = req.isMet; const fillClass = isCompleted ? 'completed' : ''; // 简化标签名称 let simpleName = req.name .replace('已读帖子(所有时间)', '已读帖子') .replace('浏览的话题(所有时间)', '浏览话题') .replace('获赞:点赞用户数量', '点赞用户') .replace('被禁言(过去 6 个月)', '被禁言') .replace('被封禁(过去 6 个月)', '被封禁') .replace('访问次数(过去', '访问次数(') .replace('个月)', '月)') .replace('回复次数(最近', '回复(近') .replace('天内)', '天)'); html += ` <div class="trust-level-item"> <span class="trust-level-name">${simpleName}</span> <div class="trust-level-progress"> <div class="trust-level-bar"> <div class="trust-level-bar-fill ${fillClass}" style="width: ${progress}%"></div> </div> <span class="trust-level-value">${req.current}/${req.required}</span> </div> </div> `; }); // 在数据下方添加总结信息 if (achievedCount === totalCount) { html += ` <div style="background: rgba(255, 255, 255, 0.25); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: #fff; font-size: 11px; font-weight: 600; text-align: center;"> ✅ 已满足 Lv${targetLevel} 要求 </div> </div> `; } else { const unmetCount = totalCount - achievedCount; html += ` <div style="background: rgba(255, 255, 255, 0.15); padding: 6px 8px; border-radius: 6px; margin: 6px 0 0 0;"> <div style="color: rgba(255, 255, 255, 0.9); font-size: 11px; font-weight: 500; text-align: center;"> 还需完成 ${unmetCount} 项升级到 Lv${targetLevel} </div> </div> `; } this.trustLevelContainer.innerHTML = html; // 添加刷新按钮事件监听 setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector('.trust-level-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadUserTrustLevel(true)); } }, 100); } // 加载用户阅读历史 async loadUserReadHistory() { const username = await this.getCurrentUsername(); if (!username) { console.log('未获取到用户名,无法加载阅读历史'); this.readTopics = []; return; } // 从 localStorage 加载该用户的阅读历史 const storageKey = `readTopics_${username}`; this.readTopics = Storage.get(storageKey, []); console.log(`已加载用户 ${username} 的阅读历史,共 ${this.readTopics.length} 篇帖子`); } // 保存用户阅读历史 async saveUserReadHistory(topicId) { const username = await this.getCurrentUsername(); if (!username) { console.log('未获取到用户名,无法保存阅读历史'); return; } // 添加到已读列表(避免重复) if (!this.readTopics.includes(topicId)) { this.readTopics.push(topicId); // 限制列表大小(最多保存1000篇) if (this.readTopics.length > 1000) { this.readTopics = this.readTopics.slice(-1000); } // 保存到 localStorage const storageKey = `readTopics_${username}`; Storage.set(storageKey, this.readTopics); console.log(`已保存帖子 ${topicId} 到用户 ${username} 的阅读历史`); } } // 检查帖子是否已读 isTopicRead(topicId) { return this.readTopics.includes(topicId); } // 启动账号切换监控 startUserSwitchMonitoring() { // 初始化当前用户 this.getCurrentUsername().then(username => { this.lastDetectedUser = username; }); // 每5秒检查一次是否切换账号 setInterval(async () => { const currentDetectedUser = await this.getCurrentUsername(); if (currentDetectedUser && this.lastDetectedUser && currentDetectedUser !== this.lastDetectedUser) { console.log(`检测到账号切换: ${this.lastDetectedUser} -> ${currentDetectedUser}`); this.lastDetectedUser = currentDetectedUser; this.currentUsername = currentDetectedUser; // 延迟一点时间再刷新,确保页面稳定 setTimeout(() => { console.log('账号切换后重新加载等级信息'); this.loadUserTrustLevel(true); }, 1000); } else if (currentDetectedUser) { this.lastDetectedUser = currentDetectedUser; } }, 5000); } toggleCleanMode() { const sidebarToggle = document.querySelector('button.btn-sidebar-toggle'); if (sidebarToggle && this.cleanModeEnabled) { if (sidebarToggle.getAttribute('aria-expanded') === 'true') { console.log('清爽模式启用,收起边栏'); sidebarToggle.click(); } } this.applyCleanModeStyles(); } applyCleanModeStyles() { let styleElement = document.getElementById('clean-mode-styles'); if (styleElement) { styleElement.remove(); } if (this.cleanModeEnabled) { styleElement = document.createElement('style'); styleElement.id = 'clean-mode-styles'; styleElement.textContent = ` p:contains("希望你喜欢这里。有问题,请提问,或搜索现有帖子。") { display: none !important; } div#global-notice-alert-global-notice.alert.alert-info.alert-global-notice { display: none !important; } a[href="https://linux.do/t/topic/482293"] { display: none !important; } div.link-bottom-line a.badge-category__wrapper { display: none !important; } td.posters.topic-list-data { display: none !important; } a.discourse-tag.box[href^="/tag/"] { display: none !important; } `; document.head.appendChild(styleElement); } } toggleGrayscaleMode() { this.applyGrayscaleModeStyles(); } applyGrayscaleModeStyles() { let styleElement = document.getElementById('grayscale-mode-styles'); if (styleElement) { styleElement.remove(); } if (this.grayscaleModeEnabled) { // 检测设备类型 const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); const isMobile = isAndroid || isIOS; const isLowEnd = navigator.hardwareConcurrency <= 4 || navigator.deviceMemory <= 4; styleElement = document.createElement('style'); styleElement.id = 'grayscale-mode-styles'; styleElement.textContent = ` /* * 黑白灰模式 - 智能高对比度版 * 作者: idear * 协议: CC BY-NC-SA 4.0 */ /* ==================== 浅色背景优化 ==================== */ @media (prefers-color-scheme: light) { /* 只对主要内容容器应用滤镜,不影响 fixed 定位元素 */ #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(108%) brightness(97%) !important; -webkit-filter: grayscale(100%) contrast(108%) brightness(97%) !important; } #main-outlet *, .d-header *, .menu-panel *, main * { text-shadow: 0 0 0.3px rgba(0, 0, 0, 0.4) !important; } } /* ==================== 深色背景优化 ==================== */ @media (prefers-color-scheme: dark) { /* 只对主要内容容器应用滤镜,不影响 fixed 定位元素 */ #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(110%) brightness(103%) !important; -webkit-filter: grayscale(100%) contrast(110%) brightness(103%) !important; } #main-outlet *, .d-header *, .menu-panel *, main * { text-shadow: 0 0 0.3px rgba(255, 255, 255, 0.5) !important; } } /* ==================== 兜底方案(无主题偏好) ==================== */ @media (prefers-color-scheme: no-preference) { #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(109%) brightness(99%) !important; -webkit-filter: grayscale(100%) contrast(109%) brightness(99%) !important; } } /* ==================== 图片对比度增强 ==================== */ img, svg, canvas, video { filter: grayscale(100%) contrast(110%) !important; -webkit-filter: grayscale(100%) contrast(110%) !important; } ${isMobile ? ` /* ==================== 移动端优化 ==================== */ html { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; text-rendering: optimizeLegibility !important; } * { -webkit-overflow-scrolling: touch !important; } ` : ''} ${isIOS ? ` /* ==================== iOS Safari 特殊优化 ==================== */ body { -webkit-transform: translateZ(0) !important; } ` : ''} ${isLowEnd ? ` /* ==================== 低端设备优化 ==================== */ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } ` : ''} `; document.head.appendChild(styleElement); // GPU 资源释放 setTimeout(() => { if (document.documentElement) { const currentWillChange = document.documentElement.style.willChange; if (currentWillChange === 'filter') { document.documentElement.style.willChange = 'auto'; } } }, 1000); // 性能日志 const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const isLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; const theme = isDark ? '深色' : (isLight ? '浅色' : '未知'); console.log('🎨 黑白灰模式已启用'); console.log(`📱 设备类型: ${isMobile ? (isIOS ? 'iOS' : 'Android') : '桌面'}`); console.log(`🔧 优化模式: ${isLowEnd ? '低端设备' : '标准'}`); console.log(`🌓 检测主题: ${theme}模式`); console.log(`✨ 浅色背景: 对比108% + 亮度97%`); console.log(`✨ 深色背景: 对比110% + 亮度103%`); console.log(`🖼️ 图片对比度: 110%`); } else { console.log('🎨 黑白灰模式已关闭'); } } initOnlyOwnerView() { this.createToggleButton(); this.observePageChanges(); this.toggleVisibility(); } toggleVisibility() { const displayMode = localStorage.getItem("on_off") || "当前查看全部"; const userId = document.getElementById("post_1")?.getAttribute('data-user-id'); if (userId) { document.querySelectorAll('article').forEach(article => { article.style.display = (displayMode === "当前只看楼主" && article.dataset.userId !== userId) ? 'none' : ''; }); } } createToggleButton() { if (document.getElementById("toggleVisibilityBtn")) { return; } const btn = document.createElement("button"); btn.id = "toggleVisibilityBtn"; btn.textContent = localStorage.getItem("on_off") || "当前查看全部"; btn.onclick = () => { const newText = btn.textContent === '当前查看全部' ? '当前只看楼主' : '当前查看全部'; document.getElementsByClassName("start-date")[0]?.click(); btn.textContent = newText; localStorage.setItem("on_off", newText); this.toggleVisibility(); }; btn.style.backgroundColor = "#333"; btn.style.color = "#FFF"; btn.style.border = "none"; btn.style.padding = "8px 16px"; btn.style.marginLeft = "10px"; btn.style.borderRadius = "5px"; btn.style.cursor = "pointer"; const saveButton = document.querySelector('.save-to-local-btn'); if (saveButton) { saveButton.parentElement.appendChild(btn); } else { const firstPostContent = document.querySelector('.boxed.onscreen-post[data-post-id] .cooked'); if (firstPostContent) { firstPostContent.appendChild(btn); } } } observePageChanges() { const observer = new MutationObserver(() => { if (document.querySelector(".timeline-footer-controls") && !document.getElementById("toggleVisibilityBtn")) { this.createToggleButton(); } this.toggleVisibility(); }); observer.observe(document.body, { childList: true, subtree: true }); } initFloorNumberDisplay() { this.addFloorNumbers(); this.initMutationObserver(); this.setupRandomJumpButton(); this.monitorURLChangeAndUpdateButton(); } addFloorNumbers() { document.querySelectorAll('.boxed.onscreen-post').forEach((post) => { if (!post.querySelector('.floor-number')) { const floorNumber = document.createElement('div'); floorNumber.className = 'floor-number'; floorNumber.textContent = '楼层: ' + post.id.split("_")[1]; floorNumber.style.cssText = 'color: grey; margin-left: 10px;'; post.querySelector('.topic-meta-data').appendChild(floorNumber); } }); this.setupSaveButton(); } initMutationObserver() { const observer = new MutationObserver(() => { this.addFloorNumbers(); this.setupSaveButton(); this.toggleCleanMode(); }); observer.observe(document.body, { childList: true, subtree: true }); } randomJump() { fetch(window.location.href + '.json') .then(response => response.json()) .then(data => { if (data && data.posts_count) { const postId = 1 + Math.floor(Math.random() * data.posts_count); const currentUrl = new URL(window.location.href); const list1 = currentUrl.pathname.split("/"); if (list1[list1.length - 2] === "topic") { list1.push(postId); } else if (list1[list1.length - 3] === "topic") { list1[list1.length - 1] = postId; } const newUrl = list1.join("/"); window.location.href = newUrl; alert('恭喜楼层【' + postId + '】的用户被抽中!'); } }) .catch(error => console.error('Error:', error)); } setupRandomJumpButton() { // 随机按钮已集成到主面板中,不需要单独创建 } setupSaveButton() { const firstPost = document.querySelector('.boxed.onscreen-post[data-post-id]'); if (firstPost && firstPost.id.includes('post_1')) { if (!firstPost.querySelector('.save-to-local-btn')) { const saveButton = document.createElement('button'); saveButton.className = 'save-to-local-btn'; saveButton.textContent = '💾 保存到本地'; Object.assign(saveButton.style, { padding: '10px 20px', fontSize: '15px', fontWeight: '600', backgroundColor: '#ff9800', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', marginTop: '10px', boxShadow: '0 4px 12px rgba(255, 152, 0, 0.3)', transition: 'all 0.3s' }); saveButton.addEventListener('mouseover', () => { saveButton.style.transform = 'translateY(-2px)'; saveButton.style.boxShadow = '0 6px 20px rgba(255, 152, 0, 0.4)'; }); saveButton.addEventListener('mouseout', () => { saveButton.style.transform = 'translateY(0)'; saveButton.style.boxShadow = '0 4px 12px rgba(255, 152, 0, 0.3)'; }); saveButton.addEventListener('click', () => this.savePostToLocal(firstPost)); const postContent = firstPost.querySelector('.cooked'); if (postContent) { postContent.appendChild(saveButton); } } } } async savePostToLocal(postElement) { try { const topicTitle = document.querySelector('.fancy-title')?.textContent.trim() || 'Untitled_Topic'; const postContent = postElement.querySelector('.cooked'); if (!postContent) { alert('无法获取帖子内容!'); return; } const contentClone = postContent.cloneNode(true); contentClone.querySelector('.save-to-local-btn')?.remove(); const images = contentClone.querySelectorAll('img'); for (const img of images) { try { const response = await fetch(img.src); const blob = await response.blob(); const reader = new FileReader(); await new Promise((resolve) => { reader.onload = resolve; reader.readAsDataURL(blob); }); img.src = reader.result; } catch (error) { console.error('图片加载失败:', img.src, error); img.alt = '[图片加载失败]'; } } const htmlContent = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${topicTitle}</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .post-content { max-width: 800px; margin: 0 auto; } img { max-width: 100%; height: auto; } </style> </head> <body> <div class="post-content"> <h1>${topicTitle}</h1> ${contentClone.innerHTML} </div> </body> </html> `; const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; const fileName = topicTitle .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, '_') + '.html'; link.download = fileName; link.click(); URL.revokeObjectURL(url); alert('帖子内容已保存到本地!'); } catch (error) { console.error('保存帖子失败:', error); alert('保存失败,请查看控制台错误信息。'); } } monitorURLChangeAndUpdateButton() { let lastURL = location.href; // 初始检查一次 this.updateButtonVisibility(); setInterval(() => { const currentURL = location.href; if (currentURL !== lastURL) { lastURL = currentURL; this.isTopicPage = location.pathname.includes('/t/topic/'); this.updateButtonVisibility(); this.toggleCleanMode(); if (this.autoLikeEnabled && currentURL.includes('/t/topic/')) { this.autoLikeTopic(); } } }, 1000); } updateButtonVisibility() { const isTopicPage = location.pathname.includes('/t/topic/'); // 整个工具功能区:只在文章页显示 if (this.toolSectionContainer) { this.toolSectionContainer.style.display = isTopicPage ? 'block' : 'none'; } // 文章页功能区上方的分隔线:只在文章页显示 if (this.divider2) { this.divider2.style.display = isTopicPage ? 'block' : 'none'; } // 文章页功能区下方的分隔线:只在文章页显示 if (this.divider3) { this.divider3.style.display = isTopicPage ? 'block' : 'none'; } console.log(`页面类型: ${isTopicPage ? '文章页' : '非文章页'},文章页功能区${isTopicPage ? '显示' : '隐藏'}`); } async handleRevealUsersClick() { if (this.userInfoHelper.revealInProgress) return; // 更新按钮状态 this.revealUsersBtn.disabled = true; this.revealUsersBtn.innerHTML = '<span class="btn-icon">⏳</span><span class="btn-text">加载中...</span>'; try { await this.userInfoHelper.revealAllVisibleReplies(); this.revealUsersBtn.innerHTML = '<span class="btn-icon">✅</span><span class="btn-text">加载完成</span>'; // 2秒后恢复按钮 setTimeout(() => { this.revealUsersBtn.disabled = false; this.revealUsersBtn.innerHTML = '<span class="btn-icon">📊</span><span class="btn-text">批量展示信息</span>'; }, 2000); } catch (error) { console.error('展示用户信息失败:', error); this.revealUsersBtn.disabled = false; this.revealUsersBtn.innerHTML = '<span class="btn-icon">❌</span><span class="btn-text">加载失败</span>'; setTimeout(() => { this.revealUsersBtn.innerHTML = '<span class="btn-icon">📊</span><span class="btn-text">批量展示信息</span>'; }, 2000); } } handleButtonClick() { if (this.isScrolling || this.autoRunning) { // 停止自动阅读 this.stopScrolling(); this.stopNavigationGuard(); this.autoRunning = false; this.setSessionStorage('autoRunning', false); this.button.innerHTML = '<span class="btn-icon">▶</span><span class="btn-text">开始阅读</span>'; this.button.classList.remove('running'); // 清理所有定时器 if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } // 停止阅读时,折叠自动阅读区 if (this.autoSectionContent) { const autoSection = this.container.querySelector('.section-collapsible'); if (autoSection && !autoSection.classList.contains('collapsed')) { autoSection.classList.add('collapsed'); this.autoSectionContent.classList.add('collapsed'); } } } else { // 开启自动阅读 this.autoRunning = true; this.setSessionStorage('autoRunning', true); this.button.innerHTML = '<span class="btn-icon">⏸</span><span class="btn-text">停止阅读</span>'; this.button.classList.add('running'); // 启动导航守护 this.startNavigationGuard(); // 开始阅读时,展开自动阅读区 if (this.autoSectionContent) { const autoSection = this.container.querySelector('.section-collapsible'); if (autoSection && autoSection.classList.contains('collapsed')) { autoSection.classList.remove('collapsed'); this.autoSectionContent.classList.remove('collapsed'); } } if (!this.firstUseChecked) { this.handleFirstUse(); } else if (this.isTopicPage) { this.startScrolling(); if (this.autoLikeEnabled) { this.autoLikeTopic(); } } else { this.getLatestTopics().then(() => this.navigateNextTopic()); } } } async autoLikeTopic() { if (!this.autoLikeEnabled) return; // 检查是否在冷却期 if (this.likeResumeTime && Date.now() < this.likeResumeTime) { console.log("点赞功能冷却中,跳过点赞"); return; } const match = window.location.pathname.match(/\/t\/topic\/(\d+)/); if (!match) { console.log("无法获取当前主题ID"); return; } const topicId = match[1]; if (this.likedTopics.includes(topicId)) { console.log(`主题 ${topicId} 已经点赞过,跳过点赞操作`); return; } console.log("正在检查是否需要自动点赞主题..."); await Utils.sleep(2000); const likeButton = document.querySelector('div.discourse-reactions-reaction-button button.btn-toggle-reaction-like'); if (likeButton && !likeButton.classList.contains('has-like') && !likeButton.classList.contains('liked')) { likeButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); await Utils.sleep(1000); console.log("找到主题点赞按钮,执行点击操作"); likeButton.click(); this.likedTopics.push(topicId); Storage.set('likedTopics', this.likedTopics); console.log(`已记录点赞主题 ${topicId}`); } else { console.log("未找到可点赞的主题按钮或已点赞"); if (likeButton && (likeButton.classList.contains('has-like') || likeButton.classList.contains('liked'))) { if (!this.likedTopics.includes(topicId)) { this.likedTopics.push(topicId); Storage.set('likedTopics', this.likedTopics); console.log(`主题 ${topicId} 已点赞,记录到列表`); } } } } async handleFirstUse() { if (!this.autoRunning) return; // 只在 linux.do 域名下执行新手教程 if (CURRENT_DOMAIN !== 'linux.do') { console.log('非 linux.do 域名,跳过新手教程'); Storage.set('firstUseChecked', true); this.firstUseChecked = true; await this.getLatestTopics(); await this.navigateNextTopic(); return; } if (!this.selectedPost) { const randomIndex = Math.floor(Math.random() * CONFIG.mustRead.posts.length); this.selectedPost = CONFIG.mustRead.posts[randomIndex]; Storage.set('selectedPost', this.selectedPost); console.log(`随机选择文章: ${this.selectedPost.url}`); window.location.href = this.selectedPost.url; return; } const currentUrl = window.location.href; if (currentUrl.includes(this.selectedPost.url)) { console.log(`当前在选中的文章页面,已点赞数: ${this.likesCount}`); while (this.likesCount < CONFIG.mustRead.likesNeeded && this.autoRunning) { await this.likeRandomComment(); if (this.likesCount >= CONFIG.mustRead.likesNeeded) { console.log('完成所需点赞数量,开始正常浏览'); Storage.set('firstUseChecked', true); this.firstUseChecked = true; await this.getLatestTopics(); await this.navigateNextTopic(); break; } await Utils.sleep(1000); } } else { window.location.href = this.selectedPost.url; } } async likeRandomComment() { if (!this.autoRunning) return false; // 检查是否在冷却期 if (this.likeResumeTime && Date.now() < this.likeResumeTime) { console.log("点赞功能冷却中,跳过点赞"); return false; } const likeButtons = Array.from(document.querySelectorAll('.like-button, .like-count, [data-like-button], .discourse-reactions-reaction-button')) .filter(button => button && button.offsetParent !== null && !button.classList.contains('has-like') && !button.classList.contains('liked') ); if (likeButtons.length > 0) { const randomButton = likeButtons[Math.floor(Math.random() * likeButtons.length)]; randomButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); await Utils.sleep(1000); if (!this.autoRunning) return false; console.log('找到可点赞的评论,准备点赞'); randomButton.click(); this.likesCount++; Storage.set('likesCount', this.likesCount); await Utils.sleep(1000); return true; } window.scrollBy({ top: 500, behavior: 'smooth' }); await Utils.sleep(1000); console.log('当前位置没有找到可点赞的评论,继续往下找'); return false; } async getLatestTopics() { let page = 1; let topicList = []; let retryCount = 0; // 根据设置选择获取最新帖子还是未读帖子 const endpoint = this.readUnreadEnabled ? 'unread' : 'latest'; console.log(`正在获取${this.readUnreadEnabled ? '未读' : '最新'}帖子列表...`); while (topicList.length < CONFIG.article.topicListLimit && retryCount < CONFIG.article.retryLimit) { try { const response = await fetch(`${BASE_URL}/${endpoint}.json?no_definitions=true&page=${page}`); const data = await response.json(); if (data?.topic_list?.topics) { const filteredTopics = data.topic_list.topics.filter(topic => topic.posts_count < CONFIG.article.commentLimit ); topicList.push(...filteredTopics); page++; // 如果是未读帖子且没有更多了,直接退出 if (this.readUnreadEnabled && filteredTopics.length === 0) { break; } } else { break; } } catch (error) { console.error('获取文章列表失败:', error); retryCount++; await Utils.sleep(1000); } } if (topicList.length > CONFIG.article.topicListLimit) { topicList = topicList.slice(0, CONFIG.article.topicListLimit); } this.topicList = topicList; this.setSessionStorage('topicList', topicList); console.log(`已获取 ${topicList.length} 篇${this.readUnreadEnabled ? '未读' : '最新'}文章`); // 如果未读帖子为空,提示用户 if (this.readUnreadEnabled && topicList.length === 0) { this.showNotification('📭 没有未读帖子,将切换到最新帖子'); this.readUnreadEnabled = false; Storage.set('readUnreadEnabled', false); // 重新获取最新帖子 await this.getLatestTopics(); } } async getNextTopic() { if (this.topicList.length === 0) { await this.getLatestTopics(); } if (this.topicList.length > 0) { const topic = this.topicList.shift(); this.setSessionStorage('topicList', this.topicList); return topic; } return null; } async startScrolling() { if (this.isScrolling) return; this.isScrolling = true; this.button.innerHTML = '<span class="btn-icon">⏸</span><span class="btn-text">停止阅读</span>'; this.button.classList.add('running'); this.lastActionTime = Date.now(); // 记录页面开始滚动的时间,用于强制跳转 this.scrollStartTime = Date.now(); // 设置最大滚动时间(30秒),超过后强制跳转,避免卡在一个页面 const maxScrollTime = 30000; // 30秒 while (this.isScrolling) { const speed = Utils.random(CONFIG.scroll.minSpeed, CONFIG.scroll.maxSpeed); const distance = Utils.random(CONFIG.scroll.minDistance, CONFIG.scroll.maxDistance); const scrollStep = distance * 2.5; window.scrollBy({ top: scrollStep, behavior: 'smooth' }); // 检查是否到达底部 if (Utils.isNearBottom()) { await Utils.sleep(800); if (Utils.isNearBottom() && Utils.isPageLoaded()) { console.log("已到达页面底部,准备导航到下一篇文章..."); await Utils.sleep(1000); await this.navigateNextTopic(); break; } } // 强制跳转检查:如果在当前页面滚动超过最大时间,强制跳转到下一篇 const scrolledTime = Date.now() - this.scrollStartTime; if (scrolledTime > maxScrollTime) { console.log(`已在当前页面滚动${Math.floor(scrolledTime/1000)}秒,强制跳转到下一篇文章...`); await this.navigateNextTopic(); break; } await Utils.sleep(speed); this.accumulateTime(); if (Math.random() < CONFIG.scroll.fastScrollChance) { const fastScroll = Utils.random(CONFIG.scroll.fastScrollMin, CONFIG.scroll.fastScrollMax); window.scrollBy({ top: fastScroll, behavior: 'smooth' }); await Utils.sleep(200); } } } async waitForPageLoad() { let attempts = 0; const maxAttempts = 5; while (attempts < maxAttempts) { if (Utils.isPageLoaded()) { return true; } await Utils.sleep(300); attempts++; } return false; } stopScrolling() { this.isScrolling = false; clearInterval(this.scrollInterval); clearTimeout(this.pauseTimeout); this.button.innerHTML = '<span class="btn-icon">▶</span><span class="btn-text">开始阅读</span>'; this.button.classList.remove('running'); } accumulateTime() { const now = Date.now(); this.accumulatedTime += now - this.lastActionTime; this.setSessionStorage('accumulatedTime', this.accumulatedTime); this.lastActionTime = now; if (this.accumulatedTime >= CONFIG.time.browseTime) { this.accumulatedTime = 0; this.setSessionStorage('accumulatedTime', 0); this.pauseForRest(); } } async pauseForRest() { this.stopScrolling(); const restMinutes = Math.floor(CONFIG.time.restTime / 60000); console.log(`休息${restMinutes}分钟...`); // 显示休息开始通知 this.showNotification(`⏸️ 开始休息 ${restMinutes} 分钟`); await Utils.sleep(CONFIG.time.restTime); console.log("休息结束,继续浏览..."); // 显示休息结束通知 this.showNotification(`✅ 休息结束,继续浏览`); this.startScrolling(); } async navigateNextTopic() { const nextTopic = await this.getNextTopic(); if (nextTopic) { console.log("导航到新文章:", nextTopic.title); // 保存当前帖子为已读 const currentMatch = window.location.pathname.match(/\/t\/topic\/(\d+)/); if (currentMatch) { const currentTopicId = currentMatch[1]; await this.saveUserReadHistory(currentTopicId); } const url = nextTopic.last_read_post_number ? `${BASE_URL}/t/topic/${nextTopic.id}/${nextTopic.last_read_post_number}` : `${BASE_URL}/t/topic/${nextTopic.id}`; console.log("正在跳转到:", url); // 设置跳转超时保护 - 如果10秒内没有跳转成功,强制重新跳转 this.navigationTimeout = setTimeout(() => { console.warn("⚠️ 跳转超时,尝试重新跳转..."); if (window.location.href !== url) { window.location.href = url; } }, 10000); // 直接跳转 window.location.href = url; } else { console.log("没有更多文章,返回首页"); window.location.href = `${BASE_URL}/latest`; } } resetFirstUse() { Storage.set('firstUseChecked', false); Storage.set('likesCount', 0); Storage.set('selectedPost', null); this.firstUseChecked = false; this.likesCount = 0; this.selectedPost = null; console.log('已重置首次使用状态'); } } // 初始化 (function() { window.browseController = new BrowseController(); })();