Linux do Level Enhanced

Enhanced script to track progress towards next trust level on linux.do with added search functionality, adjusted posts read limit, and a breathing icon animation.

目前為 2024-04-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Linux do Level Enhanced
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.5
  5. // @description Enhanced script to track progress towards next trust level on linux.do with added search functionality, adjusted posts read limit, and a breathing icon animation.
  6. // @author Hua, Reno, NullUser
  7. // @match https://linux.do/*
  8. // @icon https://www.google.com/s2/favicons?domain=linux.do
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const StyleManager = {
  17. styles: `
  18. @keyframes breathAnimation {
  19. 0%, 100% { transform: scale(1); box-shadow: 0 0 5px rgba(0,0,0,0.5); }
  20. 50% { transform: scale(1.1); box-shadow: 0 0 10px rgba(0,0,0,0.7); }
  21. }
  22. .breath-animation { animation: breathAnimation 4s ease-in-out infinite; }
  23. .minimized { border-radius: 50%; cursor: pointer; }
  24. .linuxDoLevelPopup { position: fixed; width: 250px; height: 150px; background: var(--d-sidebar-background); box-shadow: 0 0 10px rgba(0,0,0,0.5); padding: 15px; z-index: 10000; font-size: 14px; border-radius: 5px; cursor: move; }
  25. .linuxDoLevelPopup input, .linuxDoLevelPopup button { width: 100%; margin-top: 10px; }
  26. .linuxDoLevelPopup button { cursor: pointer; }
  27. .minimizeButton { position: absolute; top: 5px; right: 5px; background: transparent; border: none; cursor: pointer; width: 30px; height: 30px; font-size: 16px; }
  28. .searchButton { width: 100%; marginTop: 10px }
  29. .searchBox { width: 100%; marginTop: 10px }
  30. `,
  31.  
  32. injectStyles: function() {
  33. const styleSheet = document.createElement('style');
  34. styleSheet.type = 'text/css';
  35. styleSheet.innerText = this.styles;
  36. document.head.appendChild(styleSheet);
  37. }
  38. };
  39.  
  40. const DataManager = {
  41. Config: {
  42. BASE_URL: 'https://linux.do',
  43. PATHS: {
  44. ABOUT: '/about.json',
  45. USER_SUMMARY: '/u/{username}/summary.json',
  46. USER_DETAIL: '/u/{username}.json',
  47. },
  48. },
  49.  
  50. levelRequirements: {
  51. 0: { 'topics_entered': 5, 'posts_read_count': 30, 'time_read': 600 },
  52. 1: { 'days_visited': 15, 'likes_given': 1, 'likes_received': 1, 'post_count': 3, 'topics_entered': 20, 'posts_read_count': 100, 'time_read': 3600 },
  53. 2: { 'days_visited': 50, 'likes_given': 30, 'likes_received': 20, 'post_count': 10 },
  54. },
  55.  
  56. levelDescriptions: {
  57. 0: "新用户 🌱",
  58. 1: "基本用户 ⭐ ",
  59. 2: "成员 ⭐⭐",
  60. 3: "活跃用户 ⭐⭐⭐",
  61. 4: "领导者 🏆"
  62. },
  63.  
  64. fetch: async function(url, options = {}) {
  65. try {
  66. const response = await fetch(url, {
  67. ...options,
  68. headers: { "Accept": "application/json", "User-Agent": "Mozilla/5.0" },
  69. method: options.method || "GET",
  70. });
  71. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  72. return await response.json();
  73. } catch (error) {
  74. console.error(`Error fetching data from ${url}:`, error);
  75. throw error;
  76. }
  77. },
  78.  
  79. fetchAboutData: function() {
  80. const url = this.buildUrl(this.Config.PATHS.ABOUT);
  81. return this.fetch(url);
  82. },
  83.  
  84. fetchSummaryData: function(username) {
  85. const url = this.buildUrl(this.Config.PATHS.USER_SUMMARY, { username });
  86. return this.fetch(url);
  87. },
  88.  
  89. fetchUserData: function(username) {
  90. const url = this.buildUrl(this.Config.PATHS.USER_DETAIL, { username });
  91. return this.fetch(url);
  92. },
  93.  
  94. buildUrl: function(path, params = {}) {
  95. let url = this.Config.BASE_URL + path;
  96. Object.keys(params).forEach(key => {
  97. url = url.replace(`{${key}}`, encodeURIComponent(params[key]));
  98. });
  99. return url;
  100. },
  101. };
  102.  
  103. const UIManager = {
  104. initPopup: function() {
  105. this.popup = this.createElement('div', { id: 'linuxDoLevelPopup', class: 'linuxDoLevelPopup' });
  106. this.content = this.createElement('div', { id: 'linuxDoLevelPopupContent' }, '欢迎使用 Linux do 等级增强插件');
  107. this.searchBox = this.createElement('input', { placeholder: '请输入用户名...', type: 'text', class: 'searchBox' });
  108. this.searchButton = this.createElement('button', { class: 'searchButton' }, '搜索');
  109. this.minimizeButton = this.createElement('button', { }, '隐藏');
  110. this.popup.style.bottom = '20px'; // 示例:距离顶部20px
  111. this.popup.style.right = '20px'; // 示例:距离左侧20px
  112. this.popup.style.width = '250px'; // 初始化宽度
  113. this.popup.style.height = 'auto'; // 高度自适应内容
  114. this.searchButton.classList.add('btn', 'btn-icon-text', 'btn-default')
  115. this.minimizeButton.classList.add('btn', 'btn-icon-text', 'btn-default')
  116.  
  117. this.popup.append(this.content, this.searchBox, this.searchButton, this.minimizeButton);
  118. document.body.appendChild(this.popup);
  119.  
  120. this.minimizeButton.addEventListener('click', () => this.togglePopupSize());
  121. this.searchButton.addEventListener('click', () => EventHandler.handleSearch());
  122. // 添加输入框的回车键事件监听器
  123. this.searchBox.addEventListener('keypress', (event) => {
  124. // 检查是否按下了回车键并且弹窗不处于最小化状态
  125. if (event.key === 'Enter' && !this.popup.classList.contains('minimized')) {
  126. EventHandler.handleSearch();
  127. }
  128. });
  129.  
  130. var checkInterval = setInterval(function() {
  131. // 查找id为current-user的li元素
  132. var currentUserLi = document.querySelector('#current-user');
  133.  
  134. // 如果找到了元素
  135. if(currentUserLi) {
  136. // 查找该元素下的button
  137. var button = currentUserLi.querySelector('button');
  138.  
  139. // 如果找到了button元素
  140. if(button) {
  141. // 获取button的href属性值
  142. var href = button.getAttribute('href');
  143. UIManager.searchBox.value = href.replace('/u/', '');
  144. clearInterval(checkInterval); // 停止检查
  145. // 这里你可以根据需要对href进行进一步操作
  146. }
  147. }
  148. }, 1000); // 每隔1秒检查一次
  149. },
  150.  
  151. createElement: function(tag, attributes, text) {
  152. const element = document.createElement(tag);
  153. for (const attr in attributes) {
  154. if (attr === 'class') {
  155. element.classList.add(attributes[attr]);
  156. } else {
  157. element.setAttribute(attr, attributes[attr]);
  158. }
  159. }
  160. if (text) element.textContent = text;
  161. return element;
  162. },
  163.  
  164. updatePopupContent: function(userSummary, user, userDetail, status) {
  165. if (!userSummary || !user || !userDetail) return;
  166.  
  167. // 初始化内容字符串,并添加用户信任等级
  168. let content = `<strong>信任等级🏅:</strong>${DataManager.levelDescriptions[user.trust_level]}<br>`;
  169.  
  170. // 获取用户的信任等级要求
  171. const requirements = DataManager.levelRequirements[user.trust_level] || {};
  172.  
  173. // 添加用户的 gamification_score
  174. if (userDetail.gamification_score) {
  175. content += `<strong>你的点数🪙:</strong><span style="color: green;">${userDetail.gamification_score}</span><br>`;
  176. }
  177.  
  178. // 添加用户的最近活跃时间
  179. content += `<strong>最近活跃🕒:</strong>${formatTimestamp(userDetail.last_seen_at)}<br>`;
  180.  
  181. // 处理2级以下用户,调用 summaryRequired 功能
  182. if (user.trust_level <= 2) {
  183. if (user.trust_level === 2) {
  184. requirements['posts_read_count'] = Math.min(parseInt(parseInt(status.posts_30_days) / 4), 20000);
  185. requirements['topics_entered'] = Math.min(parseInt(parseInt(status.topics_30_days) / 4), 500);
  186. }
  187. let summary = summaryRequired(requirements, userSummary, this.translateStat.bind(this));
  188. content += summary;
  189. } else {
  190. // 处理2级以上用户,调用 analyzeAbility 功能
  191. if (userSummary.top_categories) {
  192. content += analyzeAbility(userSummary.top_categories);
  193. }
  194. }
  195.  
  196. // 更新弹窗内容
  197. this.content.innerHTML = content;
  198. },
  199.  
  200. togglePopupSize: function() {
  201. if (this.popup.classList.contains('minimized')) {
  202. this.popup.classList.remove('minimized');
  203. this.popup.style.width = '250px';
  204. this.popup.style.height = 'auto';
  205. this.content.style.display = 'block';
  206. this.searchBox.style.display = 'block';
  207. this.searchButton.style.display = 'block';
  208. this.minimizeButton.textContent = '隐藏';
  209. this.minimizeButton.style.color = 'black';
  210. this.popup.classList.remove('breath-animation');
  211. } else {
  212. this.popup.classList.add('minimized');
  213. this.popup.style.width = '50px';
  214. this.popup.style.height = '50px';
  215. this.content.style.display = 'none';
  216. this.searchBox.style.display = 'none';
  217. this.searchButton.style.display = 'none';
  218. this.popup.classList.add('breath-animation');
  219.  
  220. // 调用 updatePercentage 函数并更新按钮文本
  221. updatePercentage().then(percentage => {
  222. let color;
  223. // 根据百分比设置颜色
  224. if (percentage > 50) {
  225. color = 'red';
  226. } else if (percentage > 30) {
  227. color = 'yellow';
  228. } else {
  229. color = 'green';
  230. }
  231.  
  232. // 更新按钮的文本和文本颜色
  233. this.minimizeButton.textContent = `${percentage.toFixed(2)}%`;
  234. this.minimizeButton.style.color = color; // 设置文本颜色
  235. }).catch(error => {
  236. console.error('Error calculating percentage:', error);
  237. // 出错时保持原有文本
  238. this.minimizeButton.textContent = '展开';
  239. this.minimizeButton.style.color = 'black';
  240. });
  241. }
  242.  
  243. // 自动校正窗口位置
  244. addDraggableFeature(this.popup);
  245. const windowWidth = window.innerWidth;
  246. const windowHeight = window.innerHeight;
  247. const popupWidth = this.popup.offsetWidth;
  248. const popupHeight = this.popup.offsetHeight;
  249. const popupTop = parseInt(this.popup.style.top);
  250. const popupLeft = parseInt(this.popup.style.left);
  251.  
  252. // 初始化新的位置
  253. let newTop = popupTop;
  254. let newLeft = popupLeft;
  255.  
  256. // 上下边界同时检查
  257. newTop = Math.min(Math.max(70, popupTop), windowHeight - popupHeight);
  258.  
  259. // 左右边界同时检查
  260. newLeft = Math.min(Math.max(5, popupLeft), windowWidth - popupWidth - 20);
  261.  
  262. this.popup.style.top = newTop + 'px';
  263. this.popup.style.left = newLeft + 'px';
  264. },
  265.  
  266. displayError: function(message) {
  267. this.content.innerHTML = `<strong>错误:</strong>${message}`;
  268. },
  269.  
  270. translateStat: function(stat) {
  271. const translations = {
  272. 'days_visited': '访问天数',
  273. 'likes_given': '给出的赞',
  274. 'likes_received': '收到的赞',
  275. 'post_count': '帖子数量',
  276. 'posts_read_count': '已读帖子',
  277. 'topics_entered': '已读主题',
  278. 'time_read': '阅读时间(秒)'
  279. };
  280. return translations[stat] || stat;
  281. }
  282. };
  283.  
  284. const EventHandler = {
  285. handleSearch: async function() {
  286. const username = UIManager.searchBox.value.trim();
  287. if (!username) return;
  288.  
  289. try {
  290. const aboutData = await DataManager.fetchAboutData();
  291. const summaryData = await DataManager.fetchSummaryData(username);
  292. const userData = await DataManager.fetchUserData(username);
  293. if (summaryData && userData && aboutData) {
  294. UIManager.updatePopupContent(summaryData.user_summary, summaryData.users ? summaryData.users[0] : { 'trust_level': 0 }, userData.user, aboutData.about.stats);
  295. }
  296. } catch (error) {
  297. console.error(error);
  298. }
  299. },
  300. // 更新拖动状态
  301. handleDragEnd: function() {
  302. UIManager.updateDragStatus(true);
  303. }
  304. };
  305.  
  306. // 添加技能分析
  307. function analyzeAbility(topCategories) {
  308. let resultStr = "<strong>技能分析🎯:</strong><br>";
  309. const icons = {
  310. "常规话题": "🌐",
  311. "wiki": "📚",
  312. "快问快答": "❓",
  313. "人工智能": "🤖",
  314. "周周热点": "🔥",
  315. "精华神贴": "✨",
  316. "高阶秘辛": "🔮",
  317. "读书成诗": "📖",
  318. "配置调优": "⚙️",
  319. "网络安全": "🔒",
  320. "软件分享": "💾",
  321. "软件开发": "💻",
  322. "嵌入式": "🔌",
  323. "机器学习": "🧠",
  324. "代码审查": "👀",
  325. "new-api": "🆕",
  326. "一机难求": "📱",
  327. "速来拼车": "🚗",
  328. "网络记忆": "💭",
  329. "非我莫属": "🏆",
  330. "赏金猎人": "💰",
  331. "搞七捻三": "🎲",
  332. "碎碎碎念": "🗨️",
  333. "金融经济": "💹",
  334. "新闻": "📰",
  335. "旅行": "✈️",
  336. "美食": "🍽️",
  337. "健身": "🏋️",
  338. "音乐": "🎵",
  339. "游戏": "🎮",
  340. "羊毛": "🐑",
  341. "树洞": "🌳",
  342. "病友": "🤒",
  343. "职场": "💼",
  344. "断舍离": "♻️",
  345. "二次元": "🎎",
  346. "运营反馈": "🔄",
  347. "老干部疗养院": "🛌",
  348. "活动": "🎉",
  349. };
  350. const scores = topCategories.map(category => category.topic_count + category.post_count);
  351. const minScore = Math.min(...scores);
  352. const maxScore = Math.max(...scores);
  353. const scoreRange = Math.max(1, maxScore - minScore);
  354. topCategories.sort((a, b) => a.name.length - b.name.length);
  355. topCategories.forEach(category => {
  356. const score = category.topic_count + category.post_count;
  357. const normalizedScore = 1 + (score - minScore) / scoreRange * 9;
  358. const numStars = Math.round(normalizedScore / 2); // Adjusted to fit the 5-star format
  359. const stars = "❤️".repeat(numStars) + "🤍".repeat(5 - numStars); // Adjusted to include empty stars
  360. let icon = icons[category.name] || "❓"; // Default icon if not found
  361. resultStr += `
  362. <div style='display: table-row;'>
  363. <div style='display: table-cell; text-align: left;'>${icon} ${category.name}</div>
  364. <div style='display: table-cell;'>:${stars} (${score}🍀)</div>
  365. </div>`;
  366. });
  367.  
  368. return resultStr;
  369. }
  370.  
  371. // 添加含水率
  372. function updatePercentage() {
  373. return new Promise((resolve, reject) => {
  374. let badIds = [11, 16, 34, 17, 18, 19, 29, 36, 35, 22, 26, 25];
  375. const badScore = [];
  376. const goodScore = [];
  377. const urls = [
  378. 'https://linux.do/latest.json?order=created',
  379. 'https://linux.do/new.json',
  380. 'https://linux.do/top.json?period=daily'
  381. ];
  382.  
  383. Promise.all(urls.map(url => fetch(url).then(resp => resp.json())))
  384. .then(data => {
  385. data.forEach(({ topic_list: { topics } }) => {
  386. topics.forEach(topic => {
  387. const score = topic.posts_count + topic.like_count + topic.reply_count;
  388. (badIds.includes(topic.category_id) ? badScore : goodScore).push(score);
  389. });
  390. });
  391.  
  392. const badTotal = badScore.reduce((acc, curr) => acc + curr, 0);
  393. const goodTotal = goodScore.reduce((acc, curr) => acc + curr, 0);
  394. const percentage = (badTotal / (badTotal + goodTotal)) * 100;
  395.  
  396. resolve(percentage);
  397. })
  398. .catch(reject);
  399. });
  400. };
  401.  
  402. // 添加时间格式化
  403. function formatTimestamp(lastSeenAt) {
  404. // 解析时间戳并去除毫秒
  405. let timestamp = new Date(lastSeenAt);
  406.  
  407. // 使用Intl.DateTimeFormat格式化时间为上海时区
  408. let formatter = new Intl.DateTimeFormat('zh-CN', {
  409. timeZone: 'Asia/Shanghai',
  410. year: 'numeric',
  411. month: 'numeric',
  412. day: 'numeric',
  413. hour: 'numeric',
  414. minute: 'numeric',
  415. second: 'numeric',
  416. });
  417.  
  418. // 获取格式化后的字符串
  419. let formattedTimestamp = formatter.format(timestamp);
  420.  
  421. return formattedTimestamp;
  422. }
  423.  
  424. // 添加用户升级进度总结
  425. function summaryRequired(required, current, translateStat) {
  426. let summary = '<strong>升级进度🌟:</strong><br>';
  427.  
  428. summary += '<table style="width: 100%; text-align: center;">';
  429. summary += '<tr>';
  430. summary += '<th style="text-align: center;">项目</th><th style="text-align: center;">要求</th><th style="text-align: center;">状态</th><th style="text-align: center;">评估</th></tr>';
  431.  
  432. for (const stat in required) {
  433. if (required.hasOwnProperty(stat) && current.hasOwnProperty(stat)) {
  434. const reqValue = required[stat];
  435. const curValue = current[stat] || 0; // 使用 || 0 确保未定义的情况下使用0
  436. let passStatus = curValue >= reqValue ? '✔️' : '❌'; // 使用对钩和红叉代替笑脸和哭脸
  437. let color = curValue >= reqValue ? 'green' : 'red';
  438.  
  439. summary += `<tr>`;
  440. // 在每个单元格样式中也添加文本居中
  441. summary += `<td style="text-align: center;">${translateStat(stat)}</td>`;
  442. summary += `<td style="text-align: center;">${reqValue}</td>`;
  443. summary += `<td style="text-align: center; color: ${color};">${curValue}</td>`;
  444. summary += `<td style="text-align: center;">${passStatus}</td>`;
  445. summary += `</tr>`;
  446. }
  447. }
  448.  
  449. summary += '</table>';
  450. return summary;
  451. }
  452.  
  453. // 添加拖动功能
  454. function addDraggableFeature(element) {
  455. let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  456.  
  457. const dragMouseDown = function(e) {
  458. // 检查事件的目标是否是输入框,按钮或其他可以忽略拖动逻辑的元素
  459. if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA' || e.target.tagName.toUpperCase() === 'BUTTON') {
  460. return; // 如果是,则不执行拖动逻辑
  461. }
  462.  
  463. e = e || window.event;
  464. e.preventDefault();
  465. pos3 = e.clientX;
  466. pos4 = e.clientY;
  467. document.onmouseup = closeDragElement;
  468. document.onmousemove = elementDrag;
  469. };
  470.  
  471. const elementDrag = function(e) {
  472. e = e || window.event;
  473. e.preventDefault();
  474. pos1 = pos3 - e.clientX;
  475. pos2 = pos4 - e.clientY;
  476. pos3 = e.clientX;
  477. pos4 = e.clientY;
  478.  
  479. element.style.top = (element.offsetTop - pos2) + "px";
  480. element.style.left = (element.offsetLeft - pos1) + "px";
  481. // 为了避免与拖动冲突,在此移除bottom和right样式
  482. element.style.bottom = '';
  483. element.style.right = '';
  484. };
  485.  
  486. const closeDragElement = function() {
  487. document.onmouseup = null;
  488. document.onmousemove = null;
  489. // 在拖动结束时更新拖动状态
  490. EventHandler.handleDragEnd();
  491. };
  492.  
  493. element.onmousedown = dragMouseDown;
  494. }
  495.  
  496. const init = () => {
  497. StyleManager.injectStyles();
  498. UIManager.initPopup();
  499. addDraggableFeature(document.getElementById('linuxDoLevelPopup')); // 确保已设置该ID
  500. UIManager.togglePopupSize(); // 初始最小化
  501. };
  502.  
  503. init();
  504.  
  505. })();