【油猴脚本】Linux do 查看距离下一级大体差距

找到了

1 个赞

image
为啥我没得看哇

2 个赞

因为3级往上升就不用看这些了,3级到4级是管理直接操作升

2 个赞

但是三级也需要保号吧
感觉也可以展示一下,我怕哪天掉回去了

1 个赞

已用上,感谢大佬

2 个赞

晋升到信任等级3,在过去的100天里…

  • 必须至少访问了 50% 的天数

  • 必须至少在10个不同的非私信话题上进行了回复

  • 在过去100天内创建的话题中,必须浏览了25%(上限为500)

  • 在过去100天内创建的帖子中,必须阅读了25%(上限为20k)

  • 必须收到20个点赞,并送出30个点赞。*

  • 不得收到超过5个垃圾邮件或冒犯性标记(每个都需不同的帖子和不同的用户,并由版主确认)

  • 过去6个月内不能被暂停或禁言

  • 这些点赞必须来自不同的用户数量最少为用户数的1/5,不同的天数最少为天数的1/4。这些点赞不可以来自私信。

上述所有标准都必须满足才能达到信任等级3。此外,与其他信任等级不同,你可以失去信任等级3的状态。如果在过去100天内你的表现低于这些要求,你将会降级回成员。然而,为了避免频繁的升降级情况,获得信任等级3后,有一个2周的宽限期,在此期间你不会被降级。[1]

这个功能会开发的,请持续关注我和 @Hua 佬的项目。目前论坛成立只有两个月,离100天大清算还有40天,而且清算的是百日的数据,只要你在最近的100天内登足了50天而且没有被举报多次,都没问题的,况且到期会有2周宽限。


  1. 【新人请看】了解Discourse信任度 ↩︎

4 个赞

好嘞 :smiling_face_with_three_hearts:
love u two

1 个赞

来了

谢谢你! 我的朋友, 你才是真正的英雄!


感谢 大佬,这样查起来方便多了,可以随时了解自己的进度

建议调整一下z-index,否则会覆盖在聊天窗口上面。我测试下面的可以正常使用

$('#linuxDoLevelPopup').css({"z-index":100})

主页刷到始皇的查询网站了,再见了我的朋友

这个div是可以拖动的

嗯,我知道的,不过默认显示在那总感觉怪怪地。

求个赞

1 个赞

加了个pk功能,大佬看看行不

// ==UserScript==
// @name         Linux do Level Enhanced
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @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.
// @author       Hua, Reno
// @match        https://linux.do/*
// @icon         https://www.google.com/s2/favicons?domain=linux.do
// @grant        none
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/489393/Linux%20do%20Level%20Enhanced.user.js
// @updateURL https://update.greasyfork.org/scripts/489393/Linux%20do%20Level%20Enhanced.meta.js
// ==/UserScript==

(function() {
    'use strict';

    const StyleManager = {
        styles: `
            @keyframes breathAnimation {
                0%, 100% { transform: scale(1); box-shadow: 0 0 5px rgba(0,0,0,0.5); }
                50% { transform: scale(1.1); box-shadow: 0 0 10px rgba(0,0,0,0.7); }
            }
            .breath-animation { animation: breathAnimation 4s ease-in-out infinite; }
            .minimized { border-radius: 50%; cursor: pointer; }
            .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; }
            .linuxDoLevelPopup input, .linuxDoLevelPopup button { width: 100%; margin-top: 10px; }
            .linuxDoLevelPopup button { cursor: pointer; }
            .minimizeButton { position: absolute; top: 5px; right: 5px; background: transparent; border: none; cursor: pointer; width: 30px; height: 30px; font-size: 16px; }
            .searchButton { width: 100%; marginTop: 10px }
            .pkButton { width: 100%; marginTop: 10px }
            .searchBox { width: 100%; marginTop: 10px }
        `,

        injectStyles: function() {
            const styleSheet = document.createElement('style');
            styleSheet.type = 'text/css';
            styleSheet.innerText = this.styles;
            document.head.appendChild(styleSheet);
        }
    };

    const DataManager = {
        Config: {
            BASE_URL: 'https://linux.do',
            PATHS: {
                ABOUT: '/about.json',
                USER_SUMMARY: '/u/{username}/summary.json',
                USER_DETAIL: '/u/{username}.json',
            },
        },

        levelRequirements: {
            0: { 'topics_entered': 5, 'posts_read_count': 30, 'time_read': 600 },
            1: { 'days_visited': 15, 'likes_given': 1, 'likes_received': 1, 'post_count': 3, 'topics_entered': 20, 'posts_read_count': 100, 'time_read': 3600 },
            2: { 'days_visited': 50, 'likes_given': 30, 'likes_received': 20, 'post_count': 10 },
        },

        pkItems: {
            'days_visited': (x) => DataManager.formatInt(x),
            'likes_given': (x) => DataManager.formatInt(x),
            'likes_received': (x) => DataManager.formatInt(x),
            'post_count': (x) => DataManager.formatInt(x),
            'posts_read_count': (x) => DataManager.formatInt(x),
            'topics_entered': (x) => DataManager.formatInt(x),
            'time_read': (x) => DataManager.formatTime(x)
        },

        levelDescriptions: {
            0: "游客",
            1: "基本用户",
            2: "成员",
            3: "活跃用户",
            4: "领导者"
        },

        formatInt: function(num){
            return num >= 1000 ? `${(num / 1000).toFixed(2)}k` : num;
        },

        formatTime: function(seconds){
            const days = Math.floor(seconds / 86400);
            seconds %= 86400;
            const hours = Math.floor(seconds / 3600);
            seconds %= 3600;
            const minutes = Math.floor(seconds / 60);
            return days > 0 ? `${days}天${hours}小时` : `${hours}小时${minutes}分钟`;
        },

        fetch: async function(url, options = {}) {
            try {
                const response = await fetch(url, {
                    ...options,
                    headers: { "Accept": "application/json", "User-Agent": "Mozilla/5.0" },
                    method: options.method || "GET",
                });
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                return await response.json();
            } catch (error) {
                console.error(`Error fetching data from ${url}:`, error);
                throw error;
            }
        },

        fetchAboutData: function() {
            const url = this.buildUrl(this.Config.PATHS.ABOUT);
            return this.fetch(url);
        },

        fetchSummaryData: function(username) {
            const url = this.buildUrl(this.Config.PATHS.USER_SUMMARY, { username });
            return this.fetch(url);
        },

        fetchUserData: function(username) {
            const url = this.buildUrl(this.Config.PATHS.USER_DETAIL, { username });
            return this.fetch(url);
        },

        buildUrl: function(path, params = {}) {
            let url = this.Config.BASE_URL + path;
            Object.keys(params).forEach(key => {
                url = url.replace(`{${key}}`, encodeURIComponent(params[key]));
            });
            return url;
        },
    };

    const UIManager = {
        initPopup: function() {
            this.popup = this.createElement('div', { id: 'linuxDoLevelPopup', class: 'linuxDoLevelPopup' });
            this.content = this.createElement('div', { id: 'linuxDoLevelPopupContent' }, '欢迎使用 Linux do 等级增强插件');
            this.searchBox = this.createElement('input', { placeholder: '请输入用户名...', type: 'text', class: 'searchBox' });
            this.searchButton = this.createElement('button', { class: 'searchButton' }, '搜索');
            this.pkButton = this.createElement('button', { class: 'pkButton' }, 'PK一下');
            this.minimizeButton = this.createElement('button', { }, '隐藏');
            this.popup.style.bottom = '20px'; // 示例:距离顶部20px
            this.popup.style.right = '20px'; // 示例:距离左侧20px
            this.popup.style.width = '250px'; // 初始化宽度
            this.popup.style.height = 'auto'; // 高度自适应内容
            this.searchButton.classList.add('btn', 'btn-icon-text', 'btn-default')
            this.pkButton.classList.add('btn', 'btn-icon-text', 'btn-default')
            this.minimizeButton.classList.add('btn', 'btn-icon-text', 'btn-default')

            this.popup.append(this.content, this.searchBox, this.searchButton, this.pkButton, this.minimizeButton);
            document.body.appendChild(this.popup);

            this.minimizeButton.addEventListener('click', () => this.togglePopupSize());
            this.searchButton.addEventListener('click', () => EventHandler.handleSearch());
            this.pkButton.addEventListener('click', () => EventHandler.handlePK());
            // 添加输入框的回车键事件监听器
            this.searchBox.addEventListener('keypress', (event) => {
                // 检查是否按下了回车键并且弹窗不处于最小化状态
                if (event.key === 'Enter' && !this.popup.classList.contains('minimized')) {
                    EventHandler.handleSearch();
                }
            });

            var checkInterval = setInterval(function() {
                // 查找id为current-user的li元素
                var currentUserLi = document.querySelector('#current-user');

                // 如果找到了元素
                if(currentUserLi) {
                    // 查找该元素下的button
                    var button = currentUserLi.querySelector('button');

                    // 如果找到了button元素
                    if(button) {
                        // 获取button的href属性值
                        var href = button.getAttribute('href');
                        UIManager.searchBox.value = href.replace('/u/', '');
                        clearInterval(checkInterval); // 停止检查
                        // 这里你可以根据需要对href进行进一步操作
                    }
                }
            }, 1000); // 每隔1秒检查一次
        },

        createElement: function(tag, attributes, text) {
            const element = document.createElement(tag);
            for (const attr in attributes) {
                if (attr === 'class') {
                    element.classList.add(attributes[attr]);
                } else {
                    element.setAttribute(attr, attributes[attr]);
                }
            }
            if (text) element.textContent = text;
            return element;
        },

        updatePopupContent: function(userSummary, userDetail, curUserSummary, curUserDetail, status, isPK) {
            if (!userSummary || !userDetail) return;
            let content = '';
            const requirements = DataManager.levelRequirements[userDetail.trust_level] || {};
            if (isPK){
                content += `<strong>${curUserDetail.username} vs ${userDetail.username}</strong><br>`
                    + `<strong>信任等级:</strong>${DataManager.levelDescriptions[curUserDetail.trust_level]} vs `
                    + `${DataManager.levelDescriptions[userDetail.trust_level]}`;
                if (userDetail.trust_level < curUserDetail.trust_level){
                    content += ', 真的太逊了<br>';
                }else if (userDetail.trust_level == curUserDetail.trust_level){
                    content += ', 旗鼓相当的对手<br>';
                }else{
                    content += ', 这是大佬<br>';
                }
                content += `<strong>${curUserDetail.username} 最近活跃:</strong>${formatTimestamp(curUserDetail.last_seen_at)}<br>` +
                            `<strong>${userDetail.username}最近活跃:</strong>${formatTimestamp(userDetail.last_seen_at)}<br>` + 
                            `<strong>升级进度:</strong><br>`;
                let summary = summaryPK(DataManager.pkItems, curUserSummary, userSummary, this.translateStat.bind(this));
                content += summary;
            }else{
                content += `<strong>信任等级:</strong>${DataManager.levelDescriptions[userDetail.trust_level]}<br>`;
                content += `<strong>邀请人:</strong> ${userDetail.invited_by ? userDetail.invited_by.username : "无"}<br>`;
                content +=`<strong>最近活跃:</strong>${formatTimestamp(userDetail.last_seen_at)}<br> <strong>升级进度:</strong><br>`;
                if (userDetail.trust_level === 2) {
                    requirements['posts_read_count'] = Math.min(parseInt(parseInt(status.posts_30_days) / 4), 20000);
                    requirements['topics_entered'] = Math.min(parseInt(parseInt(status.topics_30_days) / 4), 500);
                }
                if (userDetail.trust_level === 3) {
                    content += '联系管理员进行py交易以升级到领导者<br>';
                } else if (userDetail.trust_level === 4) {
                    content += '您已是最高信任等级<br>';
                } else {
                    let summary = summaryRequired(requirements, userSummary, this.translateStat.bind(this));
                    content += summary;
                }
            }
            this.content.innerHTML = content;
        },

        togglePopupSize: function() {
            if (this.popup.classList.contains('minimized')) {
                this.popup.classList.remove('minimized');
                this.popup.style.width = '250px';
                this.popup.style.height = 'auto';
                this.content.style.display = 'block';
                this.searchBox.style.display = 'block';
                this.searchButton.style.display = 'block';
                this.pkButton.style.display = 'block';
                this.minimizeButton.textContent = '隐藏';
                this.popup.classList.remove('breath-animation');
            } else {
                this.popup.classList.add('minimized');
                this.popup.style.width = '50px';
                this.popup.style.height = '50px';
                this.content.style.display = 'none';
                this.searchBox.style.display = 'none';
                this.searchButton.style.display = 'none';
                this.pkButton.style.display = 'none';
                this.minimizeButton.textContent = '展开';
                this.popup.classList.add('breath-animation');
            }

            // 自动校正窗口位置
            addDraggableFeature(this.popup);
            const windowWidth = window.innerWidth;
            const windowHeight = window.innerHeight;
            const popupWidth = this.popup.offsetWidth;
            const popupHeight = this.popup.offsetHeight;
            const popupTop = parseInt(this.popup.style.top);
            const popupLeft = parseInt(this.popup.style.left);

            // 初始化新的位置
            let newTop = popupTop;
            let newLeft = popupLeft;

            // 上下边界同时检查
            newTop = Math.min(Math.max(70, popupTop), windowHeight - popupHeight);

            // 左右边界同时检查
            newLeft = Math.min(Math.max(5, popupLeft), windowWidth - popupWidth - 20);

            this.popup.style.top = newTop + 'px';
            this.popup.style.left = newLeft + 'px';
        },

        displayError: function(message) {
            this.content.innerHTML = `<strong>错误:</strong>${message}`;
        },

        translateStat: function(stat) {
            const translations = {
                'days_visited': '访问天数',
                'likes_given': '给出的赞',
                'likes_received': '收到的赞',
                'post_count': '帖子数量',
                'posts_read_count': '已读帖子',
                'topics_entered': '已读主题',
                'time_read': '阅读时间(秒)'
            };
            return translations[stat] || stat;
        }
    };

    const EventHandler = {
        handleSearch: async function() {
            const username = UIManager.searchBox.value.trim();
            if (!username) return;
            try {
                const aboutData = await DataManager.fetchAboutData();
                const summaryData = await DataManager.fetchSummaryData(username);
                const userData = await DataManager.fetchUserData(username);
                if (summaryData && userData && aboutData) {
                    UIManager.updatePopupContent(summaryData.user_summary, 
                         userData.user, null, null, aboutData.about.stats, false);
                }
            } catch (error) {
                console.error(error);
            }
        },
        handlePK: async function() {
            const username = UIManager.searchBox.value.trim();
            if (!username) return;
            const h = document.querySelectorAll("#current-user > button");
            if (h.length == 0) return;
            const currentUserHref = h[0].getAttribute('href');
            const currentUsername = currentUserHref.split('/').pop();
            try {
                const summaryData = await DataManager.fetchSummaryData(username);
                const userData = await DataManager.fetchUserData(username);
                const curSummayData = await DataManager.fetchSummaryData(currentUsername);
                const curUserData = await DataManager.fetchUserData(currentUsername); 
                if (summaryData && userData && curSummayData && curUserData) {
                    UIManager.updatePopupContent(summaryData.user_summary, 
                         userData.user, curSummayData.user_summary, curUserData.user, null, true);
                }
            } catch (error) {
                console.error(error);
            }
        },
        // 更新拖动状态
        handleDragEnd: function() {
            UIManager.updateDragStatus(true);
        }
    };

    // 添加时间格式化
    function formatTimestamp(lastSeenAt) {
        // 解析时间戳并去除毫秒
        let timestamp = new Date(lastSeenAt);

        // 使用Intl.DateTimeFormat格式化时间为上海时区
        let formatter = new Intl.DateTimeFormat('zh-CN', {
            timeZone: 'Asia/Shanghai',
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: 'numeric',
            minute: 'numeric',
            second: 'numeric',
        });

        // 获取格式化后的字符串
        let formattedTimestamp = formatter.format(timestamp);

        return formattedTimestamp;
    }

    // 添加用户升级进度总结
    function summaryRequired(required, current, translateStat) {
        let summary = '';
        let allMet = true;

        for (const stat in required) {
            if (required.hasOwnProperty(stat) && current.hasOwnProperty(stat)) {
                const reqValue = required[stat];
                const curValue = current[stat] || 0; // 使用 || 0 确保未定义的情况下使用0
                if (curValue < reqValue) {
                    allMet = false;
                    const diff = reqValue - curValue;
                    summary += `${translateStat(stat)}: <span style="color: red;"> ${curValue} < ${reqValue},还差 ${diff}</span><br>`;
                } else {
                    // 如果当前值满足或超过了要求值,也打印出来,但使用不同的颜色或提示信息
                    summary += `${translateStat(stat)}: <span style="color: green;"> ${curValue} ≥ ${reqValue},已合格</span><br>`;
                }
            }
        }

        if (allMet) {
            return "恭喜您!所有项次都已达到合格标准。<br>" + summary;
        } else {
            return summary;
        }
    }

    function summaryPK(pkItems, current, other, translateStat){
        let winCnt = 0;
        let failCnt = 0;
        let drawCnt = 0;
        let summary = '';
        for (const stat in pkItems) {
            if (current.hasOwnProperty(stat) && other.hasOwnProperty(stat)) {
                const curValue = current[stat] || 0; // 使用 || 0 确保未定义的情况下使用0
                const otherValue = other[stat] || 0; 
                if (curValue < otherValue) {
                    failCnt ++;
                    summary += `${translateStat(stat)}: <span style="color: red;"> ${pkItems[stat](current[stat])} < ${pkItems[stat](other[stat])}</span><br>`;
                } else if (curValue == otherValue) {
                    drawCnt ++;
                    summary += `${translateStat(stat)}: <span> ${pkItems[stat](current[stat])} == ${pkItems[stat](other[stat])}</span><br>`;
                }else  {
                    winCnt ++;
                    summary += `${translateStat(stat)}: <span style="color: green;"> ${pkItems[stat](current[stat])} > ${pkItems[stat](other[stat])}</span><br>`;
                }
            }
        }
        return  summary + `<br><strong> <span style="color: green;">${winCnt}胜</span><span style="color: red;">${failCnt}负</span>`+
                `${drawCnt}平</strong>`;
    }

    // 添加拖动功能
    function addDraggableFeature(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

        const dragMouseDown = function(e) {
            // 检查事件的目标是否是输入框,按钮或其他可以忽略拖动逻辑的元素
            if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA' || e.target.tagName.toUpperCase() === 'BUTTON') {
                return; // 如果是,则不执行拖动逻辑
            }

            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        };

        const elementDrag = function(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
            // 为了避免与拖动冲突,在此移除bottom和right样式
            element.style.bottom = '';
            element.style.right = '';
        };

        const closeDragElement = function() {
            document.onmouseup = null;
            document.onmousemove = null;
            // 在拖动结束时更新拖动状态
            EventHandler.handleDragEnd();
        };

        element.onmousedown = dragMouseDown;
    }

    const init = () => {
        StyleManager.injectStyles();
        UIManager.initPopup();
        addDraggableFeature(document.getElementById('linuxDoLevelPopup')); // 确保已设置该ID
        UIManager.togglePopupSize(); // 初始最小化
    };

    init();

})();
1 个赞

不错啊 很强,花了不少时间吧 :smiley: 谢谢你对本项目的支持:smiling_face_with_three_hearts: 俺也不是大佬啦 @Hua 这位才是
目前看来你写的很好,就是展示效果还可以继续优化,以下是我的建议:

  • PK模式一些不重要的内容,比如升级进度,邀请人可以删掉,尽量保持窗口的黄金比例,看起来会更舒服
  • 7胜0负0平 这是个总结的文字可以放到最后并改变颜色放大字体以突出
  • 已读帖子单位可以转为k,阅读时间单位可以转为天或小时,这样更好对比

1 个赞

很强,致敬一波,有学习到东西

牛逼

感谢回复, 你们都是大佬 :smiling_face_with_three_hearts:, 刚改了下,好看点了