显示新标签页打开图标(带设置菜单和实时预览)

鼠标悬停在链接上时,如果会在新标签页打开,则显示箭头图标,支持中英双语设置菜单及实时预览

目前為 2024-11-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name               显示新标签页打开图标(带设置菜单和实时预览)
// @name:en            Show Icon for Links Opening in New Tab (Settings Menu with Preview)
// @namespace          http://tampermonkey.net/
// @version            1.9
// @description        鼠标悬停在链接上时,如果会在新标签页打开,则显示箭头图标,支持中英双语设置菜单及实时预览
// @description:en    Hover over a link to show an arrow icon if it opens in a new tab. Supports bilingual settings menu and live preview.
// @match              *://*/*
// @grant              GM_registerMenuCommand
// @grant              GM_setValue
// @grant              GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // 自动选择合适的语言
    const userLang = navigator.language || navigator.userLanguage;
    const language = userLang.startsWith("zh") ? "zh" : "en";  // 默认选择中文或英文

    const defaultIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV4dGVybmFsLWxpbmsiPjxwYXRoIGQ9Ik0xNSAzaDZ2NiIvPjxwYXRoIGQ9Ik0xMCAxNCAyMSAzIi8+PHBhdGggZD0iTTE4IDEzdjZhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJWOGEyIDIgMCAwIDEgMi0yaDYiLz48L3N2Zz4=";

    // 获取设置或默认值
    const settings = {
        iconUrl: GM_getValue("iconUrl", defaultIcon),
        size: GM_getValue("size", 24),
        offsetX: GM_getValue("offsetX", 10),
        offsetY: GM_getValue("offsetY", 10),
        iconOpacity: GM_getValue("iconOpacity", 0.8), // 默认透明度为0.8
        outlineSize: GM_getValue("outlineSize", 2),
        outlineColor: GM_getValue("outlineColor", "black"),
        outlineOpacity: GM_getValue("outlineOpacity", 1),
        invertForDarkMode: GM_getValue("invertForDarkMode", true),
        language: GM_getValue("language", language) // 根据浏览器语言选择
    };

    // 本地化文本
    const translations = {
        zh: {
            settingsTitle: "图标设置",
            iconUrl: "图标 URL",
            size: "图标大小",
            offsetX: "横向偏移",
            offsetY: "纵向偏移",
            iconOpacity: "图标透明度",
            outlineSize: "描边大小",
            outlineColor: "描边颜色",
            outlineOpacity: "描边透明度",
            invertForDarkMode: "夜间模式反色",
            language: "语言",
            apply: "应用",
            cancel: "取消",
            preview: "预览"
        },
        en: {
            settingsTitle: "Icon Settings",
            iconUrl: "Icon URL",
            size: "Icon Size",
            offsetX: "Horizontal Offset",
            offsetY: "Vertical Offset",
            iconOpacity: "Icon Opacity",
            outlineSize: "Outline Size",
            outlineColor: "Outline Color",
            outlineOpacity: "Outline Opacity",
            invertForDarkMode: "Invert for Dark Mode",
            language: "Language",
            apply: "Apply",
            cancel: "Cancel",
            preview: "Preview"
        }
    };

    const text = translations[settings.language];

    // 创建图标元素
    const icon = document.createElement("div");
    icon.style.position = "absolute";
    icon.style.width = `${settings.size}px`;
    icon.style.height = `${settings.size}px`;
    icon.style.backgroundImage = `url(${settings.iconUrl})`;
    icon.style.backgroundSize = "contain";
    icon.style.pointerEvents = "none";
    icon.style.zIndex = "10000";
    icon.style.opacity = settings.iconOpacity;
    icon.style.display = "none";
    updateIconFilter();
    document.body.appendChild(icon);

    document.addEventListener("mouseover", (event) => {
        const link = event.target.closest("a");
        if (link && link.target === "_blank") {
            icon.style.display = "block";
        } else {
            icon.style.display = "none";
        }
    });

    document.addEventListener("mousemove", (event) => {
        icon.style.left = `${event.pageX + settings.offsetX}px`;
        icon.style.top = `${event.pageY + settings.offsetY}px`;
    });

    document.addEventListener("mouseout", (event) => {
        if (event.target.closest("a")) {
            icon.style.display = "none";
        }
    });

    GM_registerMenuCommand(text.settingsTitle, openSettingsDialog);

    // 打开设置弹窗
    function openSettingsDialog() {
        const dialog = document.createElement("div");
        dialog.style.position = "fixed";
        dialog.style.top = "50%";
        dialog.style.left = "50%";
        dialog.style.transform = "translate(-50%, -50%)";
        dialog.style.backgroundColor = "white";
        dialog.style.border = "1px solid #ccc";
        dialog.style.padding = "20px";
        dialog.style.zIndex = "10001";
        dialog.style.width = "300px";
        dialog.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";

        dialog.innerHTML = `
            <h2>${text.settingsTitle}</h2>
            <label>${text.iconUrl}: <input id="iconUrl" type="text" value="${settings.iconUrl}" /></label><br>
            <label>${text.size}: <input id="size" type="number" value="${settings.size}" /></label><br>
            <label>${text.offsetX}: <input id="offsetX" type="number" value="${settings.offsetX}" /></label><br>
            <label>${text.offsetY}: <input id="offsetY" type="number" value="${settings.offsetY}" /></label><br>
            <label>${text.iconOpacity}: <input id="iconOpacity" type="number" step="0.1" min="0" max="1" value="${settings.iconOpacity}" /></label><br>
            <label>${text.outlineSize}: <input id="outlineSize" type="number" value="${settings.outlineSize}" /></label><br>
            <label>${text.outlineColor}: <input id="outlineColor" type="color" value="${settings.outlineColor}" /></label><br>
            <label>${text.outlineOpacity}: <input id="outlineOpacity" type="number" step="0.1" min="0" max="1" value="${settings.outlineOpacity}" /></label><br>
            <label><input id="invertForDarkMode" type="checkbox" ${settings.invertForDarkMode ? 'checked' : ''} /> ${text.invertForDarkMode}</label><br>
            <label>${text.language}: <select id="language">
                <option value="zh" ${settings.language === "zh" ? "selected" : ""}>中文</option>
                <option value="en" ${settings.language === "en" ? "selected" : ""}>English</option>
            </select></label><br><br>
            <div id="preview" style="text-align:center;">
                <h4>${text.preview}</h4>
                <div id="previewIcon" style="
                    display: inline-block;
                    background-image: url(${settings.iconUrl});
                    background-size: contain;
                    width: ${settings.size}px;
                    height: ${settings.size}px;
                    opacity: ${settings.iconOpacity};
                    filter: drop-shadow(0 0 ${settings.outlineSize}px ${settings.outlineColor}) opacity(${settings.outlineOpacity});
                "></div>
            </div><br>
            <button id="apply">${text.apply}</button>
            <button id="cancel">${text.cancel}</button>
        `;

        document.body.appendChild(dialog);

        // 预览更新
        const inputs = dialog.querySelectorAll("input, select");
        inputs.forEach(input => {
            input.addEventListener("input", updatePreview);
        });

        // 应用设置
        dialog.querySelector("#apply").addEventListener("click", () => {
            settings.iconUrl = document.getElementById("iconUrl").value;
            settings.size = parseInt(document.getElementById("size").value);
            settings.offsetX = parseInt(document.getElementById("offsetX").value);
            settings.offsetY = parseInt(document.getElementById("offsetY").value);
            settings.iconOpacity = parseFloat(document.getElementById("iconOpacity").value);
            settings.outlineSize = parseInt(document.getElementById("outlineSize").value);
            settings.outlineColor = document.getElementById("outlineColor").value;
            settings.outlineOpacity = parseFloat(document.getElementById("outlineOpacity").value);
            settings.invertForDarkMode = document.getElementById("invertForDarkMode").checked;
            settings.language = document.getElementById("language").value;
            saveSettings();
            updateIconAppearance();
            document.body.removeChild(dialog);
        });

        // 取消
        document.getElementById("cancel").addEventListener("click", () => {
            document.body.removeChild(dialog);
        });
    }

    // 更新图标预览
    function updatePreview() {
        const previewIcon = document.getElementById("previewIcon");
        const iconUrl = document.getElementById("iconUrl").value;
        const size = parseInt(document.getElementById("size").value);
        const opacity = parseFloat(document.getElementById("iconOpacity").value);
        const outlineSize = parseInt(document.getElementById("outlineSize").value);
        const outlineColor = document.getElementById("outlineColor").value;
        const outlineOpacity = parseFloat(document.getElementById("outlineOpacity").value);

        previewIcon.style.backgroundImage = `url(${iconUrl})`;
        previewIcon.style.width = `${size}px`;
        previewIcon.style.height = `${size}px`;
        previewIcon.style.opacity = opacity;
        previewIcon.style.filter = `drop-shadow(0 0 ${outlineSize}px ${outlineColor}) opacity(${outlineOpacity})`;
    }

    // 更新图标外观
    function updateIconAppearance() {
        icon.style.backgroundImage = `url(${settings.iconUrl})`;
        icon.style.width = `${settings.size}px`;
        icon.style.height = `${settings.size}px`;
        icon.style.opacity = settings.iconOpacity;
        updateIconFilter();
    }

    // 更新图标的过滤效果
    function updateIconFilter() {
        const invertFilter = settings.invertForDarkMode && window.matchMedia("(prefers-color-scheme: dark)").matches ? "invert(1)" : "";
        icon.style.filter = `drop-shadow(0 0 ${settings.outlineSize}px ${settings.outlineColor}) opacity(${settings.outlineOpacity}) ${invertFilter}`;
    }

    // 保存设置
    function saveSettings() {
        Object.keys(settings).forEach(key => {
            GM_setValue(key, settings[key]);
        });
    }
})();