// ==UserScript== // @name LINUXDO FlowReader // @namespace linux.do_FlowReader // @match https://linux.do/t/topic/* // @icon https://linux.do/favicon.ico // @grant GM_setValue // @grant GM_getValue // @version 2.0 // @author Neuroplexus // ==/UserScript== // 注入样式 const style = document.createElement('style'); style.textContent = ` .userscript-rb .rb-overlay { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; background: rgba(0, 0, 0, 0.85) !important; display: flex !important; justify-content: center !important; align-items: center !important; z-index: 9999 !important; opacity: 0 !important; transition: opacity 0.3s ease !important; } .userscript-rb .rb-container { background: var(--secondary) !important; color: var(--primary) !important; padding: 40px !important; width: 100% !important; height: 100% !important; max-width: 100% !important; max-height: 100% !important; overflow-y: auto !important; transform: translateY(20px) !important; opacity: 0 !important; transition: all 0.3s ease !important; display: flex !important; flex-direction: column !important; } .userscript-rb .rb-overlay.show { opacity: 1 !important; } .userscript-rb .rb-overlay.show .rb-container { transform: translateY(0) !important; opacity: 1 !important; } .userscript-rb .rb-header { display: flex !important; align-items: center !important; gap: 10px !important; margin-bottom: 40px !important; } .userscript-rb .rb-icon { width: 32px !important; height: 32px !important; color: var(--tertiary) !important; } .userscript-rb .rb-title { font-size: 24px !important; font-weight: bold !important; margin: 0 !important; } .userscript-rb .rb-content { margin-bottom: 40px !important; flex: 1 !important; } .userscript-rb .rb-settings { display: grid !important; gap: 20px !important; max-width: 600px !important; margin: 0 auto !important; } .userscript-rb .rb-setting-group { display: flex !important; align-items: center !important; gap: 20px !important; } .userscript-rb .rb-setting-group label { flex: 1 !important; font-size: 16px !important; } .userscript-rb .rb-setting-group input { width: 120px !important; padding: 8px 12px !important; border: 1px solid var(--primary-low) !important; border-radius: 4px !important; background: var(--secondary) !important; color: var(--primary) !important; font-size: 14px !important; } .userscript-rb .rb-buttons { display: flex !important; justify-content: flex-end !important; gap: 10px !important; margin-top: 40px !important; } .userscript-rb .rb-btn { padding: 12px 24px !important; border: 1px solid currentColor !important; background: transparent !important; color: inherit !important; border-radius: 4px !important; cursor: pointer !important; font-size: 16px !important; transition: all 0.3s ease !important; display: flex !important; align-items: center !important; gap: 8px !important; } .userscript-rb .rb-btn:hover { background: rgba(142, 142, 160, 0.1) !important; } .userscript-rb .rb-btn svg { width: 20px !important; height: 20px !important; } .userscript-rb .rb-status { position: fixed !important; bottom: 20px !important; right: 20px !important; padding: 12px 24px !important; border-radius: 4px !important; background: var(--secondary) !important; color: var(--primary) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important; z-index: 9998 !important; display: flex !important; align-items: center !important; gap: 8px !important; transform: translateY(100px) !important; opacity: 0 !important; transition: all 0.3s ease !important; } .userscript-rb .rb-status.show { transform: translateY(0) !important; opacity: 1 !important; } .userscript-rb .rb-status.success { border-left: 4px solid #10b981 !important; } .userscript-rb .rb-status.warning { border-left: 4px solid #f59e0b !important; } .userscript-rb .rb-status.error { border-left: 4px solid #ef4444 !important; } .userscript-rb .rb-float-button { position: fixed !important; right: 20px !important; top: 50% !important; transform: translateY(-50%) !important; background: var(--tertiary) !important; color: #fff !important; width: 48px !important; height: 48px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important; z-index: 9997 !important; transition: all 0.3s ease !important; } .userscript-rb .rb-float-button:hover { transform: translateY(-50%) scale(1.1) !important; } .userscript-rb .rb-float-button svg { width: 24px !important; height: 24px !important; } @media (prefers-color-scheme: dark) { .userscript-rb .rb-overlay { background: rgba(0, 0, 0, 0.95) !important; } .userscript-rb .rb-container, .userscript-rb .rb-status { background: var(--secondary-dark, #1a1b1e) !important; color: var(--primary-dark, #fff) !important; } .userscript-rb .rb-setting-group input { background: var(--secondary-dark, #1a1b1e) !important; border-color: var(--primary-low-dark, #2d2d2d) !important; } } @media (prefers-reduced-motion: reduce) { .userscript-rb .rb-overlay, .userscript-rb .rb-container, .userscript-rb .rb-status, .userscript-rb .rb-float-button { transition: none !important; } } `; document.head.appendChild(style); // 默认配置 const DEFAULT_CONFIG = { baseDelay: 2500, randomDelayRange: 800, minReqSize: 8, maxReqSize: 20, minReadTime: 800, maxReadTime: 3000, autoStart: false, lastPosition: 0 }; // 全局变量 let config = { ...DEFAULT_CONFIG, ...getStoredConfig() }; const topicID = window.location.pathname.split("/")[3]; const repliesInfo = document.querySelector("div[class=timeline-replies]").textContent.trim(); const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10)); const csrfToken = document.querySelector("meta[name=csrf-token]").getAttribute("content"); // 初始化 function initialize() { // 检查首次使用 if (!GM_getValue("hasAgreed", false)) { showDialog({ title: "首次使用提示", content: `

检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:

使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责。

这是一个开源脚本,你可以自由审核其中的内容。

如果你同意以上内容,请点击"同意"按钮。

`, buttons: [ { text: "同意", onClick: () => { GM_setValue("hasAgreed", true); setupUI(); } }, { text: "取消", onClick: () => { showStatus("您未同意风险提示,脚本已停止运行。", "error"); throw new Error("未同意风险提示"); } } ] }); } else { setupUI(); } } // 设置UI function setupUI() { const headerButtons = document.querySelector(".header-buttons"); const settingsButton = createButton("设置", () => showSettings()); headerButtons.appendChild(settingsButton); // 添加浮动开始按钮 const floatButton = document.createElement("div"); floatButton.className = "userscript-rb"; floatButton.innerHTML = `
`; document.body.appendChild(floatButton); floatButton.querySelector(".rb-float-button").addEventListener("click", startReading); if (config.autoStart) { startReading(); } } // 创建按钮 function createButton(label, onClick) { const button = document.createElement("button"); button.className = "btn btn-small btn-icon-text"; button.innerHTML = `${label}`; button.addEventListener("click", onClick); return button; } // 显示对话框 function showDialog({ title, content, buttons = [] }) { const wrapper = document.createElement("div"); wrapper.className = "userscript-rb"; const overlay = document.createElement("div"); overlay.className = "rb-overlay"; overlay.innerHTML = `

${title}

${content}
${buttons.map(btn => ` `).join('')}
`; wrapper.appendChild(overlay); document.body.appendChild(wrapper); setTimeout(() => overlay.classList.add("show"), 10); buttons.forEach(btn => { overlay.querySelector(`[data-action="${btn.text}"]`).addEventListener("click", () => { overlay.classList.remove("show"); setTimeout(() => { overlay.remove(); btn.onClick?.(); }, 300); }); }); } // 显示设置 function showSettings() { const settingsContent = `
`; showDialog({ title: "ReadBoost 设置", content: settingsContent, buttons: [ { text: "开始运行", onClick: () => startReading() }, { text: "保存", onClick: saveSettings }, { text: "恢复默认", onClick: resetSettings }, { text: "关闭", onClick: () => {} } ] }); // 绑定高级模式切换事件 document.getElementById("advancedMode").addEventListener("change", (e) => { if (e.target.checked) { showDialog({ title: "警告", content: "如果你不知道你在修改什么,那么不建议开启高级设置,随意修改可能会提高脚本崩溃、账号被禁等风险的可能!", buttons: [ { text: "继续", onClick: () => toggleAdvancedMode(true) }, { text: "取消", onClick: () => { e.target.checked = false; toggleAdvancedMode(false); } } ] }); } else { toggleAdvancedMode(false); } }); } // 切换高级模式 function toggleAdvancedMode(enabled) { const inputs = [ "baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime" ]; inputs.forEach(id => { document.getElementById(id).disabled = !enabled; }); config.advancedMode = enabled; } // 保存设置 function saveSettings() { const newConfig = { baseDelay: parseInt(document.getElementById("baseDelay").value), randomDelayRange: parseInt(document.getElementById("randomDelayRange").value), minReqSize: parseInt(document.getElementById("minReqSize").value), maxReqSize: parseInt(document.getElementById("maxReqSize").value), minReadTime: parseInt(document.getElementById("minReadTime").value), maxReadTime: parseInt(document.getElementById("maxReadTime").value), autoStart: document.getElementById("autoStart").checked, advancedMode: document.getElementById("advancedMode").checked }; Object.entries(newConfig).forEach(([key, value]) => { GM_setValue(key, value); config[key] = value; }); showStatus("设置已保存", "success"); } // 重置设置 function resetSettings() { Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => { GM_setValue(key, value); config[key] = value; }); showStatus("已恢复默认设置", "success"); location.reload(); } // 获取存储的配置 function getStoredConfig() { return Object.keys(DEFAULT_CONFIG).reduce((acc, key) => { acc[key] = GM_getValue(key, DEFAULT_CONFIG[key]); return acc; }, {}); } // 显示状态提示 function showStatus(message, type = "success") { const wrapper = document.createElement("div"); wrapper.className = "userscript-rb"; const statusDiv = document.createElement("div"); statusDiv.className = `rb-status ${type}`; statusDiv.innerHTML = ` ${type === "success" ? '' : type === "error" ? '' : '' } ${message} `; wrapper.appendChild(statusDiv); document.body.appendChild(wrapper); setTimeout(() => statusDiv.classList.add("show"), 10); setTimeout(() => { statusDiv.classList.remove("show"); setTimeout(() => statusDiv.remove(), 300); }, 3000); } // 随机数生成 function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // 创建请求参数 function createBatchParams(startId, endId) { const params = new URLSearchParams(); for (let i = startId; i <= endId; i++) { params.append(`timings[${i}]`, getRandomInt(config.minReadTime, config.maxReadTime).toString()); } const topicTime = getRandomInt( config.minReadTime * (endId - startId + 1), config.maxReadTime * (endId - startId + 1) ).toString(); params.append('topic_time', topicTime); params.append('topic_id', topicID); return params; } // 发送请求 async function sendBatch(startId, endId, retryCount = 3) { const params = createBatchParams(startId, endId); try { const response = await fetch("https://linux.do/topics/timings", { headers: { "accept": "*/*", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "discourse-background": "true", "discourse-logged-in": "true", "discourse-present": "true", "priority": "u=1, i", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest", "x-silence-logger": "true" }, referrer: `https://linux.do/`, body: params.toString(), method: "POST", mode: "cors", credentials: "include" }); if (!response.ok) { throw new Error(`HTTP请求失败,状态码:${response.status}`); } showStatus(`成功处理回复 ${startId} - ${endId}`, "success"); return true; } catch (error) { console.error(`处理回复 ${startId} - ${endId} 失败: `, error); if (retryCount > 0) { showStatus(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`, "warning"); await new Promise(r => setTimeout(r, 2000)); return sendBatch(startId, endId, retryCount - 1); } else { showStatus(`处理回复 ${startId} - ${endId} 失败,自动跳过`, "error"); return false; } } } // 开始阅读处理 async function startReading() { showStatus("开始处理阅读...", "success"); for (let i = 1; i <= totalReplies;) { const batchSize = getRandomInt(config.minReqSize, config.maxReqSize); const startId = i; const endId = Math.min(i + batchSize - 1, totalReplies); const success = await sendBatch(startId, endId); if (success) { const delay = config.baseDelay + getRandomInt(0, config.randomDelayRange); await new Promise(r => setTimeout(r, delay)); } i = endId + 1; } showStatus("所有回复处理完成", "success"); } // 启动脚本 initialize();