diff --git a/entrypoints/content/contextMenuHandler.ts b/entrypoints/content/contextMenuHandler.ts new file mode 100644 index 0000000..f412274 --- /dev/null +++ b/entrypoints/content/contextMenuHandler.ts @@ -0,0 +1,62 @@ +import { MessageAction, onMessage } from '@/utils/messages'; +import { getTextStats } from '@/utils/textStatistics'; +import { showTimestampResult, showTextStatsResult, hidePopover } from './uiPopover'; +import type { ContextMenuClickedPayload } from '@/utils/messages'; + +function convertTimestamp(input: string): string { + const num = Number(input.trim()); + if (isNaN(num)) { + return '无效时间戳'; + } + + const d = num > 1e12 ? new Date(num) : new Date(num * 1000); + + if (isNaN(d.getTime())) { + return '无效时间戳'; + } + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + + return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; +} + +let lastClickX = 0; +let lastClickY = 0; + +document.addEventListener( + 'contextmenu', + (e) => { + lastClickX = e.clientX; + lastClickY = e.clientY; + }, + true, +); + +export function initContextMenuHandler(): void { + onMessage(MessageAction.CONTEXT_MENU_CLICKED, (message) => { + const { featureKey, payload } = message.data as ContextMenuClickedPayload; + + switch (featureKey) { + case 'timestamp': { + const result = convertTimestamp(payload); + showTimestampResult(lastClickX, lastClickY, payload, result); + break; + } + + case 'textStatistics': { + const stats = getTextStats(payload); + showTextStatsResult(lastClickX, lastClickY, payload, stats); + break; + } + + default: + hidePopover(); + break; + } + }); +} diff --git a/entrypoints/content/messageHandler.ts b/entrypoints/content/messageHandler.ts index 7993b0c..26b3743 100644 --- a/entrypoints/content/messageHandler.ts +++ b/entrypoints/content/messageHandler.ts @@ -1 +1,5 @@ -export function initMessageHandler() {} +import { initContextMenuHandler } from './contextMenuHandler'; + +export function initMessageHandler(): void { + initContextMenuHandler(); +} diff --git a/entrypoints/content/uiPopover.ts b/entrypoints/content/uiPopover.ts new file mode 100644 index 0000000..008f82c --- /dev/null +++ b/entrypoints/content/uiPopover.ts @@ -0,0 +1,247 @@ +const POPOVER_ID = 'testing-tools-popover'; +const POPOVER_STYLE_ID = 'testing-tools-popover-style'; + +function injectStyles(): void { + if (document.getElementById(POPOVER_STYLE_ID)) return; + + const style = document.createElement('style'); + style.id = POPOVER_STYLE_ID; + style.textContent = ` + #${POPOVER_ID} { + position: fixed; + z-index: 2147483647; + max-width: 400px; + min-width: 200px; + padding: 12px 16px; + background: #1a1a2e; + color: #e0e0e0; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + line-height: 1.5; + opacity: 0; + transform: translateY(-8px); + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; + } + + #${POPOVER_ID}.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; + } + + #${POPOVER_ID} .popover-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + #${POPOVER_ID} .popover-title { + font-weight: 600; + font-size: 12px; + color: #a0a0b0; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + #${POPOVER_ID} .popover-close { + background: none; + border: none; + color: #808090; + cursor: pointer; + padding: 2px; + font-size: 16px; + line-height: 1; + } + + #${POPOVER_ID} .popover-close:hover { + color: #e0e0e0; + } + + #${POPOVER_ID} .popover-content { + word-break: break-all; + white-space: pre-wrap; + } + + #${POPOVER_ID} .popover-label { + color: #808090; + font-size: 11px; + margin-bottom: 4px; + } + + #${POPOVER_ID} .popover-value { + color: #ffffff; + font-family: 'SF Mono', 'Consolas', 'Monaco', monospace; + font-size: 14px; + padding: 6px 8px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + margin-bottom: 8px; + } + + #${POPOVER_ID} .popover-value:last-child { + margin-bottom: 0; + } + + #${POPOVER_ID} .stat-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + + #${POPOVER_ID} .stat-item { + padding: 6px 8px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + } + + #${POPOVER_ID} .stat-label { + font-size: 11px; + color: #808090; + } + + #${POPOVER_ID} .stat-value { + font-size: 16px; + font-weight: 600; + color: #ffffff; + } + `; + document.head.appendChild(style); +} + +function getOrCreatePopover(): HTMLElement { + let popover = document.getElementById(POPOVER_ID); + if (!popover) { + injectStyles(); + popover = document.createElement('div'); + popover.id = POPOVER_ID; + document.body.appendChild(popover); + } + return popover; +} + +function positionPopover(popover: HTMLElement, x: number, y: number): void { + const rect = popover.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let left = x; + let top = y; + + if (left + rect.width > viewportWidth - 16) { + left = viewportWidth - rect.width - 16; + } + if (left < 16) { + left = 16; + } + + if (top + rect.height > viewportHeight - 16) { + top = y - rect.height - 8; + } + if (top < 16) { + top = 16; + } + + popover.style.left = `${left}px`; + popover.style.top = `${top}px`; +} + +let hideTimeout: ReturnType | null = null; + +export function showPopover( + x: number, + y: number, + content: string, + title?: string, + duration: number = 5000, +): void { + const popover = getOrCreatePopover(); + + const titleHtml = title + ? `
+ ${title} + +
` + : ''; + + popover.innerHTML = ` + ${titleHtml} +
${content}
+ `; + + popover.classList.remove('visible'); + + requestAnimationFrame(() => { + positionPopover(popover, x, y); + popover.classList.add('visible'); + }); + + if (hideTimeout) { + clearTimeout(hideTimeout); + } + + if (duration > 0) { + hideTimeout = setTimeout(() => { + hidePopover(); + }, duration); + } +} + +export function hidePopover(): void { + const popover = document.getElementById(POPOVER_ID); + if (popover) { + popover.classList.remove('visible'); + } + if (hideTimeout) { + clearTimeout(hideTimeout); + hideTimeout = null; + } +} + +export function showTimestampResult(x: number, y: number, timestamp: string, result: string): void { + const content = ` +
输入时间戳
+
${timestamp}
+
转换结果
+
${result}
+ `; + showPopover(x, y, content, '⏰ 时间戳转换'); +} + +export function showTextStatsResult( + x: number, + y: number, + text: string, + stats: { characters: number; words: number; lines: number; bytes: number }, +): void { + const truncatedText = text.length > 50 ? text.substring(0, 50) + '...' : text; + const content = ` +
选中文本
+
${truncatedText}
+
+
+
字符
+
${stats.characters}
+
+
+
单词
+
${stats.words}
+
+
+
行数
+
${stats.lines}
+
+
+
字节
+
${stats.bytes}
+
+
+ `; + showPopover(x, y, content, '📊 文本统计'); +}