├── LDStatus.user.js
└── README.md
/LDStatus.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name LDStatus
3 | // @namespace http://tampermonkey.net/
4 | // @version 1.11
5 | // @description 在 Linux.do 页面显示信任级别进度
6 | // @author 1e0n
7 | // @match https://linux.do/*
8 | // @grant GM_xmlhttpRequest
9 | // @grant GM_setValue
10 | // @grant GM_getValue
11 | // @grant GM_info
12 | // @connect connect.linux.do
13 | // @connect github.com
14 | // @connect raw.githubusercontent.com
15 | // @updateURL https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js
16 | // @downloadURL https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js
17 | // ==/UserScript==
18 |
19 | (function() {
20 | 'use strict';
21 |
22 | // 创建样式 - 使用更特定的选择器以避免影响帖子界面的按钮
23 | const style = document.createElement('style');
24 | style.textContent = `
25 | /* 深色主题 */
26 | #ld-trust-level-panel.ld-dark-theme {
27 | background-color: #2d3748;
28 | color: #e2e8f0;
29 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
30 | }
31 |
32 | #ld-trust-level-panel.ld-dark-theme #ld-trust-level-header {
33 | background-color: #1a202c;
34 | color: white;
35 | }
36 |
37 | #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-success .ld-value {
38 | color: #68d391;
39 | }
40 |
41 | #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-fail .ld-value {
42 | color: #fc8181;
43 | }
44 |
45 | #ld-trust-level-panel.ld-dark-theme .ld-loading {
46 | color: #a0aec0;
47 | }
48 |
49 | #ld-trust-level-panel.ld-dark-theme .ld-daily-stats-title {
50 | color: #a0aec0;
51 | }
52 |
53 | #ld-trust-level-panel.ld-dark-theme .ld-daily-stats-item .ld-value {
54 | color: #68d391;
55 | }
56 |
57 | #ld-trust-level-panel.ld-dark-theme .ld-version {
58 | color: #a0aec0;
59 | }
60 |
61 | /* 亮色主题 - 提高对比度 */
62 | #ld-trust-level-panel.ld-light-theme {
63 | background-color: #ffffff;
64 | color: #1a202c;
65 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
66 | border: 1px solid #e2e8f0;
67 | }
68 |
69 | #ld-trust-level-panel.ld-light-theme #ld-trust-level-header {
70 | background-color: #3182ce; /* 更深的蓝色 */
71 | color: #ffffff;
72 | border-bottom: 1px solid #2c5282; /* 添加底部边框 */
73 | }
74 |
75 | #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-success .ld-value {
76 | color: #276749; /* 更深的绿色 */
77 | font-weight: bold;
78 | }
79 |
80 | #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-fail .ld-value {
81 | color: #c53030;
82 | font-weight: bold;
83 | }
84 |
85 | /* 亮色主题下的文本颜色 */
86 | #ld-trust-level-panel.ld-light-theme .ld-name {
87 | color: #2d3748; /* 深灰色 */
88 | }
89 |
90 | #ld-trust-level-panel.ld-light-theme .ld-loading {
91 | color: #4a5568;
92 | }
93 |
94 | #ld-trust-level-panel.ld-light-theme .ld-daily-stats-title {
95 | color: #4a5568;
96 | font-weight: bold;
97 | }
98 |
99 | #ld-trust-level-panel.ld-light-theme .ld-daily-stats-item .ld-value {
100 | color: #2c7a4b;
101 | font-weight: bold;
102 | }
103 |
104 | #ld-trust-level-panel.ld-light-theme .ld-version {
105 | color: #e2e8f0;
106 | }
107 |
108 | /* 共用样式 */
109 | #ld-trust-level-panel {
110 | position: fixed;
111 | left: 10px;
112 | top: 100px;
113 | width: 210px;
114 | border-radius: 8px;
115 | z-index: 9999;
116 | font-family: Arial, sans-serif;
117 | transition: all 0.3s ease;
118 | overflow: hidden;
119 | font-size: 12px;
120 | }
121 |
122 | #ld-trust-level-header {
123 | padding: 8px 10px;
124 | cursor: move;
125 | display: flex;
126 | justify-content: space-between;
127 | align-items: center;
128 | user-select: none;
129 | }
130 |
131 | .ld-header-content {
132 | display: flex;
133 | width: 100%;
134 | align-items: center;
135 | justify-content: space-between;
136 | white-space: nowrap;
137 | }
138 |
139 | .ld-header-content > span:first-child {
140 | margin-right: auto;
141 | font-weight: bold;
142 | }
143 |
144 | #ld-trust-level-content {
145 | padding: 10px;
146 | max-height: none;
147 | overflow-y: visible;
148 | }
149 |
150 | .ld-trust-level-item {
151 | margin-bottom: 6px;
152 | display: flex;
153 | white-space: nowrap;
154 | width: 100%;
155 | justify-content: space-between;
156 | }
157 |
158 | .ld-trust-level-item .ld-name {
159 | flex: 0 1 auto;
160 | overflow: hidden;
161 | text-overflow: ellipsis;
162 | max-width: 60%;
163 | }
164 |
165 | .ld-trust-level-item .ld-value {
166 | font-weight: bold;
167 | flex: 0 0 auto;
168 | text-align: right;
169 | min-width: 70px;
170 | }
171 |
172 | /* 这些样式已移动到主题特定样式中 */
173 |
174 | .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn {
175 | background: none;
176 | border: none;
177 | color: white;
178 | cursor: pointer;
179 | font-size: 14px;
180 | margin-left: 5px;
181 | }
182 |
183 | .ld-version {
184 | font-size: 10px;
185 | color: #a0aec0;
186 | margin-left: 5px;
187 | font-weight: normal;
188 | }
189 |
190 | .ld-collapsed {
191 | width: 40px !important;
192 | height: 40px !important;
193 | min-width: 40px !important;
194 | max-width: 40px !important;
195 | border-radius: 8px;
196 | overflow: hidden;
197 | transform: none !important;
198 | }
199 |
200 | .ld-collapsed #ld-trust-level-header {
201 | justify-content: center;
202 | width: 40px !important;
203 | height: 40px !important;
204 | min-width: 40px !important;
205 | max-width: 40px !important;
206 | padding: 0;
207 | display: flex;
208 | align-items: center;
209 | }
210 |
211 | .ld-collapsed #ld-trust-level-header > div {
212 | justify-content: center;
213 | width: 100%;
214 | height: 100%;
215 | }
216 |
217 | .ld-collapsed #ld-trust-level-content {
218 | display: none !important;
219 | }
220 |
221 | .ld-collapsed .ld-header-content > span,
222 | .ld-collapsed .ld-refresh-btn,
223 | .ld-collapsed .ld-update-btn,
224 | .ld-collapsed .ld-theme-btn,
225 | .ld-collapsed .ld-version {
226 | display: none !important;
227 | }
228 |
229 | .ld-collapsed .ld-toggle-btn {
230 | margin: 0;
231 | font-size: 16px;
232 | display: flex;
233 | justify-content: center;
234 | align-items: center;
235 | width: 100%;
236 | height: 100%;
237 | }
238 |
239 | .ld-loading {
240 | text-align: center;
241 | padding: 10px;
242 | }
243 |
244 | /* 深色主题下的变化指示器 */
245 | .ld-dark-theme .ld-increase {
246 | color: #ffd700; /* 黄色 */
247 | }
248 |
249 | .ld-dark-theme .ld-decrease {
250 | color: #4299e1; /* 蓝色 */
251 | }
252 |
253 | /* 亮色主题下的变化指示器 */
254 | .ld-light-theme .ld-increase {
255 | color: #d69e2e; /* 深黄色 */
256 | font-weight: bold;
257 | }
258 |
259 | .ld-light-theme .ld-decrease {
260 | color: #2b6cb0; /* 深蓝色 */
261 | font-weight: bold;
262 | }
263 |
264 | /* 所有主题下的活动数据区域 */
265 | .ld-daily-stats {
266 | margin-top: 10px;
267 | font-size: 11px;
268 | }
269 |
270 | /* 深色主题下的分隔线 */
271 | .ld-dark-theme .ld-daily-stats {
272 | border-top: 1px solid #4a5568;
273 | padding-top: 10px;
274 | }
275 |
276 | /* 亮色主题下的分隔线 */
277 | .ld-light-theme .ld-daily-stats {
278 | border-top: 1px solid #cbd5e0;
279 | padding-top: 10px;
280 | }
281 |
282 | .ld-daily-stats-title {
283 | font-weight: bold;
284 | margin-bottom: 5px;
285 | }
286 |
287 | .ld-daily-stats-item {
288 | display: flex;
289 | justify-content: space-between;
290 | margin-bottom: 4px;
291 | }
292 |
293 | .ld-daily-stats-item .ld-name {
294 | flex: 0 1 auto;
295 | color: inherit;
296 | }
297 |
298 | .ld-daily-stats-item .ld-value {
299 | flex: 0 0 auto;
300 | font-weight: bold;
301 | color: inherit;
302 | }
303 |
304 | /* 添加两天数据的样式 */
305 | .ld-dual-stats {
306 | display: flex;
307 | justify-content: flex-end;
308 | gap: 5px;
309 | min-width: 70px;
310 | text-align: right;
311 | }
312 |
313 | .ld-day-stat {
314 | min-width: 25px;
315 | width: 25px;
316 | text-align: right;
317 | display: inline-block;
318 | }
319 |
320 | .ld-day1 {
321 | color: #68d391; /* 跟上部一致的绿色 */
322 | }
323 |
324 | .ld-day2 {
325 | color: #a0aec0; /* 跟上部一致的灰色 */
326 | }
327 |
328 | .ld-light-theme .ld-day1 {
329 | color: #276749; /* 浅色主题下与主面板绿色一致 */
330 | }
331 |
332 | .ld-light-theme .ld-day2 {
333 | color: #2d3748; /* 浅色主题下更深的灰色 */
334 | }
335 |
336 | .ld-dark-theme .ld-day2 {
337 | color: #cbd5e1; /* 深色主题下更亮的灰色,增强可读性 */
338 | }
339 |
340 | .ld-trend-indicator {
341 | margin-left: 2px;
342 | display: inline-block;
343 | min-width: 25px;
344 | width: 25px;
345 | text-align: left;
346 | }
347 |
348 | .ld-stats-header {
349 | display: flex;
350 | justify-content: space-between;
351 | margin-bottom: 6px;
352 | font-size: 10px;
353 | color: inherit;
354 | }
355 |
356 | .ld-light-theme .ld-stats-header {
357 | color: #2d3748;
358 | }
359 |
360 | .ld-dark-theme .ld-stats-header {
361 | color: #e2e8f0;
362 | }
363 |
364 | .ld-stats-header-cols {
365 | display: flex;
366 | gap: 5px;
367 | min-width: 70px;
368 | justify-content: flex-end;
369 | }
370 |
371 | .ld-stats-header-col {
372 | min-width: 25px;
373 | width: 25px;
374 | text-align: center;
375 | }
376 |
377 | .ld-stats-header-trend {
378 | min-width: 25px;
379 | width: 25px;
380 | text-align: center;
381 | }
382 | `;
383 | document.head.appendChild(style);
384 |
385 | // 定义存储键
386 | const STORAGE_KEY_POSITION = 'ld_panel_position';
387 | const STORAGE_KEY_COLLAPSED = 'ld_panel_collapsed';
388 | const STORAGE_KEY_THEME = 'ld_panel_theme';
389 |
390 | // 创建面板
391 | const panel = document.createElement('div');
392 | panel.id = 'ld-trust-level-panel';
393 |
394 | // 设置默认主题
395 | const currentTheme = GM_getValue(STORAGE_KEY_THEME, 'dark');
396 | panel.classList.add(currentTheme === 'dark' ? 'ld-dark-theme' : 'ld-light-theme');
397 |
398 | // 获取脚本版本号
399 | const scriptVersion = GM_info.script.version;
400 |
401 | // 创建面板头部
402 | const header = document.createElement('div');
403 | header.id = 'ld-trust-level-header';
404 | header.innerHTML = `
405 |
413 | `;
414 |
415 | // 创建内容区域
416 | const content = document.createElement('div');
417 | content.id = 'ld-trust-level-content';
418 | content.innerHTML = '加载中...
';
419 |
420 | // 组装面板
421 | panel.appendChild(header);
422 | panel.appendChild(content);
423 | document.body.appendChild(panel);
424 |
425 | // 保存窗口位置的函数
426 | function savePanelPosition() {
427 | const transform = window.getComputedStyle(panel).transform;
428 | if (transform && transform !== 'none') {
429 | const matrix = new DOMMatrix(transform);
430 | GM_setValue(STORAGE_KEY_POSITION, { x: matrix.e, y: matrix.f });
431 | }
432 | }
433 |
434 | // 保存窗口折叠状态的函数
435 | function savePanelCollapsedState() {
436 | GM_setValue(STORAGE_KEY_COLLAPSED, panel.classList.contains('ld-collapsed'));
437 | }
438 |
439 | // 恢复窗口状态
440 | function restorePanelState() {
441 | // 恢复折叠状态
442 | const isCollapsed = GM_getValue(STORAGE_KEY_COLLAPSED, false);
443 | if (isCollapsed) {
444 | panel.classList.add('ld-collapsed');
445 | toggleBtn.textContent = '▶'; // 右箭头
446 | } else {
447 | panel.classList.remove('ld-collapsed');
448 | toggleBtn.textContent = '◀'; // 左箭头
449 | }
450 |
451 | // 恢复位置
452 | const position = GM_getValue(STORAGE_KEY_POSITION, null);
453 | if (position) {
454 | panel.style.transform = `translate(${position.x}px, ${position.y}px)`;
455 | }
456 | }
457 |
458 | // 拖动功能
459 | let isDragging = false;
460 | let lastX, lastY;
461 |
462 | header.addEventListener('mousedown', (e) => {
463 | if (panel.classList.contains('ld-collapsed')) return;
464 |
465 | isDragging = true;
466 | lastX = e.clientX;
467 | lastY = e.clientY;
468 |
469 | // 添加拖动时的样式
470 | panel.style.transition = 'none';
471 | document.body.style.userSelect = 'none';
472 | });
473 |
474 | document.addEventListener('mousemove', (e) => {
475 | if (!isDragging) return;
476 |
477 | // 使用 transform 而不是改变 left/top 属性,性能更好
478 | const dx = e.clientX - lastX;
479 | const dy = e.clientY - lastY;
480 |
481 | const currentTransform = window.getComputedStyle(panel).transform;
482 | const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform);
483 |
484 | const newX = matrix.e + dx;
485 | const newY = matrix.f + dy;
486 |
487 | panel.style.transform = `translate(${newX}px, ${newY}px)`;
488 |
489 | lastX = e.clientX;
490 | lastY = e.clientY;
491 | });
492 |
493 | document.addEventListener('mouseup', () => {
494 | if (!isDragging) return;
495 |
496 | isDragging = false;
497 | panel.style.transition = '';
498 | document.body.style.userSelect = '';
499 |
500 | // 保存窗口位置
501 | savePanelPosition();
502 | });
503 |
504 | // 展开/收起功能
505 | const toggleBtn = header.querySelector('.ld-toggle-btn');
506 | toggleBtn.addEventListener('click', () => {
507 | panel.classList.toggle('ld-collapsed');
508 | toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀';
509 |
510 | // 保存折叠状态
511 | savePanelCollapsedState();
512 | });
513 |
514 | // 刷新按钮
515 | const refreshBtn = header.querySelector('.ld-refresh-btn');
516 | refreshBtn.addEventListener('click', fetchTrustLevelData);
517 |
518 | // 检查更新按钮
519 | const updateBtn = header.querySelector('.ld-update-btn');
520 | updateBtn.addEventListener('click', checkForUpdates);
521 |
522 | // 主题切换按钮
523 | const themeBtn = header.querySelector('.ld-theme-btn');
524 | themeBtn.addEventListener('click', toggleTheme);
525 |
526 | // 更新主题按钮图标
527 | updateThemeButtonIcon();
528 |
529 | // 切换主题函数
530 | function toggleTheme() {
531 | const isDarkTheme = panel.classList.contains('ld-dark-theme');
532 |
533 | // 切换主题类
534 | panel.classList.remove(isDarkTheme ? 'ld-dark-theme' : 'ld-light-theme');
535 | panel.classList.add(isDarkTheme ? 'ld-light-theme' : 'ld-dark-theme');
536 |
537 | // 保存主题设置
538 | GM_setValue(STORAGE_KEY_THEME, isDarkTheme ? 'light' : 'dark');
539 |
540 | // 更新主题按钮图标
541 | updateThemeButtonIcon();
542 | }
543 |
544 | // 更新主题按钮图标
545 | function updateThemeButtonIcon() {
546 | const isDarkTheme = panel.classList.contains('ld-dark-theme');
547 | themeBtn.textContent = isDarkTheme ? '🌙' : '☀️'; // 月亮或太阳图标
548 | themeBtn.title = isDarkTheme ? '切换为亮色主题' : '切换为深色主题';
549 |
550 | // 在亮色主题下调整按钮颜色
551 | if (!isDarkTheme) {
552 | document.querySelectorAll('.ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn').forEach(btn => {
553 | btn.style.color = 'white'; // 亮色主题下按钮使用白色,因为标题栏是蓝色
554 | btn.style.textShadow = '0 0 1px rgba(0,0,0,0.3)'; // 添加文字阴影增强可读性
555 | });
556 | } else {
557 | document.querySelectorAll('.ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn').forEach(btn => {
558 | btn.style.color = 'white';
559 | btn.style.textShadow = 'none';
560 | });
561 | }
562 | }
563 |
564 | // 检查脚本更新
565 | function checkForUpdates() {
566 | const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js';
567 |
568 | // 显示正在检查的状态
569 | updateBtn.textContent = '⌛'; // 沙漏图标
570 | updateBtn.title = '正在检查更新...';
571 |
572 | GM_xmlhttpRequest({
573 | method: 'GET',
574 | url: updateURL,
575 | onload: function(response) {
576 | if (response.status === 200) {
577 | // 提取远程脚本的版本号
578 | const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/);
579 | if (versionMatch && versionMatch[1]) {
580 | const remoteVersion = versionMatch[1];
581 |
582 | // 比较版本
583 | if (remoteVersion > scriptVersion) {
584 | // 有新版本
585 | updateBtn.textContent = '⚠️'; // 警告图标
586 | updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`;
587 | updateBtn.style.color = '#ffd700'; // 黄色
588 |
589 | // 点击按钮跳转到更新页面
590 | updateBtn.onclick = function() {
591 | window.open(updateURL, '_blank');
592 | };
593 | } else {
594 | // 已是最新版本
595 | updateBtn.textContent = '✔'; // 勾选图标
596 | updateBtn.title = '已是最新版本';
597 | updateBtn.style.color = '#68d391'; // 绿色
598 |
599 | // 3秒后恢复原样式
600 | setTimeout(() => {
601 | updateBtn.textContent = '🔎'; // 放大镜图标
602 | updateBtn.title = '检查更新';
603 | updateBtn.style.color = 'white';
604 | updateBtn.onclick = checkForUpdates;
605 | }, 3000);
606 | }
607 | } else {
608 | handleUpdateError();
609 | }
610 | } else {
611 | handleUpdateError();
612 | }
613 | },
614 | onerror: handleUpdateError
615 | });
616 |
617 | // 处理更新检查错误
618 | function handleUpdateError() {
619 | updateBtn.textContent = '❌'; // 错误图标
620 | updateBtn.title = '检查更新失败,请稍后再试';
621 | updateBtn.style.color = '#fc8181'; // 红色
622 |
623 | // 3秒后恢复原样式
624 | setTimeout(() => {
625 | updateBtn.textContent = '🔎'; // 放大镜图标
626 | updateBtn.title = '检查更新';
627 | updateBtn.style.color = 'white';
628 | }, 3000);
629 | }
630 | }
631 |
632 | // 获取信任级别数据
633 | function fetchTrustLevelData() {
634 | content.innerHTML = '加载中...
';
635 |
636 | GM_xmlhttpRequest({
637 | method: 'GET',
638 | url: 'https://connect.linux.do',
639 | onload: function(response) {
640 | if (response.status === 200) {
641 | parseTrustLevelData(response.responseText);
642 | } else {
643 | content.innerHTML = '获取数据失败,请稍后再试
';
644 | }
645 | },
646 | onerror: function() {
647 | content.innerHTML = '获取数据失败,请稍后再试
';
648 | }
649 | });
650 | }
651 |
652 | // 解析信任级别数据
653 | function parseTrustLevelData(html) {
654 | const parser = new DOMParser();
655 | const doc = parser.parseFromString(html, 'text/html');
656 |
657 | // 查找信任级别区块
658 | const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => {
659 | const heading = div.querySelector('h2');
660 | return heading && heading.textContent.includes('信任级别');
661 | });
662 |
663 | if (!trustLevelSection) {
664 | content.innerHTML = '未找到信任级别数据,请确保已登录
';
665 | return;
666 | }
667 |
668 | // 获取用户名和当前级别
669 | const heading = trustLevelSection.querySelector('h2').textContent.trim();
670 | const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/);
671 | const username = match ? match[1] : '未知用户';
672 | const targetLevel = match ? match[2] : '未知';
673 |
674 | // 获取表格数据
675 | const tableRows = trustLevelSection.querySelectorAll('table tr');
676 | const requirements = [];
677 |
678 | for (let i = 1; i < tableRows.length; i++) { // 跳过表头
679 | const row = tableRows[i];
680 | const cells = row.querySelectorAll('td');
681 |
682 | if (cells.length >= 3) {
683 | const name = cells[0].textContent.trim();
684 | const current = cells[1].textContent.trim();
685 | const required = cells[2].textContent.trim();
686 | const isSuccess = cells[1].classList.contains('text-green-500');
687 |
688 | // 提取当前完成数的数字部分
689 | const currentMatch = current.match(/(\d+)/);
690 | const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0;
691 |
692 | // 查找上一次的数据记录
693 | let changeValue = 0;
694 | let hasChanged = false;
695 |
696 | if (previousRequirements.length > 0) {
697 | const prevReq = previousRequirements.find(pr => pr.name === name);
698 | if (prevReq) {
699 | // 如果完成数有变化,更新变化值
700 | if (currentValue !== prevReq.currentValue) {
701 | changeValue = currentValue - prevReq.currentValue;
702 | hasChanged = true;
703 | } else if (prevReq.changeValue) {
704 | // 如果完成数没有变化,但之前有变化值,保留之前的变化值
705 | changeValue = prevReq.changeValue;
706 | hasChanged = true;
707 | }
708 | }
709 | }
710 |
711 | requirements.push({
712 | name,
713 | current,
714 | required,
715 | isSuccess,
716 | currentValue,
717 | changeValue, // 变化值
718 | hasChanged // 是否有变化
719 | });
720 | }
721 | }
722 |
723 | // 获取总体结果
724 | const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500');
725 | const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false;
726 |
727 | // 存储自然日的活动数据
728 | const dailyChanges = saveDailyStats(requirements);
729 |
730 | // 渲染数据
731 | renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges);
732 |
733 | // 保存当前数据作为下次比较的基准
734 | previousRequirements = [...requirements];
735 | }
736 |
737 | // 渲染信任级别数据
738 | function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) {
739 | let html = `
740 |
741 | ${username} - 信任级别 ${targetLevel}
742 |
743 |
744 | ${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求
745 |
746 | `;
747 |
748 | requirements.forEach(req => {
749 | // 简化项目名称
750 | let name = req.name;
751 | // 将一些常见的长名称缩短
752 | name = name.replace('已读帖子(所有时间)', '已读帖子(总)');
753 | name = name.replace('浏览的话题(所有时间)', '浏览话题(总)');
754 | name = name.replace('获赞:点赞用户数量', '点赞用户数');
755 | name = name.replace('获赞:单日最高数量', '总获赞天数');
756 | name = name.replace('被禁言(过去 6 个月)', '被禁言');
757 | name = name.replace('被封禁(过去 6 个月)', '被封禁');
758 |
759 | // 提取数字部分以简化显示
760 | let current = req.current;
761 | let required = req.required;
762 |
763 | // 尝试从字符串中提取数字
764 | const currentMatch = req.current.match(/(\d+)/);
765 | const requiredMatch = req.required.match(/(\d+)/);
766 |
767 | if (currentMatch) current = currentMatch[1];
768 | if (requiredMatch) required = requiredMatch[1];
769 |
770 | // 添加目标完成数变化的标识
771 | let changeIndicator = '';
772 | if (req.hasChanged) {
773 | const diff = req.changeValue;
774 | if (diff > 0) {
775 | changeIndicator = ` ▲${diff}`; // 增加标识,黄色
776 | } else if (diff < 0) {
777 | changeIndicator = ` ▼${Math.abs(diff)}`; // 减少标识,蓝色
778 | }
779 | }
780 |
781 | html += `
782 |
783 | ${name}
784 | ${current}${changeIndicator} / ${required}
785 |
786 | `;
787 | });
788 |
789 | // 添加近期活动数据显示
790 | html += `
791 |
792 |
近期的活动
793 |
801 | `;
802 |
803 | // 添加每个数据项
804 | const dailyStatsItems = [
805 | { name: '浏览话题', key: '浏览的话题(所有时间)' },
806 | { name: '回复话题', key: '回复的话题' },
807 | { name: '已读帖子', key: '已读帖子(所有时间)' },
808 | { name: '获得点赞', key: '获赞:点赞用户数量' },
809 | { name: '点赞帖子', key: '点赞' }
810 | ];
811 |
812 | dailyStatsItems.forEach(item => {
813 | const data = dailyChanges[item.key] || { day1: 0, day2: 0, trend: 0 };
814 |
815 | // 创建趋势指示器
816 | let trendIndicator = '';
817 | if (data.trend > 0) {
818 | trendIndicator = `
▲${Math.abs(data.trend)}`;
819 | } else if (data.trend < 0) {
820 | trendIndicator = `
▼${Math.abs(data.trend)}`;
821 | } else {
822 | trendIndicator = `
0`;
823 | }
824 |
825 | html += `
826 |
827 | ${item.name}
828 |
829 |
830 | ${data.day2}
831 | ${data.day1}
832 | ${trendIndicator}
833 |
834 |
835 |
836 | `;
837 | });
838 |
839 | html += `
`;
840 |
841 | content.innerHTML = html;
842 | }
843 |
844 | // 存储上一次获取的数据,用于比较变化
845 | let previousRequirements = [];
846 |
847 | // 存储自然日的活动数据
848 | function saveDailyStats(requirements) {
849 | // 定义要跟踪的数据项
850 | const statsToTrack = [
851 | '浏览的话题(所有时间)', // 浏览话题总数
852 | '回复的话题', // 回复话题数
853 | '已读帖子(所有时间)', // 已读帖子总数
854 | '获赞:点赞用户数量', // 获赞数
855 | '点赞' // 点赞数
856 | ];
857 |
858 | // 调试信息:输出所有数据项的名称
859 | console.log('数据项名称:', requirements.map(r => r.name));
860 | console.log('要跟踪的数据项:', statsToTrack);
861 |
862 | // 获取当前时间和日期边界
863 | const now = new Date();
864 | const currentTime = now.getTime();
865 | const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); // 今天0点
866 | const yesterdayStart = todayStart - (24 * 60 * 60 * 1000); // 昨天0点
867 |
868 | // 从 localStorage 中获取已存储的数据
869 | let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]');
870 |
871 | // 优化存储:只保留昨天和今天的数据点,且每天只保留自然日的第一个和最后一个数据点
872 | const newDailyStats = [];
873 |
874 | // 对于每个数据项,分别处理
875 | statsToTrack.forEach(statName => {
876 | // 过滤出当前数据项的所有记录
877 | const allRecords = dailyStats.filter(item => item.name === statName);
878 | if (allRecords.length === 0) {
879 | return; // 跳过这个数据项
880 | }
881 |
882 | // 查找今天和昨天自然日范围内的最早和最晚记录
883 | const todayRecords = allRecords.filter(item => item.timestamp >= todayStart);
884 | const yesterdayRecords = allRecords.filter(item =>
885 | item.timestamp >= yesterdayStart && item.timestamp < todayStart);
886 |
887 | // 处理昨天的数据:只保留最早和最晚的记录
888 | if (yesterdayRecords.length > 0) {
889 | // 查找昨天的第一条记录
890 | const firstRecord = yesterdayRecords.reduce((earliest, current) =>
891 | current.timestamp < earliest.timestamp ? current : earliest, yesterdayRecords[0]);
892 |
893 | // 查找昨天的最后一条记录
894 | const lastRecord = yesterdayRecords.reduce((latest, current) =>
895 | current.timestamp > latest.timestamp ? current : latest, yesterdayRecords[0]);
896 |
897 | // 如果最早和最晚的记录不同,则都保留
898 | newDailyStats.push(firstRecord);
899 | if (lastRecord !== firstRecord) {
900 | newDailyStats.push(lastRecord);
901 | }
902 | }
903 |
904 | // 处理今天的数据:保留最早的记录和当前这个最新记录
905 | if (todayRecords.length > 0) {
906 | // 查找今天的第一条记录
907 | const firstRecord = todayRecords.reduce((earliest, current) =>
908 | current.timestamp < earliest.timestamp ? current : earliest, todayRecords[0]);
909 |
910 | // 保留今天最早的记录
911 | newDailyStats.push(firstRecord);
912 | }
913 |
914 | // 添加当前最新记录
915 | const req = requirements.find(r => r.name === statName);
916 | if (req) {
917 | // 提取数字值
918 | const currentMatch = req.current.match(/(\d+)/);
919 | const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0;
920 |
921 | // 创建新记录
922 | const newRecord = {
923 | name: statName,
924 | value: currentValue,
925 | timestamp: currentTime
926 | };
927 |
928 | // 如果今天还没有记录,或者当前值与最新记录不同,则添加
929 | const latestRecord = todayRecords.length > 0 ?
930 | todayRecords.reduce((latest, current) =>
931 | current.timestamp > latest.timestamp ? current : latest, todayRecords[0]) : null;
932 |
933 | if (!latestRecord || latestRecord.value !== currentValue) {
934 | newDailyStats.push(newRecord);
935 | }
936 | }
937 | });
938 |
939 | // 保存优化后的数据
940 | console.log('优化后的存储数据长度:', newDailyStats.length, '(之前:', dailyStats.length, ')');
941 | console.log('存储的数据详情:', JSON.stringify(newDailyStats.map(item => ({
942 | name: item.name,
943 | value: item.value,
944 | date: new Date(item.timestamp).toLocaleString()
945 | }))));
946 | localStorage.setItem('ld_daily_stats', JSON.stringify(newDailyStats));
947 |
948 | return calculateDailyChanges(newDailyStats);
949 | }
950 |
951 | // 计算自然日内的活动数据
952 | function calculateDailyChanges(dailyStats) {
953 | // 定义要跟踪的数据项
954 | const statsToTrack = [
955 | '浏览的话题(所有时间)', // 浏览话题总数
956 | '回复的话题', // 回复话题数
957 | '已读帖子(所有时间)', // 已读帖子总数
958 | '获赞:点赞用户数量', // 获赞数
959 | '点赞' // 点赞数
960 | ];
961 |
962 | const result = {};
963 |
964 | // 获取今天和昨天的日期范围
965 | const now = new Date();
966 | const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); // 今天0点
967 | const yesterdayStart = todayStart - (24 * 60 * 60 * 1000); // 昨天0点
968 | const yesterdayEnd = todayStart - 1; // 昨天23:59:59
969 |
970 | console.log('时间范围: ',
971 | '今天起始:', new Date(todayStart).toLocaleString(),
972 | '昨天起始:', new Date(yesterdayStart).toLocaleString(),
973 | '昨天结束:', new Date(yesterdayEnd).toLocaleString()
974 | );
975 |
976 | // 对于每个要跟踪的数据项,计算今天和昨天的变化
977 | statsToTrack.forEach(statName => {
978 | // 初始化结果对象结构
979 | result[statName] = {
980 | day1: 0, // 今天的变化
981 | day2: 0, // 昨天的变化
982 | trend: 0 // 趋势(今天与昨天的差异)
983 | };
984 |
985 | // 过滤出当前数据项的所有记录
986 | const allRecords = dailyStats.filter(item => item.name === statName);
987 | if (allRecords.length === 0) {
988 | return; // 跳过这个数据项
989 | }
990 |
991 | // 查找今天和昨天自然日范围内的最早和最晚记录
992 | const todayRecords = allRecords.filter(item => item.timestamp >= todayStart);
993 | const yesterdayRecords = allRecords.filter(item =>
994 | item.timestamp >= yesterdayStart && item.timestamp <= yesterdayEnd);
995 |
996 | // 找到今天的最早记录和最新记录
997 | const todayFirstRecord = todayRecords.length > 0 ?
998 | todayRecords.reduce((earliest, current) =>
999 | current.timestamp < earliest.timestamp ? current : earliest, todayRecords[0]) : null;
1000 |
1001 | const todayLastRecord = todayRecords.length > 0 ?
1002 | todayRecords.reduce((latest, current) =>
1003 | current.timestamp > latest.timestamp ? current : latest, todayRecords[0]) : null;
1004 |
1005 | // 找到昨天的最早记录和最晚记录
1006 | const yesterdayFirstRecord = yesterdayRecords.length > 0 ?
1007 | yesterdayRecords.reduce((earliest, current) =>
1008 | current.timestamp < earliest.timestamp ? current : earliest, yesterdayRecords[0]) : null;
1009 |
1010 | const yesterdayLastRecord = yesterdayRecords.length > 0 ?
1011 | yesterdayRecords.reduce((latest, current) =>
1012 | current.timestamp > latest.timestamp ? current : latest, yesterdayRecords[0]) : null;
1013 |
1014 | // 计算今天的变化值(最新记录 - 今天最早记录或昨天最晚记录)
1015 | if (todayLastRecord) {
1016 | const baseValue = todayFirstRecord ?
1017 | todayFirstRecord.value :
1018 | (yesterdayLastRecord ? yesterdayLastRecord.value : 0);
1019 |
1020 | result[statName].day1 = todayLastRecord.value - baseValue;
1021 | }
1022 |
1023 | // 计算昨天的变化值
1024 | if (yesterdayFirstRecord && yesterdayLastRecord) {
1025 | // 如果昨天有多个记录点,用最晚记录 - 最早记录
1026 | result[statName].day2 = yesterdayLastRecord.value - yesterdayFirstRecord.value;
1027 | } else if (yesterdayFirstRecord) {
1028 | // 如果昨天只有一个记录点,尝试用这个记录点减去前天的最后记录点
1029 | // 获取前天的结束时间
1030 | const dayBeforeYesterdayStart = yesterdayStart - (24 * 60 * 60 * 1000); // 前天0点
1031 | const dayBeforeYesterdayEnd = yesterdayStart - 1; // 前天23:59:59
1032 |
1033 | // 查找前天的记录
1034 | const dayBeforeYesterdayRecords = allRecords.filter(item =>
1035 | item.timestamp >= dayBeforeYesterdayStart && item.timestamp <= dayBeforeYesterdayEnd);
1036 |
1037 | if (dayBeforeYesterdayRecords.length > 0) {
1038 | // 找到前天的最后一条记录
1039 | const lastDayBeforeYesterdayRecord = dayBeforeYesterdayRecords.reduce((latest, current) =>
1040 | current.timestamp > latest.timestamp ? current : latest, dayBeforeYesterdayRecords[0]);
1041 |
1042 | // 计算昨天的变化:昨天唯一记录 - 前天最后记录
1043 | result[statName].day2 = yesterdayFirstRecord.value - lastDayBeforeYesterdayRecord.value;
1044 | } else {
1045 | // 如果没有前天的记录,则直接使用昨天的记录值作为变化值
1046 | // 这是一种近似处理,至少确保昨天的数据不会是0
1047 | result[statName].day2 = yesterdayFirstRecord.value;
1048 | }
1049 | }
1050 |
1051 | // 计算趋势(今天与昨天的差异)
1052 | result[statName].trend = result[statName].day1 - result[statName].day2;
1053 |
1054 | // 添加每个数据项的详细日志
1055 | console.log(`${statName} 数据计算:`, {
1056 | '今天记录数': todayRecords.length,
1057 | '昨天记录数': yesterdayRecords.length,
1058 | '今天第一条': todayFirstRecord ?
1059 | `${todayFirstRecord.value} (${new Date(todayFirstRecord.timestamp).toLocaleString()})` : '无',
1060 | '今天最后一条': todayLastRecord ?
1061 | `${todayLastRecord.value} (${new Date(todayLastRecord.timestamp).toLocaleString()})` : '无',
1062 | '昨天第一条': yesterdayFirstRecord ?
1063 | `${yesterdayFirstRecord.value} (${new Date(yesterdayFirstRecord.timestamp).toLocaleString()})` : '无',
1064 | '昨天最后一条': yesterdayLastRecord ?
1065 | `${yesterdayLastRecord.value} (${new Date(yesterdayLastRecord.timestamp).toLocaleString()})` : '无',
1066 | '今天变化': result[statName].day1,
1067 | '昨天变化': result[statName].day2,
1068 | '趋势': result[statName].trend
1069 | });
1070 | });
1071 |
1072 | console.log('自然日变化数据:', result);
1073 | return result;
1074 | }
1075 |
1076 | // 初始加载
1077 | fetchTrustLevelData();
1078 |
1079 | // 恢复窗口状态和主题
1080 | // 在所有DOM操作完成后执行,确保 toggleBtn 和 themeBtn 已经定义
1081 | setTimeout(() => {
1082 | restorePanelState();
1083 | updateThemeButtonIcon();
1084 | }, 100);
1085 |
1086 | // 定时刷新(每五分钟)
1087 | setInterval(fetchTrustLevelData, 300000);
1088 | })();
1089 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LDStatus
2 |
3 | LDStatus 是一个油猴脚本,用于在浏览 Linux.do 网站时显示用户的信任级别进度。通过这个脚本,您可以实时查看自己的信任级别进度,而无需频繁切换到 connect.linux.do 页面。
4 |
5 | ## 功能特点
6 |
7 | - **浮动窗口**:在 Linux.do 页面左侧显示一个可拖动的浮动窗口
8 | - **实时数据**:从 connect.linux.do 自动获取信任级别数据
9 | - **清晰展示**:以"目标: 已完成数 / 需要完成数"的形式展示数据,并显示24小时内的活动数据
10 | - **折叠功能**:支持窗口折叠为小图标,不影响浏览体验
11 | - **自动刷新**:每五分钟自动刷新数据,保持信息最新
12 | - **可拖动**:支持拖动调整窗口位置,放置在您喜欢的位置
13 | - **直观颜色**:绿色数字表示已达成目标,红色数字表示未达成目标
14 | - **主题切换**:支持深色和亮色两种主题,可根据个人喜好随时切换
15 | - **自动更新**:支持脚本自动更新,无需手动重新安装即可获取最新版本
16 | - **状态记忆**:自动记忆窗口位置和折叠状态,下次访问时自动恢复
17 |
18 | ## 安装方法
19 |
20 | ### 前提条件
21 |
22 | 在安装脚本之前,您需要先安装一个用户脚本管理器扩展。推荐使用 Tampermonkey,它支持大多数主流浏览器:
23 |
24 | - [Chrome 版 Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)
25 | - [Firefox 版 Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/)
26 | - [Edge 版 Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd)
27 | - [Safari 版 Tampermonkey](https://apps.apple.com/app/apple-store/id1482490089)
28 |
29 | ### 方法一:直接安装(推荐)
30 |
31 | 1. 确保您已经安装了 Tampermonkey 或其他用户脚本管理器
32 | 2. 点击以下链接直接安装脚本:
33 | - [安装 LDStatus 脚本](https://github.com/1e0n/LinuxDoStatus/raw/master/LDStatus.user.js)
34 | 3. Tampermonkey 将自动检测并提示您安装脚本
35 | 4. 点击"安装"按钮完成安装
36 | 5. 安装后,脚本将自动检测更新,无需手动重新安装
37 |
38 | ### 方法二:手动安装
39 |
40 | 1. 安装 Tampermonkey 浏览器扩展
41 | 2. 访问 [LDStatus.user.js 文件](https://github.com/1e0n/LinuxDoStatus/blob/master/LDStatus.user.js)
42 | 3. 点击"Raw"按钮查看原始文件
43 | 4. Tampermonkey 应该会自动检测并提示安装
44 | 5. 如果没有自动提示,请手动复制文件内容
45 |
46 | ### 方法三:手动复制粘贴
47 |
48 | 1. 安装 Tampermonkey 浏览器扩展
49 | 2. 点击浏览器工具栏中的 Tampermonkey 图标
50 | 3. 选择"添加新脚本"
51 | 4. 删除编辑器中的所有默认代码
52 | 5. 将 [LDStatus.user.js](https://github.com/1e0n/LinuxDoStatus/blob/master/LDStatus.user.js) 的内容复制并粘贴到编辑器中
53 | 6. 点击"文件"菜单,然后选择"保存"
54 |
55 | ## 使用方法
56 |
57 | 安装脚本后,访问 [Linux.do](https://linux.do) 网站,脚本将自动运行并在页面左侧显示信任级别浮动窗口。
58 |
59 | - **展开/折叠**:点击窗口右上角的箭头按钮可以展开/折叠窗口
60 | - **刷新数据**:点击刷新按钮可以手动刷新数据(脚本也会每五分钟自动刷新)
61 | - **切换主题**:点击主题切换按钮(🌙/☀️)可以在深色和亮色主题之间切换
62 | - **移动窗口**:拖动窗口标题栏可以调整窗口位置
63 | - **查看进度**:绿色数字表示已达成目标,红色数字表示未达成目标
64 | - **变化标识**:当目标完成数有变化时,会显示黄色的⬆(增加)或蓝色的⬇(减少)标识及变化数值,即使刷新后数值没有变化也会保留标识
65 | - **活动统计**:在窗口底部显示24小时内的活动数据,包括浏览话题数、回复话题数、已读帖子数、获赞数和点赞数
66 |
67 | ## 注意事项
68 |
69 | - 脚本需要您已经登录 Linux.do 账号
70 | - 如果数据加载失败,请确保您已登录并刷新页面
71 | - 脚本仅在 Linux.do 域名下运行,不会在其他网站上激活
72 |
73 | ## 自动更新
74 |
75 | LDStatus 脚本支持自动更新功能。当 GitHub 仓库中的脚本版本更新后,您的浏览器将自动检测并提示您更新到最新版本。这意味着您无需手动重新安装脚本即可获得最新功能和修复。
76 |
77 | 自动更新的工作原理:
78 | 1. 脚本每天会自动检查是否有新版本
79 | 2. 如果发现新版本,Tampermonkey 会提示您更新
80 | 3. 点击更新按钮即可完成更新,无需重新访问 GitHub
81 |
82 | ## 更新日志
83 | ### v1.11
84 | - 文本优化,“获赞:单日最高数量”按照原始英文解释,这里应该指的是"获得过赞的总天数"
85 |
86 | ### v1.10
87 | - 完全重构时间统计逻辑,改用自然日计算替代相对时间窗口
88 | - "今天"一栏现在显示今天0点到现在的数据变化
89 | - "昨天"一栏现在显示昨天一整天(0-24点)的数据变化
90 | - 优化数据存储机制,每天只保留首末两个数据点,显著减少存储压力
91 | - 自动刷新间隔保持为5分钟
92 |
93 | ### v1.9
94 | - "近期的活动"区块支持显示最近两天(昨天/今天)的数据,并以列形式展示,增加趋势箭头和变化数值,便于对比每日活跃度。
95 | - 所有数字列和表头均右对齐,视觉更整齐。
96 | - 新增表头,明确每列含义(昨天/今天/变化)。
97 | - 亮色和暗色主题下,所有下部(近期的活动)文本和数值颜色与上部保持一致,风格统一。
98 | - 优化暗色主题下昨天数值的颜色,提升可读性。
99 | - 优化亮色主题下昨天和今天的数值颜色,提升对比度和一致性。
100 | - 数据自动刷新时间从两分钟改为五分钟,减少服务器压力。
101 | - 主题切换、面板拖动、折叠、刷新、自动更新等功能保持兼容。
102 |
103 | ### v1.8
104 | - 添加了亮色和深色两种主题,可以根据个人喜好切换
105 | - 在标题栏添加了主题切换按钮(🌙/☀️图标)
106 | - 修复了点赞帖子数据显示不正确的问题
107 | - 优化了数据处理逻辑,提高了数据准确性
108 |
109 | ### v1.7
110 | - 在窗口标题添加了当前脚本的版本号
111 | - 在窗口右上角添加了一个检查更新按钮(🔎图标)
112 | - 修复了之前自动更新会失败的问题
113 |
114 | ### v1.6
115 | - 添加窗口状态记忆功能,自动记忆窗口位置和折叠状态
116 |
117 | ### v1.5
118 | - 添加自动更新功能,脚本现在可以自动检测并更新到最新版本
119 |
120 | ### v1.4
121 | - 添加24小时内活动数据统计功能
122 | - 在浮动窗口底部显示用户24小时内的浏览话题数、回复话题数、已读帖子数、获赞数和点赞数
123 |
124 | ### v1.3
125 | - 修复帖子界面按钮消失的问题
126 | - 使用更特定的CSS选择器避免与网站原有元素冲突
127 |
128 | ### v1.2
129 | - 改进目标完成数变化的标识功能,即使刷新后数值没有变化也会保留标识
130 | - 增加变化标识的颜色:黄色表示增加,蓝色表示减少
131 |
132 | ### v1.1
133 | - 将数据刷新时间从每分钟改为每两分钟
134 | - 添加目标完成数变化的标识功能(⬆表示增加,⬇表示减少)
135 |
136 | ### v1.0
137 | - 初始版本发布
138 | - 实现基本的信任级别数据获取和显示
139 | - 添加浮动窗口和折叠功能
140 | - 支持自动刷新和手动刷新
141 |
142 | ## 反馈与贡献
143 |
144 | 如果您有任何问题、建议或反馈,请在 [GitHub Issues](https://github.com/1e0n/LinuxDoStatus/issues) 上提交。
145 |
146 | 欢迎通过 Pull Requests 贡献代码改进脚本。
147 |
--------------------------------------------------------------------------------