LinuxDo EnhanceX (LinuxDo Quick Bookmark)

Adds customizable features such as logos, click count visualization, image resize, and quick bookmarking to LinuxDo.

目前为 2024-07-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         LinuxDo EnhanceX (LinuxDo Quick Bookmark)
// @name:zh-CN   LinuxDo 增强X (原LinuxDo快速收藏)
// @description  Adds customizable features such as logos, click count visualization, image resize, and quick bookmarking to LinuxDo.
// @description:zh-CN 在帖子中添加了快速收藏功能。用户可以通过星形图标快速收藏或取消收藏帖子。为 LinuxDo 添加自定义徽标、点击数可视化、图像缩放和快速书签等功能。
// @namespace    http://tampermonkey.net/
// @version      0.2.5
// @author       Yearly
// @match        https://linux.do/*
// @icon         data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjZWVlIiBkPSJNMCAwaDEwMHYxMDBIMHoiLz48cGF0aCBkPSJNNCA0aDkydjMwSDR6Ii8+PHBhdGggZmlsbD0iI0ZBMCIgZD0iTTQgNjZoOTJ2MzBINHoiLz48L3N2Zz4=
// @license      AGPL-v3.0
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    var settings = {};
    (function() {
        'use strict';

        const settingsConfig = {
            quick_mark    : { type: 'checkbox', label: '快速收藏          ', default: true },
            image_view    : { type: 'checkbox', label: '增强大图查看      ', default: true },
            cnts_colorful : { type: 'checkbox', label: '链接点击次数高亮色阶', default: true },
            raw_search    : { type: 'checkbox', label: '默认使用浏览器自带CTRL+F搜索', default: true },
            icon_custom   : { type: 'checkbox', label: '自定义LOGO图标      ', default: true },
            icon_main     : { type: 'text', label: 'icon main', default: GM_info.script.icon, dependentOn: 'icon_custom' },
            icon_wide     : { type: 'text', label: 'icon wide', default: GM_info.script.icon, dependentOn: 'icon_custom' },
        };

        Object.keys(settingsConfig).forEach(key => {
            settings[key] = GM_getValue(key, settingsConfig[key].default);
        });

        GM_registerMenuCommand('Open Settings', openSettings);

        function openSettings() {
            const panel = document.createElement('div');
            panel.style = `position: fixed; top: 40%; left: 50%; transform: translate(-50%, -50%);
            background: white; padding: 10px 20px; border: 1px solid #ccc; z-index: 10000; color:#000`;
            let html = `<style type="text/css">
        :scope label {display:flex; justify-content:space-between; align-items:center; margin-top:15px;}
        :scope label input[type=text] {width:200px; padding:1px 5px; margin:0 5px;}
        :scope label input[disabled] {background: #CCC;}
        :scope label button {user-select: none; color: #000; padding: 2px 10px; margin-top:10px;}
        </style>`;
            html += '<h2 style="text-align:center; margin-top:.5rem;">Settings</h2>';
            Object.keys(settingsConfig).forEach(key => {
                const cfg = settingsConfig[key];
                const val = settings[key];
                const checked = cfg.type === 'checkbox' && val ? 'checked' : '';
                const disabled = cfg.dependentOn && !settings[cfg.dependentOn] ? 'disabled' : '';
                html += `<label>${cfg.label}: <input type="${cfg.type}" id="userjs_setting_${key}" value="${val}" ${checked} ${disabled}></label>`;
            });
            html += `<label><button id="ld_userjs_save">保存</button>
           <span></span><button id="ld_userjs_apply">保存并刷新</button>
           <span></span><button id="ld_userjs_close">取消</button></label>`;
            panel.innerHTML = html;
            document.body.appendChild(panel);

            Object.keys(settingsConfig).forEach(key => {
                if (settingsConfig[key].dependentOn) {
                    document.getElementById(`userjs_setting_${settingsConfig[key].dependentOn}`).addEventListener('change', updateDependencies);
                }
            });

            function updateDependencies() {
                Object.keys(settingsConfig).forEach(key => {
                    if (settingsConfig[key].dependentOn) {
                        document.getElementById(`userjs_setting_${key}`).disabled = !document.getElementById(`userjs_setting_${settingsConfig[key].dependentOn}`).checked;
                    }
                });
            }

            document.querySelector('button#ld_userjs_save').addEventListener('click', () => {
                Object.keys(settingsConfig).forEach(key => {
                    const element = document.getElementById(`userjs_setting_${key}`);
                    settings[key] = element.type === 'checkbox' ? element.checked : element.value;
                    GM_setValue(key, settings[key]);
                });
                alert('Settings saved!');
                panel.remove();
            });

            document.querySelector('button#ld_userjs_apply').addEventListener('click', () => {
                Object.keys(settingsConfig).forEach(key => {
                    const element = document.getElementById(`userjs_setting_${key}`);
                    settings[key] = element.type === 'checkbox' ? element.checked : element.value;
                    GM_setValue(key, settings[key]);
                });
                panel.remove();
                window.location.reload();
            });

            document.querySelector('button#ld_userjs_close').addEventListener('click', () => panel.remove());

            updateDependencies();
        }

    })();

    // Function 1: Custom Logo
    if (settings.icon_custom) {
        GM_addStyle(`
            #site-logo {
                object-fit: scale-down;
                object-position: -999vw;
                background-size: contain;
                background-repeat: no-repeat;
            }
            #site-logo.logo-small {
                background-image: url('${settings.icon_main}');
            }
            #site-logo.logo-big {
                background-image: url('${settings.icon_wide}');
            }

            #site-logo:hover {
                object-position: unset;
                background-image: none;
            }
        `);

        function replaceIcon() {
            document.querySelector('link[rel="icon"]').href = settings.icon_main;
        }

        const observer = new MutationObserver(replaceIcon);
        observer.observe(document.head, { childList: true, subtree: true });

        replaceIcon();
    }

    // Function 2: Click Counts Visualization
    if (settings.cnts_colorful) {
        (function countsColorful() {
            const badges = document.querySelectorAll("span.badge.badge-notification.clicks");
            let values = Array.from(badges, badge => parseInt(badge.title || badge.textContent));
            let maxValue = Math.max(...values);
            let minValue = Math.min(...values);
            if (maxValue < 100 || (maxValue - minValue < 10)) maxValue = maxValue * 1.5;
            badges.forEach(badge => {
                if (!badge.style.backgroundColor) {
                    const number = parseInt(badge.title || badge.textContent);
                    const hue = 180 - (number / maxValue) * 180;
                    badge.style.backgroundColor = `hsl(${hue}, 50%, 50%)`;
                    badge.style.color = "#fff";
                    const sl = document.createElement('span');
                    sl.style = `height: 1em; display: inline-block; float: right; background: hsl(${hue}, 50%, 50%); width: ${100 * (number / maxValue)}px;`;
                    badge.after(sl);
                }
            });
            setTimeout(countsColorful, 1500);
        })();
    }

    // Function 3: Image Resize and Drag
    if (settings.image_view) {
        let sizePercent = 80;
        let isDragging = false;
        let startX, startY, initialX, initialY;

        function adjustSize(event) {
            let contentImg = document.querySelector('div.mfp-content img');
            let contentDiv = document.querySelector('div.mfp-content');
            if (contentImg) {
                let delta = event.deltaY > 0 ? -10 : 10;
                sizePercent += delta;
                if (sizePercent > 150) sizePercent = 150;
                if (sizePercent < 5) sizePercent = 5;

                contentImg.style.width = sizePercent + '%';
                contentImg.style.maxWidth = sizePercent + '%';
                contentImg.style.height = sizePercent + '%';
                contentImg.style.maxHeight = sizePercent + '%';

                contentDiv.style.width = contentImg.clientWidth;
            }
        }

        function startDrag(event) {
            let contentDiv = document.querySelector('div.mfp-content img');
            if (contentDiv) {
                isDragging = true;
                startX = event.clientX;
                startY = event.clientY;
                initialX = contentDiv.offsetLeft;
                initialY = contentDiv.offsetTop;
                event.preventDefault();
            }
        }

        function drag(event) {
            if (isDragging) {
                let contentImg = document.querySelector('div.mfp-content img');
                let contentDiv = document.querySelector('div.mfp-content');
                if (contentImg) {
                    let dx = event.clientX - startX;
                    let dy = event.clientY - startY;
                    contentImg.style.left = (initialX + dx) + 'px';
                    contentImg.style.top = (initialY + dy) + 'px';
                }
            }
        }

        function stopDrag(event) {
            isDragging = false;
        }

        let observer = new MutationObserver(function(mutations) {
            mutations.forEach(function(mutation) {
                mutation.addedNodes.forEach(function(node) {
                    if (document.querySelector('div.mfp-content img')) {
                        let contentDiv = document.querySelector('div.mfp-content img');
                        contentDiv.onwheel = adjustSize;
                    }
                });
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });

    }

    // Function 4: Quick Bookmark
    if (settings.quick_mark) {
        const starSvg = `<svg class="svg-icon" aria-hidden="true" style="text-indent: 1px; transform: scale(1); width:18px; height:18px;">
             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
             <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path></svg></svg> `;
        let markMap = new Map();

        function handleResponse(xhr, successCallback, errorCallback) {
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        successCallback(xhr);
                    } else {
                        errorCallback(xhr);
                    }
                }
            };
        }

        function deleteStarMark(mark_btn, data_id) {
            if (markMap.has(data_id)) {
                const mark_id = markMap.get(data_id);
                var xhr = new XMLHttpRequest();
                xhr.open('DELETE', `/bookmarks/${mark_id}`, true);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
                xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);

                handleResponse(xhr, (xhr) => {
                    mark_btn.style.color = '#777';
                    mark_btn.title = "收藏";
                    mark_btn.onclick = () => addStarMark(mark_btn, data_id);
                }, (xhr) => {
                    alert('删除失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
                });

                xhr.send();
            }
        }

        function TryParseJson(str) {
            try {
                const jsonObj = JSON.parse(str);
                return JSON.stringify(jsonObj, null, 1);
            } catch (error) {
                return str;
            }
        }

        function addStarMark(mark_btn, data_id) {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/bookmarks', true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
            xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
            xhr.setRequestHeader('discourse-logged-in', ' true');
            xhr.setRequestHeader('discourse-present', ' true');
            xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
            const postData = `name=%E6%94%B6%E8%97%8F&auto_delete_preference=3&bookmarkable_id=${data_id}&bookmarkable_type=Post`;

            handleResponse(xhr, (xhr) => {
                mark_btn.style.color = '#fdd459';
                mark_btn.title = "删除收藏";
                mark_btn.onclick = () => deleteStarMark(mark_btn, data_id);
            }, (xhr) => {
                alert('收藏失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
            });

            xhr.send(postData);
        }

        function addMarkBtn() {
            let articles = document.querySelectorAll("article[data-post-id]");
            if (articles.length <= 0) return;

            articles.forEach(article => {
                const target = article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > div.actions");
                if (target && !article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > span.star-bookmark")) {
                    const dataPostId = article.getAttribute('data-post-id');
                    const starButton = document.createElement('span');

                    starButton.innerHTML = starSvg;
                    starButton.className = "star-bookmark";
                    starButton.style.cursor = 'pointer';
                    starButton.style.margin = '0px 12px';

                    if (markMap.has(dataPostId)) {
                        starButton.style.color = '#fdd459';
                        starButton.title = "删除收藏";
                        starButton.onclick = () => deleteStarMark(starButton, dataPostId);
                    } else {
                        starButton.style.color = '#777';
                        starButton.title = "收藏";
                        starButton.onclick = () => addStarMark(starButton, dataPostId);
                    }
                    target.after(starButton);
                }
            });
        }

        function getStarMark() {
            let articles = document.querySelectorAll("article[data-post-id]");
            if (articles.length <= 0) return;

            const currentUserElement = document.querySelector('#current-user button');
            const currentUsername = currentUserElement ? currentUserElement.getAttribute('href').replace('/u/', '') : null;

            const xhr = new XMLHttpRequest();
            xhr.open('GET', `/u/${currentUsername}/user-menu-bookmarks`, true);
            xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);

            handleResponse(xhr, (xhr) => {
                var response = JSON.parse(xhr.responseText);
                response.bookmarks.forEach(mark => {
                    markMap.set(mark.bookmarkable_id.toString(), mark.id.toString());
                });
                addMarkBtn();
            }, (xhr) => {
                console.error('GET请求失败:', xhr.statusText);
            });

            xhr.send();
        }

        let lastUpdateMarkTime = 0;
        let lastUpdateButnTime = 0;
        function mutationCallback() {
            const currentTime = Date.now();
            if (currentTime - lastUpdateMarkTime > 9000) {
                setTimeout(getStarMark, 500);
                lastUpdateMarkTime = currentTime;
            }
            if (currentTime - lastUpdateButnTime > 1000) {
                setTimeout(addMarkBtn, 500);
                lastUpdateButnTime = currentTime;
            }
        }

        const mainNode = document.querySelector("#main-outlet");
        if (mainNode) {
            const observer = new MutationObserver(mutationCallback);
            observer.observe(mainNode, { childList: true, subtree: true });
        }

        getStarMark();

    }

    //Function 5: restore Browser Default Ctrl+F Search, Removes custom Ctrl+F event listeners。
    if (settings.raw_search) {
        function preventCtrlF(e) {
            if (e.ctrlKey && e.key === 'f') {
                e.stopPropagation();
                return false;
            }
        }
        document.addEventListener('keydown', preventCtrlF, true);
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址