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-03-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Linux do Level Enhanced
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  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
  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. ABOUT_DATA_URL: 'https://linux.do/about.json',
  42. USER_DATA_URL_PREFIX: 'https://linux.do/u/',
  43. USER_DATA_URL_SUFFIX: '/summary.json',
  44. FETCH_REQUEST_OPTIONS: {
  45. headers: { "Accept": "application/json", "User-Agent": "Mozilla/5.0" },
  46. method: "GET",
  47. },
  48. levelRequirements: {
  49. 0: { 'topics_entered': 5, 'posts_read_count': 30, 'time_read': 600 },
  50. 1: { 'days_visited': 15, 'likes_given': 1, 'likes_received': 1, 'post_count': 3, 'topics_entered': 20, 'posts_read_count': 100, 'time_read': 3600 },
  51. 2: { 'days_visited': 50, 'likes_given': 30, 'likes_received': 20, 'post_count': 10 }
  52. },
  53. levelDescriptions: {
  54. 0: "游客", 1: "基本用户", 2: "成员", 3: "活跃用户", 4: "领导者"
  55. },
  56.  
  57. fetchAboutData: async function() {
  58. try {
  59. const response = await fetch(this.ABOUT_DATA_URL, this.FETCH_REQUEST_OPTIONS);
  60. if (!response.ok) throw new Error(`HTTP 错误!状态:${response.status}`);
  61. return await response.json();
  62. } catch (error) {
  63. UIManager.displayError("获取关于页面数据失败");
  64. throw error;
  65. }
  66. },
  67.  
  68. fetchUserData: async function(username) {
  69. try {
  70. const url = `${this.USER_DATA_URL_PREFIX}${username}${this.USER_DATA_URL_SUFFIX}`;
  71. const response = await fetch(url, this.FETCH_REQUEST_OPTIONS);
  72. if (!response.ok) throw new Error(`HTTP 错误!状态:${response.status}`);
  73. return await response.json();
  74. } catch (error) {
  75. UIManager.displayError("获取用户数据失败");
  76. throw error;
  77. }
  78. }
  79. };
  80.  
  81. const UIManager = {
  82. initPopup: function() {
  83. this.popup = this.createElement('div', { id: 'linuxDoLevelPopup', class: 'linuxDoLevelPopup' });
  84. this.content = this.createElement('div', { id: 'linuxDoLevelPopupContent' }, '欢迎使用 Linux do 等级增强插件');
  85. this.searchBox = this.createElement('input', { placeholder: '请输入用户名...', type: 'text', class: 'searchBox' });
  86. this.searchButton = this.createElement('button', { class: 'searchButton' }, '搜索');
  87. this.minimizeButton = this.createElement('button', { }, '隐藏');
  88. this.popup.style.bottom = '20px'; // 示例:距离顶部20px
  89. this.popup.style.right = '20px'; // 示例:距离左侧20px
  90. this.popup.style.width = '250px'; // 初始化宽度
  91. this.popup.style.height = 'auto'; // 高度自适应内容
  92. this.searchButton.classList.add('btn', 'btn-icon-text', 'btn-default')
  93. this.minimizeButton.classList.add('btn', 'btn-icon-text', 'btn-default')
  94.  
  95. this.popup.append(this.content, this.searchBox, this.searchButton, this.minimizeButton);
  96. document.body.appendChild(this.popup);
  97.  
  98. this.minimizeButton.addEventListener('click', () => this.togglePopupSize());
  99. this.searchButton.addEventListener('click', () => EventHandler.handleSearch());
  100. // 添加输入框的回车键事件监听器
  101. this.searchBox.addEventListener('keypress', (event) => {
  102. // 检查是否按下了回车键并且弹窗不处于最小化状态
  103. if (event.key === 'Enter' && !this.popup.classList.contains('minimized')) {
  104. EventHandler.handleSearch();
  105. }
  106. });
  107.  
  108. var checkInterval = setInterval(function() {
  109. // 查找id为current-user的li元素
  110. var currentUserLi = document.querySelector('#current-user');
  111.  
  112. // 如果找到了元素
  113. if(currentUserLi) {
  114. // 查找该元素下的button
  115. var button = currentUserLi.querySelector('button');
  116.  
  117. // 如果找到了button元素
  118. if(button) {
  119. // 获取button的href属性值
  120. var href = button.getAttribute('href');
  121. UIManager.searchBox.value = href.replace('/u/', '');
  122. clearInterval(checkInterval); // 停止检查
  123. // 这里你可以根据需要对href进行进一步操作
  124. }
  125. }
  126. }, 1000); // 每隔1秒检查一次
  127. },
  128.  
  129. createElement: function(tag, attributes, text) {
  130. const element = document.createElement(tag);
  131. for (const attr in attributes) {
  132. if (attr === 'class') {
  133. element.classList.add(attributes[attr]);
  134. } else {
  135. element.setAttribute(attr, attributes[attr]);
  136. }
  137. }
  138. if (text) element.textContent = text;
  139. return element;
  140. },
  141.  
  142. updatePopupContent: function(userSummary, user, status) {
  143. if (!userSummary || !user) return;
  144.  
  145. let content = `<strong>信任等级:</strong>${DataManager.levelDescriptions[user.trust_level]}<br><strong>升级进度:</strong><br>`;
  146. const requirements = DataManager.levelRequirements[user.trust_level] || {};
  147.  
  148. if (user.trust_level === 2) {
  149. requirements['posts_read_count'] = Math.min(parseInt(parseInt(status.posts_30_days) / 4), 20000);
  150. requirements['topics_entered'] = Math.min(parseInt(parseInt(status.topics_30_days) / 4), 500);
  151. }
  152.  
  153. if (user.trust_level === 3) {
  154. content += '联系管理员进行py交易以升级到领导者<br>';
  155. } else if (user.trust_level === 4) {
  156. content += '您已是最高信任等级<br>';
  157. } else {
  158. let summary = summaryRequired(requirements, userSummary, this.translateStat.bind(this));
  159. content += summary;
  160. }
  161. this.content.innerHTML = content;
  162. },
  163.  
  164. togglePopupSize: function() {
  165. if (this.popup.classList.contains('minimized')) {
  166. this.popup.classList.remove('minimized');
  167. this.popup.style.width = '250px';
  168. this.popup.style.height = 'auto';
  169. this.content.style.display = 'block';
  170. this.searchBox.style.display = 'block';
  171. this.searchButton.style.display = 'block';
  172. this.minimizeButton.textContent = '隐藏';
  173. this.popup.classList.remove('breath-animation');
  174. } else {
  175. this.popup.classList.add('minimized');
  176. this.popup.style.width = '50px';
  177. this.popup.style.height = '50px';
  178. this.content.style.display = 'none';
  179. this.searchBox.style.display = 'none';
  180. this.searchButton.style.display = 'none';
  181. this.minimizeButton.textContent = '展开';
  182. this.popup.classList.add('breath-animation');
  183. }
  184. // 确保拖动时更新整个弹窗的位置而不仅是底部
  185. addDraggableFeature(this.popup);
  186. const windowWidth = window.innerWidth;
  187. const windowHeight = window.innerHeight;
  188. const popupWidth = this.popup.offsetWidth;
  189. const popupHeight = this.popup.offsetHeight;
  190. const popupTop = parseInt(this.popup.style.top);
  191. const popupLeft = parseInt(this.popup.style.left);
  192.  
  193. let newTop = popupTop;
  194. let newLeft = popupLeft;
  195. if (!this.popup.classList.contains('minimized')) {
  196. // 如果不是最小化状态
  197. if (popupTop + popupHeight > windowHeight) {
  198. newTop = windowHeight - popupHeight - 25;
  199. }
  200.  
  201. if (popupLeft + popupWidth > windowWidth) {
  202. newLeft = windowWidth - popupWidth - 20;
  203. }
  204.  
  205. if (popupTop < 60) {
  206. newTop = 70;
  207. }
  208.  
  209. if (popupLeft < 0) {
  210. newLeft = 5;
  211. }
  212. }
  213. else {
  214. // 如果是最小化状态
  215. if (popupTop - popupHeight < 60) {
  216. newTop = 70;
  217. }
  218.  
  219. if (popupLeft < 0) {
  220. newLeft = 5;
  221. }
  222.  
  223. if (popupLeft + popupWidth > windowWidth) {
  224. newLeft = windowWidth - popupWidth - 20;
  225. }
  226. }
  227. this.popup.style.top = newTop + 'px';
  228. this.popup.style.left = newLeft + 'px';
  229. },
  230.  
  231. displayError: function(message) {
  232. this.content.innerHTML = `<strong>错误:</strong>${message}`;
  233. },
  234.  
  235. translateStat: function(stat) {
  236. const translations = {
  237. 'days_visited': '访问天数',
  238. 'likes_given': '给出的赞',
  239. 'likes_received': '收到的赞',
  240. 'post_count': '帖子数量',
  241. 'posts_read_count': '阅读的帖子数',
  242. 'topics_entered': '进入的主题数',
  243. 'time_read': '阅读时间(秒)'
  244. };
  245. return translations[stat] || stat;
  246. }
  247. };
  248.  
  249. const EventHandler = {
  250. handleSearch: async function() {
  251. const username = UIManager.searchBox.value.trim();
  252. if (!username) return;
  253.  
  254. try {
  255. const aboutData = await DataManager.fetchAboutData();
  256. const userData = await DataManager.fetchUserData(username);
  257. if (userData && aboutData) {
  258. UIManager.updatePopupContent(userData.user_summary, userData.users[0], aboutData.about.stats);
  259. }
  260. } catch (error) {
  261. console.error(error); // Already handled within data manager functions
  262. }
  263. },
  264. // 更新拖动状态
  265. handleDragEnd: function() {
  266. UIManager.updateDragStatus(true);
  267. }
  268. };
  269.  
  270. // 添加用户升级进度总结
  271. function summaryRequired(required, current, translateStat) {
  272. let summary = '';
  273. let allMet = true;
  274.  
  275. for (const stat in required) {
  276. if (required.hasOwnProperty(stat) && current.hasOwnProperty(stat)) {
  277. const reqValue = required[stat];
  278. const curValue = current[stat] || 0; // 使用 || 0 确保未定义的情况下使用0
  279. if (curValue < reqValue) {
  280. allMet = false;
  281. const diff = reqValue - curValue;
  282. summary += `${translateStat(stat)}: <br><span style="color: red;">当前值: ${curValue}, 需求值: ${reqValue}, 差额: ${diff}</span><br>`;
  283. } else {
  284. // 如果当前值满足或超过了要求值,也打印出来,但使用不同的颜色或提示信息
  285. summary += `${translateStat(stat)}: <br><span style="color: green;">当前值: ${curValue}, 需求值: ${reqValue}, 已合格</span><br>`;
  286. }
  287. }
  288. }
  289.  
  290. if (allMet) {
  291. return "恭喜您!所有项次都已达到合格标准。<br>" + summary;
  292. } else {
  293. return summary;
  294. }
  295. }
  296.  
  297. // 添加拖动功能
  298. function addDraggableFeature(element) {
  299. let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  300.  
  301. const dragMouseDown = function(e) {
  302. // 检查事件的目标是否是输入框,按钮或其他可以忽略拖动逻辑的元素
  303. if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA' || e.target.tagName.toUpperCase() === 'BUTTON') {
  304. return; // 如果是,则不执行拖动逻辑
  305. }
  306.  
  307. e = e || window.event;
  308. e.preventDefault();
  309. pos3 = e.clientX;
  310. pos4 = e.clientY;
  311. document.onmouseup = closeDragElement;
  312. document.onmousemove = elementDrag;
  313. };
  314.  
  315. const elementDrag = function(e) {
  316. e = e || window.event;
  317. e.preventDefault();
  318. pos1 = pos3 - e.clientX;
  319. pos2 = pos4 - e.clientY;
  320. pos3 = e.clientX;
  321. pos4 = e.clientY;
  322.  
  323. element.style.top = (element.offsetTop - pos2) + "px";
  324. element.style.left = (element.offsetLeft - pos1) + "px";
  325. // 为了避免与拖动冲突,在此移除bottom和right样式
  326. element.style.bottom = '';
  327. element.style.right = '';
  328. };
  329.  
  330. const closeDragElement = function() {
  331. document.onmouseup = null;
  332. document.onmousemove = null;
  333. // 在拖动结束时更新拖动状态
  334. EventHandler.handleDragEnd();
  335. };
  336.  
  337. element.onmousedown = dragMouseDown;
  338. }
  339.  
  340. const init = () => {
  341. StyleManager.injectStyles();
  342. UIManager.initPopup();
  343. addDraggableFeature(document.getElementById('linuxDoLevelPopup')); // 确保已设置该ID
  344. UIManager.togglePopupSize(); // 初始最小化
  345. };
  346.  
  347. init();
  348.  
  349. })();