From ea0976b75b14eca553b205ad6ef79b218158c161 Mon Sep 17 00:00:00 2001 From: ytc1012 <18001193130@163.com> Date: Thu, 11 Dec 2025 11:44:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8C=BA=E5=9F=9F=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- content.css | 138 ++++++++++----- content.js | 335 ++++++++++++++++++++++++++++++++++-- popup.html | 6 +- popup.js | 13 +- test.html | 2 +- 6 files changed, 433 insertions(+), 64 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1e2b006..204c4d4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(wc:*)" + "Bash(wc:*)", + "Bash(cat:*)" ] } } diff --git a/content.css b/content.css index 66f191f..5e529f0 100644 --- a/content.css +++ b/content.css @@ -10,59 +10,30 @@ position: relative !important; } -/* 元素模式下的悬停效果 */ -body.blurtext-mode .blurtext-blurred:hover::after { - content: '点击取消模糊' !important; - position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; +/* 元素模式下的悬停效果 - 移除伪元素方案,改用 JS 动态创建 */ +/* body.blurtext-mode .blurtext-blurred:hover::after 已移除 */ + +/* 元素模式悬停提示工具栏(JS 动态创建) */ +.blurtext-element-tooltip { + position: fixed !important; background: rgba(102, 126, 234, 0.95) !important; color: white !important; - padding: 4px 8px !important; - border-radius: 4px !important; - font-size: 12px !important; - white-space: nowrap !important; - z-index: 999999 !important; - pointer-events: none !important; - filter: none !important; -} - -/* 文本选择模式包裹的元素悬停效果 */ -.blurtext-selection-wrapped { - transition: all 0.1s ease !important; -} - -.blurtext-selection-wrapped:hover { - background-color: rgba(102, 126, 234, 0.2) !important; - outline: 2px solid rgba(102, 126, 234, 0.5) !important; - outline-offset: 2px !important; -} - -.blurtext-selection-wrapped:hover::after { - content: '🔓 点击恢复' !important; - position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - background: rgba(102, 126, 234, 0.98) !important; - color: white !important; padding: 6px 12px !important; border-radius: 6px !important; font-size: 13px !important; font-weight: 600 !important; white-space: nowrap !important; - z-index: 999999 !important; + z-index: 2147483647 !important; pointer-events: none !important; - filter: none !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; - animation: blurtext-tooltipPop 0.15s ease !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; + animation: blurtext-tooltipFadeIn 0.15s ease !important; } -@keyframes blurtext-tooltipPop { +@keyframes blurtext-tooltipFadeIn { from { opacity: 0; - transform: translate(-50%, -50%) scale(0.8); + transform: translate(-50%, -50%) scale(0.9); } to { opacity: 1; @@ -70,6 +41,42 @@ body.blurtext-mode .blurtext-blurred:hover::after { } } + +/* 文本选择模式包裹的元素悬停效果 */ +.blurtext-selection-wrapped { + display: inline !important; + position: relative !important; + cursor: pointer !important; +} + +/* 移除伪元素提示,改用 JS 动态创建(避免被模糊效果影响) */ + +/* 文本选择错误提示(显示在选区下方) */ +.blurtext-selection-error { + background: #f44336 !important; + color: white !important; + padding: 8px 16px !important; + border-radius: 6px !important; + font-size: 13px !important; + font-weight: 500 !important; + box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4) !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; + animation: blurtext-errorPop 0.2s ease !important; + pointer-events: none !important; + white-space: nowrap !important; +} + +@keyframes blurtext-errorPop { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(-5px) scale(0.9); + } + 100% { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } +} + /* 模糊模式下的鼠标样式 */ body.blurtext-mode * { cursor: crosshair !important; @@ -149,7 +156,52 @@ body.blurtext-mode * { } } -/* 文本选择模式包裹的元素 */ -.blurtext-selection-wrapped { - display: inline !important; +/* ========== 区域模式样式 ========== */ + +/* 绘制框(拖动时显示) */ +.blurtext-drawing-box { + border: 2px dashed #667eea !important; + background: rgba(102, 126, 234, 0.1) !important; + pointer-events: none !important; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5) !important; +} + +/* 区域覆盖层 */ +.blurtext-area-overlay { + background: rgba(255, 255, 255, 0.1) !important; + backdrop-filter: blur(var(--blur-intensity, 10px)) !important; + -webkit-backdrop-filter: blur(var(--blur-intensity, 10px)) !important; + border: none !important; + box-sizing: border-box !important; + transition: all 0.2s ease !important; + user-select: none !important; + cursor: pointer !important; +} + +.blurtext-area-overlay:hover { + outline: 2px solid rgba(102, 126, 234, 0.5) !important; + outline-offset: -2px !important; + background: rgba(255, 255, 255, 0.15) !important; +} + +/* 区域覆盖层悬停提示 */ +.blurtext-area-overlay:hover::after { + content: '点击恢复此区域' !important; + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: rgba(102, 126, 234, 0.95) !important; + color: white !important; + padding: 6px 12px !important; + border-radius: 6px !important; + font-size: 13px !important; + font-weight: 600 !important; + white-space: nowrap !important; + z-index: 999999 !important; + pointer-events: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + filter: none !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; } diff --git a/content.js b/content.js index 0d6275e..a610e7f 100644 --- a/content.js +++ b/content.js @@ -5,11 +5,19 @@ console.log('[BlurText] Content script loaded'); let isBlurMode = false; - let blurMode = 'element'; // 'element' or 'selection' + let blurMode = 'element'; // 'element', 'selection', or 'area' let blurIntensity = 10; let blurredElements = new Set(); let hintElement = null; let blurButton = null; // 浮动模糊按钮 + let elementTooltip = null; // 元素模式悬停提示 + + // 区域模式相关变量 + let isDrawing = false; + let startX = 0; + let startY = 0; + let drawingBox = null; + let areaOverlays = []; // 存储所有区域覆盖层 // 初始化:从存储中加载配置 chrome.storage.local.get(['blurIntensity'], (result) => { @@ -53,7 +61,12 @@ function enableBlurMode() { console.log('[BlurText] Enabling blur mode, mode:', blurMode); - const modeText = blurMode === 'selection' ? '文本选择模式 - 拖动选择文字后点击模糊按钮' : '元素模式 - 点击元素进行模糊'; + let modeText = '元素模式 - 点击元素进行模糊'; + if (blurMode === 'selection') { + modeText = '文本选择模式 - 拖动选择文字后点击模糊按钮'; + } else if (blurMode === 'area') { + modeText = '区域模式 - 拖动鼠标绘制模糊区域'; + } showHint(`模糊模式已开启 - ${modeText},按 ESC 退出`); // 根据模式添加对应的样式和事件 @@ -65,9 +78,15 @@ } else if (blurMode === 'selection') { // 文本选择模式不需要 crosshair 光标 document.addEventListener('mouseup', handleTextSelection, true); + } else if (blurMode === 'area') { + // 区域模式:使用 crosshair 光标并添加绘制事件 + document.body.classList.add('blurtext-mode'); + document.addEventListener('mousedown', handleAreaMouseDown, true); + document.addEventListener('mousemove', handleAreaMouseMove, true); + document.addEventListener('mouseup', handleAreaMouseUp, true); } - // 键盘事件对两种模式都需要 + // 键盘事件对所有模式都需要 document.addEventListener('keydown', handleKeydown, true); console.log('[BlurText] Event listeners attached'); @@ -78,6 +97,8 @@ document.body.classList.remove('blurtext-mode'); hideHint(); hideBlurButton(); + hideDrawingBox(); + hideElementTooltip(); // 移除所有事件监听 document.removeEventListener('click', handleClick, true); @@ -85,6 +106,9 @@ document.removeEventListener('mouseover', handleMouseOver, true); document.removeEventListener('mouseout', handleMouseOut, true); document.removeEventListener('mouseup', handleTextSelection, true); + document.removeEventListener('mousedown', handleAreaMouseDown, true); + document.removeEventListener('mousemove', handleAreaMouseMove, true); + document.removeEventListener('mouseup', handleAreaMouseUp, true); // 清除所有高亮和预览 removeAllHighlights(); @@ -102,10 +126,15 @@ document.removeEventListener('mouseover', handleMouseOver, true); document.removeEventListener('mouseout', handleMouseOut, true); document.removeEventListener('mouseup', handleTextSelection, true); + document.removeEventListener('mousedown', handleAreaMouseDown, true); + document.removeEventListener('mousemove', handleAreaMouseMove, true); + document.removeEventListener('mouseup', handleAreaMouseUp, true); // 清除旧模式的UI元素 hideBlurButton(); removeAllHighlights(); + hideDrawingBox(); + hideElementTooltip(); // 根据新模式添加事件监听和样式 if (blurMode === 'element') { @@ -118,6 +147,13 @@ // 文本选择模式不需要 crosshair 光标 document.addEventListener('mouseup', handleTextSelection, true); showHint('已切换到文本选择模式 - 拖动选择文字后点击模糊按钮', 3000); + } else if (blurMode === 'area') { + // 区域模式 + document.body.classList.add('blurtext-mode'); + document.addEventListener('mousedown', handleAreaMouseDown, true); + document.addEventListener('mousemove', handleAreaMouseMove, true); + document.addEventListener('mouseup', handleAreaMouseUp, true); + showHint('已切换到区域模式 - 拖动鼠标绘制模糊区域', 3000); } console.log('[BlurText] Mode switched successfully'); @@ -222,6 +258,33 @@ } } + // 显示元素悬停提示 + function showElementTooltip(element, text) { + // 移除旧提示 + hideElementTooltip(); + + // 获取元素位置 + const rect = element.getBoundingClientRect(); + + // 创建提示元素 + elementTooltip = document.createElement('div'); + elementTooltip.className = 'blurtext-element-tooltip'; + elementTooltip.textContent = text; + elementTooltip.style.left = `${rect.left + rect.width / 2}px`; + elementTooltip.style.top = `${rect.top + rect.height / 2}px`; + elementTooltip.style.transform = 'translate(-50%, -50%)'; + + document.body.appendChild(elementTooltip); + } + + // 隐藏元素悬停提示 + function hideElementTooltip() { + if (elementTooltip && elementTooltip.parentNode) { + elementTooltip.parentNode.removeChild(elementTooltip); + elementTooltip = null; + } + } + // 模糊选中的文本 function blurSelectedText() { const selection = window.getSelection(); @@ -230,21 +293,76 @@ try { const range = selection.getRangeAt(0); + // 获取选中的文本内容 + const selectedText = range.toString(); + + // 验证选区是否符合要求 + if (!selectedText.trim()) { + console.log('[BlurText] No text selected'); + hideBlurButton(); + return; + } + + // 限制:不允许包含换行 + if (selectedText.includes('\n') || selectedText.includes('\r')) { + console.log('[BlurText] Selection contains line breaks'); + showSelectionError(selection, '不支持跨行选择'); + hideBlurButton(); + return; + } + + // 限制:不允许包含多个空格(允许单个空格) + if (/\s{2,}/.test(selectedText)) { + console.log('[BlurText] Selection contains multiple spaces'); + showSelectionError(selection, '不支持多个连续空格'); + hideBlurButton(); + return; + } + + // 限制:检查是否跨越多个元素 + const startContainer = range.startContainer; + const endContainer = range.endContainer; + + // 如果起始和结束容器不同,则跨越了多个节点 + if (startContainer !== endContainer) { + console.log('[BlurText] Selection spans multiple elements'); + showSelectionError(selection, '不支持跨元素选择'); + hideBlurButton(); + return; + } + + // 限制:只能选择文本节点 + if (startContainer.nodeType !== Node.TEXT_NODE) { + console.log('[BlurText] Selection is not in a text node'); + showSelectionError(selection, '请选择纯文本内容'); + hideBlurButton(); + return; + } + // 创建 span 包裹选中的文本 const span = document.createElement('span'); span.className = 'blurtext-blurred blurtext-selection-wrapped'; span.style.setProperty('--blur-intensity', `${blurIntensity}px`); span.style.cursor = 'pointer'; - span.title = '点击恢复此段文本'; + + // 添加鼠标悬停事件,显示恢复提示 + span.addEventListener('mouseenter', () => { + showElementTooltip(span, '点击恢复此文本'); + }); + + span.addEventListener('mouseleave', () => { + hideElementTooltip(); + }); // 添加点击事件,允许单独恢复 span.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); + hideElementTooltip(); // 点击后立即隐藏提示 unblurSelectionSpan(span); }); - // 包裹选中的内容 + // 使用 surroundContents(在严格限制下应该不会失败) range.surroundContents(span); // 添加到模糊元素集合 @@ -257,16 +375,43 @@ // 隐藏按钮 hideBlurButton(); - - // 显示提示 - showHint('文本已模糊(点击可单独恢复)', 2000); } catch (error) { console.error('[BlurText] Error blurring selection:', error); - showHint('无法模糊该选区(可能包含复杂的HTML结构)', 3000); + showSelectionError(selection, '无法模糊该选区'); hideBlurButton(); } } + // 在选区位置显示错误提示 + function showSelectionError(selection, message) { + if (!selection.rangeCount) return; + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + // 创建错误提示元素 + const errorTooltip = document.createElement('div'); + errorTooltip.className = 'blurtext-selection-error'; + errorTooltip.textContent = message; + errorTooltip.style.position = 'fixed'; + errorTooltip.style.left = `${rect.left + rect.width / 2}px`; + errorTooltip.style.top = `${rect.bottom + 10}px`; + errorTooltip.style.transform = 'translateX(-50%)'; + errorTooltip.style.zIndex = '2147483647'; + + document.body.appendChild(errorTooltip); + + // 清除选择 + selection.removeAllRanges(); + + // 2秒后自动移除 + setTimeout(() => { + if (errorTooltip.parentNode) { + errorTooltip.parentNode.removeChild(errorTooltip); + } + }, 2000); + } + // 恢复单个选择模式的模糊段落 function unblurSelectionSpan(span) { if (!span || !span.parentNode) return; @@ -276,7 +421,7 @@ // 从集合中移除 blurredElements.delete(span); - // 获取 span 的内容 + // 获取 span 的内容(保持子节点结构) const fragment = document.createDocumentFragment(); while (span.firstChild) { fragment.appendChild(span.firstChild); @@ -285,9 +430,6 @@ // 用原内容替换 span span.parentNode.replaceChild(fragment, span); - // 显示提示 - showHint('已恢复此段文本', 1500); - console.log('[BlurText] Selection span removed, remaining elements:', blurredElements.size); } @@ -296,13 +438,20 @@ if (!isBlurMode || blurMode !== 'element') return; const target = e.target; - if (target.classList.contains('blurtext-hint') || - target.classList.contains('blurtext-blurred')) { + if (target.classList.contains('blurtext-hint')) { return; } - // 移除之前的高亮 + // 如果是已模糊的元素,显示恢复提示(不添加高亮) + if (target.classList.contains('blurtext-blurred')) { + removeAllHighlights(); // 移除其他高亮 + showElementTooltip(target, '点击取消模糊'); + return; + } + + // 移除之前的高亮和提示 removeAllHighlights(); + hideElementTooltip(); // 直接高亮元素 target.classList.add('blurtext-highlight'); @@ -320,6 +469,9 @@ function handleMouseOut(e) { if (!isBlurMode || blurMode !== 'element') return; + // 隐藏提示 + hideElementTooltip(); + // 延迟移除,避免在元素间移动时闪烁 setTimeout(() => { // 检查鼠标是否还在模糊模式下的元素上 @@ -346,6 +498,9 @@ element.classList.remove('blurtext-blurred'); element.style.removeProperty('--blur-intensity'); blurredElements.delete(element); + + // 隐藏提示气泡(因为元素已不再模糊) + hideElementTooltip(); } else { // 添加模糊 console.log('[BlurText] Adding blur to element, intensity:', blurIntensity); @@ -364,12 +519,160 @@ }); blurredElements.clear(); + // 清除所有区域覆盖层 + areaOverlays.forEach(overlay => { + if (overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + }); + areaOverlays = []; + // 如果在模糊模式下,显示提示 if (isBlurMode) { showHint('已清除所有模糊效果', 2000); } } + // ========== 区域模式相关函数 ========== + + // 处理区域模式鼠标按下 + function handleAreaMouseDown(e) { + if (!isBlurMode || blurMode !== 'area') return; + + // 忽略提示元素和已有的区域覆盖层 + if (e.target.classList.contains('blurtext-hint') || + e.target.classList.contains('blurtext-area-overlay') || + e.target.classList.contains('blurtext-area-close')) { + return; + } + + // 检查是否点击了关闭按钮 + if (e.target.classList.contains('blurtext-area-close')) { + const overlay = e.target.parentElement; + removeAreaOverlay(overlay); + e.preventDefault(); + e.stopPropagation(); + return; + } + + isDrawing = true; + startX = e.pageX; + startY = e.pageY; + + // 创建绘制框 + drawingBox = document.createElement('div'); + drawingBox.className = 'blurtext-drawing-box'; + drawingBox.style.position = 'absolute'; + drawingBox.style.left = startX + 'px'; + drawingBox.style.top = startY + 'px'; + drawingBox.style.width = '0px'; + drawingBox.style.height = '0px'; + drawingBox.style.zIndex = '2147483646'; + document.body.appendChild(drawingBox); + + e.preventDefault(); + e.stopPropagation(); + } + + // 处理区域模式鼠标移动 + function handleAreaMouseMove(e) { + if (!isBlurMode || blurMode !== 'area' || !isDrawing || !drawingBox) return; + + const currentX = e.pageX; + const currentY = e.pageY; + + const width = Math.abs(currentX - startX); + const height = Math.abs(currentY - startY); + const left = Math.min(currentX, startX); + const top = Math.min(currentY, startY); + + drawingBox.style.left = left + 'px'; + drawingBox.style.top = top + 'px'; + drawingBox.style.width = width + 'px'; + drawingBox.style.height = height + 'px'; + } + + // 处理区域模式鼠标释放 + function handleAreaMouseUp(e) { + if (!isBlurMode || blurMode !== 'area' || !isDrawing || !drawingBox) return; + + isDrawing = false; + + const width = parseInt(drawingBox.style.width); + const height = parseInt(drawingBox.style.height); + + // 只有当区域足够大时才创建模糊覆盖层(至少 20x20 像素) + if (width > 20 && height > 20) { + createAreaOverlay( + parseInt(drawingBox.style.left), + parseInt(drawingBox.style.top), + width, + height + ); + } + + // 移除绘制框 + if (drawingBox.parentNode) { + drawingBox.parentNode.removeChild(drawingBox); + } + drawingBox = null; + + e.preventDefault(); + e.stopPropagation(); + } + + // 创建区域覆盖层 + function createAreaOverlay(left, top, width, height) { + const overlay = document.createElement('div'); + overlay.className = 'blurtext-area-overlay'; + overlay.style.position = 'absolute'; + overlay.style.left = left + 'px'; + overlay.style.top = top + 'px'; + overlay.style.width = width + 'px'; + overlay.style.height = height + 'px'; + overlay.style.setProperty('--blur-intensity', `${blurIntensity}px`); + overlay.style.zIndex = '2147483645'; + + // 点击覆盖层恢复区域 + overlay.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + removeAreaOverlay(overlay); + }); + + document.body.appendChild(overlay); + areaOverlays.push(overlay); + + // 添加到模糊元素集合(用于统一管理强度) + blurredElements.add(overlay); + + console.log('[BlurText] Area overlay created, total overlays:', areaOverlays.length); + } + + // 移除区域覆盖层 + function removeAreaOverlay(overlay) { + const index = areaOverlays.indexOf(overlay); + if (index > -1) { + areaOverlays.splice(index, 1); + } + blurredElements.delete(overlay); + if (overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + console.log('[BlurText] Area overlay removed, remaining:', areaOverlays.length); + } + + // 隐藏绘制框 + function hideDrawingBox() { + if (drawingBox && drawingBox.parentNode) { + drawingBox.parentNode.removeChild(drawingBox); + drawingBox = null; + } + isDrawing = false; + } + + // ========== 区域模式函数结束 ========== + // 更新所有已模糊元素的强度 function updateAllBlurIntensity() { blurredElements.forEach(element => { diff --git a/popup.html b/popup.html index a1651df..a1ea2c3 100644 --- a/popup.html +++ b/popup.html @@ -168,7 +168,7 @@ .mode-buttons { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; gap: 8px; } @@ -243,6 +243,10 @@ 📝 文本选择 + diff --git a/popup.js b/popup.js index 037b3c2..2c3328e 100644 --- a/popup.js +++ b/popup.js @@ -13,6 +13,7 @@ const intensityValue = document.getElementById('intensityValue'); const statusDiv = document.getElementById('status'); const modeElementBtn = document.getElementById('modeElement'); const modeSelectionBtn = document.getElementById('modeSelection'); +const modeAreaBtn = document.getElementById('modeArea'); const usageTip = document.getElementById('usageTip'); // 从存储中加载模糊强度 @@ -41,7 +42,7 @@ blurIntensitySlider.addEventListener('input', (e) => { }); // 模式切换 -[modeElementBtn, modeSelectionBtn].forEach(btn => { +[modeElementBtn, modeSelectionBtn, modeAreaBtn].forEach(btn => { btn.addEventListener('click', () => { const mode = btn.dataset.mode; currentMode = mode; @@ -75,7 +76,7 @@ function updateUsageTip(mode) { 3. 点击元素进行模糊
4. 再次点击取消模糊 `; - } else { + } else if (mode === 'selection') { usageTip.innerHTML = ` 文本选择模式:
1. 点击"开启模糊模式"
@@ -83,6 +84,14 @@ function updateUsageTip(mode) { 3. 点击浮动按钮模糊文本
4. 可多次选择和模糊 `; + } else if (mode === 'area') { + usageTip.innerHTML = ` + 区域模式:
+ 1. 点击"开启模糊模式"
+ 2. 拖动鼠标绘制矩形区域
+ 3. 释放鼠标创建模糊区域
+ 4. 点击区域恢复 + `; } } diff --git a/test.html b/test.html index 0fe67bb..3fb2fb3 100644 --- a/test.html +++ b/test.html @@ -136,7 +136,7 @@

📝 测试区域 1:普通文本

-

这是一段普通的文本内容,你可以点击这段文字来测试模糊功能。

+

这是一段普通的文本内容,你可以点击这段文字来测试模糊功能。这是一段普通的文本内容,你可以点击这段文字来测试模糊功能。这是一段普通的文本内容,你可以点击这段文字来测试模糊功能。

模糊功能应该可以立即生效,不需要屏幕录制或分享。