diff --git a/package.json b/package.json index 6e361dcb..94d228f8 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,14 @@ "prepare": "simple-git-hooks && npm run rebuild" }, "dependencies": { + "@dagrejs/dagre": "^1.1.5", "@elysiajs/cors": "^1.2.0", "@elysiajs/node": "^1.2.5", "@elysiajs/swagger": "^1.2.2", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.46.5", + "@vue-flow/minimap": "^1.5.4", "@vueuse/core": "^12.7.0", "better-sqlite3": "^12.4.1", "change-case": "^5.4.4", @@ -88,6 +93,7 @@ "@types/chroma-js": "^3.1.1", "@types/codemirror": "^5.60.15", "@types/crypto-js": "^4.2.2", + "@types/dagre": "^0.7.53", "@types/dom-to-image": "^2.6.7", "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e954a01c..c423995e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@dagrejs/dagre': + specifier: ^1.1.5 + version: 1.1.5 '@elysiajs/cors': specifier: ^1.2.0 version: 1.2.0(elysia@1.2.15(@sinclair/typebox@0.34.27)(openapi-types@12.1.3)(typescript@5.7.3)) @@ -17,6 +20,18 @@ importers: '@elysiajs/swagger': specifier: ^1.2.2 version: 1.2.2(elysia@1.2.15(@sinclair/typebox@0.34.27)(openapi-types@12.1.3)(typescript@5.7.3)) + '@vue-flow/background': + specifier: ^1.3.2 + version: 1.3.2(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) + '@vue-flow/controls': + specifier: ^1.1.3 + version: 1.1.3(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) + '@vue-flow/core': + specifier: ^1.46.5 + version: 1.46.5(vue@3.5.13(typescript@5.7.3)) + '@vue-flow/minimap': + specifier: ^1.5.4 + version: 1.5.4(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) '@vueuse/core': specifier: ^12.7.0 version: 12.7.0(typescript@5.7.3) @@ -162,6 +177,9 @@ importers: '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/dagre': + specifier: ^0.7.53 + version: 0.7.53 '@types/dom-to-image': specifier: ^2.6.7 version: 2.6.7 @@ -427,6 +445,13 @@ packages: resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} engines: {node: '>=v18'} + '@dagrejs/dagre@1.1.5': + resolution: {integrity: sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==} + + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -1222,6 +1247,9 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/dagre@0.7.53': + resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1376,6 +1404,29 @@ packages: '@vscode/markdown-it-katex@1.1.1': resolution: {integrity: sha512-3KTlbsRBPJQLE2YmLL7K6nunTlU+W9T5+FjfNdWuIUKgxSS6HWLQHaO3L4MkJi7z7MpIPpY+g4N+cWNBPE/MSA==} + '@vue-flow/background@1.3.2': + resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + + '@vue-flow/controls@1.1.3': + resolution: {integrity: sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + + '@vue-flow/core@1.46.5': + resolution: {integrity: sha512-G1m9qNhCTgzw9K5Q8ZcRKbCXssgytWj00R/THUQA9fdO6oYJfBR8kwbI5tA+WHYHTRSCUvgz4xvp0wlF0tSaVg==} + peerDependencies: + vue: ^3.3.0 + + '@vue-flow/minimap@1.5.4': + resolution: {integrity: sha512-l4C+XTAXnRxsRpUdN7cAVFBennC1sVRzq4bDSpVK+ag7tdMczAnhFYGgbLkUw3v3sY6gokyWwMl8CDonp8eB2g==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -5215,6 +5266,12 @@ snapshots: '@types/conventional-commits-parser': 5.0.1 chalk: 5.4.1 + '@dagrejs/dagre@1.1.5': + dependencies: + '@dagrejs/graphlib': 2.2.4 + + '@dagrejs/graphlib@2.2.4': {} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.12.6 @@ -5995,6 +6052,8 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 + '@types/dagre@0.7.53': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -6182,6 +6241,34 @@ snapshots: dependencies: katex: 0.16.22 + '@vue-flow/background@1.3.2(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vue-flow/core': 1.46.5(vue@3.5.13(typescript@5.7.3)) + vue: 3.5.13(typescript@5.7.3) + + '@vue-flow/controls@1.1.3(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vue-flow/core': 1.46.5(vue@3.5.13(typescript@5.7.3)) + vue: 3.5.13(typescript@5.7.3) + + '@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3)) + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.5.13(typescript@5.7.3) + transitivePeerDependencies: + - '@vue/composition-api' + + '@vue-flow/minimap@1.5.4(@vue-flow/core@1.46.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@vue-flow/core': 1.46.5(vue@3.5.13(typescript@5.7.3)) + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.5.13(typescript@5.7.3) + '@vue/compiler-core@3.5.13': dependencies: '@babel/parser': 7.26.7 diff --git a/src/main/i18n/locales/cs_CZ/menu.json b/src/main/i18n/locales/cs_CZ/menu.json index 05a22266..1b324a3f 100644 --- a/src/main/i18n/locales/cs_CZ/menu.json +++ b/src/main/i18n/locales/cs_CZ/menu.json @@ -52,6 +52,7 @@ "format": "Formátovat", "previewCode": "Náhled kódu", "previewScreenshot": "Náhled snímku obrazovky", + "previewJson": "Náhled JSON", "fontSizeIncrease": "Zvětšit velikost písma", "fontSizeDecrease": "Zmenšit velikost písma", "fontSizeReset": "Obnovit velikost písma" diff --git a/src/main/i18n/locales/de_DE/menu.json b/src/main/i18n/locales/de_DE/menu.json index dceb276b..e39d7c0e 100644 --- a/src/main/i18n/locales/de_DE/menu.json +++ b/src/main/i18n/locales/de_DE/menu.json @@ -52,6 +52,7 @@ "format": "Formatieren", "previewCode": "Code-Vorschau", "previewScreenshot": "Screenshot-Vorschau", + "previewJson": "JSON-Vorschau", "fontSizeIncrease": "Schriftgröße erhöhen", "fontSizeDecrease": "Schriftgröße verringern", "fontSizeReset": "Schriftgröße zurücksetzen" diff --git a/src/main/i18n/locales/el_GR/menu.json b/src/main/i18n/locales/el_GR/menu.json index f8a8d1ae..317897e1 100644 --- a/src/main/i18n/locales/el_GR/menu.json +++ b/src/main/i18n/locales/el_GR/menu.json @@ -52,6 +52,7 @@ "format": "Μορφοποίηση", "previewCode": "Προεπισκόπηση Κώδικα", "previewScreenshot": "Προεπισκόπηση Screenshot", + "previewJson": "Προεπισκόπηση JSON", "fontSizeIncrease": "Αύξηση Μεγέθους Γραμματοσειράς", "fontSizeDecrease": "Μείωση Μεγέθους Γραμματοσειράς", "fontSizeReset": "Επαναφορά Μεγέθους Γραμματοσειράς" diff --git a/src/main/i18n/locales/en_US/menu.json b/src/main/i18n/locales/en_US/menu.json index c1603dbd..1eafbeae 100644 --- a/src/main/i18n/locales/en_US/menu.json +++ b/src/main/i18n/locales/en_US/menu.json @@ -52,6 +52,7 @@ "format": "Format", "previewCode": "Preview Code", "previewScreenshot": "Preview Screenshot", + "previewJson": "Preview JSON", "fontSizeIncrease": "Font Size Increase", "fontSizeDecrease": "Font Size Decrease", "fontSizeReset": "Font Size Reset" diff --git a/src/main/i18n/locales/es_ES/menu.json b/src/main/i18n/locales/es_ES/menu.json index 73fda595..772abcd2 100644 --- a/src/main/i18n/locales/es_ES/menu.json +++ b/src/main/i18n/locales/es_ES/menu.json @@ -52,6 +52,7 @@ "format": "Formatear", "previewCode": "Vista Previa del Código", "previewScreenshot": "Vista Previa de Captura", + "previewJson": "Vista previa JSON", "fontSizeIncrease": "Aumentar Tamaño de Fuente", "fontSizeDecrease": "Disminuir Tamaño de Fuente", "fontSizeReset": "Restablecer Tamaño de Fuente" diff --git a/src/main/i18n/locales/fa_IR/menu.json b/src/main/i18n/locales/fa_IR/menu.json index 9d9755bb..9363f2c3 100644 --- a/src/main/i18n/locales/fa_IR/menu.json +++ b/src/main/i18n/locales/fa_IR/menu.json @@ -52,6 +52,7 @@ "format": "قالب‌بندی", "previewCode": "پیش‌نمایش کد", "previewScreenshot": "پیش‌نمایش تصویر", + "previewJson": "پیش‌نمایش JSON", "fontSizeIncrease": "افزایش اندازه فونت", "fontSizeDecrease": "کاهش اندازه فونت", "fontSizeReset": "بازنشانی اندازه فونت" diff --git a/src/main/i18n/locales/fr_FR/menu.json b/src/main/i18n/locales/fr_FR/menu.json index d2744fa9..19bd2411 100644 --- a/src/main/i18n/locales/fr_FR/menu.json +++ b/src/main/i18n/locales/fr_FR/menu.json @@ -52,6 +52,7 @@ "format": "Formater", "previewCode": "Aperçu du code", "previewScreenshot": "Aperçu de la capture d'écran", + "previewJson": "Aperçu JSON", "fontSizeIncrease": "Augmenter la taille de la police", "fontSizeDecrease": "Diminuer la taille de la police", "fontSizeReset": "Réinitialiser la taille de la police" diff --git a/src/main/i18n/locales/ja_JP/menu.json b/src/main/i18n/locales/ja_JP/menu.json index 56e50077..6c5737bc 100644 --- a/src/main/i18n/locales/ja_JP/menu.json +++ b/src/main/i18n/locales/ja_JP/menu.json @@ -52,6 +52,7 @@ "format": "フォーマット", "previewCode": "コードのプレビュー", "previewScreenshot": "スクリーンショットのプレビュー", + "previewJson": "JSON プレビュー", "fontSizeIncrease": "フォントサイズを大きく", "fontSizeDecrease": "フォントサイズを小さく", "fontSizeReset": "フォントサイズをリセット" diff --git a/src/main/i18n/locales/pl_PL/menu.json b/src/main/i18n/locales/pl_PL/menu.json index 642a9fbb..38a69f62 100644 --- a/src/main/i18n/locales/pl_PL/menu.json +++ b/src/main/i18n/locales/pl_PL/menu.json @@ -52,6 +52,7 @@ "format": "Formatuj", "previewCode": "Podgląd kodu", "previewScreenshot": "Podgląd zrzutu ekranu", + "previewJson": "Podgląd JSON", "fontSizeIncrease": "Zwiększ rozmiar czcionki", "fontSizeDecrease": "Zmniejsz rozmiar czcionki", "fontSizeReset": "Resetuj rozmiar czcionki" diff --git a/src/main/i18n/locales/pt_BR/menu.json b/src/main/i18n/locales/pt_BR/menu.json index 4a48a201..e0ae487f 100644 --- a/src/main/i18n/locales/pt_BR/menu.json +++ b/src/main/i18n/locales/pt_BR/menu.json @@ -52,6 +52,7 @@ "format": "Formatar", "previewCode": "Visualizar Código", "previewScreenshot": "Visualizar Screenshot", + "previewJson": "Prévia do JSON", "fontSizeIncrease": "Aumentar Tamanho da Fonte", "fontSizeDecrease": "Diminuir Tamanho da Fonte", "fontSizeReset": "Redefinir Tamanho da Fonte" diff --git a/src/main/i18n/locales/ro_RO/menu.json b/src/main/i18n/locales/ro_RO/menu.json index 50d47d6c..9ec2c7f5 100644 --- a/src/main/i18n/locales/ro_RO/menu.json +++ b/src/main/i18n/locales/ro_RO/menu.json @@ -52,6 +52,7 @@ "format": "Formatare", "previewCode": "Previzualizare Cod", "previewScreenshot": "Previzualizare Screenshot", + "previewJson": "Previzualizare JSON", "fontSizeIncrease": "Mărește Dimensiunea Fontului", "fontSizeDecrease": "Micșorează Dimensiunea Fontului", "fontSizeReset": "Resetează Dimensiunea Fontului" diff --git a/src/main/i18n/locales/ru_RU/menu.json b/src/main/i18n/locales/ru_RU/menu.json index c0f8e338..d15a0870 100644 --- a/src/main/i18n/locales/ru_RU/menu.json +++ b/src/main/i18n/locales/ru_RU/menu.json @@ -52,6 +52,7 @@ "format": "Форматировать", "previewCode": "Предпросмотр кода", "previewScreenshot": "Предпросмотр скриншота", + "previewJson": "Предпросмотр JSON", "fontSizeIncrease": "Увеличить размер шрифта", "fontSizeDecrease": "Уменьшить размер шрифта", "fontSizeReset": "Сбросить размер шрифта" diff --git a/src/main/i18n/locales/tr_TR/menu.json b/src/main/i18n/locales/tr_TR/menu.json index dc3ae5a1..a4bece7c 100644 --- a/src/main/i18n/locales/tr_TR/menu.json +++ b/src/main/i18n/locales/tr_TR/menu.json @@ -52,6 +52,7 @@ "format": "Biçimlendir", "previewCode": "Kodu Önizle", "previewScreenshot": "Ekran Görüntüsünü Önizle", + "previewJson": "JSON Önizleme", "fontSizeIncrease": "Yazı Boyutunu Artır", "fontSizeDecrease": "Yazı Boyutunu Azalt", "fontSizeReset": "Yazı Boyutunu Sıfırla" diff --git a/src/main/i18n/locales/uk_UA/menu.json b/src/main/i18n/locales/uk_UA/menu.json index 38874418..70ad7c8a 100644 --- a/src/main/i18n/locales/uk_UA/menu.json +++ b/src/main/i18n/locales/uk_UA/menu.json @@ -52,6 +52,7 @@ "format": "Форматувати", "previewCode": "Попередній перегляд коду", "previewScreenshot": "Попередній перегляд знімка екрану", + "previewJson": "Попередній перегляд JSON", "fontSizeIncrease": "Збільшити розмір шрифту", "fontSizeDecrease": "Зменшити розмір шрифту", "fontSizeReset": "Скинути розмір шрифту" diff --git a/src/main/i18n/locales/zh_CN/menu.json b/src/main/i18n/locales/zh_CN/menu.json index 1cefb9ea..314bb5a0 100644 --- a/src/main/i18n/locales/zh_CN/menu.json +++ b/src/main/i18n/locales/zh_CN/menu.json @@ -52,6 +52,7 @@ "format": "格式化", "previewCode": "预览代码", "previewScreenshot": "预览截图", + "previewJson": "预览 JSON", "fontSizeIncrease": "增大字体", "fontSizeDecrease": "减小字体", "fontSizeReset": "重置字体大小" diff --git a/src/main/i18n/locales/zh_HK/menu.json b/src/main/i18n/locales/zh_HK/menu.json index 28a538f0..a54946e7 100644 --- a/src/main/i18n/locales/zh_HK/menu.json +++ b/src/main/i18n/locales/zh_HK/menu.json @@ -52,6 +52,7 @@ "format": "格式化", "previewCode": "預覽程式碼", "previewScreenshot": "預覽截圖", + "previewJson": "預覽 JSON", "fontSizeIncrease": "增大字體", "fontSizeDecrease": "減小字體", "fontSizeReset": "重置字體大小" diff --git a/src/main/i18n/locales/zh_TW/menu.json b/src/main/i18n/locales/zh_TW/menu.json index bd390704..7c4d30d3 100644 --- a/src/main/i18n/locales/zh_TW/menu.json +++ b/src/main/i18n/locales/zh_TW/menu.json @@ -52,6 +52,7 @@ "format": "格式化", "previewCode": "預覽程式碼", "previewScreenshot": "預覽截圖", + "previewJson": "預覽 JSON", "fontSizeIncrease": "增大字體", "fontSizeDecrease": "減小字體", "fontSizeReset": "重置字體大小" diff --git a/src/main/menu/main.ts b/src/main/menu/main.ts index 3e8abcd6..51830c44 100644 --- a/src/main/menu/main.ts +++ b/src/main/menu/main.ts @@ -219,6 +219,11 @@ const editorMenuItems: MenuConfig[] = [ click: () => send('main-menu:preview-code'), accelerator: 'Alt+CommandOrControl+P', }, + { + label: i18n.t('menu:editor.previewJson'), + click: () => send('main-menu:preview-json'), + accelerator: 'Alt+CommandOrControl+J', + }, { type: 'separator', }, diff --git a/src/main/types/ipc.ts b/src/main/types/ipc.ts index b9a2a920..7efb23f4 100644 --- a/src/main/types/ipc.ts +++ b/src/main/types/ipc.ts @@ -19,6 +19,7 @@ type MainMenuAction = | 'preview-markdown' | 'preview-mindmap' | 'preview-code' + | 'preview-json' | 'presentation-mode' type DBAction = diff --git a/src/renderer/components/editor/Editor.vue b/src/renderer/components/editor/Editor.vue index 3bc7d528..2d0fd9a5 100644 --- a/src/renderer/components/editor/Editor.vue +++ b/src/renderer/components/editor/Editor.vue @@ -38,6 +38,7 @@ const { isShowCodeImage, isShowMarkdownPresentation, isFocusedSearch, + isShowJsonVisualizer, } = useApp() const { addToUpdateContentQueue } = useSnippetUpdate() @@ -77,6 +78,7 @@ const isShowEditor = computed(() => { && !isShowMindmap.value && !isShowCodeImage.value && !isShowMarkdownPresentation.value + && !isShowJsonVisualizer.value && !isEmpty.value && selectedSnippet.value !== undefined ) @@ -88,6 +90,10 @@ watch(selectedSnippetContent, () => { isShowMindmap.value = false } + if (selectedSnippetContent.value?.language !== 'json') { + isShowJsonVisualizer.value = false + } + if (!isAvailableToCodePreview.value) { isShowCodePreview.value = false } @@ -243,6 +249,12 @@ async function init() { }) }, ) + + watch(searchQuery, () => { + nextTick(() => { + updateSearchOverlay() + }) + }) } function setValue(value: string, programmatic = true) { @@ -395,12 +407,6 @@ function updateSearchOverlay() { onMounted(() => { init() - - watch(searchQuery, () => { - nextTick(() => { - updateSearchOverlay() - }) - }) }) @@ -433,6 +439,7 @@ onMounted(() => { +
@@ -148,6 +170,14 @@ function onCodeImageToggle() { > + + + - + +import type { Node } from '@vue-flow/core' +import type { NodeData } from './types' +import { useClipboard, useDark, useDebounceFn } from '@vueuse/core' +import CodeMirror from 'codemirror' +import { Copy } from 'lucide-vue-next' +import { onMounted, watch } from 'vue' +import 'codemirror/lib/codemirror.css' +import 'codemirror/theme/neo.css' +import 'codemirror/theme/oceanic-next.css' + +interface Props { + node: Node +} + +const props = defineProps() + +const { copy } = useClipboard() +const isDark = useDark() + +let editor: CodeMirror.Editor | null = null +const editorRef = useTemplateRef('editorRef') + +const scrollBarOpacity = ref('1') +const theme = computed(() => (isDark.value ? 'oceanic-next' : 'neo')) + +const hideScrollbar = useDebounceFn(() => { + scrollBarOpacity.value = '0' +}, 1000) + +function toJsonString(value: any) { + return JSON.stringify(value, null, 2) || '{}' +} + +function init() { + if (!editorRef.value) { + return + } + + editor = CodeMirror(editorRef.value, { + value: toJsonString(props.node.data?.value), + mode: 'json', + lineNumbers: true, + theme: theme.value, + readOnly: true, + lineWrapping: true, + scrollbarStyle: 'null', + }) + + editor.on('scroll', () => { + scrollBarOpacity.value = '1' + editor?.setOption('scrollbarStyle', 'overlay') + }) + + editor.on('scroll', hideScrollbar) + + watch( + () => props.node, + (newNode) => { + if (editor && newNode.data?.value !== undefined) { + const jsonString = toJsonString(newNode.data.value) + editor.setValue(jsonString) + } + }, + { deep: true }, + ) + + watch(isDark, (v) => { + if (v) { + editor?.setOption('theme', 'oceanic-next') + } + else { + editor?.setOption('theme', 'neo') + } + }) +} + +function onCopy() { + copy(editor?.getValue() || '') + editorRef.value?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }), + ) +} + +onMounted(() => { + init() +}) + + + + + diff --git a/src/renderer/components/editor/json-visualizer/JsonVisualizer.vue b/src/renderer/components/editor/json-visualizer/JsonVisualizer.vue new file mode 100644 index 00000000..350d9005 --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/JsonVisualizer.vue @@ -0,0 +1,241 @@ + + + diff --git a/src/renderer/components/editor/json-visualizer/composables/useLayout.ts b/src/renderer/components/editor/json-visualizer/composables/useLayout.ts new file mode 100644 index 00000000..8db3357f --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/composables/useLayout.ts @@ -0,0 +1,66 @@ +import type { Edge, Node } from '@vue-flow/core' +import type { LayoutDirection, NodeData } from '../types' + +import dagre from '@dagrejs/dagre' +import { Position, useVueFlow } from '@vue-flow/core' +import { ref } from 'vue' + +/** + * Composable to run the layout algorithm on the graph. + * It uses the `dagre` library to calculate the layout of the nodes and edges. + */ +export function useLayout() { + const { findNode } = useVueFlow() + + const graph = ref(new dagre.graphlib.Graph()) + + const previousDirection = ref('LR') + + function layout( + nodes: Node[], + edges: Edge[], + direction: LayoutDirection, + ): Node[] { + // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there + const dagreGraph = new dagre.graphlib.Graph() + + graph.value = dagreGraph + + dagreGraph.setDefaultEdgeLabel(() => ({})) + + const isHorizontal = direction === 'LR' + dagreGraph.setGraph({ rankdir: direction }) + + previousDirection.value = direction + + for (const node of nodes) { + // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type) + const graphNode = findNode(node.id) + + dagreGraph.setNode(node.id, { + width: graphNode?.dimensions.width || 150, + height: graphNode?.dimensions.height || 50, + }) + } + + for (const edge of edges) { + dagreGraph.setEdge(edge.source, edge.target) + } + + dagre.layout(dagreGraph) + + // set nodes with updated positions + return nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id) + + return { + ...node, + targetPosition: isHorizontal ? Position.Left : Position.Top, + sourcePosition: isHorizontal ? Position.Right : Position.Bottom, + position: { x: nodeWithPosition.x, y: nodeWithPosition.y }, + } + }) + } + + return { graph, layout, previousDirection } +} diff --git a/src/renderer/components/editor/json-visualizer/nodes/ArrayNode.vue b/src/renderer/components/editor/json-visualizer/nodes/ArrayNode.vue new file mode 100644 index 00000000..1253ba8a --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/nodes/ArrayNode.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/renderer/components/editor/json-visualizer/nodes/ObjectNode.vue b/src/renderer/components/editor/json-visualizer/nodes/ObjectNode.vue new file mode 100644 index 00000000..04368d52 --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/nodes/ObjectNode.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/renderer/components/editor/json-visualizer/types/index.ts b/src/renderer/components/editor/json-visualizer/types/index.ts new file mode 100644 index 00000000..d9d6bb10 --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/types/index.ts @@ -0,0 +1,25 @@ +import type { Edge, Node } from '@vue-flow/core' + +export interface NodeData { + label: string + value: any + type: 'object' | 'array' | 'primitive' + keysCount?: number + length?: number +} + +export interface GraphData { + nodes: Node[] + edges: Edge[] +} + +export type NodeType = 'object' | 'array' | 'primitive' +export type ValueType = + | 'null' + | 'array' + | 'object' + | 'string' + | 'number' + | 'boolean' + | 'undefined' +export type LayoutDirection = 'TB' | 'LR' diff --git a/src/renderer/components/editor/json-visualizer/utils/index.ts b/src/renderer/components/editor/json-visualizer/utils/index.ts new file mode 100644 index 00000000..98f4506e --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/utils/index.ts @@ -0,0 +1 @@ +export * from './json-parser' diff --git a/src/renderer/components/editor/json-visualizer/utils/json-parser.ts b/src/renderer/components/editor/json-visualizer/utils/json-parser.ts new file mode 100644 index 00000000..02ee0964 --- /dev/null +++ b/src/renderer/components/editor/json-visualizer/utils/json-parser.ts @@ -0,0 +1,158 @@ +import type { Edge, Node } from '@vue-flow/core' +import type { GraphData, NodeData, NodeType, ValueType } from '../types' + +let nodeIdCounter = 0 + +function generateNodeId(): string { + return `node-${nodeIdCounter++}` +} + +function getValueType(value: any): ValueType { + if (value === null) + return 'null' + if (Array.isArray(value)) + return 'array' + if (typeof value === 'object') + return 'object' + return typeof value as ValueType +} + +function createNode( + id: string, + type: NodeType, + data: NodeData, + position: { x: number, y: number } = { x: 0, y: 0 }, +): Node { + return { + id, + type, + data, + position, + } +} + +function createEdge(sourceId: string, targetId: string): Edge { + return { + id: `edge-${sourceId}-${targetId}`, + source: sourceId, + target: targetId, + type: 'default', + animated: false, + } +} + +function parseValue( + value: any, + key: string | null = null, + parentId: string | null = null, + nodes: Node[] = [], + edges: Edge[] = [], +): string { + const nodeId = generateNodeId() + const valueType = getValueType(value) + + if (valueType === 'object') { + // Создаем узел для объекта + const node = createNode(nodeId, 'object', { + label: key || 'root', + value, + type: 'object', + keysCount: Object.keys(value).length, + }) + nodes.push(node) + + // Если есть родитель, создаем связь + if (parentId !== null) { + edges.push(createEdge(parentId, nodeId)) + } + + // Рекурсивно обрабатываем дочерние элементы + Object.entries(value).forEach(([childKey, childValue]) => { + const childType = getValueType(childValue) + // Создаем узлы только для объектов и массивов + if (childType === 'object' || childType === 'array') { + parseValue(childValue, childKey, nodeId, nodes, edges) + } + }) + } + else if (valueType === 'array') { + // Создаем узел для массива + const node = createNode(nodeId, 'array', { + label: key || 'root', + value, + type: 'array', + length: value.length, + }) + nodes.push(node) + + // Если есть родитель, создаем связь + if (parentId !== null) { + edges.push(createEdge(parentId, nodeId)) + } + + // Рекурсивно обрабатываем элементы массива + value.forEach((item: any, index: number) => { + const itemType = getValueType(item) + // Создаем узлы только для объектов и массивов + if (itemType === 'object' || itemType === 'array') { + parseValue(item, `[${index}]`, nodeId, nodes, edges) + } + }) + } + // Примитивные значения не создают отдельные узлы + + return nodeId +} + +export function parseJsonToGraph(jsonData: any): GraphData { + // Сброс счетчика для новых узлов + nodeIdCounter = 0 + + const nodes: Node[] = [] + const edges: Edge[] = [] + + // Проверяем, является ли корневой элемент объектом или массивом + const valueType = getValueType(jsonData) + + if (valueType === 'object') { + // Создаем корневой узел для объекта + const rootId = generateNodeId() + const rootNode = createNode(rootId, 'object', { + label: 'root', + value: jsonData, + type: 'object', + keysCount: Object.keys(jsonData).length, + }) + nodes.push(rootNode) + + // Парсим дочерние элементы + Object.entries(jsonData).forEach(([key, value]) => { + const childType = getValueType(value) + if (childType === 'object' || childType === 'array') { + parseValue(value, key, rootId, nodes, edges) + } + }) + } + else if (valueType === 'array') { + // Создаем корневой узел для массива + const rootId = generateNodeId() + const rootNode = createNode(rootId, 'array', { + label: 'root', + value: jsonData, + type: 'array', + length: jsonData.length, + }) + nodes.push(rootNode) + + // Парсим элементы массива + jsonData.forEach((item: any, index: number) => { + const itemType = getValueType(item) + if (itemType === 'object' || itemType === 'array') { + parseValue(item, `[${index}]`, rootId, nodes, edges) + } + }) + } + // Если корень - примитив, то не создаем узлов + + return { nodes, edges } +} diff --git a/src/renderer/components/ui/button/variants.ts b/src/renderer/components/ui/button/variants.ts index cd2e24a4..60775997 100644 --- a/src/renderer/components/ui/button/variants.ts +++ b/src/renderer/components/ui/button/variants.ts @@ -9,7 +9,7 @@ export const variants = cva( default: 'bg-button text-button-fg hover:bg-button-hover', primary: 'bg-primary text-white hover:bg-primary/70', danger: 'bg-red-700 text-white hover:bg-red-700/70', - icon: 'bg-transparent hover:bg-button-hover hover:[&>svg]:text-button-fg hover:text-button-fg', + icon: 'bg-transparent hover:bg-button-hover hover:[&>svg]:text-button-fg hover:text-button-fg [&>svg]:text-button-fg', }, size: { sm: 'px-2 h-5', diff --git a/src/renderer/composables/useApp.ts b/src/renderer/composables/useApp.ts index 5abeb7f8..c4b80114 100644 --- a/src/renderer/composables/useApp.ts +++ b/src/renderer/composables/useApp.ts @@ -23,6 +23,7 @@ const isShowMarkdownPresentation = ref(false) const isShowMindmap = ref(false) const isShowCodePreview = ref(false) const isShowCodeImage = ref(false) +const isShowJsonVisualizer = ref(false) const sidebarWidth = useCssVar('--sidebar-width') const snippetListWidth = useCssVar('--snippet-list-width') @@ -74,6 +75,7 @@ export function useApp() { isShowMarkdown, isShowMarkdownPresentation, isShowMindmap, + isShowJsonVisualizer, isSponsored, restoreStateSnapshot, saveStateSnapshot, diff --git a/src/renderer/ipc/listeners/main-menu.ts b/src/renderer/ipc/listeners/main-menu.ts index 1f448c1e..28e62ae2 100644 --- a/src/renderer/ipc/listeners/main-menu.ts +++ b/src/renderer/ipc/listeners/main-menu.ts @@ -9,6 +9,7 @@ const { isShowMindmap, isShowCodePreview, isShowMarkdownPresentation, + isShowJsonVisualizer, } = useApp() export function registerMainMenuListeners() { @@ -44,6 +45,10 @@ export function registerMainMenuListeners() { isShowCodePreview.value = !isShowCodePreview.value }) + ipc.on('main-menu:preview-json', () => { + isShowJsonVisualizer.value = !isShowJsonVisualizer.value + }) + ipc.on('main-menu:presentation-mode', () => { isShowMarkdownPresentation.value = !isShowMarkdownPresentation.value router.push({ name: RouterName.markdownPresentation })