diff --git a/LDStatus.user.js b/LDStatus.user.js index 3ba9164..cacb656 100644 --- a/LDStatus.user.js +++ b/LDStatus.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name LDStatus // @namespace http://tampermonkey.net/ -// @version 1.7 +// @version 2.0 // @description 在 Linux.do 页面显示信任级别进度 // @author 1e0n // @match https://linux.do/* @@ -19,669 +19,1238 @@ (function() { 'use strict'; - // 创建样式 - 使用更特定的选择器以避免影响帖子界面的按钮 - const style = document.createElement('style'); - style.textContent = ` - #ld-trust-level-panel { - position: fixed; - left: 10px; - top: 100px; - width: 210px; - background-color: #2d3748; - border-radius: 8px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); - z-index: 9999; - font-family: Arial, sans-serif; - transition: all 0.3s ease; - overflow: hidden; - color: #e2e8f0; - font-size: 12px; - } + // 模块化结构 + const LDStatus = { + // 配置 + config: { + refreshInterval: 300000, // 5分钟刷新一次 + storageKeys: { + position: 'ld_panel_position', + collapsed: 'ld_panel_collapsed', + theme: 'ld_panel_theme', + lastData: 'ld_last_successful_data' + }, + maxStorageItems: 500, + statsToTrack: [ + '浏览的话题(所有时间)', + '回复的话题', + '已读帖子(所有时间)', + '获赞:点赞用户数量', + '点赞' + ], + nameMapping: { + '已读帖子(所有时间)': '已读帖子(总)', + '浏览的话题(所有时间)': '浏览话题(总)', + '获赞:点赞用户数量': '点赞用户数', + '获赞:单日最高数量': '单日最高获赞', + '被禁言(过去 6 个月)': '被禁言', + '被封禁(过去 6 个月)': '被封禁' + }, + icons: { + SEARCH: '🔎', + REFRESH: '🔄', + THEME_DARK: '🌙', + THEME_LIGHT: '☀️', + COLLAPSE: '◀', + EXPAND: '▶', + LOADING: '⌛', + UPDATE_AVAILABLE: '⚠️', + UP_TO_DATE: '✔', + ERROR: '❌', + ARROW_UP: '▲', + ARROW_DOWN: '▼', + TREND_INCREASE: '▲', + TREND_DECREASE: '▼', + TREND_STABLE: '–' // Using '–' for better visual than '0' + } + }, + + // 变量 + vars: { + panel: null, + header: null, + content: null, + toggleBtn: null, + refreshBtn: null, + updateBtn: null, + themeBtn: null, + isDragging: false, + lastX: 0, + lastY: 0, + refreshTimer: null, + previousRequirements: [], + isDarkTheme: true + }, + + // UI相关方法 + ui: { + // 创建CSS样式 + createStyles: function() { + const style = document.createElement('style'); + style.textContent = ` + /* CSS变量 - 主题颜色 */ + :root { + --ld-bg-color: #ffffff; + --ld-text-color: #1a202c; + --ld-header-bg: #3182ce; + --ld-header-color: #ffffff; + --ld-success-color: #276749; + --ld-fail-color: #c53030; + --ld-border-color: #e2e8f0; + --ld-shadow: 0 0 10px rgba(0, 0, 0, 0.15); + --ld-secondary-color: #4a5568; + --ld-increase-color: #d69e2e; + --ld-decrease-color: #2b6cb0; + --ld-day1-color: #276749; + --ld-day2-color: #2d3748; + } - #ld-trust-level-header { - background-color: #1a202c; - color: white; - padding: 8px 10px; - cursor: move; - display: flex; - justify-content: space-between; - align-items: center; - user-select: none; - } + .ld-dark-theme { + --ld-bg-color: #2d3748; + --ld-text-color: #e2e8f0; + --ld-header-bg: #1a202c; + --ld-header-color: #ffffff; + --ld-success-color: #68d391; + --ld-fail-color: #fc8181; + --ld-border-color: #4a5568; + --ld-shadow: 0 0 10px rgba(0, 0, 0, 0.4); + --ld-secondary-color: #a0aec0; + --ld-increase-color: #ffd700; + --ld-decrease-color: #4299e1; + --ld-day1-color: #68d391; + --ld-day2-color: #cbd5e1; + } - .ld-header-content { - display: flex; - width: 100%; - align-items: center; - justify-content: space-between; - white-space: nowrap; - } + /* 面板基础样式 */ + #ld-trust-level-panel { + position: fixed; + left: 10px; + top: 100px; + width: 210px; + border-radius: 8px; + z-index: 9999; + font-family: Arial, sans-serif; + transition: all 0.3s ease; + overflow: hidden; + font-size: 12px; + background-color: var(--ld-bg-color); + color: var(--ld-text-color); + box-shadow: var(--ld-shadow); + border: 1px solid var(--ld-border-color); + } - .ld-header-content > span:first-child { - margin-right: auto; - font-weight: bold; - } + #ld-trust-level-header { + padding: 8px 10px; + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; + background-color: var(--ld-header-bg); + color: var(--ld-header-color); + } - #ld-trust-level-content { - padding: 10px; - max-height: none; - overflow-y: visible; - } + .ld-header-content { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + white-space: nowrap; + } - .ld-trust-level-item { - margin-bottom: 6px; - display: flex; - white-space: nowrap; - width: 100%; - justify-content: space-between; - } + .ld-header-content > span:first-child { + margin-right: auto; + font-weight: bold; + } - .ld-trust-level-item .ld-name { - flex: 0 1 auto; - overflow: hidden; - text-overflow: ellipsis; - max-width: 60%; - } + #ld-trust-level-content { + padding: 10px; + max-height: none; + overflow-y: visible; + } - .ld-trust-level-item .ld-value { - font-weight: bold; - flex: 0 0 auto; - text-align: right; - min-width: 70px; - } + .ld-trust-level-item { + margin-bottom: 6px; + display: flex; + white-space: nowrap; + width: 100%; + justify-content: space-between; + } - .ld-trust-level-item.ld-success .ld-value { - color: #68d391; - } + .ld-trust-level-item .ld-name { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + max-width: 60%; + } - .ld-trust-level-item.ld-fail .ld-value { - color: #fc8181; - } + .ld-trust-level-item .ld-value { + font-weight: bold; + flex: 0 0 auto; + text-align: right; + min-width: 70px; + } - .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn { - background: none; - border: none; - color: white; - cursor: pointer; - font-size: 14px; - margin-left: 5px; - } + .ld-trust-level-item.ld-success .ld-value { + color: var(--ld-success-color); + } - .ld-version { - font-size: 10px; - color: #a0aec0; - margin-left: 5px; - font-weight: normal; - } + .ld-trust-level-item.ld-fail .ld-value { + color: var(--ld-fail-color); + } - .ld-collapsed { - width: 40px !important; - height: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - border-radius: 8px; - overflow: hidden; - transform: none !important; - } + .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn { + background: none; + border: none; + color: var(--ld-header-color); + cursor: pointer; + font-size: 14px; + margin-left: 5px; + } - .ld-collapsed #ld-trust-level-header { - justify-content: center; - width: 40px !important; - height: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - padding: 0; - display: flex; - align-items: center; - } + .ld-version { + font-size: 10px; + color: var(--ld-secondary-color); + margin-left: 5px; + font-weight: normal; + } - .ld-collapsed #ld-trust-level-header > div { - justify-content: center; - width: 100%; - height: 100%; - } + .ld-collapsed { + width: 40px !important; + height: 40px !important; + min-width: 40px !important; + max-width: 40px !important; + border-radius: 8px; + overflow: hidden; + transform: none !important; + } - .ld-collapsed #ld-trust-level-content { - display: none !important; - } + .ld-collapsed #ld-trust-level-header { + justify-content: center; + width: 40px !important; + height: 40px !important; + min-width: 40px !important; + max-width: 40px !important; + padding: 0; + display: flex; + align-items: center; + } - .ld-collapsed .ld-header-content > span, - .ld-collapsed .ld-refresh-btn, - .ld-collapsed .ld-update-btn, - .ld-collapsed .ld-version { - display: none !important; - } + .ld-collapsed #ld-trust-level-header > div { + justify-content: center; + width: 100%; + height: 100%; + } - .ld-collapsed .ld-toggle-btn { - margin: 0; - font-size: 16px; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - } + .ld-collapsed #ld-trust-level-content { + display: none !important; + } - .ld-loading { - text-align: center; - padding: 10px; - color: #a0aec0; - } + .ld-collapsed .ld-header-content > span, + .ld-collapsed .ld-refresh-btn, + .ld-collapsed .ld-update-btn, + .ld-collapsed .ld-theme-btn, + .ld-collapsed .ld-version { + display: none !important; + } - .ld-increase { - color: #ffd700; /* 黄色 */ - } + .ld-collapsed .ld-toggle-btn { + margin: 0; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } - .ld-decrease { - color: #4299e1; /* 蓝色 */ - } + .ld-loading { + text-align: center; + padding: 10px; + color: var(--ld-secondary-color); + } - .ld-daily-stats { - margin-top: 15px; - padding-top: 10px; - border-top: 1px solid #4a5568; - font-size: 11px; - } + .ld-increase { + color: var(--ld-increase-color); + } - .ld-daily-stats-title { - font-weight: bold; - margin-bottom: 5px; - color: #a0aec0; - } + .ld-decrease { + color: var(--ld-decrease-color); + } - .ld-daily-stats-item { - display: flex; - justify-content: space-between; - margin-bottom: 4px; - } + /* 活动数据区域 */ + .ld-daily-stats { + margin-top: 10px; + font-size: 11px; + border-top: 1px solid var(--ld-border-color); + padding-top: 10px; + } - .ld-daily-stats-item .ld-name { - flex: 0 1 auto; - } + .ld-daily-stats-title { + font-weight: bold; + margin-bottom: 5px; + color: var(--ld-secondary-color); + } - .ld-daily-stats-item .ld-value { - flex: 0 0 auto; - font-weight: bold; - color: #68d391; - } - `; - document.head.appendChild(style); - - // 定义存储键 - const STORAGE_KEY_POSITION = 'ld_panel_position'; - const STORAGE_KEY_COLLAPSED = 'ld_panel_collapsed'; - - // 创建面板 - const panel = document.createElement('div'); - panel.id = 'ld-trust-level-panel'; - - // 获取脚本版本号 - const scriptVersion = GM_info.script.version; - - // 创建面板头部 - const header = document.createElement('div'); - header.id = 'ld-trust-level-header'; - header.innerHTML = ` -
| elements for name, current progress, and required progress.
+ const tableElement = trustLevelSection.querySelector('table');
+ if (!tableElement) {
+ console.warn('LDStatus: Trust level table element not found.');
+ LDStatus.vars.content.innerHTML = ' 数据格式错误(未找到表格),请检查页面结构或脚本。 ';
+ return; // Stop if table is missing
}
+ const tableRows = tableElement.querySelectorAll('tr');
+ const requirements = [];
+
+ for (let i = 1; i < tableRows.length; i++) { // 跳过表头
+ const row = tableRows[i];
+ const cells = row.querySelectorAll('td');
+
+ // Ensure cells and their textContent are valid
+ if (cells.length >= 3 &&
+ cells[0] && cells[0].textContent &&
+ cells[1] && cells[1].textContent &&
+ cells[2] && cells[2].textContent) {
+
+ const name = cells[0].textContent.trim();
+ const currentText = cells[1].textContent.trim(); // Keep original text for display
+ const requiredText = cells[2].textContent.trim(); // Keep original text for display
+ const isSuccess = cells[1].classList ? cells[1].classList.contains('text-green-500') : false;
+
+ // Robustly extract current value as a number for logic
+ let currentValue = 0;
+ const currentNumMatch = currentText.match(/(\d+)/);
+ if (currentNumMatch && currentNumMatch[0]) {
+ currentValue = parseInt(currentNumMatch[0], 10);
+ } else {
+ console.warn(`LDStatus: Could not parse current value number from: "${currentText}" for item "${name}"`);
+ }
+
+ // (Optional: Robustly extract required value as a number if needed for logic later)
+ // let requiredValue = 0;
+ // const requiredNumMatch = requiredText.match(/(\d+)/);
+ // if (requiredNumMatch && requiredNumMatch[0]) {
+ // requiredValue = parseInt(requiredNumMatch[0], 10);
+ // } else {
+ // console.warn(`LDStatus: Could not parse required value number from: "${requiredText}" for item "${name}"`);
+ // }
+
+ // `previousRequirements` Usage:
+ // `LDStatus.vars.previousRequirements` stores the full `requirements` array from the *last successful parse*.
+ // This allows comparison to detect changes in `currentValue` for each requirement.
+ let changeValue = 0;
+ let hasChanged = false;
+
+ if (LDStatus.vars.previousRequirements.length > 0) {
+ const prevReq = LDStatus.vars.previousRequirements.find(pr => pr.name === name);
+ if (prevReq) {
+ // If current numeric value differs from the previous one, calculate the new change.
+ if (currentValue !== prevReq.currentValue) {
+ changeValue = currentValue - prevReq.currentValue;
+ hasChanged = true;
+ } else if (prevReq.changeValue) {
+ // Persisted Change Logic:
+ // If current value matches previous, but previous had a change indicator (non-zero changeValue),
+ // persist that indicator (changeValue and hasChanged flag).
+ // This aligns with the feature: "Even if the value does not change after refresh, the indicator will be preserved."
+ changeValue = prevReq.changeValue;
+ hasChanged = true;
+ }
+ // If currentValue matches prevReq.currentValue AND prevReq.changeValue was 0,
+ // then changeValue remains 0 and hasChanged remains false (no new change, no persisted change).
+ }
+ }
+
+ requirements.push({
+ name,
+ current: currentText, // Use original text for display
+ required: requiredText, // Use original text for display
+ isSuccess,
+ currentValue, // Parsed number for logic
+ changeValue,
+ hasChanged
+ });
+ } else {
+ console.warn(`LDStatus: Skipping table row at index ${i} due to insufficient cells or missing text content. Cells found: ${cells.length}.`);
+ }
+ }
+
+ // 获取总体结果
+ const resultTextElement = trustLevelSection.querySelector('p.text-red-500, p.text-green-500');
+ let isMeetingRequirements = false; // Default to false
+
+ if (resultTextElement && resultTextElement.classList) {
+ isMeetingRequirements = !resultTextElement.classList.contains('text-red-500');
+ } else {
+ console.warn('LDStatus: Result text (overall status) element not found or classList is not available. Defaulting to "not meeting requirements".');
+ // Optionally, inform the user in the panel if this is critical
+ // LDStatus.ui.showNotice("无法确定总体状态", "warn");
+ }
+
+ // 存储48小时内的数据变化
+ const dailyChanges = this.saveDailyStats(requirements); // This calculates and stores daily activity.
+
+ // Data Caching:
+ // The full parsed data (including calculated changes and daily activity) is cached to GM_setValue.
+ // This allows the panel to display the last known data if the user is offline
+ // or if a subsequent fetch from connect.linux.do fails.
+ GM_setValue(LDStatus.config.storageKeys.lastData, {
+ username,
+ targetLevel,
+ requirements,
+ isMeetingRequirements,
+ dailyChanges,
+ timestamp: new Date().getTime()
+ });
+
+ // 渲染数据
+ LDStatus.ui.renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges);
+
+ // `previousRequirements` Update:
+ // Save the current set of requirements (with their currentValues, changeValues, and hasChanged flags)
+ // to `LDStatus.vars.previousRequirements` for the next fetch cycle's comparison.
+ // A shallow copy is made to prevent modifications to the rendered data from affecting the next comparison.
+ LDStatus.vars.previousRequirements = [...requirements];
},
- onerror: function() {
- content.innerHTML = '获取数据失败,请稍后再试 ';
- }
- });
- }
-
- // 解析信任级别数据
- function parseTrustLevelData(html) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- // 查找信任级别区块
- const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => {
- const heading = div.querySelector('h2');
- return heading && heading.textContent.includes('信任级别');
- });
-
- if (!trustLevelSection) {
- content.innerHTML = '未找到信任级别数据,请确保已登录 ';
- return;
- }
- // 获取用户名和当前级别
- const heading = trustLevelSection.querySelector('h2').textContent.trim();
- const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/);
- const username = match ? match[1] : '未知用户';
- const targetLevel = match ? match[2] : '未知';
-
- // 获取表格数据
- const tableRows = trustLevelSection.querySelectorAll('table tr');
- const requirements = [];
-
- for (let i = 1; i < tableRows.length; i++) { // 跳过表头
- const row = tableRows[i];
- 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 isSuccess = cells[1].classList.contains('text-green-500');
-
- // 提取当前完成数的数字部分
- const currentMatch = current.match(/(\d+)/);
- const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0;
-
- // 查找上一次的数据记录
- let changeValue = 0;
- let hasChanged = false;
-
- if (previousRequirements.length > 0) {
- const prevReq = previousRequirements.find(pr => pr.name === name);
- if (prevReq) {
- // 如果完成数有变化,更新变化值
- if (currentValue !== prevReq.currentValue) {
- changeValue = currentValue - prevReq.currentValue;
- hasChanged = true;
- } else if (prevReq.changeValue) {
- // 如果完成数没有变化,但之前有变化值,保留之前的变化值
- changeValue = prevReq.changeValue;
- hasChanged = true;
+ // 存储48小时内的数据变化
+ saveDailyStats: function(requirements) {
+ const statsToTrack = LDStatus.config.statsToTrack;
+ const now = new Date().getTime();
+ const twoDaysAgo = now - 48 * 60 * 60 * 1000;
+
+ // 1. Load Existing Data
+ let allDailyStats = [];
+ try {
+ const storedStats = localStorage.getItem('ld_daily_stats');
+ if (storedStats) {
+ allDailyStats = JSON.parse(storedStats);
+ if (!Array.isArray(allDailyStats)) { // Basic validation
+ console.warn("LDStatus: ld_daily_stats from localStorage was not an array. Resetting.");
+ allDailyStats = [];
}
}
+ } catch (e) {
+ console.error("LDStatus: Error parsing ld_daily_stats from localStorage:", e);
+ allDailyStats = []; // Start fresh if parsing fails
}
- requirements.push({
- name,
- current,
- required,
- isSuccess,
- currentValue,
- changeValue, // 变化值
- hasChanged // 是否有变化
+ // 2. Filter Old Data
+ allDailyStats = allDailyStats.filter(item => item.timestamp > twoDaysAgo);
+
+ // 3. Add New Data
+ statsToTrack.forEach(statName => {
+ const req = requirements.find(r => r.name === statName);
+ if (req) {
+ // req.currentValue is already parsed as a number in parseTrustLevelData
+ const currentValue = req.currentValue;
+
+ allDailyStats.push({
+ name: statName,
+ value: currentValue,
+ timestamp: now
+ });
+ }
});
- }
- }
- // 获取总体结果
- const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500');
- const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false;
-
- // 存储24小时内的数据变化
- const dailyChanges = saveDailyStats(requirements);
-
- // 渲染数据
- renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges);
-
- // 保存当前数据作为下次比较的基准
- previousRequirements = [...requirements];
- }
-
- // 渲染信任级别数据
- function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) {
- let html = `
-
- ${username} - 信任级别 ${targetLevel}
-
-
- ${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求
-
- `;
-
- requirements.forEach(req => {
- // 简化项目名称
- let name = req.name;
- // 将一些常见的长名称缩短
- name = name.replace('已读帖子(所有时间)', '已读帖子(总)');
- name = name.replace('浏览的话题(所有时间)', '浏览话题(总)');
- name = name.replace('获赞:点赞用户数量', '点赞用户数');
- name = name.replace('获赞:单日最高数量', '单日最高获赞');
- name = name.replace('被禁言(过去 6 个月)', '被禁言');
- name = name.replace('被封禁(过去 6 个月)', '被封禁');
-
- // 提取数字部分以简化显示
- let current = req.current;
- let required = req.required;
-
- // 尝试从字符串中提取数字
- const currentMatch = req.current.match(/(\d+)/);
- const requiredMatch = req.required.match(/(\d+)/);
-
- if (currentMatch) current = currentMatch[1];
- if (requiredMatch) required = requiredMatch[1];
-
- // 添加目标完成数变化的标识
- let changeIndicator = '';
- if (req.hasChanged) {
- const diff = req.changeValue;
- if (diff > 0) {
- changeIndicator = ` ▲${diff}`; // 增加标识,黄色
- } else if (diff < 0) {
- changeIndicator = ` ▼${Math.abs(diff)}`; // 减少标识,蓝色
+ // 4. Clean Up/Limit (ensure cleanupStorage returns the cleaned array)
+ allDailyStats = this.cleanupStorage(allDailyStats); // cleanupStorage sorts and slices
+
+ // 5. Save Data
+ localStorage.setItem('ld_daily_stats', JSON.stringify(allDailyStats));
+
+ // Return the result of calculateDailyChanges based on the new allDailyStats
+ return this.calculateDailyChanges(allDailyStats);
+ },
+
+ // 清理过量的存储数据
+ cleanupStorage: function(stats) {
+ const maxItems = LDStatus.config.maxStorageItems;
+
+ if (stats.length > maxItems) {
+ // 按时间戳排序并只保留最新的数据 (descending to keep newest)
+ stats.sort((a, b) => b.timestamp - a.timestamp);
+ return stats.slice(0, maxItems); // Important: it returns the sliced array
}
- }
+ return stats; // Return the original array if not over limit
+ },
- html += `
-
- ${name}
- ${current}${changeIndicator} / ${required}
-
- `;
- });
-
- // 添加24小时内的活动数据显示
- html += `
-
- `;
-
- content.innerHTML = html;
- }
-
- // 存储上一次获取的数据,用于比较变化
- let previousRequirements = [];
-
- // 存储24小时内的数据变化
- function saveDailyStats(requirements) {
- // 定义要跟踪的数据项
- const statsToTrack = [
- '浏览的话题(所有时间)', // 浏览话题总数
- '回复的话题', // 回复话题数
- '已读帖子(所有时间)', // 已读帖子总数
- '获赞:点赞用户数量', // 获赞数
- '点赞的帖子' // 点赞数
- ];
-
- // 获取当前时间
- const now = new Date().getTime();
-
- // 从 localStorage 中获取已存储的数据
- let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]');
-
- // 删除超过24小时的数据
- const oneDayAgo = now - 24 * 60 * 60 * 1000;
- dailyStats = dailyStats.filter(item => item.timestamp > oneDayAgo);
-
- // 对于每个要跟踪的数据项,找到当前值并添加到历史记录中
- statsToTrack.forEach(statName => {
- const req = requirements.find(r => r.name === statName);
- if (req) {
- // 添加新的数据点
- dailyStats.push({
- name: statName,
- value: req.currentValue,
- timestamp: now
+ // 计算近两天内的变化量
+ // Purpose: Calculates the net change in tracked statistics over two distinct 24-hour periods:
+ // 1. The most recent 24 hours (Last 24h).
+ // 2. The 24-hour period immediately preceding the "Last 24h" (24h-48h Ago).
+ // It also calculates the 'trend', which is the difference between these two net changes.
+ // Input: dailyStats - Array of objects: { name: string, value: number, timestamp: number }
+ // Output: An object where keys are statNames and values are { changeLast24h, change24hTo48hAgo, trend }.
+ calculateDailyChanges: function(dailyStats) {
+ const statsToTrack = LDStatus.config.statsToTrack;
+ const result = {};
+ const now = new Date().getTime();
+
+ // Time Period Definitions:
+ // `oneDayAgo` marks the boundary between "Last 24h" and "24h-48h Ago".
+ // `twoDaysAgo` marks the older boundary for the "24h-48h Ago" period.
+ const oneDayAgo = now - 24 * 60 * 60 * 1000;
+ const twoDaysAgo = now - 48 * 60 * 60 * 1000;
+
+ statsToTrack.forEach(statName => {
+ // Filter records for the current stat and sort them by timestamp (ascending).
+ // This makes it easier to find the earliest and latest records in a period.
+ const statRecords = dailyStats
+ .filter(item => item.name === statName)
+ .sort((a, b) => a.timestamp - b.timestamp);
+
+ result[statName] = {
+ changeLast24h: 0, // Net change in value over the most recent 24 hours.
+ change24hTo48hAgo: 0, // Net change in value over the 24-hour period before the most recent one.
+ trend: 0 // Difference: changeLast24h - change24hTo48hAgo.
+ };
+
+ if (statRecords.length >= 2) {
+ // Record Identification:
+ // `newest`: The absolute latest record available for this statistic.
+ const newest = statRecords[statRecords.length - 1];
+
+ // `oldestLast24h`: The earliest record that falls within the "Last 24h" window (timestamp > oneDayAgo).
+ // Used as the starting point to calculate change during the most recent 24 hours.
+ const oldestLast24h = statRecords.filter(item => item.timestamp > oneDayAgo)[0];
+
+ // Records for the "24h-48h Ago" period.
+ const records24hTo48hAgo = statRecords.filter(item =>
+ item.timestamp <= oneDayAgo && item.timestamp > twoDaysAgo);
+
+ // `oldest24hTo48hAgo`: Earliest record in the "24h-48h Ago" window.
+ // `newest24hTo48hAgo`: Latest record in the "24h-48h Ago" window.
+ // These are used to calculate the net change *within* that specific 24-hour slot.
+ const oldest24hTo48hAgo = records24hTo48hAgo.length > 0 ? records24hTo48hAgo[0] : null;
+ const newest24hTo48hAgo = records24hTo48hAgo.length > 0 ?
+ records24hTo48hAgo[records24hTo48hAgo.length - 1] : null;
+
+ // Change Calculation:
+ // `changeLast24h`: Net change from the start of the "Last 24h" period to the `newest` record.
+ if (oldestLast24h) {
+ result[statName].changeLast24h = newest.value - oldestLast24h.value;
+ }
+
+ // `change24hTo48hAgo`: Net change from the start to the end of the "24h-48h Ago" period.
+ if (oldest24hTo48hAgo && newest24hTo48hAgo) {
+ result[statName].change24hTo48hAgo = newest24hTo48hAgo.value - oldest24hTo48hAgo.value;
+ }
+
+ // `trend`: Difference between the net change in the last 24h and the net change in the 24h before that.
+ // A positive trend means activity is increasing more (or decreasing less) in the most recent 24h
+ // compared to the prior 24h period.
+ result[statName].trend = result[statName].changeLast24h - result[statName].change24hTo48hAgo;
+ }
+ });
+
+ return result;
+ },
+
+ // 检查脚本更新
+ checkForUpdates: function() {
+ const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js';
+ const updateBtn = LDStatus.vars.updateBtn;
+
+ // 显示正在检查的状态
+ updateBtn.textContent = LDStatus.config.icons.LOADING;
+ updateBtn.title = '正在检查更新...';
+
+ GM_xmlhttpRequest({
+ method: 'GET',
+ url: updateURL,
+ onload: function(response) {
+ if (response.status === 200) {
+ // 提取远程脚本的版本号
+ const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/);
+ if (versionMatch && versionMatch[1]) {
+ const remoteVersion = versionMatch[1];
+ const scriptVersion = GM_info.script.version;
+
+ // 比较版本
+ if (remoteVersion > scriptVersion) {
+ // 有新版本
+ updateBtn.textContent = LDStatus.config.icons.UPDATE_AVAILABLE;
+ updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`;
+ updateBtn.style.color = 'var(--ld-increase-color)'; // 黄色
+
+ // 点击按钮跳转到更新页面
+ updateBtn.onclick = function() {
+ window.open(updateURL, '_blank');
+ };
+ } else {
+ // 已是最新版本
+ updateBtn.textContent = LDStatus.config.icons.UP_TO_DATE;
+ updateBtn.title = '已是最新版本,点击再次检查'; // Updated title for clarity
+ updateBtn.style.color = 'var(--ld-success-color)'; // 绿色
+ // Ensure onclick allows re-checking
+ updateBtn.onclick = LDStatus.events.onUpdateBtnClick;
+ // Removed setTimeout to make the "up-to-date" state persistent
+ }
+ } else {
+ // This case means versionMatch failed, treat as an error in parsing response
+ console.warn("LDStatus: Could not parse version from update check response.");
+ LDStatus.data.handleUpdateError(); // Call existing error handler
+ }
+ } else {
+ // HTTP error
+ console.warn(`LDStatus: Update check HTTP error: ${response.status}`);
+ LDStatus.data.handleUpdateError();
+ }
+ },
+ onerror: function(error) { // Network error
+ console.warn("LDStatus: Update check network error.", error);
+ LDStatus.data.handleUpdateError();
+ }
});
+ },
+
+ // 处理更新检查错误
+ handleUpdateError: function() {
+ const updateBtn = LDStatus.vars.updateBtn;
+ updateBtn.textContent = LDStatus.config.icons.ERROR;
+ updateBtn.title = '检查更新失败,点击再次检查'; // Updated title for clarity
+ updateBtn.style.color = 'var(--ld-fail-color)'; // 红色
+ // Ensure onclick allows re-checking
+ updateBtn.onclick = LDStatus.events.onUpdateBtnClick;
+ // Removed setTimeout to make the error state persistent until user interaction
}
- });
-
- // 将更新后的数据保存回 localStorage
- localStorage.setItem('ld_daily_stats', JSON.stringify(dailyStats));
-
- return calculateDailyChanges(dailyStats);
- }
-
- // 计箞24小时内的变化量
- function calculateDailyChanges(dailyStats) {
- // 定义要跟踪的数据项
- const statsToTrack = [
- '浏览的话题(所有时间)', // 浏览话题总数
- '回复的话题', // 回复话题数
- '已读帖子(所有时间)', // 已读帖子总数
- '获赞:点赞用户数量', // 获赞数
- '点赞的帖子' // 点赞数
- ];
-
- const result = {};
-
- // 对于每个要跟踪的数据项,计算24小时内的变化
- statsToTrack.forEach(statName => {
- // 过滤出当前数据项的所有记录,并按时间戳排序
- const statRecords = dailyStats
- .filter(item => item.name === statName)
- .sort((a, b) => a.timestamp - b.timestamp);
-
- if (statRecords.length >= 2) {
- // 获取最早和最新的记录
- const oldest = statRecords[0];
- const newest = statRecords[statRecords.length - 1];
-
- // 计算变化量
- const change = newest.value - oldest.value;
-
- // 存储结果
- result[statName] = change;
- } else {
- // 如果没有足够的数据点,设置为0
- result[statName] = 0;
+ },
+
+ // 事件处理相关方法
+ events: {
+ // 注册所有事件监听
+ registerEvents: function() {
+ // 拖动面板相关事件
+ this.setupDragEvents();
+
+ // 按钮点击事件
+ LDStatus.vars.toggleBtn.addEventListener('click', this.onToggleBtnClick);
+ LDStatus.vars.refreshBtn.addEventListener('click', this.onRefreshBtnClick);
+ LDStatus.vars.updateBtn.addEventListener('click', this.onUpdateBtnClick);
+ LDStatus.vars.themeBtn.addEventListener('click', this.onThemeBtnClick);
+
+ // 页面可见性变化时刷新数据
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
+ },
+
+ // 设置拖动面板的事件
+ setupDragEvents: function() {
+ const header = LDStatus.vars.header;
+
+ header.addEventListener('mousedown', this.onPanelDragStart);
+ document.addEventListener('mousemove', this.onPanelDragMove);
+ document.addEventListener('mouseup', this.onPanelDragEnd);
+ },
+
+ // 面板拖动开始
+ onPanelDragStart: function(e) {
+ if (LDStatus.vars.panel.classList.contains('ld-collapsed')) return;
+
+ LDStatus.vars.isDragging = true;
+ LDStatus.vars.lastX = e.clientX;
+ LDStatus.vars.lastY = e.clientY;
+
+ // 添加拖动时的样式
+ LDStatus.vars.panel.style.transition = 'none';
+ document.body.style.userSelect = 'none';
+ },
+
+ // 面板拖动中
+ onPanelDragMove: function(e) {
+ if (!LDStatus.vars.isDragging) return;
+
+ // 使用 transform 而不是改变 left/top 属性,性能更好
+ const dx = e.clientX - LDStatus.vars.lastX;
+ const dy = e.clientY - LDStatus.vars.lastY;
+
+ const currentTransform = window.getComputedStyle(LDStatus.vars.panel).transform;
+ const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform);
+
+ const newX = matrix.e + dx;
+ const newY = matrix.f + dy;
+
+ LDStatus.vars.panel.style.transform = `translate(${newX}px, ${newY}px)`;
+
+ LDStatus.vars.lastX = e.clientX;
+ LDStatus.vars.lastY = e.clientY;
+ },
+
+ // 面板拖动结束
+ onPanelDragEnd: function() {
+ if (!LDStatus.vars.isDragging) return;
+
+ LDStatus.vars.isDragging = false;
+ LDStatus.vars.panel.style.transition = '';
+ document.body.style.userSelect = '';
+
+ // 保存窗口位置
+ LDStatus.storage.savePanelPosition();
+ },
+
+ // 折叠/展开面板按钮点击
+ onToggleBtnClick: function() {
+ const panel = LDStatus.vars.panel;
+ panel.classList.toggle('ld-collapsed');
+ LDStatus.vars.toggleBtn.textContent = panel.classList.contains('ld-collapsed')
+ ? LDStatus.config.icons.EXPAND
+ : LDStatus.config.icons.COLLAPSE;
+
+ // 保存折叠状态
+ LDStatus.storage.savePanelCollapsedState();
+ },
+
+ // 刷新按钮点击
+ onRefreshBtnClick: function() {
+ LDStatus.data.fetchTrustLevelData();
+ },
+
+ // 更新按钮点击
+ onUpdateBtnClick: function() {
+ LDStatus.data.checkForUpdates();
+ },
+
+ // 主题按钮点击
+ onThemeBtnClick: function() {
+ const panel = LDStatus.vars.panel;
+ const isDarkTheme = panel.classList.contains('ld-dark-theme');
+
+ // 切换主题类
+ panel.classList.remove(isDarkTheme ? 'ld-dark-theme' : 'ld-light-theme');
+ panel.classList.add(isDarkTheme ? 'ld-light-theme' : 'ld-dark-theme');
+
+ // 更新主题变量
+ LDStatus.vars.isDarkTheme = !isDarkTheme;
+
+ // 更新按钮图标
+ LDStatus.ui.updateThemeButtonIcon();
+
+ // 保存主题设置
+ GM_setValue(LDStatus.config.storageKeys.theme, LDStatus.vars.isDarkTheme ? 'dark' : 'light');
+ },
+
+ // 页面可见性变化处理
+ onVisibilityChange: function() {
+ if (!document.hidden) {
+ // 检查上次刷新时间,如果超过指定间隔则刷新数据
+ const lastRefreshTime = LDStatus.vars.lastRefreshTime || 0;
+ const now = Date.now();
+
+ if (now - lastRefreshTime > LDStatus.config.refreshInterval) {
+ LDStatus.data.fetchTrustLevelData();
+ LDStatus.vars.lastRefreshTime = now;
+ }
+ }
+ }
+ },
+
+ // 存储相关方法
+ storage: {
+ // 初始化存储
+ initStorage: function() {
+ // 恢复面板位置
+ this.restorePanelPosition();
+
+ // 恢复折叠状态
+ this.restorePanelCollapsedState();
+ },
+
+ // 保存面板位置
+ savePanelPosition: function() {
+ const style = window.getComputedStyle(LDStatus.vars.panel);
+ const transform = style.transform;
+
+ if (transform !== 'none') {
+ GM_setValue(LDStatus.config.storageKeys.position, transform);
+ }
+ },
+
+ // 恢复面板位置
+ restorePanelPosition: function() {
+ const savedPosition = GM_getValue(LDStatus.config.storageKeys.position, null);
+
+ if (savedPosition) {
+ LDStatus.vars.panel.style.transform = savedPosition;
+ }
+ },
+
+ // 保存面板折叠状态
+ savePanelCollapsedState: function() {
+ const isCollapsed = LDStatus.vars.panel.classList.contains('ld-collapsed');
+ GM_setValue(LDStatus.config.storageKeys.collapsed, isCollapsed);
+ },
+
+ // 恢复面板折叠状态
+ restorePanelCollapsedState: function() {
+ const isCollapsed = GM_getValue(LDStatus.config.storageKeys.collapsed, false);
+
+ if (isCollapsed) {
+ LDStatus.vars.panel.classList.add('ld-collapsed');
+ LDStatus.vars.toggleBtn.textContent = LDStatus.config.icons.EXPAND;
+ } else {
+ LDStatus.vars.panel.classList.remove('ld-collapsed');
+ LDStatus.vars.toggleBtn.textContent = LDStatus.config.icons.COLLAPSE;
+ }
}
- });
+ },
+
+ // 初始化方法
+ init: function() {
+ // 初始化 UI
+ this.ui.createStyles();
+ this.ui.createPanel();
- return result;
- }
+ // 初始化存储
+ this.storage.initStorage();
- // 初始加载
- fetchTrustLevelData();
+ // 注册事件
+ this.events.registerEvents();
- // 恢复窗口状态
- // 在所有DOM操作完成后执行,确保 toggleBtn 已经定义
- setTimeout(restorePanelState, 100);
+ // 获取数据
+ this.data.fetchTrustLevelData();
+
+ // 设置定时刷新
+ LDStatus.vars.refreshTimer = setInterval(function() {
+ LDStatus.data.fetchTrustLevelData();
+ LDStatus.vars.lastRefreshTime = Date.now();
+ }, this.config.refreshInterval);
+
+ // 记录初始化时间
+ LDStatus.vars.lastRefreshTime = Date.now();
+ }
+ };
- // 定时刷新(每两分钟)
- setInterval(fetchTrustLevelData, 120000);
+ // 初始化脚本
+ LDStatus.init();
})();
24小时内的活动
- `;
-
- // 添加每个数据项
- const dailyStatsItems = [
- { name: '浏览话题', key: '浏览的话题(所有时间)' },
- { name: '回复话题', key: '回复的话题' },
- { name: '已读帖子', key: '已读帖子(所有时间)' },
- { name: '获得点赞', key: '获赞:点赞用户数量' },
- { name: '点赞帖子', key: '点赞的帖子' }
- ];
-
- dailyStatsItems.forEach(item => {
- const value = dailyChanges[item.key] || 0;
- html += `
-
- ${item.name}
- ${value}
-
- `;
- });
-
- html += ` |