diff --git a/package-lock.json b/package-lock.json index 9d2fb4d..1312fc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "chartjs-plugin-zoom": "^2.2.0", "i18next": "^25.4.2", "lucide-react": "^0.522.0", + "lz-string": "^1.5.0", "postcss": "^8.5.6", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", @@ -4029,9 +4030,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } diff --git a/package.json b/package.json index 693739c..b3c7a52 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-i18next": "^15.7.2", - "tailwindcss": "^3.4.4" + "tailwindcss": "^3.4.4", + "lz-string": "^1.5.0" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1aa629c..9afc026 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -29,6 +29,10 @@ "chart.diffStats": "Difference Statistics", "chart": "chart", "resetConfig": "Reset Config", + "shareConfig": "Share Config", + "shareConfig.copied": "URL copied to clipboard", + "importConfig": "Import Config", + "importConfig.error": "Invalid configuration file", "useStepKeyword": "Use Step keyword", "placeholder.step": "step:", "resize.drag": "Drag to resize chart height", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 8ae331d..486ef83 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -29,6 +29,10 @@ "chart.diffStats": "差值统计", "chart": "图表", "resetConfig": "重置配置", + "shareConfig": "分享配置", + "shareConfig.copied": "链接已复制到剪贴板", + "importConfig": "导入配置", + "importConfig.error": "配置文件无效", "useStepKeyword": "使用 Step 关键字", "placeholder.step": "step:", "resize.drag": "拖拽调整图表高度", diff --git a/src/App.jsx b/src/App.jsx index 961d291..b623656 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,7 @@ import { ThemeToggle } from './components/ThemeToggle'; import { Header } from './components/Header'; import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { mergeFilesWithReplacement } from './utils/mergeFiles.js'; +import { encodeConfig, decodeConfig } from './utils/shareConfig.js'; // Default global parsing configuration export const DEFAULT_GLOBAL_PARSING_CONFIG = { @@ -56,6 +57,20 @@ function App() { const [sidebarVisible, setSidebarVisible] = useState(true); const savingDisabledRef = useRef(false); + // Load config from URL hash if present + useEffect(() => { + const hash = window.location.hash.slice(1); // remove leading '#' + const params = new URLSearchParams(hash); + const cfg = params.get('config'); + if (cfg) { + const data = decodeConfig(cfg); + if (data?.globalParsingConfig && data?.uploadedFiles) { + setGlobalParsingConfig(data.globalParsingConfig); + setUploadedFiles(data.uploadedFiles); + } + } + }, []); + // Persist configuration to localStorage useEffect(() => { if (savingDisabledRef.current) return; @@ -182,6 +197,41 @@ function App() { }, 0); }, []); + const handleShareConfig = useCallback(() => { + const data = encodeConfig({ globalParsingConfig, uploadedFiles }); + const url = `${window.location.origin}${window.location.pathname}#config=${data}`; + if (navigator?.clipboard?.writeText) { + navigator.clipboard.writeText(url).then(() => { + alert(t('shareConfig.copied')); + }).catch(() => { + window.prompt('', url); + }); + } else { + window.prompt('', url); + } + }, [globalParsingConfig, uploadedFiles, t]); + + const handleImportConfigFile = useCallback((e) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const json = JSON.parse(ev.target.result); + if (json.globalParsingConfig && json.uploadedFiles) { + setGlobalParsingConfig(json.globalParsingConfig); + setUploadedFiles(json.uploadedFiles); + } else { + alert(t('importConfig.error')); + } + } catch { + alert(t('importConfig.error')); + } + }; + reader.readAsText(file); + e.target.value = ''; + }, [t]); + // Global drag event handlers const handleGlobalDragEnter = useCallback((e) => { e.preventDefault(); @@ -366,6 +416,27 @@ function App() { > {t('resetConfig')} + + + diff --git a/src/utils/__tests__/shareConfig.test.js b/src/utils/__tests__/shareConfig.test.js new file mode 100644 index 0000000..4c54f9c --- /dev/null +++ b/src/utils/__tests__/shareConfig.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { encodeConfig, decodeConfig } from '../shareConfig.js'; + +describe('shareConfig', () => { + it('encodes and decodes data', () => { + const data = { + globalParsingConfig: { metrics: [], useStepKeyword: false, stepKeyword: 'step:' }, + uploadedFiles: [{ name: 'a.log', content: 'loss:1', enabled: true }] + }; + const encoded = encodeConfig(data); + const decoded = decodeConfig(encoded); + expect(decoded).toEqual(data); + }); + + it('compresses data compared to base64', () => { + const data = { text: 'a'.repeat(1000) }; + const encoded = encodeConfig(data); + const json = JSON.stringify(data); + const base64 = Buffer.from(json, 'utf-8').toString('base64'); + const raw = encodeURIComponent(base64); + expect(encoded.length).toBeLessThan(raw.length); + }); + + it('returns null for invalid input', () => { + expect(decodeConfig('%')).toBeNull(); + }); +}); diff --git a/src/utils/shareConfig.js b/src/utils/shareConfig.js new file mode 100644 index 0000000..793edcc --- /dev/null +++ b/src/utils/shareConfig.js @@ -0,0 +1,16 @@ +import LZString from 'lz-string'; + +export function encodeConfig(data) { + const json = JSON.stringify(data); + return LZString.compressToEncodedURIComponent(json); +} + +export function decodeConfig(encoded) { + try { + const json = LZString.decompressFromEncodedURIComponent(encoded); + return json ? JSON.parse(json) : null; + } catch (e) { + console.error('Failed to decode config', e); + return null; + } +}