Greasy Fork

来自缓存

Greasy Fork is available in English.

复制网页标题和链接 (配置快捷键)

默认 Alt+S 复制网页标题和链接。快捷键可在油猴菜单通过直接按键组合配置。

当前为 2025-06-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         复制网页标题和链接 (配置快捷键)
// @namespace    http://greasyfork.icu/
// @version      0.1.0
// @description  默认 Alt+S 复制网页标题和链接。快捷键可在油猴菜单通过直接按键组合配置。
// @author       妮娜可
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    // 配置管理类
    class ShortcutConfig {
        constructor() {
            this.key = GM_getValue('shortcut_key', 's');
            this.ctrl = GM_getValue('shortcut_ctrl', false);
            this.alt = GM_getValue('shortcut_alt', true);
            this.shift = GM_getValue('shortcut_shift', false);
        }

        save() {
            GM_setValue('shortcut_key', this.key);
            GM_setValue('shortcut_ctrl', this.ctrl);
            GM_setValue('shortcut_alt', this.alt);
            GM_setValue('shortcut_shift', this.shift);
        }

        getDisplayString() {
            const parts = [];
            if (this.ctrl) parts.push('Ctrl');
            if (this.alt) parts.push('Alt');
            if (this.shift) parts.push('Shift');

            let displayKey = this.key.toLowerCase();
            if (displayKey === ' ') displayKey = 'Space';
            else if (displayKey.length === 1) displayKey = displayKey.toUpperCase();
            else displayKey = displayKey.replace(/\b\w/g, l => l.toUpperCase());

            parts.push(displayKey);
            return parts.join(' + ');
        }

        matches(event) {
            const keyMatch = event.key.toLowerCase() === this.key.toLowerCase();
            const ctrlMatch = event.ctrlKey === this.ctrl;
            const altMatch = event.altKey === this.alt;
            const shiftMatch = event.shiftKey === this.shift;

            return keyMatch && ctrlMatch && altMatch && shiftMatch;
        }
    }

    // UI 管理类
    class UIManager {
        constructor() {
            this.toastElement = null;
            this.promptElement = null;
            this.initElements();
        }

        initElements() {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.createElements());
            } else {
                this.createElements();
            }
        }

        createElements() {
            this.createToastElement();
            this.createPromptElement();
        }

        createToastElement() {
            this.toastElement = document.createElement('div');
            this.toastElement.className = 'copy-toast-message';
            this.toastElement.style.display = 'none';
            document.body.appendChild(this.toastElement);
        }

        createPromptElement() {
            this.promptElement = document.createElement('div');
            this.promptElement.className = 'shortcut-setup-prompt';
            this.promptElement.style.display = 'none';
            document.body.appendChild(this.promptElement);
        }

        showToast(message, type = 'info', duration = 2500) {
            if (!this.toastElement) return;

            this.toastElement.innerHTML = message;
            this.toastElement.className = 'copy-toast-message';

            if (type === 'success') {
                this.toastElement.classList.add('success');
            } else if (type === 'error') {
                this.toastElement.classList.add('error');
            }

            this.toastElement.style.opacity = '0';
            this.toastElement.style.display = 'flex';
            void this.toastElement.offsetWidth; // 触发重排
            this.toastElement.style.opacity = '1';

            setTimeout(() => {
                this.toastElement.style.opacity = '0';
                setTimeout(() => {
                    this.toastElement.style.display = 'none';
                }, 300);
            }, duration);
        }

        showPrompt(content) {
            if (!this.promptElement) return;

            this.promptElement.innerHTML = content;
            this.promptElement.style.display = 'flex';
        }

        hidePrompt() {
            if (!this.promptElement) return;

            this.promptElement.style.display = 'none';
        }
    }

    // 主应用类
    class CopyTitleApp {
        constructor() {
            this.config = new ShortcutConfig();
            this.ui = new UIManager();
            this.isSettingShortcut = false;
            this.init();
        }

        init() {
            this.bindEvents();
            this.registerMenuCommand();
            this.addStyles();
        }

        bindEvents() {
            // 使用 addEventListener 而不是覆盖 onkeydown
            document.addEventListener('keydown', (e) => this.handleKeyDown(e), true);
        }

        registerMenuCommand() {
            GM_registerMenuCommand('设置复制快捷键', () => {
                this.startShortcutSetting();
            });
        }

        startShortcutSetting() {
            this.isSettingShortcut = true;
            const content = `
                请按下您想设置的新快捷键组合 (例如 Alt+S)。<br>
                当前: ${this.config.getDisplayString()}<br>
                按 ESC 键取消。
            `;
            this.ui.showPrompt(content);
        }

        handleKeyDown(e) {
            if (this.isSettingShortcut) {
                this.handleShortcutSetting(e);
            } else {
                this.handleCopyShortcut(e);
            }
        }

        handleShortcutSetting(e) {
            e.preventDefault();
            e.stopPropagation();

            if (e.key === 'Escape') {
                this.cancelShortcutSetting();
                return;
            }

            // 忽略单独的修饰键
            if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) {
                this.updatePromptWithModifiers(e);
                return;
            }

            // 设置新的快捷键
            this.setNewShortcut(e);
        }

        updatePromptWithModifiers(e) {
            const currentModifiers = [];
            if (e.ctrlKey) currentModifiers.push('Ctrl');
            if (e.altKey) currentModifiers.push('Alt');
            if (e.shiftKey) currentModifiers.push('Shift');

            let heldModifiers = currentModifiers.join(' + ');
            if (heldModifiers) heldModifiers += ' + ';

            const content = `
                请按下您想设置的新快捷键组合。<br>
                当前按下: ${heldModifiers} _ <br>
                (请继续按下主键, 如 A, B, 1, Enter 等)<br>
                按 ESC 键取消。
            `;
            this.ui.showPrompt(content);
        }

        setNewShortcut(e) {
            this.config.key = e.key.toLowerCase();
            this.config.ctrl = e.ctrlKey;
            this.config.alt = e.altKey;
            this.config.shift = e.shiftKey;

            this.config.save();

            this.isSettingShortcut = false;
            this.ui.hidePrompt();
            this.ui.showToast(`快捷键已更新为: ${this.config.getDisplayString()}`, 'success', 3000);
        }

        cancelShortcutSetting() {
            this.isSettingShortcut = false;
            this.ui.hidePrompt();
            this.ui.showToast('快捷键设置已取消', 'info');
        }

        handleCopyShortcut(e) {
            if (!this.config.matches(e)) return;

            // 防止修饰键本身作为主键的歧义情况
            if (['control', 'alt', 'shift', 'meta'].includes(this.config.key.toLowerCase()) &&
                !(this.config.ctrl || this.config.alt || this.config.shift)) {
                return;
            }

            e.preventDefault();
            e.stopPropagation();

            this.copyTitleAndUrl();
        }

        copyTitleAndUrl() {
            const title = document.title;
            const url = location.href;

            if (!title || !url) {
                this.ui.showToast(this.getErrorMessage('无标题或URL'), 'error');
                return;
            }

            try {
                GM_setClipboard('『' + title + '』\n' + url);
                this.ui.showToast(this.getSuccessMessage(), 'success');
            } catch (err) {
                console.error("GM_setClipboard error:", err);
                this.ui.showToast(this.getErrorMessage('复制失败 (权限?)'), 'error');
            }
        }

        getSuccessMessage() {
            return `
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; color: #4CAF50;">
                    <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                    <polyline points="22 4 12 14.01 9 11.01"></polyline>
                </svg>
                <span>复制成功!</span>
            `;
        }

        getErrorMessage(text) {
            return `
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; color: #F44336;">
                    <circle cx="12" cy="12" r="10"></circle>
                    <line x1="15" y1="9" x2="9" y2="15"></line>
                    <line x1="9" y1="9" x2="15" y2="15"></line>
                </svg>
                <span>${text}</span>
            `;
        }

        addStyles() {
            GM_addStyle(`
                .copy-toast-message {
                    position: fixed;
                    left: 50%;
                    top: 50px;
                    transform: translateX(-50%);
                    background: rgba(50, 50, 50, 0.85);
                    backdrop-filter: blur(8px) saturate(150%);
                    -webkit-backdrop-filter: blur(8px) saturate(150%);
                    padding: 12px 20px;
                    border-radius: 8px;
                    z-index: 2147483646;
                    color: #fff;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
                    font-size: 16px;
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
                    display: flex;
                    align-items: center;
                    opacity: 0;
                    transition: opacity 0.3s ease-in-out;
                    min-width: 180px;
                    justify-content: center;
                }

                .copy-toast-message svg {
                    vertical-align: middle;
                }

                .shortcut-setup-prompt {
                    position: fixed;
                    left: 50%;
                    top: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(30, 30, 30, 0.92);
                    backdrop-filter: blur(10px) saturate(180%);
                    -webkit-backdrop-filter: blur(10px) saturate(180%);
                    padding: 25px 35px;
                    border-radius: 12px;
                    z-index: 2147483647;
                    color: #eee;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                    font-size: 18px;
                    text-align: center;
                    line-height: 1.6;
                    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    min-width: 300px;
                    max-width: 90%;
                }
            `);
        }
    }

    // 初始化应用
    new CopyTitleApp();
})();