diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json index fc9491f..b8b3a33 100644 --- a/public/_locales/zh/messages.json +++ b/public/_locales/zh/messages.json @@ -760,6 +760,10 @@ "message": "毫秒 (ms)", "description": "Translation key: timestamp_unitMs" }, + "timestamp_unitS": { + "message": "秒 (s)", + "description": "Translation key: timestamp_unitS" + }, "timestamp_currentTs": { "message": "当前时间戳", "description": "Translation key: timestamp_currentTs" diff --git a/src/entrypoints/rightClickRestorer.content.ts b/src/entrypoints/rightClickRestorer.content.ts index 5d59201..8d860ff 100644 --- a/src/entrypoints/rightClickRestorer.content.ts +++ b/src/entrypoints/rightClickRestorer.content.ts @@ -1,57 +1,7 @@ import { MessageAction, onMessage, sendMessage } from '@/utils/messages'; -const BADGE_ID = 'testing-tools-right-click-restorer-badge'; -const BADGE_STYLE_ID = 'testing-tools-right-click-restorer-badge-style'; - let isRestored = false; -function updateBadge(): void { - const badge = document.getElementById(BADGE_ID); - if (badge) { - badge.style.opacity = isRestored ? '1' : '0'; - } -} - -function createBadge(): void { - if (document.getElementById(BADGE_ID)) return; - - if (!document.getElementById(BADGE_STYLE_ID)) { - const style = document.createElement('style'); - style.id = BADGE_STYLE_ID; - style.textContent = ` - #${BADGE_ID} { - position: fixed; - bottom: 16px; - right: 16px; - z-index: 2147483646; - padding: 6px 12px; - background: #2e7d32; - color: #ffffff; - border-radius: 20px; - font-size: 12px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); - opacity: 0; - transition: opacity 0.2s ease-out; - pointer-events: none; - user-select: none; - } - @media (prefers-color-scheme: dark) { - #${BADGE_ID} { - background: #4caf50; - box-shadow: 0 2px 8px rgba(0,0,0,0.4); - } - } - `; - document.head.appendChild(style); - } - - const badge = document.createElement('div'); - badge.id = BADGE_ID; - badge.textContent = '\u53f3\u952e\u5df2\u89e3\u9501'; - document.body.appendChild(badge); -} - /* ======================== Main World 注入 ======================== */ async function injectMainWorldScript(): Promise { @@ -90,64 +40,6 @@ function installEventIntercepts(): void { ); } -/* ======================== 遮罩层穿透 ======================== */ - -const MEDIA_TAGS = new Set(['IMG', 'VIDEO', 'CANVAS', 'SVG']); - -function initMousePenetration(): void { - window.addEventListener( - 'mousedown', - (e) => { - if (!isRestored || e.button !== 2) return; - - const elements = document.elementsFromPoint(e.clientX, e.clientY); - if (!elements.length) return; - - let targetMedia: HTMLElement | null = null; - for (const el of elements) { - if (MEDIA_TAGS.has(el.tagName)) { - targetMedia = el as HTMLElement; - break; - } - } - - if (!targetMedia) return; - - const modified: Array<{ el: HTMLElement; original: string | null }> = []; - let foundMedia = false; - - for (const el of elements) { - const htmlEl = el as HTMLElement; - - if (el === targetMedia) { - foundMedia = true; - const original = htmlEl.style.pointerEvents || null; - htmlEl.style.setProperty('pointer-events', 'all', 'important'); - modified.push({ el: htmlEl, original }); - continue; - } - - if (!foundMedia) { - const original = htmlEl.style.pointerEvents || null; - htmlEl.style.setProperty('pointer-events', 'none', 'important'); - modified.push({ el: htmlEl, original }); - } - } - - setTimeout(() => { - for (const { el, original } of modified) { - if (original === null || original === '') { - el.style.removeProperty('pointer-events'); - } else { - el.style.pointerEvents = original; - } - } - }, 300); - }, - true, - ); -} - /* ======================== 激活保护 ======================== */ async function activateProtection(): Promise { @@ -159,10 +51,8 @@ async function activateProtection(): Promise { if (isRestored) return; installEventIntercepts(); - initMousePenetration(); isRestored = true; - updateBadge(); } /* ======================== 消息通信 ======================== */ @@ -184,11 +74,6 @@ export default defineContentScript({ matches: [''], runAt: 'document_start', main() { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createBadge, { once: true }); - } else { - createBadge(); - } initMessaging(); }, }); diff --git a/src/pages/RightClickRestorer/useRightClickRestorer.ts b/src/pages/RightClickRestorer/useRightClickRestorer.ts index d1ec990..b215743 100644 --- a/src/pages/RightClickRestorer/useRightClickRestorer.ts +++ b/src/pages/RightClickRestorer/useRightClickRestorer.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { MessageAction, sendMessageToContent } from '@/utils/messages'; const UNSUPPORTED_PROTOCOLS = new Set([ @@ -43,7 +43,6 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { if (isUnsupportedPage(url)) { setIsUnsupported(true); - setIsLoading(false); return; } @@ -61,7 +60,7 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { void load(); }, []); - const unlock = useCallback(async () => { + async function unlock() { if (isUnsupported) return; try { @@ -72,7 +71,7 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { } catch (err) { console.error('[RightClickRestorer] Failed to unlock:', err); } - }, [isUnsupported]); + } return { domain, diff --git a/src/pages/Timestamp/LiveClock.tsx b/src/pages/Timestamp/LiveClock.tsx index 0ded535..a75b352 100644 --- a/src/pages/Timestamp/LiveClock.tsx +++ b/src/pages/Timestamp/LiveClock.tsx @@ -1,8 +1,9 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Clock } from 'lucide-react'; import { toast } from 'sonner'; import CopyButton from '@/components/CopyButton'; import type { UnitType } from './constants'; +import { msToUnit } from './constants'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; @@ -11,41 +12,20 @@ interface LiveClockProps extends React.HTMLAttributes { onUseNow: (val: number) => void; } -const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => { +export default function LiveClock({ unit, onUseNow, className, ...props }: LiveClockProps) { const { t } = useI18n('timestamp'); - const onUseNowRef = useRef(onUseNow); - const [currentDisplay, setCurrentDisplay] = useState(() => { - const initNow = Date.now(); - return { - rawTime: initNow, - text: String(Math.floor(initNow / (unit === 'ms' ? 1 : 1000))), - }; - }); - - useEffect(() => { - onUseNowRef.current = onUseNow; - }, [onUseNow]); + const [rawTime, setRawTime] = useState(() => Date.now()); useEffect(() => { const tick = () => { - const rightNow = Date.now(); - const nextText = String(Math.floor(rightNow / (unit === 'ms' ? 1 : 1000))); - - setCurrentDisplay((prev) => { - if (prev.text === nextText) return prev; - return { rawTime: rightNow, text: nextText }; - }); + setRawTime(Date.now()); }; - const tickId = setInterval(tick, 200); return () => clearInterval(tickId); }, [unit]); - const handleUseNow = useCallback(() => { - onUseNowRef.current(currentDisplay.rawTime); - toast.success(t('timestamp:usedSuccess')); - }, [currentDisplay.rawTime, t]); + const text = String(msToUnit(rawTime, unit)); return (
- {currentDisplay.text} + {text}
); -}); - -LiveClock.displayName = 'LiveClock'; - -export default LiveClock; +} diff --git a/src/pages/Timestamp/ResultView.tsx b/src/pages/Timestamp/ResultView.tsx index 71087a7..076d01e 100644 --- a/src/pages/Timestamp/ResultView.tsx +++ b/src/pages/Timestamp/ResultView.tsx @@ -1,119 +1,52 @@ -import React, { useMemo } from 'react'; -import dayjs from '@/utils/dayjs'; +import React from 'react'; import CopyButton from '@/components/CopyButton'; -import type { UnitType } from './constants'; -import { DATE_FORMAT } from './constants'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; interface ResultViewProps extends React.HTMLAttributes { result: string; - mode: 'ts2dt' | 'dt2ts'; - unit: UnitType; - zone: string; - showEmptyPlaceholder?: boolean; } -const ResultView = React.memo( - ({ - result, - mode, - unit, - zone, - showEmptyPlaceholder = false, - className, - ...props - }: ResultViewProps) => { - const { t } = useI18n('timestamp'); - - const extraInfo = useMemo(() => { - if (!result) return null; - const d = - mode === 'ts2dt' - ? dayjs(result, DATE_FORMAT).tz(zone) - : unit === 'ms' - ? dayjs(Number(result)) - : dayjs.unix(Number(result)); - - return { - relative: d.fromNow(), - iso: d.toISOString(), - utc: d.utc().format(DATE_FORMAT) + ' UTC', - }; - }, [result, mode, zone, unit]); - - if (!result) { - if (!showEmptyPlaceholder) return null; - return ( -
- {t('timestamp:resultEmpty')} -
- ); - } +export default function ResultView({ + result, + showEmptyPlaceholder = false, + className, + ...props +}: ResultViewProps) { + const { t } = useI18n('timestamp'); + if (!result) { + if (!showEmptyPlaceholder) return null; return ( -
- {/* 顶部小标签 */} - - {t('timestamp:resultLabel')} - - -
- - {result} - - -
- -
- {[ - { label: t('timestamp:relativeTime'), value: extraInfo?.relative }, - { label: t('timestamp:iso8601'), value: extraInfo?.iso, isMono: true }, - { label: t('timestamp:utcTime'), value: extraInfo?.utc, isMono: true }, - ].map((item) => ( -
- - {item.label} - -
- - {item.value} - - {item.value && ( - - )} -
-
- ))} -
+
+ {t('timestamp:resultEmpty')}
); - }, -); + } -ResultView.displayName = 'ResultView'; + return ( +
+ + {t('timestamp:resultLabel')} + -export default ResultView; +
+ + {result} + + +
+
+ ); +} diff --git a/src/pages/Timestamp/constants.ts b/src/pages/Timestamp/constants.ts index ae08b00..6a84613 100644 --- a/src/pages/Timestamp/constants.ts +++ b/src/pages/Timestamp/constants.ts @@ -1,6 +1,22 @@ +import type { Dayjs } from 'dayjs'; +import dayjs from '@/utils/dayjs'; + export const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss'; export const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const; +export type ModeType = 'ts2dt' | 'dt2ts'; export type UnitType = 'ms' | 's'; export type ZoneType = (typeof ZONES)[number]; + +const DIVISORS: Record = { ms: 1, s: 1000 }; + +/** Convert milliseconds to display value based on unit. */ +export function msToUnit(ms: number, unit: UnitType): number { + return Math.floor(ms / DIVISORS[unit]); +} + +/** Build a dayjs object from a numeric timestamp according to unit. */ +export function dayjsFromTimestamp(ts: number, unit: UnitType): Dayjs { + return unit === 'ms' ? dayjs(ts) : dayjs.unix(ts); +} diff --git a/src/pages/Timestamp/index.tsx b/src/pages/Timestamp/index.tsx index e554860..2120708 100644 --- a/src/pages/Timestamp/index.tsx +++ b/src/pages/Timestamp/index.tsx @@ -1,5 +1,6 @@ import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import { ZONES } from './constants'; +import type { ModeType, UnitType, ZoneType } from './constants'; import LiveClock from './LiveClock'; import ResultView from './ResultView'; import { useTimestampConverter } from './useTimestampConverter'; @@ -15,6 +16,16 @@ import { SelectValue, } from '@/components/ui/select'; +const MODE_OPTIONS: { value: ModeType; label: string }[] = [ + { value: 'ts2dt', label: 'timestamp:tsToDate' }, + { value: 'dt2ts', label: 'timestamp:dateToTs' }, +]; + +const UNIT_OPTIONS: { value: UnitType; label: string }[] = [ + { value: 'ms', label: 'timestamp:unitMs' }, + { value: 's', label: 'timestamp:unitS' }, +]; + export default function Index() { const { t } = useI18n('timestamp'); @@ -42,11 +53,8 @@ export default function Index() {
setMode(newMode as 'ts2dt' | 'dt2ts')} + options={MODE_OPTIONS.map((o) => ({ ...o, label: t(o.label) }))} + onChange={setMode} size="small" /> @@ -57,7 +65,7 @@ export default function Index() { mode === 'ts2dt' ? t('timestamp:placeholderTs') : t('timestamp:placeholderDate') } value={input} - onChange={(e: { target: { value: string } }) => setInput(e.target.value)} + onChange={(e) => setInput(e.target.value)} className={cn( 'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background', error && 'border-destructive focus-visible:ring-destructive', @@ -70,16 +78,13 @@ export default function Index() {
setUnit(v as 'ms' | 's')} + options={UNIT_OPTIONS.map((o) => ({ ...o, label: t(o.label) }))} + onChange={setUnit} size="small" className="sm:w-auto shrink-0" /> - setZone(v as ZoneType)}> @@ -100,7 +105,7 @@ export default function Index() {
- +
diff --git a/src/pages/Timestamp/useTimestampConverter.ts b/src/pages/Timestamp/useTimestampConverter.ts index 31d8be8..7562c85 100644 --- a/src/pages/Timestamp/useTimestampConverter.ts +++ b/src/pages/Timestamp/useTimestampConverter.ts @@ -1,39 +1,42 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import dayjs from '@/utils/dayjs'; -import type { UnitType, ZoneType } from './constants'; -import { DATE_FORMAT } from './constants'; +import type { UnitType, ZoneType, ModeType } from './constants'; +import { DATE_FORMAT, msToUnit, dayjsFromTimestamp } from './constants'; import { useI18n } from '@/utils/chromeI18n'; import { useContextMenuData } from '@/utils/useContextMenuData'; export interface UseTimestampConverterReturn { - mode: 'ts2dt' | 'dt2ts'; + mode: ModeType; input: string; unit: UnitType; zone: ZoneType; result: string; error: string; - setMode: (mode: 'ts2dt' | 'dt2ts') => void; + setMode: (mode: ModeType) => void; setInput: (value: string) => void; setUnit: (unit: UnitType) => void; setZone: (zone: ZoneType) => void; handleUseNow: (now: number) => void; } +const TIMESTAMP_REGEX = /^\d+$/; +const MS_TIMESTAMP_MIN_LENGTH = 13; + function isTimestampLike(input: string): boolean { const trimmed = input.trim(); - return /^\d+$/.test(trimmed) && trimmed.length >= 10; + return TIMESTAMP_REGEX.test(trimmed) && trimmed.length >= 10; } export function useTimestampConverter(): UseTimestampConverterReturn { const { t } = useI18n('timestamp'); - const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt'); + const [mode, setMode] = useState('ts2dt'); const [unit, setUnit] = useState('ms'); const [zone, setZone] = useState('Asia/Shanghai'); const [input, setInput] = useState(() => String(Date.now())); - const conversionPipeline = useMemo(() => { + const { result, error } = useMemo(() => { const rawInput = input.trim(); if (!rawInput) return { result: '', error: '' }; @@ -42,7 +45,7 @@ export function useTimestampConverter(): UseTimestampConverterReturn { if (isNaN(num)) { return { result: '', error: t('timestamp:errors.invalidNumber') }; } - const d = unit === 'ms' ? dayjs(num) : dayjs.unix(num); + const d = dayjsFromTimestamp(num, unit); if (!d.isValid()) { return { result: '', error: t('timestamp:errors.invalidTimestamp') }; } @@ -53,19 +56,15 @@ export function useTimestampConverter(): UseTimestampConverterReturn { return { result: '', error: t('timestamp:errors.invalidFormat') }; } const ms = d.valueOf(); - const outputTs = unit === 'ms' ? String(ms) : String(Math.floor(ms / 1000)); - return { result: outputTs, error: '' }; + return { result: String(msToUnit(ms, unit)), error: '' }; } }, [input, mode, unit, zone, t]); - const { result, error } = conversionPipeline; - - const handleContextMenuData = useCallback((payload: string) => { + const handleContextMenuData = (payload: string) => { const trimmed = payload.trim(); if (isTimestampLike(trimmed)) { setMode('ts2dt'); - const detectedUnit: UnitType = trimmed.length >= 13 ? 'ms' : 's'; - setUnit(detectedUnit); + setUnit(trimmed.length >= MS_TIMESTAMP_MIN_LENGTH ? 'ms' : 's'); setInput(trimmed); } else { const d = dayjs(trimmed); @@ -77,30 +76,24 @@ export function useTimestampConverter(): UseTimestampConverterReturn { setInput(trimmed); } } - }, []); + }; useContextMenuData({ featureKey: 'timestamp', onData: handleContextMenuData }); - const handleUseNow = useCallback( - (now: number) => { - if (mode === 'ts2dt') { - setInput(String(unit === 'ms' ? now : Math.floor(now / 1000))); - } else { - setInput(dayjs(now).tz(zone).format(DATE_FORMAT)); - } - }, - [mode, unit, zone], - ); + const handleUseNow = (now: number) => { + if (mode === 'ts2dt') { + setInput(String(msToUnit(now, unit))); + } else { + setInput(dayjs(now).tz(zone).format(DATE_FORMAT)); + } + }; - const handleSetMode = useCallback( - (newMode: 'ts2dt' | 'dt2ts') => { - setMode(newMode); - if (result && !error) { - setInput(result); - } - }, - [result, error], - ); + const handleSetMode = (newMode: ModeType) => { + setMode(newMode); + if (result && !error) { + setInput(result); + } + }; return { mode,