// ==UserScript==
// @name 复制网页标题和链接 (配置快捷键)
// @namespace https://greasyfork.org/
// @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
// @downloadURL none
// ==/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)。
当前: ${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;
}
// 忽略单独的修饰键
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, Enter 等)
按 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 `
复制成功!
`;
}
getErrorMessage(text) {
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, "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();
})();