// ==UserScript== // @name 复制网页标题和链接 (配置快捷键) // @name:zh-CN 复制网页标题和链接 (配置快捷键) // @namespace https://greasyfork.org/ // @version 0.2.0 // @description Copies the page title and URL with a configurable shortcut (Default: Alt+S). The shortcut can be set by a key combination in the UserScript menu. // @description:zh-CN 默认 Alt+S 复制网页标题和链接。快捷键可在油猴菜单通过直接按键组合配置。 // @author 妮娜可 // @match *://*/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @downloadURL https://update.greasyfork.icu/scripts/538632/%E5%A4%8D%E5%88%B6%E7%BD%91%E9%A1%B5%E6%A0%87%E9%A2%98%E5%92%8C%E9%93%BE%E6%8E%A5%20%28%E9%85%8D%E7%BD%AE%E5%BF%AB%E6%8D%B7%E9%94%AE%29.user.js // @updateURL https://update.greasyfork.icu/scripts/538632/%E5%A4%8D%E5%88%B6%E7%BD%91%E9%A1%B5%E6%A0%87%E9%A2%98%E5%92%8C%E9%93%BE%E6%8E%A5%20%28%E9%85%8D%E7%BD%AE%E5%BF%AB%E6%8D%B7%E9%94%AE%29.meta.js // ==/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'); // 格式化按键显示:' ' -> 'Space', 'a' -> 'A', 'arrowdown' -> 'ArrowDown' 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'; 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'; } } // 主应用类:整合配置和UI,处理核心逻辑 class CopyTitleApp { constructor() { this.config = new ShortcutConfig(); this.ui = new UIManager(); this.isSettingShortcut = false; this.init(); } init() { this.bindEvents(); this.registerMenuCommand(); this.addStyles(); } bindEvents() { // 使用事件捕获(第三个参数为 true),确保能优先处理按键事件,防止被页面其他脚本拦截。 document.addEventListener('keydown', (e) => this.handleKeyDown(e), true); } registerMenuCommand() { GM_registerMenuCommand(`设置复制快捷键 (当前: ${this.config.getDisplayString()})`, () => { this.startShortcutSetting(); }); } startShortcutSetting() { this.isSettingShortcut = true; const content = ` 请按下您想设置的新快捷键组合。
当前: ${this.config.getDisplayString()}
按 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; } // 忽略单独的修饰键(如只按下 Alt),等待主键的输入 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 = ` 请按下您想设置的新快捷键组合。
当前按下: ${heldModifiers} _
(请继续按下主键, 如 A, B, 1, 等)
按 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); // 动态更新菜单项,无需刷新页面 this.registerMenuCommand(); } cancelShortcutSetting() { this.isSettingShortcut = false; this.ui.hidePrompt(); this.ui.showToast('快捷键设置已取消', 'info'); } handleCopyShortcut(e) { if (!this.config.matches(e)) return; // 阻止将单独的修饰键(如 "Alt")作为快捷键触发,避免误操作 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() { // 使用 document.title 和 location.href 可以正确获取顶层或 iframe 内的标题和链接 const title = document.title; const url = location.href; if (!title && !url) { this.ui.showToast(this.getErrorMessage('无标题或URL'), 'error'); return; } try { const textToCopy = `『${title || '无标题'}』\n${url}`; GM_setClipboard(textToCopy); this.ui.showToast(this.getSuccessMessage(), 'success'); } catch (err) { console.error("GM_setClipboard error:", err); this.ui.showToast(this.getErrorMessage('复制失败 (权限?)'), 'error'); } } getSuccessMessage() { // [优化] 为SVG增加无障碍属性 aria-hidden 和 focusable return ` 复制成功! `; } getErrorMessage(text) { // [优化] 为SVG增加无障碍属性 aria-hidden 和 focusable return ` ${text} `; } 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; 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(); })();