按下 ALT + E 打开自定义 CSS 编辑器
// ==UserScript==
// @name 自定义网页样式
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 按下 ALT + E 打开自定义 CSS 编辑器
// @author Verlif
// @license MIT
// @match https://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(async function () {
'use strict';
let panel = null;
let editor = null;
let styleEl = null;
let isResizing = false;
let originalCSS = '';
let livePreview = true;
const STORAGE_KEY_PREFIX = 'custom_css_';
const WINDOW_STATE_KEY = 'custom_css_window_state';
// --- 读取保存的 CSS 并应用 ---
const savedCSS = await GM_getValue(STORAGE_KEY_PREFIX + location.host, '');
if (savedCSS) applyCSS(savedCSS);
// --- 初始化窗口状态 ---
const windowState = await GM_getValue(WINDOW_STATE_KEY, {});
let panelLeft = windowState.left || 100;
let panelTop = windowState.top || 100;
let panelWidth = windowState.width || 500;
let panelHeight = windowState.height || 350;
// --- ALT + E 打开/关闭面板 ---
document.addEventListener('keydown', (e) => {
if (e.altKey && e.key.toLowerCase() === 'e') {
e.preventDefault();
if (panel) closePanel(false);
else createPanel();
}
});
// --- 关闭面板 ---
async function closePanel(saved) {
if (!panel) return;
if (!saved) applyCSS(originalCSS);
await saveWindowState();
panel.remove();
panel = null;
editor = null;
}
// --- 保存窗口状态 ---
async function saveWindowState() {
if (!panel) return;
const newState = {
left: panel.offsetLeft,
top: panel.offsetTop,
width: panel.offsetWidth,
height: panel.offsetHeight,
};
panelLeft = newState.left;
panelTop = newState.top;
panelWidth = newState.width;
panelHeight = newState.height;
await GM_setValue(WINDOW_STATE_KEY, newState);
}
// --- 创建面板 ---
async function createPanel() {
originalCSS = styleEl ? styleEl.textContent : '';
const savedCSS = await GM_getValue(STORAGE_KEY_PREFIX + location.host, '');
panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
top: ${panelTop}px;
left: ${panelLeft}px;
width: ${panelWidth}px;
height: ${panelHeight}px;
background: #fff;
border: 1px solid #ccc;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
border-radius: 8px;
z-index: 999999;
display: flex;
flex-direction: column;
font-family: sans-serif;
`;
const header = document.createElement('div');
header.textContent = '自定义 CSS 编辑器';
header.style.cssText = `
padding: 8px 12px;
background: #f4f4f4;
border-bottom: 1px solid #ddd;
cursor: move;
color: black;
user-select: none;
font-weight: bold;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
`;
const toggleLive = document.createElement('button');
toggleLive.textContent = livePreview ? '实时预览:开' : '实时预览:关';
toggleLive.style.cssText = `
font-size: 12px;
padding: 2px 6px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fafafa;
color: black;
cursor: pointer;
`;
toggleLive.onclick = () => {
livePreview = !livePreview;
toggleLive.textContent = livePreview ? '实时预览:开' : '实时预览:关';
if (livePreview && editor) applyCSS(editor.getValue());
else applyCSS(originalCSS);
};
header.appendChild(toggleLive);
const editorContainer = document.createElement('div');
editorContainer.style.cssText = `
flex: 1;
overflow: hidden;
position: relative;
`;
const footer = document.createElement('div');
footer.style.cssText = `
padding: 8px;
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid #ddd;
background: #fafafa;
border-radius: 0 0 8px 8px;
`;
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存并应用';
saveBtn.style.cssText = `
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.cssText = `
background: #e0e0e0;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
color: black;
`;
footer.appendChild(saveBtn);
footer.appendChild(closeBtn);
const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = `
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #007bff 50%);
border-bottom-right-radius: 8px;
z-index: 10;
`;
editorContainer.appendChild(resizeHandle);
panel.appendChild(header);
panel.appendChild(editorContainer);
panel.appendChild(footer);
document.body.appendChild(panel);
makePanelDraggable(panel, header);
makePanelResizable(panel, editorContainer, resizeHandle);
closeBtn.onclick = () => closePanel(false);
saveBtn.onclick = async () => {
const css = editor.getValue();
await GM_setValue(STORAGE_KEY_PREFIX + location.host, css);
applyCSS(css);
showToast('样式已保存并应用');
closePanel(true);
};
await loadCodeMirror(editorContainer, savedCSS);
// 修复 CodeMirror 初始化光标错位
setTimeout(() => {
if (editor && editor.refresh) editor.refresh();
}, 150);
}
// --- 拖动逻辑 ---
function makePanelDraggable(panel, dragHandle) {
let offsetX, offsetY, isDragging = false;
let rafId;
dragHandle.addEventListener('mousedown', (e) => {
if (isResizing) return;
isDragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
function move(e) {
if (!isDragging) return;
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
panel.style.left = e.clientX - offsetX + 'px';
panel.style.top = e.clientY - offsetY + 'px';
});
}
function stop() {
isDragging = false;
saveWindowState();
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', stop);
}
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', stop);
});
}
// --- 右下角拖拽调整大小 ---
function makePanelResizable(panel, editorContainer, handle) {
let startX, startY, startWidth, startHeight;
let rafId;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = panel.offsetWidth;
startHeight = panel.offsetHeight;
function resize(e) {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const newWidth = Math.max(300, startWidth + (e.clientX - startX));
const newHeight = Math.max(200, startHeight + (e.clientY - startY));
panel.style.width = newWidth + 'px';
panel.style.height = newHeight + 'px';
editorContainer.style.height = `calc(100% - 90px)`;
if (editor && editor.refresh) editor.refresh();
saveWindowState();
});
}
function stop() {
isResizing = false;
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stop);
}
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stop);
e.preventDefault();
});
}
// --- 加载 CodeMirror 编辑器 ---
async function loadCodeMirror(container, initialValue) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.css';
document.head.appendChild(link);
await loadScript('https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.js');
await loadScript('https://cdn.jsdelivr.net/npm/[email protected]/mode/css/css.js');
const textarea = document.createElement('textarea');
container.appendChild(textarea);
editor = CodeMirror.fromTextArea(textarea, {
mode: 'css',
lineNumbers: true,
lineWrapping: true,
theme: 'default'
});
editor.setValue(initialValue);
editor.getWrapperElement().style.height = '100%';
editor.getWrapperElement().style.fontSize = '16px';
editor.on('change', () => {
if (livePreview) applyCSS(editor.getValue());
});
}
// --- 加载外部脚本 ---
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// --- 应用 CSS ---
function applyCSS(css) {
if (styleEl) styleEl.remove();
styleEl = document.createElement('style');
styleEl.textContent = css;
document.head.appendChild(styleEl);
}
// --- 提示气泡 ---
function showToast(msg) {
const toast = document.createElement('div');
toast.textContent = msg;
toast.style.cssText = `
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
z-index: 9999999;
transition: opacity 0.3s;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 500);
}, 1500);
}
})();